Tidewater¶
an FFT sea meeting a mirror-calm river under golden-hour rain.
▶ Run in browserTags: 3d ocean water river rain sky reflections
One coastal scene that combines most of the 3D renderer’s features, each doing real work in the frame:
Open sea.
OceanSurface3Dis a large FFT-ocean plane: rolling swell, choppy crests and foam, styled by aWaterMaterial. It starts at the river mouth and runs to the horizon, one step below the river.Mirror river. A
WaterSurface3Dwith a gentleWaterMaterialruns between two rocky banks. Itsreflectionis aPlanarReflection3Dplaced on the same plane, which re-renders the scene mirrored every frame, so the banks and the sky invert in the water.Procedural sky.
sky_mode="procedural"on theWorldEnvironmentbuilds the sky from the lowDirectionalLight3D, and that sky also lights the scene.Rain and weather.
Rain3Dis a GPU rain emitter that follows the camera and slants with the environment’swind_directionandwind_strength. The environment’swetness,rain_intensityandripple_strengthdarken and gloss every material created withwetness_affected=Trueand ripple its upward faces.Global illumination. An
IrradianceVolume3Dbakes spherical-harmonic probes over the shore so the rocks pick up bounced light, whilessgi_*adds screen-space bounce,ssr_enabledscreen-space reflections andbloom_*a glow on the sun’s glint.
The camera sways slowly over the river, driven by tree.now. Pressing SPACE,
clicking or tapping sets tree.paused, which stops the scene clock that the
camera sway, the water and the rain all run on, so the whole scene freezes.
The HUD that toggles the pause is created with update_mode=UpdateMode.ALWAYS, since a paused node
hears no input; the mode is inherited, so it goes on the HUD and never on the
scene root.
Source¶
1# /// simvx
2# tags = ["3d", "ocean", "water", "river", "rain", "sky", "reflections"]
3# screenshot_frame = 54
4# ///
5"""Tidewater: an FFT sea meeting a mirror-calm river under golden-hour rain.
6
7One coastal scene that combines most of the 3D renderer's features, each doing
8real work in the frame:
9
10* **Open sea.** `OceanSurface3D` is a large FFT-ocean plane: rolling swell,
11 choppy crests and foam, styled by a `WaterMaterial`. It starts at the river
12 mouth and runs to the horizon, one step below the river.
13* **Mirror river.** A `WaterSurface3D` with a gentle `WaterMaterial` runs
14 between two rocky banks. Its `reflection` is a `PlanarReflection3D` placed on
15 the same plane, which re-renders the scene mirrored every frame, so the banks
16 and the sky invert in the water.
17* **Procedural sky.** `sky_mode="procedural"` on the `WorldEnvironment` builds
18 the sky from the low `DirectionalLight3D`, and that sky also lights the scene.
19* **Rain and weather.** `Rain3D` is a GPU rain emitter that follows the camera
20 and slants with the environment's `wind_direction` and `wind_strength`. The
21 environment's `wetness`, `rain_intensity` and `ripple_strength` darken and
22 gloss every material created with `wetness_affected=True` and ripple its
23 upward faces.
24* **Global illumination.** An `IrradianceVolume3D` bakes spherical-harmonic
25 probes over the shore so the rocks pick up bounced light, while `ssgi_*`
26 adds screen-space bounce, `ssr_enabled` screen-space reflections and
27 `bloom_*` a glow on the sun's glint.
28
29The camera sways slowly over the river, driven by `tree.now`. Pressing SPACE,
30clicking or tapping sets `tree.paused`, which stops the scene clock that the
31camera sway, the water and the rain all run on, so the whole scene freezes.
32The HUD that toggles the pause is created with `update_mode=UpdateMode.ALWAYS`, since a paused node
33hears no input; the mode is inherited, so it goes on the HUD and never on the
34scene root.
35"""
36
37import math
38
39from simvx.core import (
40 Anchor,
41 Camera3D,
42 CanvasLayer,
43 DirectionalLight3D,
44 IrradianceVolume3D,
45 Key,
46 Label,
47 Material,
48 Mesh,
49 MeshInstance3D,
50 Node,
51 OceanSurface3D,
52 PlanarReflection3D,
53 Pointer,
54 Rain3D,
55 UpdateMode,
56 WaterMaterial,
57 WaterSurface3D,
58 WorldEnvironment,
59 on_input,
60)
61from simvx.graphics import App
62
63RIVER_LEVEL = 0.5
64SEA_LEVEL = -0.5 # a step below the river, so the river spills into the sea
65RIVER_HALF_WIDTH = 9.0
66SHELF_EDGE_Z = -20.0 # where the banks end and the open sea begins
67
68ROCK = (0.44, 0.41, 0.37, 1.0)
69ROCK_MOSS = (0.38, 0.42, 0.32, 1.0)
70ROCK_PALE = (0.54, 0.51, 0.47, 1.0)
71
72# (x, z, radius, colour, roughness) of the boulders tumbling down to the waterline.
73BOULDERS = [
74 (-RIVER_HALF_WIDTH - 0.5, 14.0, 2.4, ROCK_MOSS, 0.7),
75 (-RIVER_HALF_WIDTH + 0.4, 6.0, 1.7, ROCK_PALE, 0.55),
76 (-RIVER_HALF_WIDTH - 1.5, -2.0, 3.0, ROCK, 0.75),
77 (-RIVER_HALF_WIDTH + 0.2, -11.0, 1.9, ROCK_MOSS, 0.6),
78 (-RIVER_HALF_WIDTH - 0.8, -17.5, 2.3, ROCK_PALE, 0.5),
79 (RIVER_HALF_WIDTH + 0.6, 12.0, 2.1, ROCK_PALE, 0.5),
80 (RIVER_HALF_WIDTH - 0.3, 4.0, 1.6, ROCK, 0.7),
81 (RIVER_HALF_WIDTH + 1.4, -4.0, 2.8, ROCK_MOSS, 0.75),
82 (RIVER_HALF_WIDTH - 0.2, -12.0, 2.0, ROCK_PALE, 0.55),
83 (RIVER_HALF_WIDTH + 0.9, -17.5, 2.5, ROCK, 0.6),
84]
85
86
87def rock(position, radius, colour=ROCK, roughness=0.8, wet=True):
88 return MeshInstance3D(
89 mesh=Mesh.sphere(radius, rings=16, segments=24),
90 material=Material(colour=colour, roughness=roughness, wetness_affected=wet),
91 position=position,
92 )
93
94
95class TidewaterScene(Node):
96 def on_ready(self):
97 self.add_child(
98 WorldEnvironment(
99 wind_direction=(0.65, 0.42),
100 wind_strength=0.7,
101 wetness=0.6,
102 rain_intensity=0.8,
103 ripple_strength=0.25,
104 sky_mode="procedural",
105 sky_turbidity=3.6,
106 sky_ground_albedo=(0.18, 0.15, 0.12, 1.0),
107 ssr_enabled=True,
108 ssgi_enabled=True,
109 ssgi_intensity=0.55,
110 ssgi_max_distance=10.0,
111 bloom_enabled=True,
112 bloom_threshold=1.15,
113 bloom_intensity=0.45,
114 )
115 )
116 # A low sun just off-centre over the sea, for a long glint down the river.
117 self.add_child(DirectionalLight3D(intensity=3.4, colour=(1.0, 0.78, 0.52), direction=(-0.14, -0.11, 0.98)))
118 self.camera = self.add_child(Camera3D(position=(0.0, 6.5, 24.0), look_at=(0.0, 0.4, -30.0), fov=55, far=900.0))
119
120 self.build_banks()
121 self.build_water()
122 # The probes' capture is dimmer than the bright low-sun sky, so intensity lifts the rocks' midtones back;
123 # a generous update_budget finishes the one-off bake within the first few frames.
124 self.add_child(
125 IrradianceVolume3D(
126 extents=(30.0, 7.0, 26.0), spacing=7.0, position=(0.0, 2.5, 0.0), intensity=3.0, update_budget=32
127 )
128 )
129 # Small, dense drops read as rain; large camera-facing sprites read as bokeh.
130 self.add_child(
131 Rain3D(
132 radius=24.0,
133 height=15.0,
134 fall_speed=28.0,
135 amount=7000,
136 wind_response=1.2,
137 start_scale=0.028,
138 end_scale=0.028,
139 )
140 )
141 self.add_child(Hud(update_mode=UpdateMode.ALWAYS))
142
143 def build_banks(self):
144 hw = RIVER_HALF_WIDTH
145 # A dark bed well below the river, so the water's depth fade has depth to fade over.
146 bed = Material(colour=(0.16, 0.17, 0.15, 1.0), roughness=0.9)
147 self.add_child(
148 MeshInstance3D(mesh=Mesh.box((2 * hw + 2, 3, 54)), material=bed, position=(0, SEA_LEVEL - 1.5, 4))
149 )
150
151 bank = Material(colour=ROCK, roughness=0.8, wetness_affected=True)
152 for side in (-1, 1):
153 self.add_child(
154 MeshInstance3D(mesh=Mesh.box((24, 5.4, 52)), material=bank, position=(side * (hw + 11), 0.2, 6))
155 )
156 # Large boulders along each ridge break up the flat slab top.
157 for i, (z, radius, colour) in enumerate(
158 [(-14, 3.4, ROCK_PALE), (-4, 4.2, ROCK), (5, 3.0, ROCK_MOSS), (14, 3.8, ROCK_PALE)]
159 ):
160 self.add_child(rock((side * (hw + 8 + 2.2 * (i % 2)), 2.9 + 0.25 * radius, z), radius, colour, 0.78))
161
162 for x, z, radius, colour, roughness in BOULDERS:
163 self.add_child(rock((x, RIVER_LEVEL + radius * 0.45, z), radius, colour, roughness))
164 # A low lip of rocks across the river mouth hides the seam where the river drops to the sea.
165 for x in range(-int(hw), int(hw) + 2, 3):
166 self.add_child(rock((x + 0.5, RIVER_LEVEL - 0.5, SHELF_EDGE_Z + 0.8), 1.7, roughness=0.65))
167 # Two stacks out at sea break the horizon and give the foam something to curl around.
168 self.add_child(rock((-26, SEA_LEVEL - 2, -55), 6.5, (0.30, 0.28, 0.26, 1.0), 0.85, wet=False))
169 self.add_child(rock((30, SEA_LEVEL - 1, -78), 4.5, (0.33, 0.31, 0.28, 1.0), 0.8, wet=False))
170
171 def build_water(self):
172 mirror = self.add_child(PlanarReflection3D(position=(0.0, RIVER_LEVEL, 4.0)))
173 # A tiny slow swell keeps the mirror crisp.
174 calm = WaterMaterial(
175 shallow_colour=(0.12, 0.30, 0.34),
176 deep_colour=(0.02, 0.07, 0.11),
177 depth_fade_distance=3.2,
178 wave_amplitude=0.03,
179 wave_length=4.5,
180 wave_steepness=0.22,
181 foam_amount=0.12,
182 refraction_strength=0.16,
183 fresnel_power=5.0,
184 normal_strength=0.25,
185 opacity=0.9,
186 )
187 self.add_child(
188 WaterSurface3D(
189 position=(0.0, RIVER_LEVEL, 4.0),
190 size=(2 * RIVER_HALF_WIDTH, 54.0),
191 subdivisions=112,
192 material=calm,
193 reflection=mirror,
194 )
195 )
196
197 # The sea's near edge sits exactly on the shelf edge, so no sea shows through the banks.
198 sea_size = 640.0
199 swell = WaterMaterial(
200 shallow_colour=(0.05, 0.24, 0.30),
201 deep_colour=(0.01, 0.05, 0.10),
202 depth_fade_distance=10.0,
203 wave_amplitude=1.2,
204 wave_steepness=0.72,
205 foam_colour=(0.92, 0.96, 1.0),
206 foam_amount=0.7,
207 refraction_strength=0.35,
208 )
209 self.add_child(
210 OceanSurface3D(
211 position=(0.0, SEA_LEVEL, SHELF_EDGE_Z - sea_size / 2),
212 size=(sea_size, sea_size),
213 subdivisions=320,
214 material=swell,
215 )
216 )
217
218 def on_update(self, dt):
219 # The scene clock stops while the tree is paused, and the camera with it.
220 t = self.tree.now
221 x = 3.0 * math.sin(t * 0.13)
222 self.camera.position = (x, 6.5 + 0.5 * math.sin(t * 0.11), 24.0 - 2.5 * math.sin(t * 0.09))
223 self.camera.look_at((x * 0.25, 0.4, -30.0))
224
225
226class Hud(CanvasLayer):
227 """The title, the pause toggle and its hint, pinned to the screen and alive while paused."""
228
229 input_context = {"pause": [Key.SPACE, Pointer.PRIMARY]}
230
231 def on_ready(self):
232 self.add_child(Label("TIDEWATER", anchor=Anchor.TOP_LEFT, inset=(24, 16), font_size=32))
233 features = "FFT ocean + planar-mirror river + procedural sky + rain + wetness + GI"
234 self.add_child(
235 Label(features, anchor=Anchor.TOP_WIDE, inset=(24, 58), size=(0, 24), font_size=17, autoshrink=True)
236 )
237 self.paused_label = self.add_child(
238 Label("PAUSED", anchor=Anchor.TOP_LEFT, inset=(24, 90), font_size=21, visible=False)
239 )
240 self.add_child(Label("SPACE / click / tap: pause", anchor=Anchor.BOTTOM_LEFT, inset=24, font_size=17))
241 for label in self.find_all(Label):
242 label.text_colour = (1.0, 1.0, 1.0, 1.0) # full white stays legible over the bright sky
243
244 @on_input("pause")
245 def toggle_pause(self, event):
246 self.tree.paused = not self.tree.paused
247 self.paused_label.visible = self.tree.paused
248
249
250if __name__ == "__main__":
251 App(width=1280, height=720, title="SimVX - Tidewater").run(TidewaterScene())