Planar water¶
a Gerstner sea reflecting the world above it with a true mirror.
▶ Run in browserTags: 3d water planar-reflection mirror refraction
Standard WaterSurface3D refraction + depth-fade + foam, but the reflection is a
real planar mirror instead of the environment cubemap. A PlanarReflection3D sits
on the water plane and re-renders the scene above it every frame; assign it to
WaterSurface3D.reflection and the surface samples that live capture (rippled
by the waves, Fresnel-blended against the refraction below) so the coloured
pillars and the spinning cube appear inverted in the water and track the world in
the same frame. Contrast with water.py (cubemap/sky reflection): a planar mirror
is exact for the flat surface and shows the actual scene geometry, not an
approximation.
Usage: uv run python examples/features/3d/planar_water.py
Source¶
1"""Planar water: a Gerstner sea reflecting the world above it with a true mirror.
2
3Standard WaterSurface3D refraction + depth-fade + foam, but the reflection is a
4real planar mirror instead of the environment cubemap. A PlanarReflection3D sits
5on the water plane and re-renders the scene above it every frame; assign it to
6``WaterSurface3D.reflection`` and the surface samples that live capture (rippled
7by the waves, Fresnel-blended against the refraction below) so the coloured
8pillars and the spinning cube appear inverted in the water and track the world in
9the same frame. Contrast with water.py (cubemap/sky reflection): a planar mirror
10is exact for the flat surface and shows the actual scene geometry, not an
11approximation.
12
13# /// simvx
14# tags = ["3d", "water", "planar-reflection", "mirror", "refraction"]
15# screenshot_frame = 40
16# ///
17
18Usage:
19 uv run python examples/features/3d/planar_water.py
20"""
21
22from simvx.core import (
23 Camera3D,
24 DirectionalLight3D,
25 Input,
26 InputMap,
27 Key,
28 Material,
29 Mesh,
30 MeshInstance3D,
31 Node,
32 PlanarReflection3D,
33 Text2D,
34 WaterMaterial,
35 WaterSurface3D,
36 WorldEnvironment,
37)
38from simvx.graphics import App
39
40WIDTH, HEIGHT = 1280, 720
41
42
43class PlanarWaterScene(Node):
44 def on_ready(self):
45 InputMap.add_action("quit", [Key.ESCAPE])
46
47 # WorldEnvironment enables the HDR chain the water refraction reads and
48 # publishes the wind that drives the Gerstner swell via FrameGlobals.
49 env = self.add_child(WorldEnvironment())
50 env.wind_direction = (0.8, 0.4)
51 env.wind_strength = 0.5
52
53 # Low camera so the water fills the lower half and the tall shapes above
54 # it reflect clearly in the mirror.
55 self.add_child(Camera3D(position=(0, 2.4, 10.0), look_at=(0, 1.2, 0), up=(0, 1, 0)))
56 sun = self.add_child(DirectionalLight3D(intensity=3.0))
57 sun.direction = (-0.4, -1.0, -0.5)
58
59 # Tall coloured shapes standing ABOVE the water: these are what the planar
60 # mirror inverts and reflects (the cubemap path could never show them).
61 self.add_child(
62 MeshInstance3D( # green pillar
63 mesh=Mesh.cube(0.7),
64 material=Material(colour=(0.3, 0.85, 0.45, 1.0), roughness=0.5),
65 position=(-3.0, 2.0, -1.5),
66 scale=(1.0, 6.0, 1.0),
67 )
68 )
69 self.add_child(
70 MeshInstance3D( # blue sphere on a stalk
71 mesh=Mesh.sphere(0.8),
72 material=Material(colour=(0.3, 0.5, 0.95, 1.0), roughness=0.25),
73 position=(3.2, 1.6, -1.0),
74 )
75 )
76 self.add_child(
77 MeshInstance3D( # red back wall: fills the mirror horizon
78 mesh=Mesh.cube(1.0),
79 material=Material(colour=(0.75, 0.28, 0.28, 1.0), roughness=0.8),
80 position=(0, 2.2, -7.5),
81 scale=(18.0, 4.5, 0.5),
82 )
83 )
84
85 # A spinning orange cube hovering over the water: proves the reflection
86 # tracks the live scene every frame (it rotates in the mirror too).
87 self.spinner = self.add_child(
88 MeshInstance3D(
89 mesh=Mesh.cube(1.1),
90 material=Material(colour=(0.95, 0.55, 0.15, 1.0), roughness=0.4),
91 position=(0, 1.8, 0.5),
92 )
93 )
94
95 # A sloped submerged floor so the refraction + depth fade + shore foam
96 # still read below the surface (the mirror is only the above-water half).
97 floor = self.add_child(
98 MeshInstance3D(
99 mesh=Mesh.cube(1.0),
100 material=Material(colour=(0.70, 0.60, 0.42, 1.0), roughness=0.9),
101 position=(0, -2.4, -5.0),
102 scale=(30.0, 0.5, 24.0),
103 )
104 )
105 floor.rotation_degrees = (14.0, 0.0, 0.0)
106
107 # The mirror on the water plane: a PlanarReflection3D at y=0 (its world +Y
108 # is the plane normal) re-renders the scene above every frame. Assign it to
109 # the water surface as the reflection source.
110 self.reflection = self.add_child(PlanarReflection3D(position=(0, 0, 0)))
111
112 self.water = self.add_child(WaterSurface3D(position=(0, 0, 0), size=(40.0, 40.0)))
113 self.water.material = WaterMaterial(
114 shallow_colour=(0.08, 0.34, 0.40),
115 deep_colour=(0.01, 0.06, 0.12),
116 depth_fade_distance=2.8,
117 wave_amplitude=0.14,
118 wave_length=6.0,
119 foam_amount=0.6,
120 refraction_strength=0.35,
121 fresnel_power=4.0,
122 )
123 self.water.reflection = self.reflection
124
125 self.add_child(Text2D(text="Planar water -> true mirror reflection", position=(20, 20), font_scale=1.8))
126
127 def on_update(self, dt):
128 if Input.is_action_pressed("quit"):
129 self.app.quit()
130 return
131 # Spin the cube so the mirror visibly tracks the world every frame.
132 self.spinner.rotate_y(1.2 * dt)
133 self.spinner.rotate_x(0.6 * dt)
134
135
136def main():
137 App(width=WIDTH, height=HEIGHT, title="SimVX - Planar Water").run(PlanarWaterScene())
138
139
140if __name__ == "__main__":
141 main()