Render Layers Demo¶
3D object visibility via render layer bitmasks.
▶ Run in browserTags: 3d
Demonstrates:
Assigning objects to different render layers (0, 1, 2)
Camera cull_mask controls which layers are visible
Toggle layers with keyboard (1, 2, 3)
Run: uv run python examples/features/3d/render_layers.py uv run python examples/features/3d/render_layers.py –test
Controls: 1 - Toggle layer 0 (red cubes) 2 - Toggle layer 1 (green spheres) 3 - Toggle layer 2 (blue cubes) A / D - Orbit camera
Source¶
1"""
2Render Layers Demo: 3D object visibility via render layer bitmasks.
3
4Demonstrates:
5 - Assigning objects to different render layers (0, 1, 2)
6 - Camera cull_mask controls which layers are visible
7 - Toggle layers with keyboard (1, 2, 3)
8
9Run:
10 uv run python examples/features/3d/render_layers.py
11 uv run python examples/features/3d/render_layers.py --test
12
13Controls:
14 1 - Toggle layer 0 (red cubes)
15 2 - Toggle layer 1 (green spheres)
16 3 - Toggle layer 2 (blue cubes)
17 A / D - Orbit camera
18"""
19
20
21import math
22import sys
23
24from simvx.core import (
25 Camera3D,
26 DirectionalLight3D,
27 Input,
28 InputMap,
29 Key,
30 Material,
31 Mesh,
32 MeshInstance3D,
33 Node3D,
34 Quat,
35 Text2D,
36 Vec3,
37)
38from simvx.graphics import App
39
40WIDTH, HEIGHT = 1280, 720
41
42
43class RenderLayerScene(Node3D):
44 def __init__(self, **kwargs):
45 super().__init__(name="RenderLayerDemo", **kwargs)
46
47 # Camera
48 self._cam_angle = 30.0
49 self.camera = self.add_child(Camera3D(name="Camera", fov=60, near=0.1, far=100.0))
50 self._update_camera()
51
52 # Lighting
53 sun = self.add_child(DirectionalLight3D(name="Sun"))
54 sun.colour = (1.0, 0.95, 0.9)
55 sun.intensity = 1.2
56 sun.rotation = Quat.from_euler(math.radians(-45), math.radians(-30), 0)
57
58 # Ground (on all layers: always visible)
59 ground = self.add_child(
60 MeshInstance3D(
61 name="Ground",
62 mesh=Mesh.cube(1.0),
63 material=Material(colour=(0.3, 0.3, 0.3)),
64 position=Vec3(0, -0.05, 0),
65 scale=Vec3(20, 0.1, 20),
66 )
67 )
68 ground.render_layer = 0b111 # layers 0, 1, 2
69
70 # Layer 0: Red cubes
71 red_mat = Material(colour=(0.9, 0.15, 0.1))
72 for i in range(3):
73 cube = self.add_child(
74 MeshInstance3D(
75 name=f"RedCube{i}",
76 mesh=Mesh.cube(1.0),
77 material=red_mat,
78 position=Vec3(-4 + i * 2, 1.0, -2),
79 scale=Vec3(0.8, 0.8, 0.8),
80 )
81 )
82 cube.render_layer = 1 << 0 # layer 0
83
84 # Layer 1: Green spheres
85 green_mat = Material(colour=(0.1, 0.85, 0.2))
86 for i in range(3):
87 sphere = self.add_child(
88 MeshInstance3D(
89 name=f"GreenSphere{i}",
90 mesh=Mesh.sphere(0.6, rings=12, segments=12),
91 material=green_mat,
92 position=Vec3(-4 + i * 2, 1.0, 0),
93 )
94 )
95 sphere.render_layer = 1 << 1 # layer 1
96
97 # Layer 2: Blue cubes
98 blue_mat = Material(colour=(0.1, 0.2, 0.9))
99 for i in range(3):
100 cube = self.add_child(
101 MeshInstance3D(
102 name=f"BlueCube{i}",
103 mesh=Mesh.cube(1.0),
104 material=blue_mat,
105 position=Vec3(-4 + i * 2, 1.0, 2),
106 scale=Vec3(0.8, 0.8, 0.8),
107 )
108 )
109 cube.render_layer = 1 << 2 # layer 2
110
111 # HUD
112 self._title = self.add_child(Text2D(text="RENDER LAYERS DEMO", x=10, y=8, font_scale=1.6))
113 self._status = self.add_child(Text2D(text="", x=10, y=40, font_scale=1.2))
114 self._controls = self.add_child(
115 Text2D(text="1:Red 2:Green 3:Blue A/D:Orbit", x=10, y=690, font_scale=1.1)
116 )
117 self._toggle_cd = 0.0
118
119 def on_ready(self):
120 InputMap.add_action("cam_left", [Key.A, Key.LEFT])
121 InputMap.add_action("cam_right", [Key.D, Key.RIGHT])
122 InputMap.add_action("toggle_0", [Key.KEY_1])
123 InputMap.add_action("toggle_1", [Key.KEY_2])
124 InputMap.add_action("toggle_2", [Key.KEY_3])
125
126 def _update_camera(self):
127
128 r = 12.0
129 y = 8.0
130 rad = math.radians(self._cam_angle)
131 self.camera.position = Vec3(math.sin(rad) * r, y, math.cos(rad) * r)
132 self.camera.look_at(Vec3(0, 0.5, 0))
133
134 def on_process(self, dt: float):
135 # Camera orbit
136 if Input.is_action_pressed("cam_left"):
137 self._cam_angle -= 60 * dt
138 self._update_camera()
139 if Input.is_action_pressed("cam_right"):
140 self._cam_angle += 60 * dt
141 self._update_camera()
142
143 # Toggle render layers
144 self._toggle_cd = max(0, self._toggle_cd - dt)
145 if self._toggle_cd <= 0:
146 for key_action, layer_idx in [("toggle_0", 0), ("toggle_1", 1), ("toggle_2", 2)]:
147 if Input.is_action_pressed(key_action):
148 enabled = self.camera.is_cull_mask_layer_enabled(layer_idx)
149 self.camera.set_cull_mask_layer(layer_idx, not enabled)
150 self._toggle_cd = 0.2
151
152 # Update status
153 layers = []
154 for i, name in [(0, "Red"), (1, "Green"), (2, "Blue")]:
155 on = self.camera.is_cull_mask_layer_enabled(i)
156 layers.append(f"{name}:{'ON' if on else 'OFF'}")
157 self._status.text = " ".join(layers)
158
159
160def main():
161 scene = RenderLayerScene()
162
163 if "--test" in sys.argv:
164 _run_headless_test(scene)
165 else:
166 app = App(title="SimVX Render Layers Demo", width=WIDTH, height=HEIGHT, physics_fps=60)
167 app.run(scene)
168
169
170def _run_headless_test(scene):
171 """Headless test: capture screenshots with different cull masks."""
172
173 app = App(width=WIDTH, height=HEIGHT, visible=False)
174
175 def on_frame(frame_idx, _t):
176 cam = scene.camera
177 if frame_idx == 2:
178 # All layers visible
179 cam.cull_mask = 0b111
180 elif frame_idx == 5:
181 # Only layer 0 (red cubes)
182 cam.cull_mask = 0b001
183 elif frame_idx == 8:
184 # Only layer 1 (green spheres)
185 cam.cull_mask = 0b010
186 elif frame_idx == 11:
187 # Only layer 2 (blue cubes)
188 cam.cull_mask = 0b100
189
190 frames = app.run_headless(scene, frames=14, on_frame=on_frame, capture_frames=[3, 6, 9, 12])
191
192 from PIL import Image
193
194 names = ["all_layers", "layer0_red", "layer1_green", "layer2_blue"]
195 for i, name in enumerate(names):
196 if i < len(frames):
197 img = Image.fromarray(frames[i])
198 path = f"/tmp/render_layers_{name}.png"
199 img.save(path)
200 print(f"Saved {path} ({img.size[0]}x{img.size[1]})")
201
202 print("Headless test complete.")
203
204
205if __name__ == "__main__":
206 main()