Render Layers¶
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
Controls: 1 - Toggle layer 0 (red cubes) 2 - Toggle layer 1 (green spheres) 3 - Toggle layer 2 (blue cubes) A / D - Orbit camera Click/tap a layer’s status entry to toggle it
Source¶
1"""
2Render Layers: 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
12Controls:
13 1 - Toggle layer 0 (red cubes)
14 2 - Toggle layer 1 (green spheres)
15 3 - Toggle layer 2 (blue cubes)
16 A / D - Orbit camera
17 Click/tap a layer's status entry to toggle it
18"""
19
20import math
21
22from simvx.core import (
23 Camera3D,
24 DirectionalLight3D,
25 Input,
26 InputMap,
27 Key,
28 Material,
29 Mesh,
30 MeshInstance3D,
31 MouseButton,
32 Node3D,
33 Quat,
34 Text2D,
35 Vec3,
36)
37from simvx.graphics import App
38
39WIDTH, HEIGHT = 1280, 720
40
41# HUD layout for the clickable layer row. Text2D has no bounds query, so the
42# click targets are derived from the same numbers that place the entries: one
43# row at HUD_ROW_Y, each entry HUD_ENTRY_WIDTH wide and HUD_ROW_HEIGHT tall at
44# font scale HUD_FONT_SCALE. Change these together and the hit boxes follow.
45HUD_ROW_Y = 40
46HUD_FONT_SCALE = 1.2
47HUD_ROW_HEIGHT = 26
48HUD_ENTRY_WIDTH = 130
49HUD_ENTRY_PAD = 6
50
51
52class RenderLayerScene(Node3D):
53 def __init__(self, **kwargs):
54 super().__init__(name="RenderLayerDemo", **kwargs)
55
56 # Camera
57 self._cam_angle = 30.0
58 self.camera = self.add_child(Camera3D(name="Camera", fov=60, near=0.1, far=100.0))
59 self._update_camera()
60
61 # Lighting
62 sun = self.add_child(DirectionalLight3D(name="Sun"))
63 sun.colour = (1.0, 0.95, 0.9)
64 sun.intensity = 1.2
65 sun.rotation = Quat.from_euler(math.radians(-45), math.radians(-30), 0)
66
67 # Ground (on all layers: always visible)
68 ground = self.add_child(
69 MeshInstance3D(
70 name="Ground",
71 mesh=Mesh.cube(1.0),
72 material=Material(colour=(0.3, 0.3, 0.3)),
73 position=Vec3(0, -0.05, 0),
74 scale=Vec3(20, 0.1, 20),
75 )
76 )
77 ground.render_layer = 0b111 # layers 0, 1, 2
78
79 # Layer 0: Red cubes
80 red_mat = Material(colour=(0.9, 0.15, 0.1))
81 for i in range(3):
82 cube = self.add_child(
83 MeshInstance3D(
84 name=f"RedCube{i}",
85 mesh=Mesh.cube(1.0),
86 material=red_mat,
87 position=Vec3(-4 + i * 2, 1.0, -2),
88 scale=Vec3(0.8, 0.8, 0.8),
89 )
90 )
91 cube.render_layer = 1 << 0 # layer 0
92
93 # Layer 1: Green spheres
94 green_mat = Material(colour=(0.1, 0.85, 0.2))
95 for i in range(3):
96 sphere = self.add_child(
97 MeshInstance3D(
98 name=f"GreenSphere{i}",
99 mesh=Mesh.sphere(0.6, rings=12, segments=12),
100 material=green_mat,
101 position=Vec3(-4 + i * 2, 1.0, 0),
102 )
103 )
104 sphere.render_layer = 1 << 1 # layer 1
105
106 # Layer 2: Blue cubes
107 blue_mat = Material(colour=(0.1, 0.2, 0.9))
108 for i in range(3):
109 cube = self.add_child(
110 MeshInstance3D(
111 name=f"BlueCube{i}",
112 mesh=Mesh.cube(1.0),
113 material=blue_mat,
114 position=Vec3(-4 + i * 2, 1.0, 2),
115 scale=Vec3(0.8, 0.8, 0.8),
116 )
117 )
118 cube.render_layer = 1 << 2 # layer 2
119
120 # HUD: one clickable status entry per layer
121 self._title = self.add_child(Text2D(text="RENDER LAYERS DEMO", position=(10, 8), font_scale=1.6))
122 self._layer_entries = []
123 for layer_idx, (label, x, colour) in enumerate(
124 [
125 ("Red", 10, (0.95, 0.35, 0.3)),
126 ("Green", 160, (0.3, 0.9, 0.4)),
127 ("Blue", 310, (0.4, 0.5, 1.0)),
128 ]
129 ):
130 entry = self.add_child(Text2D(text="", position=(x, HUD_ROW_Y), font_scale=HUD_FONT_SCALE))
131 self._layer_entries.append((layer_idx, label, x, colour, entry))
132 self._controls = self.add_child(
133 Text2D(text="1:Red 2:Green 3:Blue A/D:Orbit Click a layer to toggle", font_scale=1.1)
134 )
135
136 def on_ready(self):
137 InputMap.add_action("cam_left", [Key.A, Key.LEFT])
138 InputMap.add_action("cam_right", [Key.D, Key.RIGHT])
139 InputMap.add_action("toggle_0", [Key.KEY_1])
140 InputMap.add_action("toggle_1", [Key.KEY_2])
141 InputMap.add_action("toggle_2", [Key.KEY_3])
142
143 def _update_camera(self):
144
145 r = 12.0
146 y = 8.0
147 rad = math.radians(self._cam_angle)
148 self.camera.position = Vec3(math.sin(rad) * r, y, math.cos(rad) * r)
149 self.camera.look_at(Vec3(0, 0.5, 0))
150
151 def _toggle_layer(self, layer_idx: int):
152 enabled = self.camera.is_cull_mask_layer_enabled(layer_idx)
153 self.camera.set_cull_mask_layer(layer_idx, not enabled)
154
155 def on_update(self, dt: float):
156 # Camera orbit
157 if Input.is_action_pressed("cam_left"):
158 self._cam_angle -= 60 * dt
159 self._update_camera()
160 if Input.is_action_pressed("cam_right"):
161 self._cam_angle += 60 * dt
162 self._update_camera()
163
164 # Toggle render layers: keys 1/2/3, edge-triggered
165 for key_action, layer_idx in [("toggle_0", 0), ("toggle_1", 1), ("toggle_2", 2)]:
166 if Input.is_action_just_pressed(key_action):
167 self._toggle_layer(layer_idx)
168
169 # Click/tap a status entry to toggle that layer
170 if Input.is_mouse_button_just_pressed(MouseButton.LEFT):
171 mx, my = Input.mouse_position
172 in_row = HUD_ROW_Y - HUD_ENTRY_PAD <= my <= HUD_ROW_Y + HUD_ROW_HEIGHT
173 for layer_idx, _label, x, _colour, _entry in self._layer_entries:
174 if in_row and x - HUD_ENTRY_PAD <= mx <= x + HUD_ENTRY_WIDTH:
175 self._toggle_layer(layer_idx)
176
177 # Update status entries and keep the controls hint pinned to the window bottom
178 for layer_idx, label, _x, colour, entry in self._layer_entries:
179 on = self.camera.is_cull_mask_layer_enabled(layer_idx)
180 entry.text = f"{label}:{'ON' if on else 'OFF'}"
181 entry.colour = colour if on else (0.5, 0.5, 0.5)
182 self._controls.position = (10, self.app.height - 30)
183
184
185def main():
186 scene = RenderLayerScene()
187 app = App(title="SimVX Render Layers Demo", width=WIDTH, height=HEIGHT)
188 app.run(scene)
189
190
191if __name__ == "__main__":
192 main()