SubViewport

Render a Live 3D Scene to a Texture.

▶ Run in browser

Tags: 3d subviewport render-to-texture

A SubViewport renders its own subtree offscreen, every frame, into a texture of its size. Nothing under it appears in the main pass: it is a separate little world with its own Camera3D and lights. Here that world is an orange cube spinning in front of a blue backdrop.

The node itself is the texture. Hand it to a material and a mesh in the main scene shows the live feed, shaded like any other surface::

feed = self.add_child(SubViewport(size=(512, 512)))
feed.add_child(Camera3D(position=(0, 0, 3), look_at=(0, 0, 0)))
...
MeshInstance3D(mesh=Mesh.box((3.2, 1.8, 0.08)), material=Material(albedo_map=feed))

The binding is made once: the renderer finds the feed’s current texture each frame, so the “monitor” slab in the middle of the scene updates as the cube spins, and the main scene’s sun rakes across it as the monitor turns.

The same node is also a Sprite2D texture. The bottom-left corner shows the feed a second time, flat, as a picture-in-picture overlay: a Control anchored to the corner holds the sprite, so it stays there when the window is resized.

Source

 1# /// simvx
 2# tags = ["3d", "subviewport", "render-to-texture"]
 3# screenshot_frame = 60
 4# ///
 5"""SubViewport: Render a Live 3D Scene to a Texture.
 6
 7A ``SubViewport`` renders its own subtree offscreen, every frame, into a
 8texture of its ``size``. Nothing under it appears in the main pass: it is a
 9separate little world with its own ``Camera3D`` and lights. Here that world is
10an orange cube spinning in front of a blue backdrop.
11
12The node itself is the texture. Hand it to a material and a mesh in the main
13scene shows the live feed, shaded like any other surface::
14
15    feed = self.add_child(SubViewport(size=(512, 512)))
16    feed.add_child(Camera3D(position=(0, 0, 3), look_at=(0, 0, 0)))
17    ...
18    MeshInstance3D(mesh=Mesh.box((3.2, 1.8, 0.08)), material=Material(albedo_map=feed))
19
20The binding is made once: the renderer finds the feed's current texture each
21frame, so the "monitor" slab in the middle of the scene updates as the cube
22spins, and the main scene's sun rakes across it as the monitor turns.
23
24The same node is also a ``Sprite2D`` texture. The bottom-left corner shows the
25feed a second time, flat, as a picture-in-picture overlay: a ``Control``
26anchored to the corner holds the sprite, so it stays there when the window is
27resized.
28"""
29
30from simvx.core import (
31    Anchor,
32    Camera3D,
33    CanvasLayer,
34    Control,
35    DirectionalLight3D,
36    Label,
37    Material,
38    Mesh,
39    MeshInstance3D,
40    Node,
41    Sprite2D,
42    SubViewport,
43)
44from simvx.graphics import App
45
46PIP_SIZE = 260
47
48
49class SubViewportScene(Node):
50    def on_ready(self):
51        # The offscreen world: its own camera, light, backdrop and a spinning cube.
52        feed = self.add_child(SubViewport(size=(512, 512)))
53        feed.add_child(Camera3D(position=(0, 0, 3), look_at=(0, 0, 0)))
54        feed.add_child(DirectionalLight3D(intensity=3.0, direction=(-0.5, -0.7, -0.5)))
55        feed.add_child(
56            MeshInstance3D(
57                mesh=Mesh.box(6.0), material=Material(colour=(0.15, 0.35, 0.85), roughness=1.0), position=(0, 0, -3.5)
58            )
59        )
60        self.cube = feed.add_child(
61            MeshInstance3D(mesh=Mesh.box(1.8), material=Material(colour=(0.95, 0.55, 0.15), roughness=0.4))
62        )
63
64        # The main scene: a monitor showing the feed, between two props the feed does not contain.
65        self.add_child(Camera3D(position=(0, 2, 6), look_at=(0, 0, 0)))
66        self.add_child(DirectionalLight3D(intensity=2.0, direction=(-0.4, -1.0, -0.6)))
67        self.monitor = self.add_child(
68            MeshInstance3D(mesh=Mesh.box((3.2, 1.8, 0.08)), material=Material(albedo_map=feed))
69        )
70        self.add_child(
71            MeshInstance3D(mesh=Mesh.box(0.6), material=Material(colour=(0.3, 0.8, 0.4)), position=(-2.6, -0.6, 0.5))
72        )
73        self.add_child(
74            MeshInstance3D(mesh=Mesh.sphere(0.5), material=Material(colour=(0.3, 0.5, 0.95)), position=(2.6, -0.6, 0.5))
75        )
76
77        # The same feed as a 2D sprite; Sprite2D draws centred on its position.
78        hud = self.add_child(CanvasLayer())
79        pip = hud.add_child(Control(anchor=Anchor.BOTTOM_LEFT, size=(PIP_SIZE, PIP_SIZE), inset=20))
80        pip.add_child(Sprite2D(texture=feed, width=PIP_SIZE, height=PIP_SIZE, position=(PIP_SIZE / 2, PIP_SIZE / 2)))
81        hud.add_child(Label("SubViewport: live monitor feed", anchor=Anchor.TOP_LEFT, inset=20, font_size=24))
82
83    def on_update(self, dt):
84        self.cube.rotate_y(1.4 * dt)
85        self.cube.rotate_x(0.7 * dt)
86        self.monitor.rotate_y(0.25 * dt)
87
88
89if __name__ == "__main__":
90    App(title="SubViewport", width=1280, height=720).run(SubViewportScene())