3D Text Rendering.¶
Three ways to put text in a 3D scene, and how non-Latin scripts are handled.
▶ Run in browserTags: 3d
Text2Ddraws 2D text placed with the usual Node2D transform. Under aCanvasLayerit stays pinned to the screen and keeps its authored colour after tonemapping. Assigningtextredraws it, as the frame counter shows.Text3Dfloats a billboard at a world position. It always faces the camera but is depth-tested, so geometry in front of it hides it: the billboard here stands behind the green cube, which hides the part of it the cube passes in front of as it spins.Text on a surface is an ordinary texture:
app.engine.create_text_texture()returns an object whosetextandcolourrasterise the string into an image, and aMaterialsamples it throughalbedo_tex_index. Changingtextlater rewrites the same image, so the material never needs rebinding: each cube writes how many turns it has made into its own texture.
Japanese (hiragana, katakana and kanji) needs no setup. On the desktop, glyphs the primary font lacks are borrowed from a CJK font installed on the machine and packed into the same atlas; in a web export the browser’s own text engine draws them on demand.
Source¶
1"""3D Text Rendering.
2
3Three ways to put text in a 3D scene, and how non-Latin scripts are handled.
4
5- ``Text2D`` draws 2D text placed with the usual Node2D transform. Under a
6 ``CanvasLayer`` it stays pinned to the screen and keeps its authored colour
7 after tonemapping. Assigning ``text`` redraws it, as the frame counter shows.
8- ``Text3D`` floats a billboard at a world position. It always faces the camera
9 but is depth-tested, so geometry in front of it hides it: the billboard here
10 stands behind the green cube, which hides the part of it the cube passes in
11 front of as it spins.
12- Text on a surface is an ordinary texture: ``app.engine.create_text_texture()``
13 returns an object whose ``text`` and ``colour`` rasterise the string into an
14 image, and a ``Material`` samples it through ``albedo_tex_index``. Changing
15 ``text`` later rewrites the same image, so the material never needs rebinding:
16 each cube writes how many turns it has made into its own texture.
17
18Japanese (hiragana, katakana and kanji) needs no setup. On the desktop, glyphs
19the primary font lacks are borrowed from a CJK font installed on the machine
20and packed into the same atlas; in a web export the browser's own text engine
21draws them on demand.
22"""
23
24import math
25
26from simvx.core import (
27 Camera3D,
28 CanvasLayer,
29 DirectionalLight3D,
30 Material,
31 Mesh,
32 MeshInstance3D,
33 Node,
34 Property,
35 Text2D,
36 Text3D,
37)
38from simvx.graphics import App
39
40# name, x position, spin in degrees per second, cube colour, label colour
41CUBES = [
42 ("RED", -2.5, 30.0, (0.9, 0.2, 0.2, 1.0), (1.0, 0.3, 0.3, 1.0)),
43 ("GREEN", 0.0, 60.0, (0.2, 0.8, 0.3, 1.0), (0.3, 1.0, 0.4, 1.0)),
44 ("BLUE", 2.5, -45.0, (0.2, 0.4, 0.9, 1.0), (0.4, 0.5, 1.0, 1.0)),
45]
46
47# text, y position, font scale, colour
48HUD_LINES = [
49 ("Text Rendering", 10, 2.0, (1.0, 1.0, 1.0, 1.0)),
50 ("Text2D overlays, a Text3D billboard, text textures on cubes", 50, 1.2, (0.71, 0.71, 0.71, 1.0)),
51 ("ひらがな: あいうえお かきくけこ", 90, 1.2, (1.0, 0.59, 0.78, 1.0)),
52 ("カタカナ: アイウエオ カキクケコ", 120, 1.2, (0.59, 0.78, 1.0, 1.0)),
53 ("漢字: 東京都 日本語テスト", 150, 1.2, (1.0, 0.9, 0.39, 1.0)),
54]
55
56
57class SpinningCube(MeshInstance3D):
58 """A cube that spins and writes its name and turn count into its own text texture."""
59
60 speed = Property(45.0, hint="Spin about Y in degrees per second")
61
62 def __init__(self, label_texture, **kwargs):
63 super().__init__(**kwargs)
64 self.label_texture = label_texture
65 self.turned = 0.0
66
67 def on_update(self, dt: float):
68 self.rotate_y(math.radians(self.speed) * dt)
69 self.turned += abs(self.speed) * dt
70 # Assigning the same string is free; a new one re-rasterises into the bound image.
71 self.label_texture.text = f"{self.name} {int(self.turned // 360)}"
72
73
74class TextDemoScene(Node):
75 def on_ready(self):
76 self.add_child(Camera3D(position=(0, 3, 8), look_at=(0, 0.6, 0), fov=50.0))
77 self.add_child(DirectionalLight3D(direction=(-1, -2, -1)))
78
79 for label, x, speed, colour, text_colour in CUBES:
80 texture = self.app.engine.create_text_texture(size=48, width=256, height=64)
81 texture.colour = text_colour
82 texture.text = f"{label} 0"
83 material = Material(colour=colour)
84 material.albedo_tex_index = texture.texture_index
85 self.add_child(
86 SpinningCube(
87 texture, name=label, position=(x, 0, 0), speed=speed, mesh=Mesh.box(1.5), material=material
88 )
89 )
90
91 # Depth-tested: it stands behind the green cube, which hides the part of it the cube passes in front of.
92 self.add_child(
93 Text3D(text="Text3D billboard", position=(0, 0.5, -1.2), font_scale=3.0, colour=(1.0, 0.85, 0.4, 1.0))
94 )
95
96 hud = self.add_child(CanvasLayer())
97 for text, y, scale, colour in HUD_LINES:
98 hud.add_child(Text2D(text=text, position=(10, y), font_scale=scale, colour=colour))
99 self.counter = hud.add_child(Text2D(position=(10, 190), colour=(0.39, 1.0, 0.39, 1.0)))
100 self.frame = 0
101
102 def on_update(self, dt: float):
103 self.frame += 1
104 self.counter.text = f"Frame: {self.frame} dt: {dt * 1000:.1f}ms"
105
106
107if __name__ == "__main__":
108 App(title="Text Rendering", width=1280, height=720).run(TextDemoScene())