Source code for simvx.graphics.renderer.point_shadow_pass

"""Point and spot light shadow map rendering pass.

Point lights use a 6-face cubemap (rendered as 6 separate passes with
different view matrices).  Spot lights use a single 2D depth texture
with a perspective projection matching the cone angle.

Shadow depth textures are registered in the bindless texture array so
the forward fragment shader can sample them via integer index.
"""

from __future__ import annotations

import logging
import math
from typing import Any

import numpy as np
import vulkan as vk

from ..gpu.pipeline import create_shader_module
from ..materials.shader_compiler import compile_shader

__all__ = ["PointShadowPass"]

log = logging.getLogger(__name__)

POINT_SHADOW_SIZE = 512
SPOT_SHADOW_SIZE = 1024
DEPTH_FORMAT = vk.VK_FORMAT_D32_SFLOAT
COLOR_FORMAT = vk.VK_FORMAT_R32_SFLOAT

# 6 cubemap face directions: +X, -X, +Y, -Y, +Z, -Z
_CUBE_FACE_TARGETS = [
    (np.array([1, 0, 0], dtype=np.float32), np.array([0, -1, 0], dtype=np.float32)),  # +X
    (np.array([-1, 0, 0], dtype=np.float32), np.array([0, -1, 0], dtype=np.float32)),  # -X
    (np.array([0, 1, 0], dtype=np.float32), np.array([0, 0, 1], dtype=np.float32)),  # +Y
    (np.array([0, -1, 0], dtype=np.float32), np.array([0, 0, -1], dtype=np.float32)),  # -Y
    (np.array([0, 0, 1], dtype=np.float32), np.array([0, -1, 0], dtype=np.float32)),  # +Z
    (np.array([0, 0, -1], dtype=np.float32), np.array([0, -1, 0], dtype=np.float32)),  # -Z
]


