environment.pyΒΆ

Part of Deep Sea Aquarium.

  1"""Environment nodes: SeaFloor and Kelp."""
  2
  3import math
  4
  5import numpy as np
  6from meshgen import compute_normals, make_kelp_ribbon
  7
  8from simvx.core import Material, Mesh, MeshInstance3D, Node3D
  9from simvx.core.math.types import Quat
 10
 11# ============================================================================
 12# SeaFloor: Concentric rings with emissive gradient (bright centre, black edge)
 13# ============================================================================
 14
 15
 16def _make_floor_ring(inner_r: float, outer_r: float, segments: int = 48, y_func=None) -> Mesh:
 17    """Annular ring mesh with terrain displacement."""
 18    verts, uvs, idxs = [], [], []
 19    rings = 8  # radial subdivisions per ring
 20    for ri in range(rings + 1):
 21        t = ri / rings
 22        r = inner_r + t * (outer_r - inner_r)
 23        for si in range(segments + 1):
 24            angle = (si / segments) * math.tau
 25            x = r * math.cos(angle)
 26            z = r * math.sin(angle)
 27            y = y_func(x, z) if y_func else -5.0
 28            verts.append([x, y, z])
 29            uvs.append([si / segments, t])
 30    for ri in range(rings):
 31        for si in range(segments):
 32            i = ri * (segments + 1) + si
 33            j = i + segments + 1
 34            idxs.extend([i, i + 1, j, i + 1, j + 1, j])
 35    positions = np.array(verts, dtype=np.float32)
 36    idx = np.array(idxs, dtype=np.uint32)
 37    texcoords = np.array(uvs, dtype=np.float32)
 38    normals = compute_normals(positions, idx)
 39    return Mesh(positions, indices=idx, normals=normals, texcoords=texcoords)
 40
 41
 42def _terrain_y(x: float, z: float) -> float:
 43    """Rocky ocean floor height: six sin/cos octaves, coarse to fine."""
 44    return (
 45        1.5 * math.sin(x * 0.3) * math.cos(z * 0.25)
 46        + 0.7 * math.sin(x * 0.7 + z * 0.5)
 47        + 0.35 * math.cos(x * 0.15 - z * 0.8)
 48        + 0.5 * math.sin(x * 2.0) * math.cos(z * 1.8)
 49        + 0.3 * math.cos(x * 3.0 + z * 2.5)
 50        + 0.15 * math.sin(x * 5.0) * math.sin(z * 4.5)
 51        - 5.0
 52    )
 53
 54
 55class SeaFloor(Node3D):
 56    """Ocean floor: concentric rings that fade from lit centre to black edges."""
 57
 58    def __init__(self, **kw):
 59        super().__init__(name="SeaFloor", **kw)
 60
 61    def on_ready(self):
 62        # Concentric rings: bright emissive centre fading to zero at edges
 63        ring_configs = [
 64            (0.0, 6.0, 0.06),  # Inner: brightest
 65            (6.0, 12.0, 0.04),
 66            (12.0, 20.0, 0.025),
 67            (20.0, 30.0, 0.012),
 68            (30.0, 45.0, 0.005),  # Dim
 69            (45.0, 80.0, 0.0),  # Black: no emissive
 70        ]
 71        base_colour = (0.06, 0.06, 0.09)
 72        for i, (inner, outer, emissive_strength) in enumerate(ring_configs):
 73            mat = Material(
 74                colour=(*base_colour, 1.0),
 75                roughness=0.9,
 76                metallic=0.0,
 77                double_sided=True,
 78            )
 79            if emissive_strength > 0:
 80                mat.emissive_colour = (0.04, 0.05, 0.1, emissive_strength * 10.0)
 81            mesh = _make_floor_ring(inner, outer, segments=48, y_func=_terrain_y)
 82            mi = MeshInstance3D(name=f"FloorRing_{i}", mesh=mesh, material=mat)
 83            self.add_child(mi)
 84
 85
 86# ============================================================================
 87# Kelp
 88# ============================================================================
 89
 90# Dark olive kelp: visible as organic silhouettes with faint bioluminescent edge
 91_KELP_MAT = Material(
 92    colour=(0.03, 0.05, 0.025, 0.5),
 93    blend="alpha",
 94    double_sided=True,
 95    emissive_colour=(0.02, 0.05, 0.025, 0.8),
 96    roughness=0.85,
 97)
 98
 99
100class Kelp(Node3D):
101    """Swaying kelp ribbon."""
102
103    def __init__(self, phase: float = 0.0, **kw):
104        super().__init__(**kw)
105        self._phase = phase
106        self._time = 0.0
107        self._ribbon: MeshInstance3D | None = None
108
109    def on_ready(self):
110        self._ribbon = MeshInstance3D(
111            name="KelpRibbon",
112            mesh=make_kelp_ribbon(height=6.0 + self._phase * 1.5, width=0.18, subdivisions=10),
113            material=_KELP_MAT,
114        )
115        self.add_child(self._ribbon)
116
117    def on_update(self, dt: float):
118        self._time += dt
119        sway = math.sin(self._time * 0.4 + self._phase) * 0.08
120        self.rotation = Quat.from_euler(0, 0, sway)