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