Galacean Logo
English
English
Design and Implementation of Text Renderer

Design and Implementation of Text Renderer

technical

Introduction

Text rendering has always been a crucial missing foundational capability in the 2D aspect of the Galacean Engine. Developers could only use DOM elements to achieve text rendering in H5 projects, making the entire interactive project development process somewhat fragmented. To provide a seamless development experience and complete the 2D foundational functionality loop of the engine, we have added a text renderer in version v0.7 of the engine. It supports basic system font rendering, including size, color, bold, italic, and other style settings. Additionally, it supports multi-line display and various alignment methods.

Technical Solution Selection

Currently, there are several solutions for implementing text rendering on the Web:

  • DOM Solution: Use DOM elements to draw text
  • Canvas Solution: First draw the text in an off-screen Canvas 2D context, then get the texture and render it in WebGL
  • BMFont Solution: Pre-make the text as textures, extract and render them in WebGL as needed
  • SDF Solution: Pre-make the text as textures, but store the distance to the text edges, and finally render using a specific shader

The comparison of the above solutions in WebGL rendering is as follows:

AdvantagesDisadvantages
DOM Solution
  • Rich text styles
  • No extra textures needed for text
  • Simple implementation |
  • Fragmented development process
  • Text positioning is cumbersome
  • Cannot display between WebGL rendered elements | | Canvas Solution |
  • Rich text styles
  • Not complex to implement |
  • Text updates require real-time generation of new textures, causing performance pressure when text updates frequently and in large amounts | | BMFont Solution |
  • Pre-generated text textures, no real-time calculation needed
  • Supports various artistic text styles
  • Consistent display across different browsers |
  • Text needs to be pre-planned, new text requires re-exporting, and too much text can lead to large textures, exceeding 2048 is generally unsupported on mobile devices | | SDF Solution |
  • Pre-generated text textures, no real-time calculation needed
  • Generated textures can be very small
  • Text remains clear even when enlarged, not easily blurred
  • Easy to implement features like inner and outer strokes |
  • Higher implementation cost, requires specific shaders for rendering |

Conclusion Our core demand is to solve the fragmented development issue for developers, addressing the problem from scratch. Therefore, the DOM solution is directly passed over, and the remaining solutions each have their suitable use cases. We plan to implement them in three versions:

  • First version: Support for system text rendering
  • Second version: Support for artistic text rendering
  • Third version: Support for SDF font rendering

Design and Implementation

As the first article on the text renderer, this will introduce the design and implementation of the first version. We adopted the Canvas solution for system text rendering. Taking the display of "Hello World" on the screen as an example, the general process is as follows:

image.png

The above process omits many details, which will be shared step by step according to the internal implementation process in the order shown in the diagram.

Step 1: Design of the Text Renderer (TextRenderer)

image.png

First, we allow developers to render system text in their projects in the form of a component (TextRenderer). The usage of the TextRenderer component is as follows:

const textRenderer = entity.addComponent(TextRenderer);
textRenderer.xxx = yyy; // Set text and styles by setting various properties of the component

TextRenderer provides settings for display text, text color, font, size, style, line spacing, horizontal and vertical alignment, whether to wrap, and whether to overflow if it exceeds the set height:

export class TextRenderer extends Renderer {
  /**
    * Rendering color for the Text.
    */
  get color(): Color;
  set color(value: Color);
  /**
    * Rendering string for the Text.
    */
  get text(): string;
  set text(value: string);
  /**
    * The width of the TextRenderer (in 3D world coordinates).
    */
  get width(): number;
  set width(value: number);
  /**
    * The height of the TextRenderer (in 3D world coordinates).
    */
  get height(): number;
  set height(value: number);
  /**
    * The font of the Text.
    */
  get font(): Font;
  set font(value: Font);
  /**
    * The font size of the Text.
    */
  get fontSize(): number;
  set fontSize(value: number);
  /**
    * The style of the font.
    */
  get fontStyle(): FontStyle;
  set fontStyle(value: FontStyle);
  /**
    * The space between two lines (in pixels).
    */
  get lineSpacing(): number;
  set lineSpacing(value: number);
  /**
    * The horizontal alignment.
    */
  get horizontalAlignment(): TextHorizontalAlignment;
  set horizontalAlignment(value: TextHorizontalAlignment);
  /**
    * The vertical alignment.
    */
  get verticalAlignment(): TextVerticalAlignment;
  set verticalAlignment(value: TextVerticalAlignment);
  /**
    * Whether wrap text to next line when exceeds the width of the container.
    */
  get enableWrapping(): boolean;
  set enableWrapping(value: boolean);
  /**
    * The overflow mode.
    */
  get overflowMode(): OverflowMode;
  set overflowMode(value: OverflowMode);
}

