Node tree text demo

3D cubes with text textures + 2D text overlay + Japanese text.

▶ Run in browser

Tags: 3d

Validates:

  • Node tree → SceneAdapter → Renderer pipeline

  • Camera3D view/projection matrices (row-major, Y-flipped)

  • MeshInstance3D model_matrix with quaternion rotation

  • Text2D overlay bridged to MSDF text renderer

  • Lazy glyph loading for Japanese characters

  • Text-as-texture on 3D geometry via Engine.create_text_texture()

Controls: ESC: Quit

Run with: uv run python examples/features/3d/text.py

Source

  1#!/usr/bin/env python3
  2"""Node tree text demo: 3D cubes with text textures + 2D text overlay + Japanese text.
  3
  4Validates:
  5  - Node tree → SceneAdapter → Renderer pipeline
  6  - Camera3D view/projection matrices (row-major, Y-flipped)
  7  - MeshInstance3D model_matrix with quaternion rotation
  8  - Text2D overlay bridged to MSDF text renderer
  9  - Lazy glyph loading for Japanese characters
 10  - Text-as-texture on 3D geometry via Engine.create_text_texture()
 11
 12Controls:
 13    ESC: Quit
 14
 15Run with:
 16    uv run python examples/features/3d/text.py
 17"""
 18
 19import math
 20
 21from simvx.core import (
 22    Camera3D,
 23    DirectionalLight3D,
 24    Input,
 25    InputMap,
 26    Key,
 27    Material,
 28    Mesh,
 29    MeshInstance3D,
 30    Node,
 31    Text2D,
 32    Vec3,
 33)
 34from simvx.graphics import App
 35
 36
 37class RotatingCube(MeshInstance3D):
 38    """A cube that rotates around its Y axis."""
 39
 40    def __init__(self, speed: float = 45.0, **kwargs):
 41        super().__init__(**kwargs)
 42        self.speed = speed
 43
 44    def on_update(self, dt: float):
 45        self.rotate_y(math.radians(self.speed) * dt)
 46
 47
 48class TextDemoScene(Node):
 49    """Main demo scene with labeled cubes and text overlays."""
 50
 51    def on_ready(self):
 52        InputMap.add_action("escape", [Key.ESCAPE])
 53
 54        engine = self.app.engine
 55
 56        # Camera: positioned to see cubes at origin
 57        camera = self.add_child(
 58            Camera3D(
 59                name="Camera",
 60                position=Vec3(0, 3, 8),
 61            )
 62        )
 63        camera.look_at(Vec3(0, 0, 0))
 64        camera.fov = 50.0
 65
 66        light = self.add_child(DirectionalLight3D(name="Sun"))
 67        light.look_at(Vec3(-1, -2, -1))
 68
 69        # Create text textures for each cube label
 70        labels = [
 71            ("RED", (1.0, 0.3, 0.3, 1.0)),
 72            ("GREEN", (0.3, 1.0, 0.4, 1.0)),
 73            ("BLUE", (0.4, 0.5, 1.0, 1.0)),
 74        ]
 75        text_textures = []
 76        for label, colour in labels:
 77            tt = engine.create_text_texture(size=48, width=256, height=64)
 78            tt.colour = colour
 79            tt.text = label
 80            text_textures.append(tt)
 81
 82        # Red cube: left, rotating slowly
 83        red = self.add_child(
 84            RotatingCube(
 85                name="RedCube",
 86                position=Vec3(-2.5, 0, 0),
 87                speed=30.0,
 88            )
 89        )
 90        red.mesh = Mesh.cube(size=1.5)
 91        red.material = Material(colour=(0.9, 0.2, 0.2, 1.0))
 92        red.material.albedo_tex_index = text_textures[0].texture_index
 93
 94        # Green cube: center, rotating faster
 95        green = self.add_child(
 96            RotatingCube(
 97                name="GreenCube",
 98                position=Vec3(0, 0, 0),
 99                speed=60.0,
100            )
101        )
102        green.mesh = Mesh.cube(size=1.5)
103        green.material = Material(colour=(0.2, 0.8, 0.3, 1.0))
104        green.material.albedo_tex_index = text_textures[1].texture_index
105
106        # Blue cube: right, counter-rotating
107        blue = self.add_child(
108            RotatingCube(
109                name="BlueCube",
110                position=Vec3(2.5, 0, 0),
111                speed=-45.0,
112            )
113        )
114        blue.mesh = Mesh.cube(size=1.5)
115        blue.material = Material(colour=(0.2, 0.4, 0.9, 1.0))
116        blue.material.albedo_tex_index = text_textures[2].texture_index
117
118        # --- 2D text overlays ---
119        self.add_child(
120            Text2D(
121                name="Title",
122                text="SimVX Node Tree Demo",
123                position=(10.0, 10.0), font_scale=2.0,
124                colour=(1.0, 1.0, 1.0, 1.0),
125            )
126        )
127
128        self.add_child(
129            Text2D(
130                name="Subtitle",
131                text="3 rotating cubes with text textures + MSDF overlay",
132                position=(10.0, 50.0), font_scale=1.2,
133                colour=(0.71, 0.71, 0.71, 1.0),
134            )
135        )
136
137        self.add_child(
138            Text2D(name="Hiragana", text="ひらがな: あいうえお かきくけこ",
139                   position=(10.0, 90.0), font_scale=1.2, colour=(1.0, 0.59, 0.78, 1.0))
140        )
141        self.add_child(
142            Text2D(name="Katakana", text="カタカナ: アイウエオ カキクケコ",
143                   position=(10.0, 120.0), font_scale=1.2, colour=(0.59, 0.78, 1.0, 1.0))
144        )
145        self.add_child(
146            Text2D(name="Kanji", text="漢字: 東京都 日本語テスト",
147                   position=(10.0, 150.0), font_scale=1.2, colour=(1.0, 0.9, 0.39, 1.0))
148        )
149
150        # Dynamic frame counter
151        self.frame_text = self.add_child(
152            Text2D(
153                name="FrameCounter",
154                text="Frame: 0",
155                position=(10.0, 190.0), font_scale=1.0,
156                colour=(0.39, 1.0, 0.39, 1.0),
157            )
158        )
159
160        self.frame = 0
161
162    def on_update(self, dt: float):
163        self.frame += 1
164        self.frame_text.text = f"Frame: {self.frame}  dt: {dt*1000:.1f}ms"
165
166        if Input.is_action_just_pressed("escape"):
167            self.app.quit()
168
169
170def main():
171    app = App(
172        title="SimVX Node Tree + Text Demo",
173        width=1280,
174        height=720,
175    )
176    app.run(TextDemoScene())
177
178
179if __name__ == "__main__":
180    main()