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