There are a few properties worth mentioning. First is the font, which is a font asset that determines the basic information of the font. For example, the name field in the current version of the font asset can be used as fontFamily, determining which system font we use. In later versions, we will continue to introduce BMFont and SDFFont. This part only requires adding two new font resources to the engine's underlying structure, as follows:

image.png

When the text is too long and you want to display it in multiple lines, you can set enableWrapping = true and set width. If width < 0, nothing will be displayed. If width > 0, it will be split into multiple lines according to the following rules:

Step 2: Draw the text to the off-screen Canvas

image.png

When calling the _render of TextRenderer in each frame, it will first check whether the text or style has been updated. The update status is achieved through a dirty flag, as shown in the following code:

const isTextureDirty = this._isContainDirtyFlag(DirtyFlag.Property);
if (isTextureDirty) {
  // 更新文本,第 2、3 步都在这里
  this._updateText();
  this._setDirtyFlagFalse(DirtyFlag.Property);
}

If it detects that the text or style has been updated, it will execute the update function. In the _updateText function, three main tasks are performed: calculating the final display information of the text, updating the content to the off-screen Canvas, and obtaining the latest texture. The second step mainly completes the first two tasks.

The final display information of the text is calculated by combining the text content, style, and wrapping rules to determine the information needed to draw to the off-screen Canvas. The specific implementation is in TextUtils.measureText, and the structure of the returned information is as follows:

export interface TextMetrics {
  width: number; // 需要 Canvas 的宽度
  height: number; // 需要 Canvas 的高度
  lines: Array<string>; // 每行文本的数组
  lineWidths: Array<number>; // 每行文本的宽度
  lineHeight: number; // 每行文本的高度,包括行间距了
}

After obtaining TextMetrics, you can draw the latest text content to the off-screen Canvas. The core code is as follows:

public static updateText(
  textMetrics: TextMetrics,
  fontStr: string,
  horizontalAlignment: TextHorizontalAlignment,
  verticalAlignment: TextVerticalAlignment
): void {
  const { canvas, context } = TextUtils.textContext();
  const { width, height } = textMetrics;
  // 重置 canvas 的宽高
  canvas.width = width;
  canvas.height = height;
  // 清理 canvas
  context.font = fontStr;
  context.clearRect(0, 0, width, height);
  // 设置文本绘制相关信息
  context.textBaseline = "middle";
  context.fillStyle = "#fff";
 
  // draw lines.
  const { lines, lineHeight, lineWidths } = textMetrics;
  const halfLineHeight = lineHeight * 0.5;
  for (let i = 0, l = lines.length; i < l; ++i) {
    const lineWidth = lineWidths[i];
    const pos = TextUtils._tempVec2;
    // 根据对齐规则计算 pos
    TextUtils._calculateLinePosition(width, height, lineWidth, lineHeight, i, l, horizontalAlignment, verticalAlignment, pos);
    const { x, y } = pos;
    // 超出 canvas 范围的就不绘制了
    if (y + lineHeight >= 0 && y < height) {
      // 绘制文本
      context.fillText(lines[i], x, y + halfLineHeight);
    }
  }
}

Step 3: Convert Canvas data to texture

image.png

In the second step, the canvas width and height in the TextMetrics we obtained are not very precise but are estimated values. The estimated values are larger than the actual text display area, meaning there are blank pixels around the text content in the canvas. We illustrate this with the following image:

image.png

In the image above, assume the black part is the estimated canvas size, and the red part is what we actually need. If we directly convert the canvas data to texture, the texture will be wasted. If there are many text renderers, this can accumulate and be unfriendly to memory. Therefore, our optimization method is to crop the estimated canvas before converting it to texture, obtaining the data of the red part and converting it to texture. The overall process is as follows:

The implementation of the canvas cropping algorithm above refers to: https://gist.github.com/remy/784508

Step 4: Render to the display screen

image.png

The last step is the simplest. It directly reuses the previous SpriteRenderer rendering. The only difference between text rendering and sprite rendering in terms of implementation is the texture source. The texture for text rendering is generated based on the user's input, while the texture for sprite rendering comes from images directly loaded by the user.

Summary

In this version, the main issue addressed is the creation of text rendering from scratch, which can already meet some simple text display needs. If you want to implement more complex features, such as bullet comments, there may be memory pressure. The optimization method for this is mainly to provide caching based on individual characters. We will support this in the next version, and it will be seamless for developers.

It looks like you haven't pasted the Markdown content yet. Please provide the content you need translated, and I'll assist you accordingly.

Galacean Logo
Make fantastic web apps with the prospective
technologies and tools.
Copyright © 2025 Galacean
All rights reserved.