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)