Planar water

a Gerstner sea reflecting the world above it with a true mirror.

▶ Run in browser

Tags: 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()