Deep Sea Aquarium¶
Deep Sea Aquarium — Bioluminescent underwater world.
A visually immersive 3D demo showcasing PBR rendering, bloom, particles, audio, and interaction. Dark ocean depths with self-illuminating creatures creating stunning visuals through emissive materials and bloom.
Controls: Mouse drag — Orbit camera Scroll — Zoom in/out Space — Toggle auto-orbit Click — Interact with creature (zoom, highlight, chime) Escape — Quit
Usage: uv run python packages/graphics/examples/deep_sea_aquarium/main.py
Source Code¶
1"""Deep Sea Aquarium — Bioluminescent underwater world.
2
3A visually immersive 3D demo showcasing PBR rendering, bloom, particles,
4audio, and interaction. Dark ocean depths with self-illuminating creatures
5creating stunning visuals through emissive materials and bloom.
6
7Controls:
8 Mouse drag — Orbit camera
9 Scroll — Zoom in/out
10 Space — Toggle auto-orbit
11 Click — Interact with creature (zoom, highlight, chime)
12 Escape — Quit
13
14Usage:
15 uv run python packages/graphics/examples/deep_sea_aquarium/main.py
16"""
17
18
19import math
20import sys
21from pathlib import Path
22
23import numpy as np
24
25from simvx.core import (
26 Camera3D,
27 Input,
28 Key,
29 MouseButton,
30 Node,
31 ParticleEmitter,
32 Text2D,
33 Vec3,
34 WorldEnvironment,
35)
36
37sys.path.insert(0, str(Path(__file__).resolve().parent))
38
39from creatures import Anemone, CoralFormation, FishSchool, Jellyfish
40from environment import Kelp, SeaFloor
41from music import AmbientMusicController
42
43
44class AquariumScene(Node):
45 """Root scene for the deep sea aquarium."""
46
47 def __init__(self, **kw):
48 super().__init__(name="Aquarium", **kw)
49 self._cam: Camera3D | None = None
50 self._time = 0.0
51 self._hud: Text2D | None = None
52 self._hud_timer = 3.0
53 self._creature_label: Text2D | None = None
54 self._creature_label_timer = 0.0
55 self._music: AmbientMusicController | None = None
56
57 # Camera orbit state
58 self._yaw = 30.0
59 self._pitch = 12.0
60 self._distance = 22.0
61 self._target = Vec3(0, 0, 0)
62 self._auto_orbit = True
63 self._dragging = False
64 self._last_mouse = (0.0, 0.0)
65
66 # Zoom-to-creature state
67 self._zoom_active = False
68 self._zoom_target_pos = Vec3(0, 0, 0)
69 self._zoom_target_dist = 5.0
70 self._zoom_timer = 0.0
71 self._zoom_return_yaw = 0.0
72 self._zoom_return_pitch = 0.0
73 self._zoom_return_dist = 0.0
74 self._zoom_return_target = Vec3(0, 0, 0)
75
76 # Startup camera animation
77 self._intro_timer = 3.0
78
79 def ready(self):
80 # Camera
81 self._cam = Camera3D(name="Camera", fov=55.0, near=0.1, far=200.0)
82 self.add_child(self._cam)
83
84 # Note: forward.frag uses hardcoded lighting, point/directional lights are cosmetic only
85 # Floor lighting achieved via emissive gradient in concentric ring materials
86
87 # Central floor spotlight — pool of light on the ground like a real tank feature light
88 from simvx.core import PointLight3D
89 # Centre floor light (cosmetic — shader doesn't use it, but kept for future PBR)
90 centre_light = PointLight3D(name="CentreFloorLight", position=Vec3(0, -3.5, 0))
91 centre_light.colour = (0.1, 0.12, 0.22)
92 centre_light.intensity = 5.0
93 centre_light.range = 15.0
94 self.add_child(centre_light)
95
96 # Sea floor — very dark, just enough to ground the scene
97 self.add_child(SeaFloor())
98
99 # Kelp strands — dark silhouettes rising from the floor
100 rng = np.random.default_rng(99)
101 kelp_positions = [
102 (-7, -5.0, -6), (-4, -5.0, 3), (2, -5.0, -7), (6, -5.0, 2),
103 (-2, -5.0, -2), (4, -5.0, -5), (-5, -5.0, 6), (1, -5.0, 5),
104 ]
105 for i, (x, y, z) in enumerate(kelp_positions):
106 phase = rng.uniform(0, math.tau)
107 kelp = Kelp(phase=phase, name=f"Kelp_{i}", position=Vec3(x, y, z))
108 self.add_child(kelp)
109
110 # Coral formations — 4 spread around the floor, scaled up to be visible
111 coral_configs = [(-5, -4.5, -4), (5, -4.5, -3), (-4, -4.5, 5), (5, -4.5, 5)]
112 for i, (x, y, z) in enumerate(coral_configs):
113 coral = CoralFormation(colour_index=i, name=f"Coral_{i}", position=Vec3(x, y, z))
114 coral.scale = Vec3(3.0, 3.0, 3.0)
115 self.add_child(coral)
116
117 # Anemones — on the floor, scaled up, vivid bioluminescent colours
118 anem_colours = [
119 (0.12, 0.65, 0.75, 4.0), (0.6, 0.32, 0.1, 3.5),
120 (0.15, 0.45, 0.7, 4.0), (0.5, 0.15, 0.55, 3.5),
121 ]
122 anem_positions = [(-3, -4.0, -4), (5, -4.0, -2), (-2, -4.0, 5), (6, -4.0, 5)]
123 for i, ((x, y, z), colour) in enumerate(zip(anem_positions, anem_colours, strict=True)):
124 anem = Anemone(colour=colour, name=f"Anemone_{i}", position=Vec3(x, y, z))
125 anem.scale = Vec3(2.0, 2.0, 2.0)
126 anem.creature_clicked.connect(self._on_creature_clicked)
127 self.add_child(anem)
128
129 # Jellyfish — 6 total, spread across all quadrants, varied scales for depth
130 jelly_configs = [
131 (0, Vec3(-4, 2.5, -3), 1.0), # Moon Jelly — front-left, standard
132 (1, Vec3(5, 5.5, -4), 1.15), # Sea Nettle — rear-right, high, slightly larger
133 (2, Vec3(-3, 3.5, 5), 0.9), # Crystal Jelly — front-right, medium
134 (3, Vec3(6, 1.5, 4), 1.1), # Atolla — right, low
135 (0, Vec3(2, 4.5, -7), 1.25), # Second Moon — rear-centre, high, largest
136 (2, Vec3(-7, 1.5, -1), 0.75), # Second Crystal — left, low, smallest
137 ]
138 for i, (species, pos, extra_scale) in enumerate(jelly_configs):
139 jelly = Jellyfish(species_index=species, name=f"Jellyfish_{i}", position=pos)
140 jelly.scale = Vec3(extra_scale, extra_scale, extra_scale)
141 jelly.creature_clicked.connect(self._on_creature_clicked)
142 self.add_child(jelly)
143
144 # Fish schools — spread wider
145 school1 = FishSchool(count=8, name="School_Blue")
146 school1.creature_clicked.connect(self._on_creature_clicked)
147 self.add_child(school1)
148
149 from creatures import FISH_WARM_COLOURS
150 school2 = FishSchool(count=6, emissive_colours=FISH_WARM_COLOURS, name="School_Warm")
151 school2.position = Vec3(5, 0.5, 4)
152 school2.creature_clicked.connect(self._on_creature_clicked)
153 self.add_child(school2)
154
155 # Floating particulate matter — visible motes drifting in the tank
156 specks = ParticleEmitter(name="WaterSpecks", amount=150, seed=1)
157 specks.emission_shape = "box"
158 specks.emission_box = (18.0, 12.0, 18.0)
159 specks.start_colour = (0.3, 0.4, 0.6, 0.5)
160 specks.end_colour = (0.1, 0.15, 0.25, 0.0)
161 specks.start_scale = 0.05
162 specks.end_scale = 0.02
163 specks.lifetime = 14.0
164 specks.emission_rate = 10.0
165 specks.gravity = (0.0, 0.012, 0.0)
166 specks.velocity_spread = 0.15
167 specks.initial_velocity = (0.0, 0.025, 0.0)
168 self.add_child(specks)
169
170 # Music
171 self._music = AmbientMusicController()
172 self.add_child(self._music)
173
174 # HUD
175 self._hud = Text2D(
176 name="HUD",
177 text="Space: orbit | Click: interact | Scroll: zoom",
178 x=20,
179 y=20,
180 font_scale=1.5,
181 font_colour=(0.5, 0.7, 0.9, 0.8),
182 )
183 self.add_child(self._hud)
184
185 self._creature_label = Text2D(
186 name="CreatureLabel",
187 text="",
188 x=20,
189 y=50,
190 font_scale=1.8,
191 font_colour=(0.8, 0.9, 1.0, 0.0),
192 )
193 self.add_child(self._creature_label)
194
195 # Post-processing — museum aquarium look via WorldEnvironment
196 env = self.add_child(WorldEnvironment(name="PostFX"))
197 env.bloom_enabled = True
198 env.bloom_intensity = 0.35
199 env.bloom_threshold = 0.4
200 env.tonemap_exposure = 0.5
201 env.vignette_enabled = True
202 env.vignette_intensity = 0.4
203 env.vignette_smoothness = 0.4
204 env.dof_enabled = True
205 env.dof_focus_distance = 0.45
206 env.dof_focus_range = 0.35
207 env.film_grain_enabled = False
208 env.chromatic_aberration_enabled = False
209
210 # Start looking down at the scene — floor visible, background minimal
211 self._distance = 16.0
212 self._target = Vec3(0, -0.5, 0)
213 self._pitch = 30.0
214 self._yaw = 15.0
215
216 def process(self, dt: float):
217 self._time += dt
218
219 # Handle input
220 self._handle_input(dt)
221
222 # Intro camera pull-back
223 if self._intro_timer > 0:
224 self._intro_timer -= dt
225 if self._intro_timer <= 0:
226 self._zoom_active = False
227 self._distance = 20.0
228 self._target = Vec3(0, -0.5, 0)
229 self._pitch = 28.0
230
231 # Camera zoom-to-creature
232 if self._zoom_active:
233 self._zoom_timer -= dt
234 lerp_speed = min(1.0, dt * 2.0)
235 self._target = self._target + (self._zoom_target_pos - self._target) * lerp_speed
236 self._distance += (self._zoom_target_dist - self._distance) * lerp_speed
237 if self._zoom_timer <= 0:
238 self._zoom_active = False
239 # Return to previous orbit
240 self._target = self._zoom_return_target
241 self._distance = self._zoom_return_dist
242
243 # Auto orbit
244 if self._auto_orbit and not self._dragging:
245 self._yaw += 3.5 * dt # Slow cinematic orbit
246
247 # Update camera position
248 self._update_camera()
249
250 # Fade HUD after 5s
251 if self._hud_timer > 0:
252 self._hud_timer -= dt
253 if self._hud_timer <= 0 and self._hud:
254 self._hud.font_colour = (0.5, 0.7, 0.9, 0.0)
255
256 # Fade creature label
257 if self._creature_label_timer > 0:
258 self._creature_label_timer -= dt
259 if self._creature_label_timer <= 0 and self._creature_label:
260 self._creature_label.text = ""
261 self._creature_label.font_colour = (0.8, 0.9, 1.0, 0.0)
262
263 def _handle_input(self, dt: float):
264 # Quit
265 if Input.is_key_just_pressed(Key.ESCAPE):
266 self.app.quit()
267 return
268
269 # Toggle auto-orbit
270 if Input.is_key_just_pressed(Key.SPACE):
271 self._auto_orbit = not self._auto_orbit
272
273 # Mouse drag for orbit
274 if Input.is_mouse_button_just_pressed(MouseButton.LEFT):
275 self._dragging = True
276 self._last_mouse = tuple(Input.mouse_position)
277 if Input.is_mouse_button_just_released(MouseButton.LEFT):
278 self._dragging = False
279
280 if self._dragging:
281 mx, my = tuple(Input.mouse_position)
282 dx = mx - self._last_mouse[0]
283 dy = my - self._last_mouse[1]
284 self._yaw -= dx * 0.3
285 self._pitch = max(-30, min(60, self._pitch + dy * 0.3))
286 self._last_mouse = (mx, my)
287
288 # Scroll zoom
289 scroll = Input.scroll_delta
290 if scroll[1] != 0:
291 self._distance = max(3.0, min(40.0, self._distance - scroll[1] * 1.5))
292
293 def _update_camera(self):
294 if not self._cam:
295 return
296 yaw_rad = math.radians(self._yaw)
297 pitch_rad = math.radians(self._pitch)
298 cp = math.cos(pitch_rad)
299 tx, ty, tz = float(self._target.x), float(self._target.y), float(self._target.z)
300 x = tx + self._distance * cp * math.sin(yaw_rad)
301 y = ty + self._distance * math.sin(pitch_rad)
302 z = tz + self._distance * cp * math.cos(yaw_rad)
303 self._cam.position = Vec3(x, y, z)
304 self._cam.look_at(self._target)
305
306 def _on_creature_clicked(self, creature_name: str):
307 """Handle creature interaction: show label, zoom, play chime."""
308 # Show creature label
309 if self._creature_label:
310 self._creature_label.text = creature_name
311 self._creature_label.font_colour = (0.8, 0.9, 1.0, 0.9)
312 self._creature_label_timer = 3.0
313
314 # Play chime
315 if self._music:
316 self._music.play_chime()
317
318 def _zoom_to(self, position: Vec3):
319 """Smooth camera zoom to a world position."""
320 if self._zoom_active:
321 return
322 self._zoom_active = True
323 self._zoom_return_yaw = self._yaw
324 self._zoom_return_pitch = self._pitch
325 self._zoom_return_dist = self._distance
326 self._zoom_return_target = Vec3(self._target)
327 self._zoom_target_pos = position
328 self._zoom_target_dist = 5.0
329 self._zoom_timer = 4.0
330
331 def handle_input(self, event):
332 """Handle unhandled clicks for water ripple effect."""
333 if getattr(event, "type", None) == "mouse_button" and getattr(event, "pressed", False):
334 if getattr(event, "button", None) == MouseButton.LEFT:
335 # Empty water click — play low chime
336 if self._music:
337 self._music.play_chime(0)
338
339
340# ============================================================================
341# Entry point
342# ============================================================================
343
344def run(visible: bool = True, **app_kw):
345 """Launch the aquarium. Returns (app, scene) for programmatic use."""
346 from simvx.graphics import App
347
348 app = App(title="Deep Sea Aquarium", width=1920, height=1080, visible=visible, **app_kw)
349 scene = AquariumScene()
350 if visible:
351 app.run(scene)
352 return app, scene
353
354
355if __name__ == "__main__":
356 run()