Screen-space refraction¶
a glass ball bends the scene behind it.
▶ Run in browserTags: 3d
A transparent material that reads the screen draws what is behind it, bent:
glass = Material(colour=(0.75, 0.87, 1.0, 0.55), blend="alpha", needs_scene_colour=True)
needs_scene_colour=True makes the renderer split the frame in two. The
opaque objects draw first, the colour they produced is copied into a texture,
and then the transparent objects draw, each screen-reading one sampling that
copy at its own pixel shifted by its surface normal. Where the normal turns,
the sample shifts, so a curved surface bends the image behind it; a flat face
seen head on has nothing to bend. The material’s alpha is how much of that
bent scene shows through the glass’s own lit colour.
The split costs nothing when unused: a frame with no screen-reading material
renders exactly as it would without the feature. WaterSurface3D builds
its water shading on the same scene-colour copy (see water.py).
Here a glass ball sits in front of a grid of coloured cubes. Turning
refraction off clears needs_scene_colour on the live material, and the ball
becomes plain tinted glass that shows the grid straight through. The camera is
an OrbitCamera3D: drag to orbit and scroll to zoom, or use the keys.
Controls: Space / button - Toggle refraction A / D / drag - Orbit the camera (arrow keys too) W / S / scroll - Zoom in / out (arrow keys too)
Source¶
1# /// simvx
2# screenshot_frame = 30
3# web = { width = 960, height = 720, responsive = true }
4# ///
5"""Screen-space refraction: a glass ball bends the scene behind it.
6
7A transparent material that reads the screen draws what is behind it, bent:
8
9 glass = Material(colour=(0.75, 0.87, 1.0, 0.55), blend="alpha", needs_scene_colour=True)
10
11``needs_scene_colour=True`` makes the renderer split the frame in two. The
12opaque objects draw first, the colour they produced is copied into a texture,
13and then the transparent objects draw, each screen-reading one sampling that
14copy at its own pixel shifted by its surface normal. Where the normal turns,
15the sample shifts, so a curved surface bends the image behind it; a flat face
16seen head on has nothing to bend. The material's alpha is how much of that
17bent scene shows through the glass's own lit colour.
18
19The split costs nothing when unused: a frame with no screen-reading material
20renders exactly as it would without the feature. ``WaterSurface3D`` builds
21its water shading on the same scene-colour copy (see ``water.py``).
22
23Here a glass ball sits in front of a grid of coloured cubes. Turning
24refraction off clears ``needs_scene_colour`` on the live material, and the ball
25becomes plain tinted glass that shows the grid straight through. The camera is
26an ``OrbitCamera3D``: drag to orbit and scroll to zoom, or use the keys.
27
28Controls:
29 Space / button - Toggle refraction
30 A / D / drag - Orbit the camera (arrow keys too)
31 W / S / scroll - Zoom in / out (arrow keys too)
32"""
33
34import math
35
36from simvx.core import (
37 Anchor,
38 Axis1D,
39 Button,
40 CanvasLayer,
41 DirectionalLight3D,
42 Input,
43 Key,
44 Label,
45 Material,
46 Mesh,
47 MeshInstance3D,
48 Node3D,
49 OrbitCamera3D,
50 PointLight3D,
51 WorldEnvironment,
52 on_input,
53)
54from simvx.graphics import App
55
56GRID_COLOURS = [
57 (0.90, 0.20, 0.20),
58 (0.20, 0.85, 0.30),
59 (0.20, 0.45, 0.95),
60 (0.95, 0.80, 0.20),
61 (0.85, 0.30, 0.85),
62 (0.25, 0.85, 0.85),
63]
64
65
66class Refraction(Node3D):
67 input_context = {
68 "refraction": [Key.SPACE],
69 "orbit": [Axis1D(low=Key.A, high=Key.D), Axis1D(low=Key.LEFT, high=Key.RIGHT)],
70 "zoom": [Axis1D(low=Key.S, high=Key.W), Axis1D(low=Key.DOWN, high=Key.UP)],
71 }
72
73 def on_ready(self):
74 self.camera = self.add_child(
75 OrbitCamera3D(yaw=0.0, pitch=math.radians(-4), distance=7, min_distance=4, max_distance=14, fov=55)
76 )
77 self.add_child(DirectionalLight3D(colour=(1.0, 0.96, 0.9), intensity=1.6, direction=(-0.4, -0.7, -0.6)))
78 self.add_child(PointLight3D(colour=(0.5, 0.6, 0.9), intensity=0.6, range=20.0, position=(-4, 3, 5)))
79 self.add_child(WorldEnvironment(bloom_enabled=True))
80
81 # The opaque grid behind the glass: what the refraction reads.
82 cube = Mesh.box(1.2)
83 grid = [(x, y) for y in (1.4, 0.0, -1.4) for x in (-2.0, 0.0, 2.0)]
84 for i, (x, y) in enumerate(grid):
85 material = Material(colour=GRID_COLOURS[i % len(GRID_COLOURS)], roughness=0.55)
86 self.add_child(MeshInstance3D(mesh=cube, material=material, position=(x, y, -1.2)))
87
88 self.glass = Material(colour=(0.75, 0.87, 1.0, 0.55), blend="alpha", roughness=0.05, needs_scene_colour=True)
89 ball = Mesh.sphere(radius=1.5, rings=32, segments=32)
90 self.add_child(MeshInstance3D(mesh=ball, material=self.glass, position=(0, 0, 0.8)))
91
92 hud = self.add_child(CanvasLayer())
93 self.button = hud.add_child(
94 Button("Refraction", action="refraction", checked=True, anchor=Anchor.TOP_LEFT, inset=12)
95 )
96
97 # Example help overlay: not part of the technique shown.
98 help_text = "Space: refraction A/D / drag: orbit W/S / scroll: zoom"
99 hud.add_child(Label(help_text, anchor=Anchor.BOTTOM_LEFT, inset=12, font_size=14))
100
101 @on_input("refraction")
102 def toggle_refraction(self, _event):
103 # The next frame renders with or without the scene-colour copy.
104 self.glass.needs_scene_colour = not self.glass.needs_scene_colour
105 self.button.checked = self.glass.needs_scene_colour
106
107 def on_update(self, dt):
108 self.camera.orbit(Input.value("orbit") * math.radians(45) * dt, 0.0)
109 self.camera.zoom(Input.value("zoom") * 8.0 * dt)
110
111
112if __name__ == "__main__":
113 App(title="Screen-Space Refraction", width=960, height=720).run(Refraction())