p3nGu1nZz commited on
Commit
4c4106b
·
1 Parent(s): 21e9f88

added basic text rendering

Browse files
Files changed (3) hide show
  1. REFERENCE.md +1 -0
  2. index.html +347 -75
  3. style.css +1 -6
REFERENCE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ https://www.bing.com/search?pglt=929&q=matrix.module.js&cvid=f489a108db90408c8b575d292bfd7c55&gs_lcrp=EgRlZGdlKgYIABBFGDkyBggAEEUYOTIGCAEQABhAMgYIAhAAGEAyBggDEAAYQDIGCAQQABhA0gEHNjI0ajBqMagCALACAA&FORM=ANNTA1&PC=U531&EPC=ExpTester
index.html CHANGED
@@ -1,89 +1,361 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Minimalist White Circle with FPS</title>
7
- <style>
8
- body {
9
- margin: 0;
10
- overflow: hidden;
11
- }
12
- canvas {
13
- display: block;
14
- }
15
- </style>
16
  </head>
 
17
  <body>
18
- <canvas id="canvas"></canvas>
19
- <script>
20
- const canvas = document.getElementById('canvas'),
21
- context = canvas.getContext('2d'),
22
- versionText = 'v1.0.0';
23
- let fps = 60,
24
- lastFrameTime = performance.now();
25
-
26
- function initializeCanvas() {
27
- canvas.width = window.innerWidth;
28
- canvas.height = window.innerHeight;
29
- context.fillStyle = 'black';
30
- context.fillRect(0, 0, canvas.width, canvas.height);
31
- }
32
-
33
- function drawWhiteCircle() {
34
- const centerX = canvas.width / 2,
35
- centerY = canvas.height / 2,
36
- radius = Math.min(canvas.width, canvas.height) * 0.00625;
37
- context.beginPath();
38
- context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
39
- context.strokeStyle = 'white';
40
- context.lineWidth = 0.125 * parseFloat(getComputedStyle(document.documentElement).fontSize);
41
- context.stroke();
42
- }
43
-
44
- function drawVersionText() {
45
- context.font = '1em Arial';
46
- context.fillStyle = 'white';
47
- context.textAlign = 'right';
48
- context.textBaseline = 'bottom';
49
- context.fillText(versionText, canvas.width - 10, canvas.height - 10);
50
- }
51
-
52
- function drawFPS() {
53
- context.font = '1em Arial';
54
- context.fillStyle = 'white';
55
- context.textAlign = 'left';
56
- context.textBaseline = 'top';
57
- context.fillText(fps.toFixed(1) + ' fps', 10, 10);
58
- }
59
-
60
- function updateFPS() {
61
- const now = performance.now(),
62
- deltaTime = now - lastFrameTime;
63
- lastFrameTime = now;
64
- if (deltaTime > 0) {
65
- const currentFPS = 1000 / deltaTime;
66
- if (!isNaN(currentFPS) && isFinite(currentFPS)) {
67
- fps = fps + (currentFPS - fps) * 0.1;
68
  }
69
  }
 
 
70
  }
