Render Layers Demo

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 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
 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
 20
 21import math
 22
 23from simvx.core import (
 24    Camera3D,
 25    DirectionalLight3D,
 26    Input,
 27    InputMap,
 28    Key,
 29    Material,
 30    Mesh,
 31    MeshInstance3D,
 32    MouseButton,
 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: one clickable status entry per layer
112        self._title = self.add_child(Text2D(text="RENDER LAYERS DEMO", position=(10, 8), font_scale=1.6))
113        self._layer_entries = []
114        for layer_idx, (label, x, colour) in enumerate(
115            [
116                ("Red", 10, (0.95, 0.35, 0.3)),
117                ("Green", 160, (0.3, 0.9, 0.4)),
118                ("Blue", 310, (0.4, 0.5, 1.0)),
119            ]
120        ):
121            entry = self.add_child(Text2D(text="", position=(x, 40), font_scale=1.2))
122            self._layer_entries.append((layer_idx, label, x, colour, entry))
123        self._controls = self.add_child(
124            Text2D(text="1:Red  2:Green  3:Blue  A/D:Orbit  Click a layer to toggle", font_scale=1.1)
125        )
126
127    def on_ready(self):
128        InputMap.add_action("cam_left", [Key.A, Key.LEFT])
129        InputMap.add_action("cam_right", [Key.D, Key.RIGHT])
130        InputMap.add_action("toggle_0", [Key.KEY_1])
131        InputMap.add_action("toggle_1", [Key.KEY_2])
132        InputMap.add_action("toggle_2", [Key.KEY_3])
133
134    def _update_camera(self):
135
136        r = 12.0
137        y = 8.0
138        rad = math.radians(self._cam_angle)
139        self.camera.position = Vec3(math.sin(rad) * r, y, math.cos(rad) * r)
140        self.camera.look_at(Vec3(0, 0.5, 0))
141
142    def _toggle_layer(self, layer_idx: int):
143        enabled = self.camera.is_cull_mask_layer_enabled(layer_idx)
144        self.camera.set_cull_mask_layer(layer_idx, not enabled)
145
146    def on_update(self, dt: float):
147        # Camera orbit
148        if Input.is_action_pressed("cam_left"):
149            self._cam_angle -= 60 * dt
150            self._update_camera()
151        if Input.is_action_pressed("cam_right"):
152            self._cam_angle += 60 * dt
153            self._update_camera()
154
155        # Toggle render layers: keys 1/2/3, edge-triggered
156        for key_action, layer_idx in [("toggle_0", 0), ("toggle_1", 1), ("toggle_2", 2)]:
157            if Input.is_action_just_pressed(key_action):
158                self._toggle_layer(layer_idx)
159
160        # Click/tap a status entry to toggle that layer
161        if Input.is_mouse_button_just_pressed(MouseButton.LEFT):
162            mx, my = Input.mouse_position
163            for layer_idx, _label, x, _colour, _entry in self._layer_entries:
164                if x - 4 <= mx <= x + 130 and 34 <= my <= 66:
165                    self._toggle_layer(layer_idx)
166
167        # Update status entries and keep the controls hint pinned to the window bottom
168        for layer_idx, label, _x, colour, entry in self._layer_entries:
169            on = self.camera.is_cull_mask_layer_enabled(layer_idx)
170            entry.text = f"{label}:{'ON' if on else 'OFF'}"
171            entry.colour = colour if on else (0.5, 0.5, 0.5)
172        self._controls.position = (10, self.app.height - 30)
173
174
175def main():
176    scene = RenderLayerScene()
177    app = App(title="SimVX Render Layers Demo", width=WIDTH, height=HEIGHT, physics_fps=60)
178    app.run(scene)
179
180
181if __name__ == "__main__":
182    main()