[docs] class PointShadowPass: """Renders depth from point/spot light POVs into shadow map textures. Point lights: 6-face atlas (6 x POINT_SHADOW_SIZE side-by-side). Spot lights: Single 2D depth texture (SPOT_SHADOW_SIZE x SPOT_SHADOW_SIZE). Uses a colour attachment (R32_SFLOAT) to store linear distance from light, plus a depth attachment for correct Z-testing during rendering. """ def __init__(self, engine: Any): self._engine = engine # Point shadow resources self._point_render_pass: Any = None self._point_framebuffer: Any = None self._point_color_image: Any = None self._point_color_memory: Any = None self._point_color_view: Any = None self._point_depth_image: Any = None self._point_depth_memory: Any = None self._point_depth_view: Any = None self._point_sampler: Any = None self._point_texture_index: int = -1 # Spot shadow resources self._spot_render_pass: Any = None self._spot_framebuffer: Any = None self._spot_color_image: Any = None self._spot_color_memory: Any = None self._spot_color_view: Any = None self._spot_depth_image: Any = None self._spot_depth_memory: Any = None self._spot_depth_view: Any = None self._spot_sampler: Any = None self._spot_texture_index: int = -1 # Shared pipeline (same shaders for point and spot) self._pipeline: Any = None self._pipeline_layout: Any = None self._vert_module: Any = None self._frag_module: Any = None self._ready = False
[docs] def setup(self, ssbo_layout: Any) -> None: """Initialize point and spot shadow map resources.""" e = self._engine device = e.ctx.device phys = e.ctx.physical_device # -- Point shadow: 6-face atlas (colour R32F + depth D32F) -- atlas_w = POINT_SHADOW_SIZE * 6 atlas_h = POINT_SHADOW_SIZE self._point_render_pass = _create_colour_depth_pass(device) self._point_color_image, self._point_color_memory, self._point_color_view = _create_render_target( device, phys, atlas_w, atlas_h, COLOR_FORMAT, vk.VK_IMAGE_ASPECT_COLOR_BIT ) self._point_depth_image, self._point_depth_memory, self._point_depth_view = _create_render_target( device, phys, atlas_w, atlas_h, DEPTH_FORMAT, vk.VK_IMAGE_ASPECT_DEPTH_BIT ) self._point_framebuffer = _create_framebuffer( device, self._point_render_pass, [self._point_color_view, self._point_depth_view], atlas_w, atlas_h, ) self._point_sampler = _create_shadow_sampler(device) # Register point shadow map in bindless texture array from ..gpu.descriptors import write_texture_descriptor if not e.texture_descriptor_set: e._init_texture_system() self._point_texture_index = e._next_texture_index write_texture_descriptor( device, e.texture_descriptor_set, self._point_texture_index, self._point_color_view, self._point_sampler, ) e._next_texture_index += 1 # -- Spot shadow: single 2D (colour R32F + depth D32F) -- self._spot_render_pass = _create_colour_depth_pass(device) self._spot_color_image, self._spot_color_memory, self._spot_color_view = _create_render_target( device, phys, SPOT_SHADOW_SIZE, SPOT_SHADOW_SIZE, COLOR_FORMAT, vk.VK_IMAGE_ASPECT_COLOR_BIT ) self._spot_depth_image, self._spot_depth_memory, self._spot_depth_view = _create_render_target( device, phys, SPOT_SHADOW_SIZE, SPOT_SHADOW_SIZE, DEPTH_FORMAT, vk.VK_IMAGE_ASPECT_DEPTH_BIT ) self._spot_framebuffer = _create_framebuffer( device, self._spot_render_pass, [self._spot_color_view, self._spot_depth_view], SPOT_SHADOW_SIZE, SPOT_SHADOW_SIZE, ) self._spot_sampler = _create_shadow_sampler(device) self._spot_texture_index = e._next_texture_index write_texture_descriptor( device, e.texture_descriptor_set, self._spot_texture_index, self._spot_color_view, self._spot_sampler, ) e._next_texture_index += 1 # -- Shadow pipeline (shared for point and spot) -- shader_dir = e.shader_dir vert_spv = compile_shader(shader_dir / "shadow_point.vert") frag_spv = compile_shader(shader_dir / "shadow_point.frag") self._vert_module = create_shader_module(device, vert_spv) self._frag_module = create_shader_module(device, frag_spv) self._pipeline, self._pipeline_layout = _create_point_shadow_pipeline( device, self._vert_module, self._frag_module, self._point_render_pass, ssbo_layout, ) self._ready = True log.debug( "Point/spot shadow pass initialized (point=%dx%d, spot=%dx%d)", atlas_w, atlas_h, SPOT_SHADOW_SIZE, SPOT_SHADOW_SIZE, )
@property def point_shadow_texture_index(self) -> int: """Bindless index of the point shadow atlas texture.""" return self._point_texture_index @property def spot_shadow_texture_index(self) -> int: """Bindless index of the spot shadow depth texture.""" return self._spot_texture_index
[docs] def render_point_shadow( self, cmd: Any, light_pos: np.ndarray, light_range: float, instances: list, ssbo_set: Any, mesh_registry: Any, ) -> None: """Render 6 cubemap faces for a point light shadow. Each face is rendered to a horizontal slice of the point shadow atlas. Linear distance from light is written to the R32F colour attachment. """ if not self._ready or not instances: return atlas_w = POINT_SHADOW_SIZE * 6 atlas_h = POINT_SHADOW_SIZE near = 0.05 # 90-degree perspective projection (square faces) proj = _perspective_vulkan(math.radians(90.0), 1.0, near, light_range) # Begin render pass with clear clears = [ vk.VkClearValue(color=vk.VkClearColorValue(float32=[1.0, 1.0, 1.0, 1.0])), vk.VkClearValue(depthStencil=vk.VkClearDepthStencilValue(depth=1.0, stencil=0)), ] rp_info = vk.VkRenderPassBeginInfo( renderPass=self._point_render_pass, framebuffer=self._point_framebuffer, renderArea=vk.VkRect2D( offset=vk.VkOffset2D(x=0, y=0), extent=vk.VkExtent2D(width=atlas_w, height=atlas_h), ), clearValueCount=2, pClearValues=clears, ) vk.vkCmdBeginRenderPass(cmd, rp_info, vk.VK_SUBPASS_CONTENTS_INLINE) vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._pipeline) vk.vkCmdBindDescriptorSets( cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._pipeline_layout, 0, 1, [ssbo_set], 0, None, ) # Group instances by mesh for batched drawing mesh_groups: dict[int, list[int]] = {} for i, (mesh_handle, _, _, _) in enumerate(instances): mesh_groups.setdefault(mesh_handle.id, []).append(i) ffi = vk.ffi for face in range(6): # Viewport for this face in the atlas vk_vp = vk.VkViewport( x=float(face * POINT_SHADOW_SIZE), y=0.0, width=float(POINT_SHADOW_SIZE), height=float(POINT_SHADOW_SIZE), minDepth=0.0, maxDepth=1.0, ) vk.vkCmdSetViewport(cmd, 0, 1, [vk_vp]) scissor = vk.VkRect2D( offset=vk.VkOffset2D(x=face * POINT_SHADOW_SIZE, y=0), extent=vk.VkExtent2D(width=POINT_SHADOW_SIZE, height=POINT_SHADOW_SIZE), ) vk.vkCmdSetScissor(cmd, 0, 1, [scissor]) # Build light view matrix for this face target, up = _CUBE_FACE_TARGETS[face] view = _look_at(light_pos, light_pos + target, up) vp = (proj @ view).T # Transpose for GLSL column-major # Push constants: mat4 light_vp (64 bytes) + vec4 light_pos_far (16 bytes) pc_bytes = np.ascontiguousarray(vp).tobytes() light_pos_far = np.array([*light_pos[:3], light_range], dtype=np.float32) pc_bytes += light_pos_far.tobytes() cbuf = ffi.new("char[]", pc_bytes) vk._vulkan.lib.vkCmdPushConstants( cmd, self._pipeline_layout, vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT, 0, 80, cbuf, ) # Draw all meshes for _mesh_id, indices in mesh_groups.items(): mesh_handle = instances[indices[0]][0] vb, ib = mesh_registry.get_buffers(mesh_handle) vk.vkCmdBindVertexBuffers(cmd, 0, 1, [vb], [0]) vk.vkCmdBindIndexBuffer(cmd, ib, 0, vk.VK_INDEX_TYPE_UINT32) for idx in indices: vk.vkCmdDrawIndexed(cmd, mesh_handle.index_count, 1, 0, 0, idx) vk.vkCmdEndRenderPass(cmd)
[docs] def render_spot_shadow( self, cmd: Any, light_pos: np.ndarray, light_dir: np.ndarray, fov: float, light_range: float, instances: list, ssbo_set: Any, mesh_registry: Any, ) -> None: """Render a single perspective shadow map for a spot light. Args: light_pos: World-space position of the spot light. light_dir: Normalized direction the spot light points. fov: Outer cone angle in degrees (used as projection FOV). light_range: Maximum range of the spot light. """ if not self._ready or not instances: return near = 0.05 # Use outer cone angle * 2 as FOV (cone angle is half-angle) proj_fov = math.radians(min(fov * 2.0, 179.0)) proj = _perspective_vulkan(proj_fov, 1.0, near, light_range) # Build view matrix looking along light_dir light_dir_n = light_dir / np.linalg.norm(light_dir) up = np.array([0, 1, 0], dtype=np.float32) if abs(np.dot(light_dir_n, up)) > 0.99: up = np.array([1, 0, 0], dtype=np.float32) view = _look_at(light_pos, light_pos + light_dir_n, up) vp = (proj @ view).T # Transpose for column-major # Begin render pass clears = [ vk.VkClearValue(color=vk.VkClearColorValue(float32=[1.0, 1.0, 1.0, 1.0])), vk.VkClearValue(depthStencil=vk.VkClearDepthStencilValue(depth=1.0, stencil=0)), ] rp_info = vk.VkRenderPassBeginInfo( renderPass=self._spot_render_pass, framebuffer=self._spot_framebuffer, renderArea=vk.VkRect2D( offset=vk.VkOffset2D(x=0, y=0), extent=vk.VkExtent2D(width=SPOT_SHADOW_SIZE, height=SPOT_SHADOW_SIZE), ), clearValueCount=2, pClearValues=clears, ) vk.vkCmdBeginRenderPass(cmd, rp_info, vk.VK_SUBPASS_CONTENTS_INLINE) vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._pipeline) vk.vkCmdBindDescriptorSets( cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._pipeline_layout, 0, 1, [ssbo_set], 0, None, ) # Viewport/scissor vk_vp = vk.VkViewport( x=0.0, y=0.0, width=float(SPOT_SHADOW_SIZE), height=float(SPOT_SHADOW_SIZE), minDepth=0.0, maxDepth=1.0, ) vk.vkCmdSetViewport(cmd, 0, 1, [vk_vp]) scissor = vk.VkRect2D( offset=vk.VkOffset2D(x=0, y=0), extent=vk.VkExtent2D(width=SPOT_SHADOW_SIZE, height=SPOT_SHADOW_SIZE), ) vk.vkCmdSetScissor(cmd, 0, 1, [scissor]) # Push constants: mat4 light_vp + vec4 light_pos_far ffi = vk.ffi pc_bytes = np.ascontiguousarray(vp).tobytes() light_pos_far = np.array([*light_pos[:3], light_range], dtype=np.float32) pc_bytes += light_pos_far.tobytes() cbuf = ffi.new("char[]", pc_bytes) vk._vulkan.lib.vkCmdPushConstants( cmd, self._pipeline_layout, vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT, 0, 80, cbuf, ) # Draw all meshes mesh_groups: dict[int, list[int]] = {} for i, (mesh_handle, _, _, _) in enumerate(instances): mesh_groups.setdefault(mesh_handle.id, []).append(i) for _mesh_id, indices in mesh_groups.items(): mesh_handle = instances[indices[0]][0] vb, ib = mesh_registry.get_buffers(mesh_handle) vk.vkCmdBindVertexBuffers(cmd, 0, 1, [vb], [0]) vk.vkCmdBindIndexBuffer(cmd, ib, 0, vk.VK_INDEX_TYPE_UINT32) for idx in indices: vk.vkCmdDrawIndexed(cmd, mesh_handle.index_count, 1, 0, 0, idx) vk.vkCmdEndRenderPass(cmd)
[docs] def get_spot_vp_matrix( self, light_pos: np.ndarray, light_dir: np.ndarray, fov: float, light_range: float, ) -> np.ndarray: """Compute the VP matrix for a spot light (for fragment shader sampling).""" near = 0.05 proj_fov = math.radians(min(fov * 2.0, 179.0)) proj = _perspective_vulkan(proj_fov, 1.0, near, light_range) light_dir_n = light_dir / np.linalg.norm(light_dir) up = np.array([0, 1, 0], dtype=np.float32) if abs(np.dot(light_dir_n, up)) > 0.99: up = np.array([1, 0, 0], dtype=np.float32) view = _look_at(light_pos, light_pos + light_dir_n, up) return (proj @ view).T # Transposed for GLSL column-major
[docs] def cleanup(self) -> None: """Release all GPU resources.""" if not self._ready: return device = self._engine.ctx.device # Destroy in reverse creation order for obj, fn in [ (self._pipeline, vk.vkDestroyPipeline), (self._pipeline_layout, vk.vkDestroyPipelineLayout), (self._vert_module, vk.vkDestroyShaderModule), (self._frag_module, vk.vkDestroyShaderModule), # Point resources (self._point_framebuffer, vk.vkDestroyFramebuffer), (self._point_color_view, vk.vkDestroyImageView), (self._point_color_image, vk.vkDestroyImage), (self._point_depth_view, vk.vkDestroyImageView), (self._point_depth_image, vk.vkDestroyImage), (self._point_sampler, vk.vkDestroySampler), (self._point_render_pass, vk.vkDestroyRenderPass), # Spot resources (self._spot_framebuffer, vk.vkDestroyFramebuffer), (self._spot_color_view, vk.vkDestroyImageView), (self._spot_color_image, vk.vkDestroyImage), (self._spot_depth_view, vk.vkDestroyImageView), (self._spot_depth_image, vk.vkDestroyImage), (self._spot_sampler, vk.vkDestroySampler), (self._spot_render_pass, vk.vkDestroyRenderPass), ]: if obj: fn(device, obj, None) for mem in [ self._point_color_memory, self._point_depth_memory, self._spot_color_memory, self._spot_depth_memory, ]: if mem: vk.vkFreeMemory(device, mem, None) self._ready = False
# ============================================================================= # Helpers # ============================================================================= def _look_at(eye: np.ndarray, target: np.ndarray, up: np.ndarray) -> np.ndarray: """Build a right-handed look-at view matrix (row-major).""" f = target - eye f = f / np.linalg.norm(f) r = np.cross(f, up) r = r / np.linalg.norm(r) u = np.cross(r, f) view = np.eye(4, dtype=np.float32) view[0, :3] = r view[1, :3] = u view[2, :3] = -f view[:3, 3] = -view[:3, :3] @ eye return view def _perspective_vulkan(fov_y: float, aspect: float, near: float, far: float) -> np.ndarray: """Build a Vulkan perspective projection matrix (depth [0,1], Y-flip).""" f = 1.0 / math.tan(fov_y * 0.5) proj = np.zeros((4, 4), dtype=np.float32) proj[0, 0] = f / aspect proj[1, 1] = -f # Vulkan Y-flip proj[2, 2] = far / (near - far) proj[2, 3] = (near * far) / (near - far) proj[3, 2] = -1.0 return proj def _create_render_target( device: Any, phys: Any, width: int, height: int, fmt: int, aspect: int, ) -> tuple[Any, Any, Any]: """Create an image + memory + view for a render target.""" from ..gpu.memory import _find_memory_type usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT if aspect == vk.VK_IMAGE_ASPECT_COLOR_BIT: usage |= vk.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT else: usage |= vk.VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT img_info = vk.VkImageCreateInfo( imageType=vk.VK_IMAGE_TYPE_2D, format=fmt, extent=vk.VkExtent3D(width=width, height=height, depth=1), mipLevels=1, arrayLayers=1, samples=vk.VK_SAMPLE_COUNT_1_BIT, tiling=vk.VK_IMAGE_TILING_OPTIMAL, usage=usage, sharingMode=vk.VK_SHARING_MODE_EXCLUSIVE, initialLayout=vk.VK_IMAGE_LAYOUT_UNDEFINED, ) image = vk.vkCreateImage(device, img_info, None) mem_reqs = vk.vkGetImageMemoryRequirements(device, image) alloc_info = vk.VkMemoryAllocateInfo( allocationSize=mem_reqs.size, memoryTypeIndex=_find_memory_type( phys, mem_reqs.memoryTypeBits, vk.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, ), ) memory = vk.vkAllocateMemory(device, alloc_info, None) vk.vkBindImageMemory(device, image, memory, 0) view_ci = vk.VkImageViewCreateInfo( image=image, viewType=vk.VK_IMAGE_VIEW_TYPE_2D, format=fmt, subresourceRange=vk.VkImageSubresourceRange( aspectMask=aspect, baseMipLevel=0, levelCount=1, baseArrayLayer=0, layerCount=1, ), ) view = vk.vkCreateImageView(device, view_ci, None) return image, memory, view def _create_shadow_sampler(device: Any) -> Any: """Create a linear-filter, clamp-to-border sampler for shadow maps.""" sampler_ci = vk.VkSamplerCreateInfo( magFilter=vk.VK_FILTER_LINEAR, minFilter=vk.VK_FILTER_LINEAR, addressModeU=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, addressModeV=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, addressModeW=vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, borderColor=vk.VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE, compareEnable=vk.VK_FALSE, anisotropyEnable=vk.VK_FALSE, unnormalizedCoordinates=vk.VK_FALSE, mipmapMode=vk.VK_SAMPLER_MIPMAP_MODE_NEAREST, ) return vk.vkCreateSampler(device, sampler_ci, None) def _create_colour_depth_pass(device: Any) -> Any: """Create a render pass with R32F colour + D32F depth for linear distance output.""" attachments = [ # Colour (R32F — stores linear distance) vk.VkAttachmentDescription( format=COLOR_FORMAT, samples=vk.VK_SAMPLE_COUNT_1_BIT, loadOp=vk.VK_ATTACHMENT_LOAD_OP_CLEAR, storeOp=vk.VK_ATTACHMENT_STORE_OP_STORE, stencilLoadOp=vk.VK_ATTACHMENT_LOAD_OP_DONT_CARE, stencilStoreOp=vk.VK_ATTACHMENT_STORE_OP_DONT_CARE, initialLayout=vk.VK_IMAGE_LAYOUT_UNDEFINED, finalLayout=vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, ), # Depth (D32F — for Z-testing) vk.VkAttachmentDescription( format=DEPTH_FORMAT, samples=vk.VK_SAMPLE_COUNT_1_BIT, loadOp=vk.VK_ATTACHMENT_LOAD_OP_CLEAR, storeOp=vk.VK_ATTACHMENT_STORE_OP_DONT_CARE, stencilLoadOp=vk.VK_ATTACHMENT_LOAD_OP_DONT_CARE, stencilStoreOp=vk.VK_ATTACHMENT_STORE_OP_DONT_CARE, initialLayout=vk.VK_IMAGE_LAYOUT_UNDEFINED, finalLayout=vk.VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, ), ] color_ref = vk.VkAttachmentReference( attachment=0, layout=vk.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, ) depth_ref = vk.VkAttachmentReference( attachment=1, layout=vk.VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, ) subpass = vk.VkSubpassDescription( pipelineBindPoint=vk.VK_PIPELINE_BIND_POINT_GRAPHICS, colorAttachmentCount=1, pColorAttachments=[color_ref], pDepthStencilAttachment=depth_ref, ) dependencies = [ vk.VkSubpassDependency( srcSubpass=vk.VK_SUBPASS_EXTERNAL, dstSubpass=0, srcStageMask=vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, srcAccessMask=vk.VK_ACCESS_SHADER_READ_BIT, dstStageMask=( vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | vk.VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT ), dstAccessMask=(vk.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | vk.VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), ), vk.VkSubpassDependency( srcSubpass=0, dstSubpass=vk.VK_SUBPASS_EXTERNAL, srcStageMask=( vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | vk.VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT ), srcAccessMask=(vk.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | vk.VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), dstStageMask=vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, dstAccessMask=vk.VK_ACCESS_SHADER_READ_BIT, ), ] create_info = vk.VkRenderPassCreateInfo( attachmentCount=2, pAttachments=attachments, subpassCount=1, pSubpasses=[subpass], dependencyCount=2, pDependencies=dependencies, ) render_pass = vk.vkCreateRenderPass(device, create_info, None) log.debug("Point shadow render pass created (colour+depth)") return render_pass def _create_framebuffer( device: Any, render_pass: Any, views: list, width: int, height: int, ) -> Any: """Create a framebuffer with the given image views.""" fb_ci = vk.VkFramebufferCreateInfo( renderPass=render_pass, attachmentCount=len(views), pAttachments=views, width=width, height=height, layers=1, ) return vk.vkCreateFramebuffer(device, fb_ci, None) def _create_point_shadow_pipeline( device: Any, vert_module: Any, frag_module: Any, render_pass: Any, ssbo_layout: Any, ) -> tuple[Any, Any]: """Create the point/spot shadow rendering pipeline. Push constants: mat4 light_vp (64 bytes) + vec4 light_pos_far (16 bytes) = 80 bytes. Has one R32F colour attachment output (linear distance). """ ffi = vk.ffi # Push constant: mat4 (64) + vec4 (16) = 80 bytes push_range = ffi.new("VkPushConstantRange*") push_range.stageFlags = vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT push_range.offset = 0 push_range.size = 80 # Pipeline layout with SSBO descriptor set set_layouts = ffi.new("VkDescriptorSetLayout[1]", [ssbo_layout]) layout_ci = ffi.new("VkPipelineLayoutCreateInfo*") layout_ci.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO layout_ci.setLayoutCount = 1 layout_ci.pSetLayouts = set_layouts layout_ci.pushConstantRangeCount = 1 layout_ci.pPushConstantRanges = push_range layout_out = ffi.new("VkPipelineLayout*") result = vk._vulkan._callApi( vk._vulkan.lib.vkCreatePipelineLayout, device, layout_ci, ffi.NULL, layout_out, ) if result != vk.VK_SUCCESS: raise RuntimeError(f"vkCreatePipelineLayout failed: {result}") pipeline_layout = layout_out[0] pi = ffi.new("VkGraphicsPipelineCreateInfo*") pi.sType = vk.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO # Shader stages stages = ffi.new("VkPipelineShaderStageCreateInfo[2]") main_name = ffi.new("char[]", b"main") stages[0].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO stages[0].stage = vk.VK_SHADER_STAGE_VERTEX_BIT stages[0].module = vert_module stages[0].pName = main_name stages[1].sType = vk.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO stages[1].stage = vk.VK_SHADER_STAGE_FRAGMENT_BIT stages[1].module = frag_module stages[1].pName = main_name pi.stageCount = 2 pi.pStages = stages # Vertex input — same as forward (32 bytes stride) binding_desc = ffi.new("VkVertexInputBindingDescription*") binding_desc.binding = 0 binding_desc.stride = 32 binding_desc.inputRate = vk.VK_VERTEX_INPUT_RATE_VERTEX attr_descs = ffi.new("VkVertexInputAttributeDescription[3]") attr_descs[0].location = 0 attr_descs[0].binding = 0 attr_descs[0].format = vk.VK_FORMAT_R32G32B32_SFLOAT attr_descs[0].offset = 0 attr_descs[1].location = 1 attr_descs[1].binding = 0 attr_descs[1].format = vk.VK_FORMAT_R32G32B32_SFLOAT attr_descs[1].offset = 12 attr_descs[2].location = 2 attr_descs[2].binding = 0 attr_descs[2].format = vk.VK_FORMAT_R32G32_SFLOAT attr_descs[2].offset = 24 vi = ffi.new("VkPipelineVertexInputStateCreateInfo*") vi.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO vi.vertexBindingDescriptionCount = 1 vi.pVertexBindingDescriptions = binding_desc vi.vertexAttributeDescriptionCount = 3 vi.pVertexAttributeDescriptions = attr_descs pi.pVertexInputState = vi # Input assembly ia = ffi.new("VkPipelineInputAssemblyStateCreateInfo*") ia.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO ia.topology = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST pi.pInputAssemblyState = ia # Viewport state (dynamic) vps = ffi.new("VkPipelineViewportStateCreateInfo*") vps.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO vps.viewportCount = 1 viewport = ffi.new("VkViewport*") viewport.width = float(POINT_SHADOW_SIZE) viewport.height = float(POINT_SHADOW_SIZE) viewport.maxDepth = 1.0 vps.pViewports = viewport scissor = ffi.new("VkRect2D*") scissor.extent.width = POINT_SHADOW_SIZE scissor.extent.height = POINT_SHADOW_SIZE vps.scissorCount = 1 vps.pScissors = scissor pi.pViewportState = vps # Rasterization — depth bias for shadow acne rs = ffi.new("VkPipelineRasterizationStateCreateInfo*") rs.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO rs.polygonMode = vk.VK_POLYGON_MODE_FILL rs.lineWidth = 1.0 rs.cullMode = vk.VK_CULL_MODE_FRONT_BIT rs.frontFace = vk.VK_FRONT_FACE_CLOCKWISE rs.depthBiasEnable = 1 rs.depthBiasConstantFactor = 1.25 rs.depthBiasSlopeFactor = 1.75 pi.pRasterizationState = rs # Multisample ms = ffi.new("VkPipelineMultisampleStateCreateInfo*") ms.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO ms.rasterizationSamples = vk.VK_SAMPLE_COUNT_1_BIT pi.pMultisampleState = ms # Depth stencil dss = ffi.new("VkPipelineDepthStencilStateCreateInfo*") dss.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO dss.depthTestEnable = 1 dss.depthWriteEnable = 1 dss.depthCompareOp = vk.VK_COMPARE_OP_LESS_OR_EQUAL pi.pDepthStencilState = dss # Colour blend — one R32F colour attachment blend_att = ffi.new("VkPipelineColorBlendAttachmentState*") blend_att.colorWriteMask = ( vk.VK_COLOR_COMPONENT_R_BIT | vk.VK_COLOR_COMPONENT_G_BIT | vk.VK_COLOR_COMPONENT_B_BIT | vk.VK_COLOR_COMPONENT_A_BIT ) blend_att.blendEnable = 0 cb = ffi.new("VkPipelineColorBlendStateCreateInfo*") cb.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO cb.attachmentCount = 1 cb.pAttachments = blend_att pi.pColorBlendState = cb # Dynamic state dyn_states = ffi.new( "VkDynamicState[2]", [ vk.VK_DYNAMIC_STATE_VIEWPORT, vk.VK_DYNAMIC_STATE_SCISSOR, ], ) ds = ffi.new("VkPipelineDynamicStateCreateInfo*") ds.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO ds.dynamicStateCount = 2 ds.pDynamicStates = dyn_states pi.pDynamicState = ds pi.layout = pipeline_layout pi.renderPass = render_pass pipeline_out = ffi.new("VkPipeline*") result = vk._vulkan._callApi( vk._vulkan.lib.vkCreateGraphicsPipelines, device, ffi.NULL, 1, pi, ffi.NULL, pipeline_out, ) if result != vk.VK_SUCCESS: raise RuntimeError(f"vkCreateGraphicsPipelines failed: {result}") log.debug("Point/spot shadow pipeline created") return pipeline_out[0], pipeline_layout