3D Text Rendering.

Three ways to put text in a 3D scene, and how non-Latin scripts are handled.

▶ Run in browser

Tags: 3d

  • Text2D draws 2D text placed with the usual Node2D transform. Under a CanvasLayer it stays pinned to the screen and keeps its authored colour after tonemapping. Assigning text redraws it, as the frame counter shows.

  • Text3D floats 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 whose text and colour rasterise the string into an image, and a Material samples it through albedo_tex_index. Changing text later 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())