Source code for simvx.graphics.renderer.grid_pass

"""Infinite ground-plane grid overlay for 3D editor viewports.

Draws an anti-aliased XZ grid using a fragment shader SDF approach.
Major lines every 10 units, minor lines every 1 unit, with distance fade.
X-axis coloured red, Z-axis coloured blue (Godot convention).
"""

import logging
from typing import Any

import numpy as np
import vulkan as vk

from ..gpu.pipeline import PipelineSpec, build_pipeline, create_shader_module
from ..materials.shader_compiler import compile_shader

__all__ = ["GridPass"]

log = logging.getLogger(__name__)

[docs] class GridPass: """Renders an infinite XZ ground-plane grid behind scene geometry. Uses a fullscreen quad with ray-plane intersection in the fragment shader to produce anti-aliased grid lines with proper depth for occlusion. """ 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._ready = False self.enabled = False # Editor enables explicitly; games should not show the grid
[docs] def setup(self) -> None: """Initialize grid pipeline and shaders.""" e = self._engine device = e.ctx.device # Compile shaders shader_dir = e.shader_dir vert_spv = compile_shader(shader_dir / "grid.vert") frag_spv = compile_shader(shader_dir / "grid.frag") self._vert_module = create_shader_module(device, vert_spv) self._frag_module = create_shader_module(device, frag_spv) # Create pipeline self._create_pipeline(device, e.render_pass, e.extent) self._ready = True log.debug("Grid pass initialized")
[docs] def rebuild_pipeline(self, render_pass: Any) -> None: """Recreate the grid pipeline against a different render pass (e.g. HDR). The grid draws inside the active scene pass; with post-processing on that is the HDR offscreen pass, whose format differs from the swapchain pass the pipeline was first compiled against (VUID-vkCmdDraw-renderPass-02684). """ 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) self._create_pipeline(device, render_pass, self._engine.extent)
def _create_pipeline(self, device: Any, render_pass: Any, extent: tuple[int, int]) -> None: """Create grid pipeline with alpha blend, depth test, and depth write. 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 (``compile_shader``), so the pre-created modules are passed directly rather than via SPIR-V paths in the spec. ``front_face`` is left at the spec default (``COUNTER_CLOCKWISE``); the hand-rolled pipeline never set it (raw-struct default 0 == CCW) and culling is ``NONE``, so the value is inert and the render is unchanged. """ spec = PipelineSpec( name="grid", topology=vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, vertex_stride=0, # fullscreen quad (6 verts) generated in the shader cull_mode=vk.VK_CULL_MODE_NONE, depth_test=True, depth_write=True, # grid writes depth for occlusion depth_compare=vk.VK_COMPARE_OP_LESS_OR_EQUAL, blend="alpha", dst_alpha_factor=vk.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, push_size=128, # mat4 view + mat4 proj push_stages=vk.VK_SHADER_STAGE_VERTEX_BIT | vk.VK_SHADER_STAGE_FRAGMENT_BIT, # Thin G-buffer: share the HDR pass's second attachment, mask writes. 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 render(self, cmd: Any, view_matrix: np.ndarray, proj_matrix: np.ndarray, extent: tuple[int, int]) -> None: """Render infinite grid. Call after skybox 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 vk.vkCmdBindPipeline(cmd, vk.VK_PIPELINE_BIND_POINT_GRAPHICS, self._pipeline) # 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 | vk.VK_SHADER_STAGE_FRAGMENT_BIT, 0, 128, cbuf, ) # Draw fullscreen quad (6 vertices, no vertex buffer) vk.vkCmdDraw(cmd, 6, 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) self._ready = False