SubViewport¶
render a live 3D scene to a texture, shown on an in-world monitor.
▶ Run in browserTags: 3d subviewport render-to-texture
A SubViewport renders its own subtree (a spinning cube on a blue background, viewed by its own Camera3D) into an offscreen texture every frame. That texture is fed to a Material on a flat “monitor” slab in the main scene: a live screen in the world. The monitor’s feed updates each frame, proving the render-to- texture is genuinely live, not a one-shot capture.
Usage: uv run python examples/features/3d/sub_viewport.py
Source¶
1"""SubViewport: render a live 3D scene to a texture, shown on an in-world monitor.
2
3A SubViewport renders its own subtree (a spinning cube on a blue background,
4viewed by its own Camera3D) into an offscreen texture every frame. That texture
5is fed to a Material on a flat "monitor" slab in the main scene: a live screen
6in the world. The monitor's feed updates each frame, proving the render-to-
7texture is genuinely live, not a one-shot capture.
8
9# /// simvx
10# tags = ["3d", "subviewport", "render-to-texture"]
11# screenshot_frame = 60
12# ///
13
14Usage:
15 uv run python examples/features/3d/sub_viewport.py
16"""
17
18from simvx.core import (
19 Camera3D,
20 DirectionalLight3D,
21 Input,
22 InputMap,
23 Key,
24 Material,
25 Mesh,
26 MeshInstance3D,
27 Node,
28 Sprite2D,
29 SubViewport,
30 Text2D,
31)
32from simvx.graphics import App
33
34WIDTH, HEIGHT = 1280, 720
35
36
37class SubViewportScene(Node):
38 def on_ready(self):
39 InputMap.add_action("quit", [Key.ESCAPE])
40
41 # ── Main-scene camera (looks at the monitor) ──────────────────────
42 self.add_child(Camera3D(position=(0, 2, 6), look_at=(0, 0, 0), up=(0, 1, 0)))
43 sun = self.add_child(DirectionalLight3D(intensity=2.0))
44 sun.direction = (-0.4, -1.0, -0.6)
45
46 # ── SubViewport: a self-contained 3D scene with its OWN camera ────
47 # Its subtree never appears in the main pass; it renders offscreen.
48 self.sub = self.add_child(SubViewport(size=(512, 512)))
49 self.sub.add_child(Camera3D(position=(0, 0, 3.0), look_at=(0, 0, 0), up=(0, 1, 0)))
50 sub_sun = self.sub.add_child(DirectionalLight3D(intensity=3.0))
51 sub_sun.direction = (-0.5, -0.7, -0.5)
52 # A large emissive backdrop fills the feed so the monitor reads as a
53 # bright screen rather than a dark panel, and makes the live update
54 # obvious as the foreground cube spins across it.
55 self.sub.add_child(
56 MeshInstance3D(
57 mesh=Mesh.cube(6.0),
58 material=Material(colour=(0.15, 0.35, 0.85, 1.0), roughness=1.0),
59 position=(0, 0, -3.5),
60 )
61 )
62 self.feed_cube = self.sub.add_child(
63 MeshInstance3D(
64 mesh=Mesh.cube(1.8),
65 material=Material(colour=(0.95, 0.55, 0.15, 1.0), roughness=0.4),
66 )
67 )
68
69 # ── The in-world monitor: a thin slab textured with the live feed ─
70 # albedo_tex_index is set each frame from the SubViewport's texture
71 # slot. The slot is stable across resizes, so this binding holds.
72 self.monitor_mat = Material(colour=(1, 1, 1, 1), roughness=1.0)
73 self.monitor = self.add_child(
74 MeshInstance3D(
75 mesh=Mesh.cube(1.0),
76 material=self.monitor_mat,
77 position=(0, 0, 0),
78 scale=(3.2, 1.8, 0.08),
79 )
80 )
81
82 # ── Reference props so the main scene clearly differs from the feed ─
83 self.add_child(
84 MeshInstance3D(
85 mesh=Mesh.cube(0.6),
86 material=Material(colour=(0.3, 0.8, 0.4, 1.0)),
87 position=(-2.6, -0.6, 0.5),
88 )
89 )
90 self.add_child(
91 MeshInstance3D(
92 mesh=Mesh.sphere(0.5),
93 material=Material(colour=(0.3, 0.5, 0.95, 1.0)),
94 position=(2.6, -0.6, 0.5),
95 )
96 )
97
98 # Picture-in-picture: the SAME live feed drawn directly as a 2D sprite in
99 # the corner. Pass the SubViewport node itself as the Sprite2D ``texture``
100 # (the canonical SubViewport-as-texture API, design §5.4): the sprite
101 # resolves the live bindless slot each draw and is treated as a dynamic
102 # drawable, so the retained 2D item cache re-emits it every frame instead
103 # of frame-skipping a feed whose slot it cannot see change.
104 self.pip = self.add_child(Sprite2D(texture=self.sub, position=(180, 600), width=260, height=260))
105
106 self.add_child(Text2D(text="SubViewport → live monitor feed", position=(20, 20), font_scale=1.8))
107
108 def on_update(self, dt):
109 if Input.is_action_pressed("quit"):
110 self.app.quit()
111 return
112 # Spin the feed cube so the monitor visibly updates each frame.
113 self.feed_cube.rotate_y(1.4 * dt)
114 self.feed_cube.rotate_x(0.7 * dt)
115 # Slowly orbit the monitor for depth.
116 self.monitor.rotate_y(0.25 * dt)
117 # Bind the SubViewport's live texture to the monitor material. The
118 # SubViewportManager publishes the bindless slot into sub.texture; we
119 # forward it to the material's direct GPU texture index.
120 if self.sub.texture >= 0:
121 self.monitor_mat.albedo_tex_index = self.sub.texture
122
123
124if __name__ == "__main__":
125 app = App(title="SubViewport", width=WIDTH, height=HEIGHT)
126 app.run(SubViewportScene())