simvx.graphics.renderer.decal_pack

Shared decal wire packing.

Dependency-free (numpy only, no Vulkan) so BOTH the desktop forward pass (forward.py -> the DecalBuffer SSBO at set0 binding 19) and, one step behind, the browser exporter (WebGPU under Pyodide where vulkan is unavailable) pack a :class:simvx.core.decal.Decal3D into the identical std430 record. One canonical layout, mirrored by the Decal struct in cube_textured.frag (desktop) and, later, forward3d.wgsl (web).

A decal projects its albedo (and an optional normal map) onto the geometry inside an oriented box, along the box’s local -Y axis. world_to_local is the inverse of the box’s world TRS (scaled by the half-extents), so a world position inside the box maps into the cube [-1, 1]^3; the uber loop reads the thin G-buffer world position, tests the box, and composites the decal over the material albedo. A scene with no Decal3D keeps decal_count at 0 and the loop never runs, so the frame is byte-identical (zero cost when unused).

Module Contents

Functions

pack_decal

Pack one decal into a :data:DECAL_DTYPE record.

compute_decal_tile_masks

Build the per-screen-tile decal bitmask for the standalone cull grid.

build_decal_buffer

Assemble the full DecalBuffer bytes (16-byte header + record array).

Data

API

simvx.graphics.renderer.decal_pack.__all__

[‘MAX_DECALS’, ‘DECAL_DTYPE’, ‘DECAL_STRIDE’, ‘DECAL_HEADER_SIZE’, ‘DECAL_BUFFER_SIZE’, ‘DECAL_TILE_…

simvx.graphics.renderer.decal_pack.MAX_DECALS

64

simvx.graphics.renderer.decal_pack.DECAL_DTYPE

‘dtype(…)’

simvx.graphics.renderer.decal_pack.DECAL_STRIDE

None

simvx.graphics.renderer.decal_pack.DECAL_HEADER_SIZE

16

simvx.graphics.renderer.decal_pack.DECAL_BUFFER_SIZE

None

simvx.graphics.renderer.decal_pack.DECAL_TILE_PX

16

simvx.graphics.renderer.decal_pack.pack_decal(model: numpy.ndarray, modulate: Any, albedo_mix: float, albedo_tex: int, normal_tex: int, normal_mix: float, upper_fade: float, lower_fade: float, dist_begin: float, dist_length: float, cull_mask: int = 4294967295) numpy.ndarray

Pack one decal into a :data:DECAL_DTYPE record.

model is the box world TRS (row-major, translate * rotate * scale where scale is the world-space half-extents); it is inverted and transposed to the column-major world_to_local the shader multiplies the world position by. albedo_tex / normal_tex are bindless texture indices (-1 = none). cull_mask is the 32-bit render-layer bitmask: a fragment only receives the decal when its instance render_layer intersects this mask. It is stored bit-exact in tex_params.w (read back with floatBitsToUint); the default 0xFFFFFFFF matches every layer, so an unmasked decal is byte-identical to the unmasked record.

simvx.graphics.renderer.decal_pack.compute_decal_tile_masks(models: list[numpy.ndarray], view: numpy.ndarray, proj: numpy.ndarray, width: int, height: int, tile_px: int = DECAL_TILE_PX) tuple[int, int, numpy.ndarray]

Build the per-screen-tile decal bitmask for the standalone cull grid.

Each of the (up to :data:MAX_DECALS) models is the box world TRS whose 8 corners are projected with proj @ view into the width x height gl_FragCoord space; the conservative screen AABB (padded one tile on every side, and widened to the whole grid for a projector that crosses the near plane) sets the decal’s bit in every tile it can touch. The mask is a 64-bit field per tile (two uint32 lanes), so the shader iterates only the decals whose bit is set for its tile. Because the coverage is always a superset of the real box footprint, the tiled loop composites the exact same decals a brute-force loop would: byte-identical output, just fewer per-fragment tests.

Returns (grid_x, grid_y, masks) where masks is a (grid_x*grid_y, 2) uint32 array in row-major tile order (tile = ty * grid_x + tx). Pure numpy so the web twin can mirror it verbatim in JS.

simvx.graphics.renderer.decal_pack.build_decal_buffer(records: list[numpy.ndarray], *, tile_masks: numpy.ndarray | None = None, grid_x: int = 0, grid_y: int = 0, tile_px: int = 0) numpy.ndarray

Assemble the full DecalBuffer bytes (16-byte header + record array).

The record region is always the fixed :data:MAX_DECALS slots (the shader declares Decal decals[MAX_DECALS] so the trailing tile-mask array has a fixed offset); only the first decal_count are populated. A zeroed header (no records) is byte-identical to the never-baked default buffer.

When tile_masks is supplied (desktop screen-tile cull) the (grid_x, grid_y, tile_px) header words are set and the uint32 mask lanes are appended after the record region as the shader’s tile_mask[] runtime array. Called without it (the web twin, one step behind) the header tiling words stay 0 and the buffer is exactly the untiled wire, so the brute-force shader path runs and nothing changes.