71
-
72
- function draw() {
73
- initializeCanvas();
74
- drawWhiteCircle();
75
- drawVersionText();
76
- drawFPS();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
-
79
- function loop() {
80
- updateFPS();
81
- draw();
82
- requestAnimationFrame(loop);
83
  }
84
-
85
- loop();
86
- window.addEventListener('resize', draw);
87
  </script>
88
  </body>
89
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>WebGPU Text Rendering</title>
 
 
 
 
 
 
 
 
 
8
  </head>
9
+
10
  <body>
11
+ <canvas></canvas>
12
+ <script type="module">
13
+ // WebGPU Simple Textured Quad - Import Canvas
14
+ // from https://webgpufundamentals.org/webgpu/webgpu-simple-textured-quad-import-canvas.html
15
+
16
+
17
+ import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';
18
+
19
+ const glyphWidth = 32;
20
+ const glyphHeight = 40;
21
+ const glyphsAcrossTexture = 16;
22
+ function genreateGlyphTextureAtlas() {
23
+ const ctx = document.createElement('canvas').getContext('2d');
24
+ ctx.canvas.width = 512;
25
+ ctx.canvas.height = 256;
26
+
27
+ let x = 0;
28
+ let y = 0;
29
+ ctx.font = '32px monospace';
30
+ ctx.textBaseline = 'middle';
31
+ ctx.textAlign = 'center';
32
+ ctx.fillStyle = 'white';
33
+ for (let c = 33; c < 128; ++c) {
34
+ ctx.fillText(String.fromCodePoint(c), x + glyphWidth / 2, y + glyphHeight / 2);
35
+ x += glyphWidth;
36
+ if (x >= ctx.canvas.width) {
37
+ x = 0;
38
+ y += glyphHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  }
41
+
42
+ return ctx.canvas;
43
  }
44
+
45
+ async function main() {
46
+ const adapter = await navigator.gpu?.requestAdapter();
47
+ const device = await adapter?.requestDevice();
48
+ if (!device) {
49
+ fail('need a browser that supports WebGPU');
50
+ return;
51
+ }
52
+
53
+ // Get a WebGPU context from the canvas and configure it
54
+ const canvas = document.querySelector('canvas');
55
+ const context = canvas.getContext('webgpu');
56
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
57
+ context.configure({
58
+ device,
59
+ format: presentationFormat,
60
+ });
61
+
62
+ const module = device.createShaderModule({
63
+ label: 'our hardcoded textured quad shaders',
64
+ code: `
65
+ struct VSInput {
66
+ @location(0) position: vec4f,
67
+ @location(1) texcoord: vec2f,
68
+ @location(2) color: vec4f,
69
+ };
70
+
71
+ struct VSOutput {
72
+ @builtin(position) position: vec4f,
73
+ @location(0) texcoord: vec2f,
74
+ @location(1) color: vec4f,
75
+ };
76
+
77
+ struct Uniforms {
78
+ matrix: mat4x4f,
79
+ };
80
+
81
+ @group(0) @binding(2) var<uniform> uni: Uniforms;
82
+
83
+ @vertex fn vs(vin: VSInput) -> VSOutput {
84
+ var vsOutput: VSOutput;
85
+ vsOutput.position = uni.matrix * vin.position;
86
+ vsOutput.texcoord = vin.texcoord;
87
+ vsOutput.color = vin.color;
88
+ return vsOutput;
89
+ }
90
+
91
+ @group(0) @binding(0) var ourSampler: sampler;
92
+ @group(0) @binding(1) var ourTexture: texture_2d<f32>;
93
+
94
+ @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
95
+ return textureSample(ourTexture, ourSampler, fsInput.texcoord) * fsInput.color;
96
+ }
97
+ `,
98
+ });
99
+
100
+ const glyphCanvas = genreateGlyphTextureAtlas();
101
+ // so we can see it
102
+ document.body.appendChild(glyphCanvas);
103
+ glyphCanvas.style.backgroundColor = '#222';
104
+
105
+ const maxGlyphs = 100;
106
+ const floatsPerVertex = 2 + 2 + 4; // 2(pos) + 2(texcoord) + 4(color)
107
+ const vertexSize = floatsPerVertex * 4; // 4 bytes each float
108
+ const vertsPerGlyph = 6;
109
+ const vertexBufferSize = maxGlyphs * vertsPerGlyph * vertexSize;
110
+ const vertexBuffer = device.createBuffer({
111
+ label: 'vertices',
112
+ size: vertexBufferSize,
113
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
114
+ });
115
+ const indexBuffer = device.createBuffer({
116
+ label: 'indices',
117
+ size: maxGlyphs * vertsPerGlyph * 4,
118
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
119
+ });
120
+ // pre fill index buffer with quad indices
121
+ {
122
+ const indices = [];
123
+ for (let i = 0; i < maxGlyphs; ++i) {
124
+ const ndx = i * 4;
125
+ indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3);
126
+ }
127
+ device.queue.writeBuffer(indexBuffer, 0, new Uint32Array(indices));
128
+ }
129
+
130
+ function generateGlyphVerticesForText(s, colors = [[1, 1, 1, 1]]) {
131
+ const vertexData = new Float32Array(maxGlyphs * floatsPerVertex * vertsPerGlyph);
132
+ const glyphUVWidth = glyphWidth / glyphCanvas.width;
133
+ const glyphUVheight = glyphHeight / glyphCanvas.height;
134
+ let offset = 0;
135
+ let x0 = 0;
136
+ let x1 = 1;
137
+ let y0 = 0;
138
+ let y1 = 1;
139
+ let width = 0;
140
+
141
+ const addVertex = (x, y, u, v, r, g, b, a) => {
142
+ vertexData[offset++] = x;
143
+ vertexData[offset++] = y;
144
+ vertexData[offset++] = u;
145
+ vertexData[offset++] = v;
146
+ vertexData[offset++] = r;
147
+ vertexData[offset++] = g;
148
+ vertexData[offset++] = b;
149
+ vertexData[offset++] = a;
150
+ };
151
+
152
+ const spacing = 0.55;
153
+ let colorNdx = 0;
154
+ for (let i = 0; i < s.length; ++i) {
155
+ // convert char code to texcoords for glyph texture
156
+ const c = s.charCodeAt(i);
157
+ if (c >= 33) {
158
+ const cNdx = c - 33;
159
+ const glyphX = cNdx % glyphsAcrossTexture;
160
+ const glyphY = Math.floor(cNdx / glyphsAcrossTexture);
161
+ const u0 = (glyphX * glyphWidth) / glyphCanvas.width;
162
+ const v1 = (glyphY * glyphHeight) / glyphCanvas.height;
163
+ const u1 = u0 + glyphUVWidth;
164
+ const v0 = v1 + glyphUVheight;
165
+ width = Math.max(x1, width);
166
+
167
+ addVertex(x0, y0, u0, v0, ...colors[colorNdx]);
168
+ addVertex(x1, y0, u1, v0, ...colors[colorNdx]);
169
+ addVertex(x0, y1, u0, v1, ...colors[colorNdx]);
170
+ addVertex(x1, y1, u1, v1, ...colors[colorNdx]);
171
+ } else {
172
+ colorNdx = (colorNdx + 1) % colors.length;
173
+ if (c === 10) {
174
+ x0 = 0;
175
+ x1 = 1;
176
+ y0 = y0 - 1;
177
+ y1 = y0 + 1;
178
+ continue;
179
+ }
180
+ }
181
+ x0 = x0 + spacing;
182
+ x1 = x0 + 1;
183
+ }
184
+
185
+ return {
186
+ vertexData,
187
+ numGlyphs: offset / floatsPerVertex,
188
+ width,
189
+ height: y1,
190
+ };
191
+ }
192
+
193
+ const { vertexData, numGlyphs, width, height } = generateGlyphVerticesForText(
194
+ 'Hello\nworld!\nText in\nWebGPU!', [
195
+ [1, 1, 0, 1],
196
+ [0, 1, 1, 1],
197
+ [1, 0, 1, 1],
198
+ [1, 0, 0, 1],
199
+ [0, .5, 1, 1],
200
+ ]);
201
+ device.queue.writeBuffer(vertexBuffer, 0, vertexData);
202
+
203
+ const pipeline = device.createRenderPipeline({
204
+ label: 'hardcoded textured quad pipeline',
205
+ layout: 'auto',
206
+ vertex: {
207
+ module,
208
+ entryPoint: 'vs',
209
+ buffers: [
210
+ {
211
+ arrayStride: vertexSize,
212
+ attributes: [
213
+ { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position
214
+ { shaderLocation: 1, offset: 8, format: 'float32x2' }, // texcoord
215
+ { shaderLocation: 2, offset: 16, format: 'float32x4' }, // color
216
+ ],
217
+ },
218
+ ],
219
+ },
220
+ fragment: {
221
+ module,
222
+ entryPoint: 'fs',
223
+ targets: [
224
+ {
225
+ format: presentationFormat,
226
+ blend: {
227
+ color: {
228
+ srcFactor: 'one',
229
+ dstFactor: 'one-minus-src-alpha',
230
+ operation: 'add',
231
+ },
232
+ alpha: {
233
+ srcFactor: 'one',
234
+ dstFactor: 'one-minus-src-alpha',
235
+ operation: 'add',
236
+ },
237
+ },
238
+ },
239
+ ],
240
+ },
241
+ });
242
+
243
+ function copySourceToTexture(device, texture, source, { flipY } = {}) {
244
+ device.queue.copyExternalImageToTexture(
245
+ { source, flipY, },
246
+ { texture, premultipliedAlpha: true },
247
+ { width: source.width, height: source.height },
248
+ );
249
+ }
250
+
251
+ function createTextureFromSource(device, source, options = {}) {
252
+ const texture = device.createTexture({
253
+ format: 'rgba8unorm',
254
+ size: [source.width, source.height],
255
+ usage: GPUTextureUsage.TEXTURE_BINDING |
256
+ GPUTextureUsage.COPY_DST |
257
+ GPUTextureUsage.RENDER_ATTACHMENT,
258
+ });
259
+ copySourceToTexture(device, texture, source, options);
260
+ return texture;
261
+ }
262
+
263
+ const texture = createTextureFromSource(device, glyphCanvas, { mips: true });
264
+ const sampler = device.createSampler({
265
+ minFilter: 'linear',
266
+ magFilter: 'linear',
267
+ });
268
+
269
+ // create a buffer for the uniform values
270
+ const uniformBufferSize =
271
+ 16 * 4; // matrix is 16 32bit floats (4bytes each)
272
+ const uniformBuffer = device.createBuffer({
273
+ label: 'uniforms for quad',
274
+ size: uniformBufferSize,
275
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
276
+ });
277
+
278
+ // create a typedarray to hold the values for the uniforms in JavaScript
279
+ const kMatrixOffset = 0;
280
+ const uniformValues = new Float32Array(uniformBufferSize / 4);
281
+ const matrix = uniformValues.subarray(kMatrixOffset, 16);
282
+
283
+ const bindGroup = device.createBindGroup({
284
+ layout: pipeline.getBindGroupLayout(0),
285
+ entries: [
286
+ { binding: 0, resource: sampler },
287
+ { binding: 1, resource: texture.createView() },
288
+ { binding: 2, resource: { buffer: uniformBuffer } },
289
+ ],
290
+ });
291
+
292
+ const renderPassDescriptor = {
293
+ label: 'our basic canvas renderPass',
294
+ colorAttachments: [
295
+ {
296
+ // view: <- to be filled out when we render
297
+ clearValue: [0.3, 0.3, 0.3, 1],
298
+ loadOp: 'clear',
299
+ storeOp: 'store',
300
+ },
301
+ ],
302
+ };
303
+
304
+ function render(time) {
305
+ time *= 0.001;
306
+
307
+ const fov = 60 * Math.PI / 180; // 60 degrees in radians
308
+ const aspect = canvas.clientWidth / canvas.clientHeight;
309
+ const zNear = 0.001;
310
+ const zFar = 50;
311
+ const projectionMatrix = mat4.perspective(fov, aspect, zNear, zFar);
312
+
313
+ const cameraPosition = [0, 0, 5];
314
+ const up = [0, 1, 0];
315
+ const target = [0, 0, 0];
316
+ const viewMatrix = mat4.lookAt(cameraPosition, target, up);
317
+ const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix);
318
+
319
+ // Get the current texture from the canvas context and
320
+ // set it as the texture to render to.
321
+ renderPassDescriptor.colorAttachments[0].view =
322
+ context.getCurrentTexture().createView();
323
+
324
+ const encoder = device.createCommandEncoder({
325
+ label: 'render quad encoder',
326
+ });
327
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
328
+ pass.setPipeline(pipeline);
329
+
330
+ mat4.rotateY(viewProjectionMatrix, time, matrix);
331
+ mat4.translate(matrix, [-width / 2, -height / 2, 0], matrix);
332
+
333
+ // copy the values from JavaScript to the GPU
334
+ device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
335
+
336
+ pass.setBindGroup(0, bindGroup);
337
+ pass.setVertexBuffer(0, vertexBuffer);
338
+ pass.setIndexBuffer(indexBuffer, 'uint32');
339
+ pass.drawIndexed(numGlyphs * 6);
340
+
341
+ pass.end();
342
+
343
+ const commandBuffer = encoder.finish();
344
+ device.queue.submit([commandBuffer]);
345
+
346
+ requestAnimationFrame(render);
347
+ }
348
+ requestAnimationFrame(render);
349
  }
350
+
351
+ function fail(msg) {
352
+ // eslint-disable-next-line no-alert
353
+ alert(msg);
 
354
  }
355
+
356
+ main();
357
+
358
  </script>
359
  </body>
360
+
361
+ </html>
style.css CHANGED
@@ -1,6 +1,7 @@
1
  body {
2
  padding: 0;
3
  margin: 0;
 
4
  font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
5
  }
6
 
@@ -27,9 +28,3 @@ p {
27
  .card p:last-child {
28
  margin-bottom: 0;
29
  }
30
-
31
- canvas {
32
- display: block;
33
- margin: 0 auto;
34
- background-color: black;
35
- }
 
1
  body {
2
  padding: 0;
3
  margin: 0;
4
+ overflow: hidden;
5
  font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
6
  }
7
 
 
28
  .card p:last-child {
29
  margin-bottom: 0;
30
  }