Source code for simvx.graphics.renderer.skybox_pass

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

from __future__ import annotations

import logging
from typing import Any

import numpy as np
import vulkan as vk

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) -> None: """Initialize skybox pipeline with a cubemap texture.""" 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 self._create_pipeline(device, 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.""" ffi = vk.ffi # Push constants: view + proj = 128 bytes push_range = ffi.new("VkPushConstantRange*") push_range.stageFlags = vk.VK_SHADER_STAGE_VERTEX_BIT push_range.offset = 0 push_range.size = 128 # Pipeline layout layout_ci = ffi.new("VkPipelineLayoutCreateInfo*") layout_ci.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO set_layouts = ffi.new("VkDescriptorSetLayout[1]", [self._descriptor_layout]) 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}") self._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 = self._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 = self._frag_module stages[1].pName = main_name pi.stageCount = 2 pi.pStages = stages # No vertex input (cube generated in shader) vi = ffi.new("VkPipelineVertexInputStateCreateInfo*") vi.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO pi.pVertexInputState = vi 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 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(extent[0]) viewport.height = float(extent[1]) viewport.maxDepth = 1.0 vps.pViewports = viewport scissor = ffi.new("VkRect2D*") scissor.extent.width = extent[0] scissor.extent.height = extent[1] vps.scissorCount = 1 vps.pScissors = scissor pi.pViewportState = vps # Rasterization — no culling (inside the cube) 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_NONE pi.pRasterizationState = rs 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: test enabled (LESS_OR_EQUAL), no write dss = ffi.new("VkPipelineDepthStencilStateCreateInfo*") dss.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO dss.depthTestEnable = 1 dss.depthWriteEnable = 0 dss.depthCompareOp = vk.VK_COMPARE_OP_LESS_OR_EQUAL pi.pDepthStencilState = dss # Colour blend cba = ffi.new("VkPipelineColorBlendAttachmentState*") cba.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 ) cb = ffi.new("VkPipelineColorBlendStateCreateInfo*") cb.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO cb.attachmentCount = 1 cb.pAttachments = cba 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 = self._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}") self._pipeline = pipeline_out[0]
[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