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