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.
"""

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. """ __slots__ = ( "_engine", "_point_render_pass", "_point_framebuffer", "_point_color_image", "_point_color_memory", "_point_color_view", "_point_depth_image", "_point_depth_memory", "_point_depth_view", "_point_sampler", "_point_texture_index", "_spot_render_pass", "_spot_framebuffer", "_spot_color_image", "_spot_color_memory", "_spot_color_view", "_spot_depth_image", "_spot_depth_memory", "_spot_depth_view", "_spot_sampler", "_spot_texture_index", "_pipeline", "_pipeline_layout", "_vert_module", "_frag_module", "_ready", ) def __init__(self, engine: Any): for slot in self.__slots__: object.__setattr__(self, slot, None) self._engine = engine self._point_texture_index = -1 self._spot_texture_index = -1 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, )
[docs] @property def point_shadow_texture_index(self) -> int: """Bindless index of the point shadow atlas texture.""" return self._point_texture_index
[docs] @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, ), ] colour_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=[colour_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