Skip to content

Commit ad7d067

Browse files
authored
Merge pull request #382 from webgpu/text-rendering-msdf
Add an MSDF-based text rendering sample
2 parents 9044129 + 3c77902 commit ad7d067

File tree

8 files changed

+967
-0
lines changed

8 files changed

+967
-0
lines changed

public/assets/font/ya-hei-ascii-msdf.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

public/assets/font/ya-hei-ascii.png

105 KB
Loading

sample/textRenderingMsdf/index.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>webgpu-samples: textRenderingMsdf</title>
7+
<style>
8+
html, body {
9+
margin: 0; /* remove default margin */
10+
height: 100%; /* make body fill the browser window */
11+
display: flex;
12+
place-content: center center;
13+
}
14+
canvas {
15+
width: 600px;
16+
height: 600px;
17+
max-width: 100%;
18+
display: block;
19+
}
20+
</style>
21+
<script defer src="main.js" type="module"></script>
22+
<script defer type="module" src="../../js/iframe-helper.js"></script>
23+
</head>
24+
<body>
25+
<canvas></canvas>
26+
</body>
27+
</html>

sample/textRenderingMsdf/main.ts

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import { mat4, vec3 } from 'wgpu-matrix';
2+
3+
import {
4+
cubeVertexArray,
5+
cubeVertexSize,
6+
cubeUVOffset,
7+
cubePositionOffset,
8+
cubeVertexCount,
9+
} from '../../meshes/cube';
10+
import { MsdfTextRenderer } from './msdfText';
11+
12+
import basicVertWGSL from '../../shaders/basic.vert.wgsl';
13+
import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl';
14+
15+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
16+
const adapter = await navigator.gpu.requestAdapter();
17+
const device = await adapter.requestDevice();
18+
19+
const context = canvas.getContext('webgpu') as GPUCanvasContext;
20+
21+
const devicePixelRatio = window.devicePixelRatio || 1;
22+
canvas.width = canvas.clientWidth * devicePixelRatio;
23+
canvas.height = canvas.clientHeight * devicePixelRatio;
24+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
25+
const depthFormat = 'depth24plus';
26+
27+
context.configure({
28+
device,
29+
format: presentationFormat,
30+
alphaMode: 'premultiplied',
31+
});
32+
33+
const textRenderer = new MsdfTextRenderer(
34+
device,
35+
presentationFormat,
36+
depthFormat
37+
);
38+
const font = await textRenderer.createFont(
39+
new URL(
40+
'../../assets/font/ya-hei-ascii-msdf.json',
41+
import.meta.url
42+
).toString()
43+
);
44+
45+
function getTextTransform(
46+
position: [number, number, number],
47+
rotation?: [number, number, number]
48+
) {
49+
const textTransform = mat4.create();
50+
mat4.identity(textTransform);
51+
mat4.translate(textTransform, position, textTransform);
52+
if (rotation && rotation[0] != 0) {
53+
mat4.rotateX(textTransform, rotation[0], textTransform);
54+
}
55+
if (rotation && rotation[1] != 0) {
56+
mat4.rotateY(textTransform, rotation[1], textTransform);
57+
}
58+
if (rotation && rotation[2] != 0) {
59+
mat4.rotateZ(textTransform, rotation[2], textTransform);
60+
}
61+
return textTransform;
62+
}
63+
64+
const textTransforms = [
65+
getTextTransform([0, 0, 1.1]),
66+
getTextTransform([0, 0, -1.1], [0, Math.PI, 0]),
67+
getTextTransform([1.1, 0, 0], [0, Math.PI / 2, 0]),
68+
getTextTransform([-1.1, 0, 0], [0, -Math.PI / 2, 0]),
69+
getTextTransform([0, 1.1, 0], [-Math.PI / 2, 0, 0]),
70+
getTextTransform([0, -1.1, 0], [Math.PI / 2, 0, 0]),
71+
];
72+
73+
const titleText = textRenderer.formatText(font, `WebGPU`, {
74+
centered: true,
75+
pixelScale: 1 / 128,
76+
});
77+
const largeText = textRenderer.formatText(
78+
font,
79+
`
80+
WebGPU exposes an API for performing operations, such as rendering
81+
and computation, on a Graphics Processing Unit.
82+
83+
Graphics Processing Units, or GPUs for short, have been essential
84+
in enabling rich rendering and computational applications in personal
85+
computing. WebGPU is an API that exposes the capabilities of GPU
86+
hardware for the Web. The API is designed from the ground up to
87+
efficiently map to (post-2014) native GPU APIs. WebGPU is not related
88+
to WebGL and does not explicitly target OpenGL ES.
89+
90+
WebGPU sees physical GPU hardware as GPUAdapters. It provides a
91+
connection to an adapter via GPUDevice, which manages resources, and
92+
the device’s GPUQueues, which execute commands. GPUDevice may have
93+
its own memory with high-speed access to the processing units.
94+
GPUBuffer and GPUTexture are the physical resources backed by GPU
95+
memory. GPUCommandBuffer and GPURenderBundle are containers for
96+
user-recorded commands. GPUShaderModule contains shader code. The
97+
other resources, such as GPUSampler or GPUBindGroup, configure the
98+
way physical resources are used by the GPU.
99+
100+
GPUs execute commands encoded in GPUCommandBuffers by feeding data
101+
through a pipeline, which is a mix of fixed-function and programmable
102+
stages. Programmable stages execute shaders, which are special
103+
programs designed to run on GPU hardware. Most of the state of a
104+
pipeline is defined by a GPURenderPipeline or a GPUComputePipeline
105+
object. The state not included in these pipeline objects is set
106+
during encoding with commands, such as beginRenderPass() or
107+
setBlendConstant().`,
108+
{ pixelScale: 1 / 256 }
109+
);
110+
111+
const text = [
112+
textRenderer.formatText(font, 'Front', {
113+
centered: true,
114+
pixelScale: 1 / 128,
115+
color: [1, 0, 0, 1],
116+
}),
117+
textRenderer.formatText(font, 'Back', {
118+
centered: true,
119+
pixelScale: 1 / 128,
120+
color: [0, 1, 1, 1],
121+
}),
122+
textRenderer.formatText(font, 'Right', {
123+
centered: true,
124+
pixelScale: 1 / 128,
125+
color: [0, 1, 0, 1],
126+
}),
127+
textRenderer.formatText(font, 'Left', {
128+
centered: true,
129+
pixelScale: 1 / 128,
130+
color: [1, 0, 1, 1],
131+
}),
132+
textRenderer.formatText(font, 'Top', {
133+
centered: true,
134+
pixelScale: 1 / 128,
135+
color: [0, 0, 1, 1],
136+
}),
137+
textRenderer.formatText(font, 'Bottom', {
138+
centered: true,
139+
pixelScale: 1 / 128,
140+
color: [1, 1, 0, 1],
141+
}),
142+
143+
titleText,
144+
largeText,
145+
];
146+
147+
// Create a vertex buffer from the cube data.
148+
const verticesBuffer = device.createBuffer({
149+
size: cubeVertexArray.byteLength,
150+
usage: GPUBufferUsage.VERTEX,
151+
mappedAtCreation: true,
152+
});
153+
new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
154+
verticesBuffer.unmap();
155+
156+
const pipeline = device.createRenderPipeline({
157+
layout: 'auto',
158+
vertex: {
159+
module: device.createShaderModule({
160+
code: basicVertWGSL,
161+
}),
162+
buffers: [
163+
{
164+
arrayStride: cubeVertexSize,
165+
attributes: [
166+
{
167+
// position
168+
shaderLocation: 0,
169+
offset: cubePositionOffset,
170+
format: 'float32x4',
171+
},
172+
{
173+
// uv
174+
shaderLocation: 1,
175+
offset: cubeUVOffset,
176+
format: 'float32x2',
177+
},
178+
],
179+
},
180+
],
181+
},
182+
fragment: {
183+
module: device.createShaderModule({
184+
code: vertexPositionColorWGSL,
185+
}),
186+
targets: [
187+
{
188+
format: presentationFormat,
189+
},
190+
],
191+
},
192+
primitive: {
193+
// Backface culling since the cube is solid piece of geometry.
194+
// Faces pointing away from the camera will be occluded by faces
195+
// pointing toward the camera.
196+
cullMode: 'back',
197+
},
198+
199+
// Enable depth testing so that the fragment closest to the camera
200+
// is rendered in front.
201+
depthStencil: {
202+
depthWriteEnabled: true,
203+
depthCompare: 'less',
204+
format: depthFormat,
205+
},
206+
});
207+
208+
const depthTexture = device.createTexture({
209+
size: [canvas.width, canvas.height],
210+
format: depthFormat,
211+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
212+
});
213+
214+
const uniformBufferSize = 4 * 16; // 4x4 matrix
215+
const uniformBuffer = device.createBuffer({
216+
size: uniformBufferSize,
217+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
218+
});
219+
220+
const uniformBindGroup = device.createBindGroup({
221+
layout: pipeline.getBindGroupLayout(0),
222+
entries: [
223+
{
224+
binding: 0,
225+
resource: {
226+
buffer: uniformBuffer,
227+
},
228+
},
229+
],
230+
});
231+
232+
const renderPassDescriptor: GPURenderPassDescriptor = {
233+
colorAttachments: [
234+
{
235+
view: undefined, // Assigned later
236+
237+
clearValue: [0, 0, 0, 1],
238+
loadOp: 'clear',
239+
storeOp: 'store',
240+
},
241+
],
242+
depthStencilAttachment: {
243+
view: depthTexture.createView(),
244+
245+
depthClearValue: 1.0,
246+
depthLoadOp: 'clear',
247+
depthStoreOp: 'store',
248+
},
249+
};
250+
251+
const aspect = canvas.width / canvas.height;
252+
const projectionMatrix = mat4.perspective((2 * Math.PI) / 5, aspect, 1, 100.0);
253+
const modelViewProjectionMatrix = mat4.create();
254+
255+
const start = Date.now();
256+
function getTransformationMatrix() {
257+
const now = Date.now() / 5000;
258+
const viewMatrix = mat4.identity();
259+
mat4.translate(viewMatrix, vec3.fromValues(0, 0, -5), viewMatrix);
260+
261+
const modelMatrix = mat4.identity();
262+
mat4.translate(modelMatrix, vec3.fromValues(0, 2, -3), modelMatrix);
263+
mat4.rotate(
264+
modelMatrix,
265+
vec3.fromValues(Math.sin(now), Math.cos(now), 0),
266+
1,
267+
modelMatrix
268+
);
269+
270+
// Update the matrix for the cube
271+
mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix);
272+
mat4.multiply(
273+
modelViewProjectionMatrix,
274+
modelMatrix,
275+
modelViewProjectionMatrix
276+
);
277+
278+
// Update the projection and view matrices for the text
279+
textRenderer.updateCamera(projectionMatrix, viewMatrix);
280+
281+
// Update the transform of all the text surrounding the cube
282+
const textMatrix = mat4.create();
283+
for (const [index, transform] of textTransforms.entries()) {
284+
mat4.multiply(modelMatrix, transform, textMatrix);
285+
text[index].setTransform(textMatrix);
286+
}
287+
288+
// Update the transform of the larger block of text
289+
const crawl = ((Date.now() - start) / 2500) % 14;
290+
mat4.identity(textMatrix);
291+
mat4.rotateX(textMatrix, -Math.PI / 8, textMatrix);
292+
mat4.translate(textMatrix, [0, crawl - 3, 0], textMatrix);
293+
titleText.setTransform(textMatrix);
294+
mat4.translate(textMatrix, [-3, -0.1, 0], textMatrix);
295+
largeText.setTransform(textMatrix);
296+
297+
return modelViewProjectionMatrix as Float32Array;
298+
}
299+
300+
function frame() {
301+
const transformationMatrix = getTransformationMatrix();
302+
device.queue.writeBuffer(
303+
uniformBuffer,
304+
0,
305+
transformationMatrix.buffer,
306+
transformationMatrix.byteOffset,
307+
transformationMatrix.byteLength
308+
);
309+
renderPassDescriptor.colorAttachments[0].view = context
310+
.getCurrentTexture()
311+
.createView();
312+
313+
const commandEncoder = device.createCommandEncoder();
314+
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
315+
passEncoder.setPipeline(pipeline);
316+
passEncoder.setBindGroup(0, uniformBindGroup);
317+
passEncoder.setVertexBuffer(0, verticesBuffer);
318+
passEncoder.draw(cubeVertexCount, 1, 0, 0);
319+
320+
textRenderer.render(passEncoder, ...text);
321+
322+
passEncoder.end();
323+
device.queue.submit([commandEncoder.finish()]);
324+
325+
requestAnimationFrame(frame);
326+
}
327+
requestAnimationFrame(frame);

sample/textRenderingMsdf/meta.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default {
2+
name: 'Text Rendering - MSDF',
3+
description: `This example uses multichannel signed distance fields (MSDF) to render text. MSDF
4+
fonts are more complex to implement than using Canvas 2D to generate text, but the resulting
5+
text looks smoother while using less memory than the Canvas 2D approach, especially at high
6+
zoom levels. They can be used to render larger amounts of text efficiently.
7+
8+
The font texture is generated using [Don McCurdy's MSDF font generation tool](https://msdf-bmfont.donmccurdy.com/)`,
9+
filename: __DIRNAME__,
10+
sources: [
11+
{ path: 'main.ts' },
12+
{ path: 'msdfText.ts' },
13+
{ path: 'msdfText.wgsl' },
14+
{ path: '../../shaders/basic.vert.wgsl' },
15+
{ path: '../../shaders/vertexPositionColor.frag.wgsl' },
16+
{ path: '../../meshes/cube.ts' },
17+
],
18+
};

0 commit comments

Comments
 (0)