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.
Currently, there are several solutions for implementing text rendering on the Web:
The comparison of the above solutions in WebGL rendering is as follows:
Advantages | Disadvantages | |
---|---|---|
DOM Solution |
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:
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:
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.
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:
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:
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);
}
}
}
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:
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
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.
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.