"""Shared ocean-surface wire packing (design D11, RM-E7).
Dependency-free (numpy + core math only, no Vulkan) so BOTH the desktop ocean
pass (``ocean_pass.py``) and a future browser twin pack an ``OceanSurface3D``
into the identical std430 record. Mirrors the ``OceanSurface`` struct in
``ocean.vert`` / ``ocean.frag`` (and, when it lands, ``ocean3d.wgsl``).
The FFT ocean reuses the :class:`~simvx.core.water.WaterMaterial` shading knobs
(design D16): the colour / depth-fade / Fresnel / foam fields are packed exactly
like the Gerstner water record; the extra ``ocean*`` vec4s carry the cascade
geometry (patch lengths, count) and the FFT-specific displacement / foam scales.
"""
from __future__ import annotations
from typing import Any
import numpy as np
__all__ = ["OCEAN_SURFACE_DTYPE", "OCEAN_SURFACE_STRIDE", "build_ocean_records", "pack_ocean_surface"]
# Per-surface std430 record mirrored by the OceanSurface struct in the ocean
# shaders. mat4 (64) + 7 vec4 (112) = 176 bytes.
OCEAN_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 + shore foam band
("shade", np.float32, (4,)), # refraction_strength, fresnel_power, roughness, opacity
("ocean0", np.float32, (4,)), # size_x, size_z, subdivisions, n_cascades
("ocean1", np.float32, (4,)), # patch_len0, patch_len1, patch_len2, choppiness
("ocean2", np.float32, (4,)), # displacement_scale, foam_intensity, normal_strength, _
]
)
OCEAN_SURFACE_STRIDE = OCEAN_SURFACE_DTYPE.itemsize # 176
[docs]
def pack_ocean_surface(
model: np.ndarray,
mat: Any,
subdivisions: int,
size: tuple[float, float],
patch_lengths: tuple[float, ...],
*,
choppiness: float,
displacement_scale: float,
foam_intensity: float,
) -> np.ndarray:
"""Pack one ocean surface (row-major ``model`` + a ``WaterMaterial``) into a record.
``model`` is transposed to column-major for the shader std430 mat4.
``patch_lengths`` is the cascade patch-length tuple (metres, longest first);
up to three are written and the count is stored in ``ocean0.w``.
"""
rec = np.zeros((), dtype=OCEAN_SURFACE_DTYPE)
rec["model"] = np.ascontiguousarray(model.T, dtype=np.float32).ravel()
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["shade"] = (
float(mat.refraction_strength),
float(mat.fresnel_power),
float(mat.roughness),
float(mat.opacity),
)
n_c = len(patch_lengths)
rec["ocean0"] = (float(size[0]), float(size[1]), float(subdivisions), float(n_c))
pl = list(patch_lengths[:3]) + [0.0] * (3 - min(n_c, 3))
rec["ocean1"] = (pl[0], pl[1], pl[2], float(choppiness))
rec["ocean2"] = (float(displacement_scale), float(foam_intensity), float(mat.normal_strength), 0.0)
return rec
[docs]
def build_ocean_records(
surfaces: list[Any],
patch_lengths: tuple[float, ...],
*,
choppiness: float,
displacement_scale: float = 1.0,
foam_intensity: float = 1.0,
) -> tuple[np.ndarray, int]:
"""Pack a list of ``OceanSurface3D`` 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_ocean_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=OCEAN_SURFACE_DTYPE)
for i, node in enumerate(surfaces):
model = mat4_from_trs(node.world_position, node.world_rotation, node.world_scale)
packed[i] = pack_ocean_surface(
model,
node.material,
int(node.subdivisions),
tuple(node.size),
patch_lengths,
choppiness=choppiness,
displacement_scale=displacement_scale,
foam_intensity=foam_intensity,
)
return packed, count