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