Source code for simvx.graphics.renderer.water_pack

"""Shared water-surface wire packing (design D16, RM-E1/E1w).

Dependency-free (numpy + core math only, no Vulkan) so BOTH the desktop water
pass (``water_pass.py``) and the browser exporter (``simvx.web`` runtime, which
runs under Pyodide where ``vulkan`` is unavailable) pack a ``WaterSurface3D`` into
the identical std430 record. One canonical layout, mirrored by the WaterSurface
struct in ``water.vert`` / ``water.frag`` (desktop) and ``water3d.wgsl`` (web).
"""

from __future__ import annotations

from typing import Any

import numpy as np

__all__ = ["WATER_SURFACE_DTYPE", "WATER_SURFACE_STRIDE", "build_water_records", "pack_water_surface"]

# Per-surface std430 record mirrored by the WaterSurface struct in the water
# shaders (desktop GLSL + web WGSL). mat4 (64) + 7 vec4 (112) = 176 bytes.
WATER_SURFACE_DTYPE = np.dtype(
    [
        ("model", np.float32, (16,)),
        ("shallow_colour", np.float32, (4,)),
        ("deep_colour", np.float32, (4,)),  # rgb + depth_fade_distance
        ("foam_colour", np.float32, (4,)),  # rgb + foam_amount
        ("wave0", np.float32, (4,)),  # amplitude, steepness, wavelength, base_dir
        ("wave1", np.float32, (4,)),  # spread, speed, normal_scroll, normal_strength
        ("shade", np.float32, (4,)),  # refraction_strength, fresnel_power, roughness, opacity
        ("extent", np.float32, (4,)),  # size_x, size_z, subdivisions, planar_reflection_tex (-1 = none)
    ]
)
WATER_SURFACE_STRIDE = WATER_SURFACE_DTYPE.itemsize  # 176


[docs] def pack_water_surface( model: np.ndarray, mat: Any, subdivisions: int, size: tuple[float, float], reflection_tex: int = -1 ) -> np.ndarray: """Pack one surface (row-major ``model`` + a ``WaterMaterial``) into a record. ``model`` is transposed to column-major for the shader std430 mat4. ``reflection_tex`` is the bindless index of a :class:`PlanarReflection3D` capture feeding a true planar mirror (RM-E2), or ``-1`` for none (the shader then keeps the cubemap/sky reflection, byte-identical to RM-E1 water). """ rec = np.zeros((), dtype=WATER_SURFACE_DTYPE) rec["model"] = np.ascontiguousarray(model.T, dtype=np.float32).ravel() # column-major for the shader rec["shallow_colour"] = (*mat.shallow_colour[:3], 0.0) rec["deep_colour"] = (*mat.deep_colour[:3], float(mat.depth_fade_distance)) rec["foam_colour"] = (*mat.foam_colour[:3], float(mat.foam_amount)) rec["wave0"] = ( float(mat.wave_amplitude), float(mat.wave_steepness), float(mat.wave_length), float(mat.wave_direction), ) rec["wave1"] = ( float(mat.direction_spread), float(mat.wave_speed), float(mat.normal_scroll_speed), float(mat.normal_strength), ) rec["shade"] = ( float(mat.refraction_strength), float(mat.fresnel_power), float(mat.roughness), float(mat.opacity), ) rec["extent"] = (float(size[0]), float(size[1]), float(subdivisions), float(reflection_tex)) return rec
[docs] def build_water_records(surfaces: list[Any]) -> tuple[np.ndarray, int]: """Pack a list of ``WaterSurface3D`` nodes into a contiguous record array. Used by the web exporter (the desktop pass packs per-frame from its own submission tuples via :func:`pack_water_surface`). Reads each node's world transform live, so a moving surface repacks correctly every frame. Returns ``(records, count)``; ``records`` is empty when ``surfaces`` is empty. """ from simvx.core.math.matrices import mat4_from_trs count = len(surfaces) packed = np.zeros(count, dtype=WATER_SURFACE_DTYPE) for i, node in enumerate(surfaces): model = mat4_from_trs(node.world_position, node.world_rotation, node.world_scale) reflection = getattr(node, "reflection", None) reflection_tex = int(getattr(reflection, "texture", -1)) if reflection is not None else -1 packed[i] = pack_water_surface( model, node.material, int(node.subdivisions), tuple(node.size), reflection_tex ) return packed, count