Render Layers

3D object visibility via render layer bitmasks.

▶ Run in browser

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