Rendering text¶
2D overlays, 3D billboards, text baked onto surfaces, and CJK fallback.
▶ Run in browserTags: 3d
Shows the three ways to put text on screen and how non-Latin scripts are handled:
Text2D nodes draw a screen-space overlay on top of the 3D scene. Set
textandfont_scale; position with the usual Node2D transform.Text3D floats a depth-tested MSDF billboard at a 3D world position, so it is occluded by geometry in front of it instead of being pasted over the frame.
Text painted onto a cube face is a normal texture:
engine.create_text_texture()rasterises a string to an image that a Material samples like any other texture.Japanese (hiragana, katakana, kanji) renders with no extra setup, by two different routes. On the desktop, glyphs the primary font lacks are borrowed from a CJK face found on the machine and packed into the same atlas. A web export bakes only the glyphs the bundled font itself can draw, so those characters are rasterised on demand by the browser’s own text engine instead.
Controls: ESC: Quit
Usage: uv run python examples/features/3d/text.py
Source¶
1#!/usr/bin/env python3
2"""Rendering text: 2D overlays, 3D billboards, text baked onto surfaces, and CJK fallback.
3
4Shows the three ways to put text on screen and how non-Latin scripts are handled:
5
6 - Text2D nodes draw a screen-space overlay on top of the 3D scene. Set ``text``
7 and ``font_scale``; position with the usual Node2D transform.
8 - Text3D floats a depth-tested MSDF billboard at a 3D world position, so it is
9 occluded by geometry in front of it instead of being pasted over the frame.
10 - Text painted onto a cube face is a normal texture: ``engine.create_text_texture()``
11 rasterises a string to an image that a Material samples like any other texture.
12 - Japanese (hiragana, katakana, kanji) renders with no extra setup, by two
13 different routes. On the desktop, glyphs the primary font lacks are borrowed
14 from a CJK face found on the machine and packed into the same atlas. A web
15 export bakes only the glyphs the bundled font itself can draw, so those
16 characters are rasterised on demand by the browser's own text engine instead.
17
18Controls:
19 ESC: Quit
20
21Usage:
22 uv run python examples/features/3d/text.py
23"""
24
25import math
26
27from simvx.core import (
28 Camera3D,
29 DirectionalLight3D,
30 Input,
31 InputMap,
32 Key,
33 Material,
34 Mesh,
35 MeshInstance3D,
36 Node,
37 Text2D,
38 Text3D,
39 Vec3,
40)
41from simvx.graphics import App
42
43
44class RotatingCube(MeshInstance3D):
45 """A cube that rotates around its Y axis."""
46
47 def __init__(self, speed: float = 45.0, **kwargs):
48 super().__init__(**kwargs)
49 self.speed = speed
50
51 def on_update(self, dt: float):
52 self.rotate_y(math.radians(self.speed) * dt)
53
54
55class TextDemoScene(Node):
56 """Main demo scene with labeled cubes and text overlays."""
57
58 def on_ready(self):
59 InputMap.add_action("escape", [Key.ESCAPE])
60
61 engine = self.app.engine
62
63 # Camera: positioned to see cubes at origin
64 camera = self.add_child(
65 Camera3D(
66 name="Camera",
67 position=Vec3(0, 3, 8),
68 )
69 )
70 camera.look_at(Vec3(0, 0, 0))
71 camera.fov = 50.0
72
73 light = self.add_child(DirectionalLight3D(name="Sun"))
74 light.look_at(Vec3(-1, -2, -1))
75
76 # Create text textures for each cube label
77 labels = [
78 ("RED", (1.0, 0.3, 0.3, 1.0)),
79 ("GREEN", (0.3, 1.0, 0.4, 1.0)),
80 ("BLUE", (0.4, 0.5, 1.0, 1.0)),
81 ]
82 text_textures = []
83 for label, colour in labels:
84 tt = engine.create_text_texture(size=48, width=256, height=64)
85 tt.colour = colour
86 tt.text = label
87 text_textures.append(tt)
88
89 # Red cube: left, rotating slowly
90 red = self.add_child(
91 RotatingCube(
92 name="RedCube",
93 position=Vec3(-2.5, 0, 0),
94 speed=30.0,
95 )
96 )
97 red.mesh = Mesh.cube(size=1.5)
98 red.material = Material(colour=(0.9, 0.2, 0.2, 1.0))
99 red.material.albedo_tex_index = text_textures[0].texture_index
100
101 # Green cube: center, rotating faster
102 green = self.add_child(
103 RotatingCube(
104 name="GreenCube",
105 position=Vec3(0, 0, 0),
106 speed=60.0,
107 )
108 )
109 green.mesh = Mesh.cube(size=1.5)
110 green.material = Material(colour=(0.2, 0.8, 0.3, 1.0))
111 green.material.albedo_tex_index = text_textures[1].texture_index
112
113 # Blue cube: right, counter-rotating
114 blue = self.add_child(
115 RotatingCube(
116 name="BlueCube",
117 position=Vec3(2.5, 0, 0),
118 speed=-45.0,
119 )
120 )
121 blue.mesh = Mesh.cube(size=1.5)
122 blue.material = Material(colour=(0.2, 0.4, 0.9, 1.0))
123 blue.material.albedo_tex_index = text_textures[2].texture_index
124
125 # --- 3D text billboard ---
126 # A world-space label above the centre cube. It faces the camera but is
127 # depth-tested against the scene, so a cube passing in front hides it.
128 self.add_child(
129 Text3D(
130 name="CubeLabel",
131 text="Text3D billboard",
132 position=Vec3(0, 1.6, 0),
133 font_scale=3.0,
134 colour=(1.0, 0.85, 0.4, 1.0),
135 )
136 )
137
138 # --- 2D text overlays ---
139 self.add_child(
140 Text2D(
141 name="Title",
142 text="Text Rendering",
143 position=(10.0, 10.0),
144 font_scale=2.0,
145 colour=(1.0, 1.0, 1.0, 1.0),
146 )
147 )
148
149 self.add_child(
150 Text2D(
151 name="Subtitle",
152 text="Baked text textures on the cubes, a Text3D billboard, and these Text2D overlays",
153 position=(10.0, 50.0),
154 font_scale=1.2,
155 colour=(0.71, 0.71, 0.71, 1.0),
156 )
157 )
158
159 self.add_child(
160 Text2D(
161 name="Hiragana",
162 text="ひらがな: あいうえお かきくけこ",
163 position=(10.0, 90.0),
164 font_scale=1.2,
165 colour=(1.0, 0.59, 0.78, 1.0),
166 )
167 )
168 self.add_child(
169 Text2D(
170 name="Katakana",
171 text="カタカナ: アイウエオ カキクケコ",
172 position=(10.0, 120.0),
173 font_scale=1.2,
174 colour=(0.59, 0.78, 1.0, 1.0),
175 )
176 )
177 self.add_child(
178 Text2D(
179 name="Kanji",
180 text="漢字: 東京都 日本語テスト",
181 position=(10.0, 150.0),
182 font_scale=1.2,
183 colour=(1.0, 0.9, 0.39, 1.0),
184 )
185 )
186
187 # Dynamic frame counter
188 self.frame_text = self.add_child(
189 Text2D(
190 name="FrameCounter",
191 text="Frame: 0",
192 position=(10.0, 190.0),
193 font_scale=1.0,
194 colour=(0.39, 1.0, 0.39, 1.0),
195 )
196 )
197
198 self.frame = 0
199
200 def on_update(self, dt: float):
201 self.frame += 1
202 self.frame_text.text = f"Frame: {self.frame} dt: {dt*1000:.1f}ms"
203
204 if Input.is_action_just_pressed("escape"):
205 self.app.quit()
206
207
208def main():
209 app = App(
210 title="Text Rendering",
211 width=1280,
212 height=720,
213 )
214 app.run(TextDemoScene())
215
216
217if __name__ == "__main__":
218 main()