Source code for simvx.graphics.renderer.skybox_pass

"""Skybox rendering pass: draws a cubemap-textured sky behind the scene."""

import logging
from typing import Any

import numpy as np
import vulkan as vk

from ..gpu.pipeline import PipelineSpec, build_pipeline
from .pass_helpers import create_sampler_descriptor_pool, load_shader_modules

__all__ = ["SkyboxPass"]

log = logging.getLogger(__name__)

[docs] class SkyboxPass: """Renders a cubemap skybox behind the scene. The skybox is drawn as a unit cube with depth test set to LESS_OR_EQUAL and depth written as 1.0 (far plane), so it renders behind all geometry. """ def __init__(self, engine: Any): self._engine = engine self._pipeline: Any = None self._pipeline_layout: Any = None self._vert_module: Any = None self._frag_module: Any = None self._descriptor_layout: Any = None self._descriptor_pool: Any = None self._descriptor_set: Any = None self._cubemap_view: Any = None self._cubemap_sampler: Any = None self._ready = False
[docs] def setup(self, cubemap_view: Any, cubemap_sampler: Any, render_pass: Any = None) -> None: """Initialize skybox pipeline with a cubemap texture. When ``render_pass`` is provided, the graphics pipeline is compiled against it: needed when the skybox is drawn inside the HDR post-process render pass (``R16G16B16A16_SFLOAT``) rather than the engine's default swapchain pass (``B8G8R8A8_SRGB``). Mismatched formats trigger VUID-vkCmdDraw-renderPass-02684 at draw time. """ e = self._engine device = e.ctx.device self._cubemap_view = cubemap_view self._cubemap_sampler = cubemap_sampler # Descriptor layout: single cubemap sampler binding = vk.VkDescriptorSetLayoutBinding( binding=0, descriptorType=vk.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount=1, stageFlags=vk.VK_SHADER_STAGE_FRAGMENT_BIT, ) self._descriptor_layout = vk.vkCreateDescriptorSetLayout(device, vk.VkDescriptorSetLayoutCreateInfo( bindingCount=1, pBindings=[binding], ), None) # Descriptor pool and set self._descriptor_pool, desc_sets = create_sampler_descriptor_pool( device, self._descriptor_layout, ) self._descriptor_set = desc_sets[0] # Write cubemap descriptor image_info = vk.VkDescriptorImageInfo( sampler=cubemap_sampler, imageView=cubemap_view, imageLayout=vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, ) vk.vkUpdateDescriptorSets(device, 1, [vk.VkWriteDescriptorSet( dstSet=self._descriptor_set, dstBinding=0, dstArrayElement=0, descriptorCount=1, descriptorType=vk.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pImageInfo=[image_info], )], 0, None) # Compile shaders self._vert_module, self._frag_module = load_shader_modules( device, e.shader_dir, "skybox.vert", "skybox.frag", ) # Create pipeline against the caller-supplied render pass when given # (HDR post-process path). Fall back to the engine's default pass for # callers that draw straight into the swapchain. self._create_pipeline(device, render_pass or e.render_pass, e.extent) self._ready = True log.debug("Skybox pass initialized")
def _create_pipeline(self, device: Any, render_pass: Any, extent: tuple[int, int]) -> None: """Create skybox pipeline with depth test <= and no vertex input. Declares its fixed-function state via :class:`PipelineSpec` and defers all cffi sub-struct plumbing (and lifetime management) to :func:`build_pipeline`. The shaders are compiled at runtime (``load_shader_modules``), so the pre-created modules are passed directly rather than via SPIR-V paths in the spec. """ spec = PipelineSpec( name="skybox", topology=vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, vertex_stride=0, # 36-vertex cube generated in the shader cull_mode=vk.VK_CULL_MODE_NONE, # drawn from inside the cube depth_test=True, depth_write=False, depth_compare=vk.VK_COMPARE_OP_LESS_OR_EQUAL, blend="opaque", set_layouts=(self._descriptor_layout,), push_size=128, # mat4 view + mat4 proj push_stages=vk.VK_SHADER_STAGE_VERTEX_BIT, # Thin G-buffer: non-lit passes share the HDR pass's second # colour attachment but mask their writes (writes_gbuffer defaults # False). Two attachments only while a consumer is active. attachment_count=(2 if self._engine.gbuffer_active else 1), ) self._pipeline, self._pipeline_layout = build_pipeline( device, spec, render_pass, extent, vert_module=self._vert_module, frag_module=self._frag_module, )
[docs] def rebuild_pipeline(self, render_pass: Any) -> None: """Recreate the skybox pipeline against *render_pass* (e.g. the thin G-buffer HDR pass, whose attachment count differs). No-op until setup.""" if not self._ready or self._pipeline is None: return device = self._engine.ctx.device if self._pipeline: vk.vkDestroyPipeline(device, self._pipeline, None) if self._pipeline_layout: vk.vkDestroyPipelineLayout(device, self._pipeline_layout, None) self._create_pipeline(device, render_pass, self._engine.extent)
[docs] def render(self, cmd: Any, view_matrix: np.ndarray, proj_matrix: np.ndarray, extent: tuple[int, int]) -> None: """Render skybox. Call after clearing but before scene geometry.""" if not self._ready: return # Set viewport/scissor vk_viewport = vk.VkViewport( x=0.0, y=0.0, width=float(extent[0]), height=float(extent[1]), minDepth=0.0, maxDepth=1.0, ) vk.vkCmdSetViewport(cmd, 0, 1, [vk_viewport]) scissor = vk.VkRect2D( offset=vk.VkOffset2D(x=0, y=0), extent=vk.VkExtent2D(width=extent[0], height=extent[1]), ) vk.vkCmdSetScissor(cmd, 0, 1, [scissor]) # Bind pipeline and descriptor 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, [self._descriptor_set], 0, None, ) # Push view + proj matrices (transposed for column-major GLSL) view_t = np.ascontiguousarray(view_matrix.T) proj_t = np.ascontiguousarray(proj_matrix.T) pc_data = view_t.tobytes() + proj_t.tobytes() ffi = vk.ffi cbuf = ffi.new("char[]", pc_data) vk._vulkan.lib.vkCmdPushConstants( cmd, self._pipeline_layout, vk.VK_SHADER_STAGE_VERTEX_BIT, 0, 128, cbuf, ) # Draw unit cube (36 vertices, no vertex buffer) vk.vkCmdDraw(cmd, 36, 1, 0, 0)
[docs] def cleanup(self) -> None: """Release GPU resources.""" if not self._ready: return device = self._engine.ctx.device if self._pipeline: vk.vkDestroyPipeline(device, self._pipeline, None) if self._pipeline_layout: vk.vkDestroyPipelineLayout(device, self._pipeline_layout, None) if self._vert_module: vk.vkDestroyShaderModule(device, self._vert_module, None) if self._frag_module: vk.vkDestroyShaderModule(device, self._frag_module, None) if self._descriptor_pool: vk.vkDestroyDescriptorPool(device, self._descriptor_pool, None) if self._descriptor_layout: vk.vkDestroyDescriptorSetLayout(device, self._descriptor_layout, None) self._ready = False