Planar Water¶
a Gerstner sea that mirrors the world above it.
▶ Run in browserTags: 3d water planar-reflection mirror refraction
A WaterSurface3D normally reflects the sky or environment cubemap, which
cannot show the objects standing in the scene. Give it a PlanarReflection3D
and it reflects the real scene instead: the reflection node re-renders
everything above its plane every frame, and the water samples that capture,
ripples it with the wave normals and blends it by Fresnel against the
refraction of what lies below.
mirror = self.add_child(PlanarReflection3D())
self.add_child(WaterSurface3D(size=(40, 40), material=WaterMaterial(...), reflection=mirror))
The reflection node’s world +Y is the plane normal, so it sits at the water’s
height, here both at the origin. resolution_scale (half the viewport by
default) trades sharpness for the cost of the second scene render.
The rest is ordinary water. A WorldEnvironment enables the HDR chain the
refraction reads, and its wind_direction and wind_strength steer the
swell. The WaterMaterial sets the shallow and deep tints, how quickly the
water darkens with depth, the wave height and length, the shore foam, and how
strongly the surface bends what is beneath it. A sloped floor under the surface
shows the depth fade, the refraction and the foam where it rises to the water.
Above the water stand a green pillar, a blue sphere, a red back wall and an orange cube that spins in the air; all four appear upside down in the water, and the cube turns in its reflection in the same frame.
Source¶
1# /// simvx
2# tags = ["3d", "water", "planar-reflection", "mirror", "refraction"]
3# screenshot_frame = 40
4# ///
5"""Planar Water: a Gerstner sea that mirrors the world above it.
6
7A ``WaterSurface3D`` normally reflects the sky or environment cubemap, which
8cannot show the objects standing in the scene. Give it a ``PlanarReflection3D``
9and it reflects the real scene instead: the reflection node re-renders
10everything above its plane every frame, and the water samples that capture,
11ripples it with the wave normals and blends it by Fresnel against the
12refraction of what lies below.
13
14 mirror = self.add_child(PlanarReflection3D())
15 self.add_child(WaterSurface3D(size=(40, 40), material=WaterMaterial(...), reflection=mirror))
16
17The reflection node's world +Y is the plane normal, so it sits at the water's
18height, here both at the origin. ``resolution_scale`` (half the viewport by
19default) trades sharpness for the cost of the second scene render.
20
21The rest is ordinary water. A ``WorldEnvironment`` enables the HDR chain the
22refraction reads, and its ``wind_direction`` and ``wind_strength`` steer the
23swell. The ``WaterMaterial`` sets the shallow and deep tints, how quickly the
24water darkens with depth, the wave height and length, the shore foam, and how
25strongly the surface bends what is beneath it. A sloped floor under the surface
26shows the depth fade, the refraction and the foam where it rises to the water.
27
28Above the water stand a green pillar, a blue sphere, a red back wall and an
29orange cube that spins in the air; all four appear upside down in the water,
30and the cube turns in its reflection in the same frame.
31"""
32
33from simvx.core import (
34 Anchor,
35 Camera3D,
36 CanvasLayer,
37 DirectionalLight3D,
38 Label,
39 Material,
40 Mesh,
41 MeshInstance3D,
42 Node,
43 PlanarReflection3D,
44 WaterMaterial,
45 WaterSurface3D,
46 WorldEnvironment,
47)
48from simvx.graphics import App
49
50
51def solid(mesh, colour, roughness, position):
52 return MeshInstance3D(mesh=mesh, material=Material(colour=colour, roughness=roughness), position=position)
53
54
55class PlanarWaterScene(Node):
56 def on_ready(self):
57 self.add_child(WorldEnvironment(wind_direction=(0.8, 0.4), wind_strength=0.5))
58 # A low camera, so the water fills the lower half with the reflection.
59 self.add_child(Camera3D(position=(0, 2.4, 10.0), look_at=(0, 1.2, 0)))
60 self.add_child(DirectionalLight3D(intensity=3.0, direction=(-0.4, -1.0, -0.5)))
61
62 self.add_child(solid(Mesh.box((0.7, 4.2, 0.7)), (0.3, 0.85, 0.45), 0.5, (-3.0, 2.0, -1.5)))
63 self.add_child(solid(Mesh.sphere(0.8), (0.3, 0.5, 0.95), 0.25, (3.2, 1.6, -1.0)))
64 self.add_child(solid(Mesh.box((18.0, 4.5, 0.5)), (0.75, 0.28, 0.28), 0.8, (0, 2.2, -7.5)))
65 self.spinner = self.add_child(solid(Mesh.box(1.1), (0.95, 0.55, 0.15), 0.4, (0, 1.8, 0.5)))
66
67 floor = self.add_child(solid(Mesh.box((30.0, 0.5, 24.0)), (0.7, 0.6, 0.42), 0.9, (0, -2.4, -5.0)))
68 floor.rotation_degrees = (14.0, 0.0, 0.0)
69
70 mirror = self.add_child(PlanarReflection3D())
71 water = WaterMaterial(
72 shallow_colour=(0.08, 0.34, 0.40),
73 deep_colour=(0.01, 0.06, 0.12),
74 depth_fade_distance=2.8,
75 wave_amplitude=0.14,
76 wave_length=6.0,
77 foam_amount=0.6,
78 refraction_strength=0.35,
79 fresnel_power=4.0,
80 )
81 self.add_child(WaterSurface3D(size=(40.0, 40.0), material=water, reflection=mirror))
82
83 caption = Label("Planar water: a true mirror reflection", anchor=Anchor.TOP_LEFT, inset=16, font_size=24)
84 self.add_child(CanvasLayer()).add_child(caption)
85
86 def on_update(self, dt):
87 self.spinner.rotate_y(1.2 * dt)
88 self.spinner.rotate_x(0.6 * dt)
89
90
91if __name__ == "__main__":
92 App(title="Planar Water", width=1280, height=720).run(PlanarWaterScene())