Planar Reflection

a mirror floor reflecting the scene above it.

▶ Run in browser

Tags: 3d planar-reflection renderview render-to-texture

A PlanarReflection3D turns any flat surface into a mirror. Every frame it renders the scene again from the active camera mirrored across its plane, and a material samples that capture as its albedo.

Using one takes two steps:

  1. Place the node on the surface. Its world position is a point on the reflection plane and its local +Y axis is the plane’s normal. Here it sits at the origin, level with the top face of the floor slab.

  2. Pass the node as the surface’s albedo, Material(albedo_map=reflection). A material fed a planar reflection samples it with a projective UV worked out from each fragment’s screen position rather than the mesh’s own UVs, so the mirror image lines up with the scene at any window size.

The mirrored camera clips at the plane, so nothing below the floor leaks into the reflection. resolution_scale (half the window by default) sets the size of the capture, and clip_bias keeps the surface out of its own reflection.

The orange cube spins, and its reflection turns with it in the same frame. A planar reflection is exact for one flat surface such as a mirror, calm water or a wet floor; for glossy curved objects use a ReflectionProbe3D cubemap.

Source

 1# /// simvx
 2# tags = ["3d", "planar-reflection", "renderview", "render-to-texture"]
 3# screenshot_frame = 30
 4# ///
 5"""Planar Reflection: a mirror floor reflecting the scene above it.
 6
 7A ``PlanarReflection3D`` turns any flat surface into a mirror. Every frame it
 8renders the scene again from the active camera mirrored across its plane, and a
 9material samples that capture as its albedo.
10
11Using one takes two steps:
12
131. **Place the node on the surface.** Its world position is a point on the
14   reflection plane and its local +Y axis is the plane's normal. Here it sits at
15   the origin, level with the top face of the floor slab.
162. **Pass the node as the surface's albedo**, ``Material(albedo_map=reflection)``.
17   A material fed a planar reflection samples it with a projective UV worked out
18   from each fragment's screen position rather than the mesh's own UVs, so the
19   mirror image lines up with the scene at any window size.
20
21The mirrored camera clips at the plane, so nothing below the floor leaks into
22the reflection. ``resolution_scale`` (half the window by default) sets the size
23of the capture, and ``clip_bias`` keeps the surface out of its own reflection.
24
25The orange cube spins, and its reflection turns with it in the same frame. A
26planar reflection is exact for one flat surface such as a mirror, calm water or
27a wet floor; for glossy curved objects use a ``ReflectionProbe3D`` cubemap.
28"""
29
30from simvx.core import (
31    Anchor,
32    Camera3D,
33    CanvasLayer,
34    DirectionalLight3D,
35    Label,
36    Material,
37    Mesh,
38    MeshInstance3D,
39    Node,
40    PlanarReflection3D,
41)
42from simvx.graphics import App
43
44
45class PlanarReflectionScene(Node):
46    def on_ready(self):
47        # A low camera, so the mirror floor fills the lower half of the view.
48        self.add_child(Camera3D(position=(0, 2.0, 7.5), look_at=(0, 0.6, 0)))
49        self.add_child(DirectionalLight3D(intensity=2.5, direction=(-0.4, -1.0, -0.6)))
50
51        # The reflection plane is y = 0, the top face of the floor slab.
52        reflection = self.add_child(PlanarReflection3D())
53        mirror = Material(albedo_map=reflection, roughness=0.05)
54        self.add_child(MeshInstance3D(mesh=Mesh.box((14, 0.5, 14)), material=mirror, position=(0, -0.25, 0)))
55
56        self.cube = self.add_child(
57            MeshInstance3D(
58                mesh=Mesh.box(1.2),
59                material=Material(colour=(0.95, 0.55, 0.15, 1), roughness=0.4),
60                position=(0, 1.1, 0),
61            )
62        )
63        self.add_child(
64            MeshInstance3D(
65                mesh=Mesh.sphere(0.55),
66                material=Material(colour=(0.3, 0.5, 0.95, 1), roughness=0.3),
67                position=(2.4, 0.55, 1.0),
68            )
69        )
70        self.add_child(
71            MeshInstance3D(  # a tall pillar reads clearly in the mirror
72                mesh=Mesh.box((0.6, 3, 0.6)),
73                material=Material(colour=(0.3, 0.8, 0.4, 1)),
74                position=(-2.4, 1.5, -0.8),
75            )
76        )
77        self.add_child(
78            MeshInstance3D(  # a back wall fills the mirror's horizon
79                mesh=Mesh.box((14, 3, 0.5)),
80                material=Material(colour=(0.75, 0.25, 0.25, 1), roughness=0.8),
81                position=(0, 1.5, -6.5),
82            )
83        )
84
85        hud = self.add_child(CanvasLayer())
86        hud.add_child(Label("PlanarReflection3D as a mirror floor", anchor=Anchor.TOP_LEFT, inset=16, font_size=22))
87
88    def on_update(self, dt):
89        self.cube.rotate_y(1.4 * dt)
90        self.cube.rotate_x(0.7 * dt)
91
92
93if __name__ == "__main__":
94    App(width=1280, height=720, title="Planar Reflection").run(PlanarReflectionScene())