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_process(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                x=10.0,
124                y=10.0,
125                font_scale=2.0,
126                colour=(1.0, 1.0, 1.0, 1.0),
127            )
128        )
129
130        self.add_child(
131            Text2D(
132                name="Subtitle",
133                text="3 rotating cubes with text textures + MSDF overlay",
134                x=10.0,
135                y=50.0,
136                font_scale=1.2,
137                colour=(0.71, 0.71, 0.71, 1.0),
138            )
139        )
140
141        self.add_child(
142            Text2D(name="Hiragana", text="ひらがな: あいうえお かきくけこ",
143                   x=10.0, y=90.0, font_scale=1.2, colour=(1.0, 0.59, 0.78, 1.0))
144        )
145        self.add_child(
146            Text2D(name="Katakana", text="カタカナ: アイウエオ カキクケコ",
147                   x=10.0, y=120.0, font_scale=1.2, colour=(0.59, 0.78, 1.0, 1.0))
148        )
149        self.add_child(
150            Text2D(name="Kanji", text="漢字: 東京都 日本語テスト",
151                   x=10.0, y=150.0, font_scale=1.2, colour=(1.0, 0.9, 0.39, 1.0))
152        )
153
154        # Dynamic frame counter
155        self.frame_text = self.add_child(
156            Text2D(
157                name="FrameCounter",
158                text="Frame: 0",
159                x=10.0,
160                y=190.0,
161                font_scale=1.0,
162                colour=(0.39, 1.0, 0.39, 1.0),
163            )
164        )
165
166        self.frame = 0
167
168    def on_process(self, dt: float):
169        self.frame += 1
170        self.frame_text.text = f"Frame: {self.frame}  dt: {dt*1000:.1f}ms"
171
172        if Input.is_action_just_pressed("escape"):
173            self.app.quit()
174
175
176def main():
177    app = App(
178        title="SimVX Node Tree + Text Demo",
179        width=1280,
180        height=720,
181    )
182    app.run(TextDemoScene())
183
184
185if __name__ == "__main__":
186    main()