├── img
└── webgpu.png
├── css
└── example.css
├── LICENSE
├── README.md
├── utils
├── Timer.js
└── utils.js
├── triangle.html
├── multi-canvas.html
├── cube.html
├── particles.html
├── pick.html
└── deferred.html
/img/webgpu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsherif/webgpu-examples/HEAD/img/webgpu.png
--------------------------------------------------------------------------------
/css/example.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | overflow: hidden;
4 | }
5 |
6 | #description {
7 | position: absolute;
8 | top: 20px;
9 | left: 20px;
10 | background-color: rgba(128, 128, 128, 0.95);
11 | color: white;
12 | padding: 0.5em;
13 | font-size: 18px;
14 | pointer-events: none;
15 | }
16 |
17 | #description h2 {
18 | margin: 0;
19 | font-size: 1.1em;
20 | }
21 |
22 | #description div {
23 | margin: 4px;
24 | }
25 |
26 | #stats {
27 | position: fixed;
28 | bottom: 20px;
29 | left: 20px;
30 | background-color: rgba(128, 128, 128, 0.95);
31 | color: white;
32 | padding: 0.5em;
33 | font-size: 18px;
34 | z-index: 1;
35 | pointer-events: none;
36 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Tarek Sherif
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WebGPU Examples
2 | ===============
3 |
4 | Rendering algorithms implemented in WebGPU.
5 |
6 | - [Triangle](https://tsherif.github.io/webgpu-examples/triangle.html): Creating vertex buffers and drawing a triangle.
7 | - [Cube](https://tsherif.github.io/webgpu-examples/cube.html): Creating a texture from an ImageBitmap, setting up vertex and uniform buffers, and animating a 3D cube.
8 | - [Particles](https://tsherif.github.io/webgpu-examples/particles.html): Simulating gravity on instanced particles using a compute shader.
9 | - [Multiple Canvases](https://tsherif.github.io/webgpu-examples/multi-canvas.html): Rendering to multiple canvases with a single device instance.
10 | - [Picking](https://tsherif.github.io/webgpu-examples/pick.html): Interact with rendered objects using color picking.
11 | - [Deferred Rendering](https://tsherif.github.io/webgpu-examples/deferred.html): Rendering mesh data to a multisampled gBuffer then lighting in a separate pass.
12 |
13 | Examples currently only run without special flags in Chrome on Windows and OSX. See the [Implementation Status](https://github.com/gpuweb/gpuweb/wiki/Implementation-Status) page for updates on support.
14 |
15 | All examples are implemented in a single HTML file with minimal use of functions, modules, classes or other abstractions. The goal is to allow the reader to easily see, in sequential order, all WebGPU calls that are made. They can be run locally by serving them from a local HTTP server, e.g. `python -m http.server`.
16 |
17 |
--------------------------------------------------------------------------------
/utils/Timer.js:
--------------------------------------------------------------------------------
1 | const DEFAULT_SAMPLE_COUNT = 50;
2 |
3 | export class Timer {
4 | static PASS_START = 1;
5 | static PASS_END = 2;
6 |
7 | constructor(device, sampleCount = DEFAULT_SAMPLE_COUNT) {
8 | this.device = device;
9 | this.sampleCount = sampleCount;
10 | this.hasGPUTimer = device.features.has("timestamp-query");
11 | this.cpuTimers = {};
12 | this.cpuTimes = {};
13 | this.gpuTimers = {};
14 | this.gpuTimes = {}
15 | }
16 |
17 | gpuPassDescriptor(timerName, flags = Timer.PASS_START | Timer.PASS_END) {
18 | if (!this.hasGPUTimer) {
19 | return undefined;
20 | }
21 |
22 | if (!this.gpuTimers[timerName]) {
23 | const querySet = this.device.createQuerySet({
24 | type: "timestamp",
25 | count: 2
26 | });
27 |
28 | const resolveBuffer = this.device.createBuffer({
29 | size: querySet.count * 8,
30 | usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
31 | });
32 |
33 | const resultBuffer = this.device.createBuffer({
34 | size: resolveBuffer.size,
35 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
36 | });
37 |
38 | this.gpuTimers[timerName] = {
39 | querySet,
40 | resolveBuffer,
41 | resultBuffer,
42 | time: 0,
43 | timeCount: 0,
44 | }
45 |
46 | this.gpuTimes[timerName] = 0;
47 | }
48 |
49 | const timer = this.gpuTimers[timerName];
50 |
51 | const descriptor = {
52 | querySet: timer.querySet
53 | };
54 |
55 | if (flags & Timer.PASS_START) {
56 | descriptor.beginningOfPassWriteIndex = 0;
57 | }
58 |
59 | if (flags & Timer.PASS_END) {
60 | descriptor.endOfPassWriteIndex = 1;
61 | }
62 |
63 | return descriptor;
64 | }
65 |
66 | gpuBeforeSubmit(commandEncoder, timerNames = Object.keys(this.gpuTimers)) {
67 | timerNames.forEach(timerName => {
68 | const timer = this.gpuTimers[timerName];
69 |
70 | if (timer && timer.resultBuffer.mapState === "unmapped") {
71 | const { querySet, resolveBuffer, resultBuffer } = timer;
72 |
73 | commandEncoder.resolveQuerySet(querySet, 0, querySet.count, resolveBuffer, 0);
74 | commandEncoder.copyBufferToBuffer(resolveBuffer, 0, resultBuffer, 0, resultBuffer.size);
75 | }
76 | });
77 | }
78 |
79 | gpuAfterSubmit(timerNames = Object.keys(this.gpuTimers)) {
80 | timerNames.forEach(timerName => {
81 | const timer = this.gpuTimers[timerName];
82 |
83 | if (timer && timer.resultBuffer.mapState === "unmapped") {
84 | const { resultBuffer } = timer;
85 | resultBuffer.mapAsync(GPUMapMode.READ).then(() => {
86 | const [start, end] = new BigInt64Array(resultBuffer.getMappedRange());
87 | resultBuffer.unmap();
88 |
89 | const time = Number(end - start);
90 |
91 | if (time >= 0) {
92 | timer.time += time;
93 | ++timer.timeCount;
94 | } else {
95 | timer.time = 0;
96 | timer.timeCount = 0;
97 | }
98 |
99 | if (timer.timeCount === this.sampleCount) {
100 | this.gpuTimes[timerName] = timer.time / this.sampleCount / 1000000;
101 | timer.time = 0;
102 | timer.timeCount = 0;
103 | }
104 | });
105 | }
106 | });
107 | }
108 |
109 | cpuTimeStart(timerName) {
110 | this.cpuTimers[timerName] ??= {
111 | startTime: 0,
112 | time: 0,
113 | timeCount: 0
114 | };
115 | this.cpuTimes[timerName] ??= 0;
116 |
117 | this.cpuTimers[timerName].startTime = performance.now();
118 | }
119 |
120 |
121 | cpuTimeEnd(timerName) {
122 | const timer = this.cpuTimers[timerName];
123 |
124 | timer.time += performance.now() - timer.startTime;
125 | ++timer.timeCount;
126 |
127 | if (timer.timeCount === this.sampleCount) {
128 | this.cpuTimes[timerName] = timer.time / this.sampleCount;
129 | timer.time = 0;
130 | timer.timeCount = 0;
131 | }
132 | }
133 |
134 | }
--------------------------------------------------------------------------------
/triangle.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
31 |
230 |
231 |
--------------------------------------------------------------------------------
/multi-canvas.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
19 |
41 |
42 |
43 |
44 | CPU Time:
45 |
46 |
47 | GPU Time:
48 |
49 |
50 |
445 |
446 |
447 |
--------------------------------------------------------------------------------
/cube.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
30 |
31 |
32 |
33 |
34 |
35 | CPU Frame Time:
36 |
37 |
38 | GPU Draw Time:
39 |
40 |
41 |
449 |
450 |
451 |
--------------------------------------------------------------------------------
/utils/utils.js:
--------------------------------------------------------------------------------
1 | ///////////////////////////////////////////////////////////////////////////////////
2 | // The MIT License (MIT)
3 | //
4 | // Copyright (c) 2020 Tarek Sherif
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | // this software and associated documentation files (the "Software"), to deal in
8 | // the Software without restriction, including without limitation the rights to
9 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10 | // the Software, and to permit persons to whom the Software is furnished to do so,
11 | // subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in all
14 | // copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | ///////////////////////////////////////////////////////////////////////////////////
23 |
24 | let translateMat;
25 | let rotateXMat;
26 | let rotateYMat;
27 | let rotateZMat;
28 | let scaleMat;
29 | let mat4;
30 |
31 | if (window.glMatrix) {
32 | mat4 = glMatrix.mat4;
33 | translateMat = mat4.create();
34 | rotateXMat = mat4.create();
35 | rotateYMat = mat4.create();
36 | rotateZMat = mat4.create();
37 | scaleMat = mat4.create();
38 | }
39 |
40 | const ZEROS = [0, 0, 0];
41 | const ONES = [1, 1, 1];
42 |
43 | let textureDataCanvas = document.createElement("canvas");
44 | let textureDataCtx = textureDataCanvas.getContext("2d");
45 |
46 |
47 | export function checkSupport() {
48 | if (!navigator.gpu) {
49 | document.body.innerHTML = `
50 | WebGPU not supported!
51 |
54 |
57 | `;
58 |
59 | throw new Error("WebGPU not supported");
60 | }
61 | }
62 |
63 | export function addDescription(title, description, url) {
64 | title = `WebGPU Example: ${title}`;
65 |
66 | const titleElement = document.createElement("title");
67 | titleElement.innerText = title;
68 | document.head.appendChild(titleElement);
69 |
70 | const descriptionElement = document.createElement("div");
71 | descriptionElement.id = "description";
72 | descriptionElement.style.width = "400px";
73 | descriptionElement.innerHTML = `
74 | ${title}
75 | ${description}
76 |
77 | `;
78 | document.body.appendChild(descriptionElement);
79 | }
80 |
81 | export function xformMatrix(xform, translate, rotate, scale) {
82 | translate = translate || ZEROS;
83 | rotate = rotate || ZEROS;
84 | scale = scale || ONES;
85 |
86 | mat4.fromTranslation(translateMat, translate);
87 | mat4.fromXRotation(rotateXMat, rotate[0]);
88 | mat4.fromYRotation(rotateYMat, rotate[1]);
89 | mat4.fromZRotation(rotateZMat, rotate[2]);
90 | mat4.fromScaling(scaleMat, scale);
91 |
92 | mat4.multiply(xform, rotateXMat, scaleMat);
93 | mat4.multiply(xform, rotateYMat, xform);
94 | mat4.multiply(xform, rotateZMat, xform);
95 | mat4.multiply(xform, translateMat, xform);
96 | }
97 |
98 | export function getImageData(img) {
99 | textureDataCanvas.width = img.width;
100 | textureDataCanvas.height = img.height;
101 | textureDataCtx.drawImage(img, 0, 0);
102 | return textureDataCtx.getImageData(0, 0, img.width, img.height).data;
103 | }
104 |
105 | export function createCube(options) {
106 | options = options || {};
107 |
108 | let dimensions = options.dimensions || [1, 1, 1];
109 | let position = options.position || [-dimensions[0] / 2, -dimensions[1] / 2, -dimensions[2] / 2];
110 | let x = position[0];
111 | let y = position[1];
112 | let z = position[2];
113 | let width = dimensions[0];
114 | let height = dimensions[1];
115 | let depth = dimensions[2];
116 |
117 | let fbl = {x: x, y: y, z: z + depth};
118 | let fbr = {x: x + width, y: y, z: z + depth};
119 | let ftl = {x: x, y: y + height, z: z + depth};
120 | let ftr = {x: x + width, y: y + height, z: z + depth};
121 | let bbl = {x: x, y: y, z: z };
122 | let bbr = {x: x + width, y: y, z: z };
123 | let btl = {x: x, y: y + height, z: z };
124 | let btr = {x: x + width, y: y + height, z: z };
125 |
126 | let positions = new Float32Array([
127 | //front
128 | fbl.x, fbl.y, fbl.z,
129 | fbr.x, fbr.y, fbr.z,
130 | ftl.x, ftl.y, ftl.z,
131 | ftl.x, ftl.y, ftl.z,
132 | fbr.x, fbr.y, fbr.z,
133 | ftr.x, ftr.y, ftr.z,
134 |
135 | //right
136 | fbr.x, fbr.y, fbr.z,
137 | bbr.x, bbr.y, bbr.z,
138 | ftr.x, ftr.y, ftr.z,
139 | ftr.x, ftr.y, ftr.z,
140 | bbr.x, bbr.y, bbr.z,
141 | btr.x, btr.y, btr.z,
142 |
143 | //back
144 | fbr.x, bbr.y, bbr.z,
145 | bbl.x, bbl.y, bbl.z,
146 | btr.x, btr.y, btr.z,
147 | btr.x, btr.y, btr.z,
148 | bbl.x, bbl.y, bbl.z,
149 | btl.x, btl.y, btl.z,
150 |
151 | //left
152 | bbl.x, bbl.y, bbl.z,
153 | fbl.x, fbl.y, fbl.z,
154 | btl.x, btl.y, btl.z,
155 | btl.x, btl.y, btl.z,
156 | fbl.x, fbl.y, fbl.z,
157 | ftl.x, ftl.y, ftl.z,
158 |
159 | //top
160 | ftl.x, ftl.y, ftl.z,
161 | ftr.x, ftr.y, ftr.z,
162 | btl.x, btl.y, btl.z,
163 | btl.x, btl.y, btl.z,
164 | ftr.x, ftr.y, ftr.z,
165 | btr.x, btr.y, btr.z,
166 |
167 | //bottom
168 | bbl.x, bbl.y, bbl.z,
169 | bbr.x, bbr.y, bbr.z,
170 | fbl.x, fbl.y, fbl.z,
171 | fbl.x, fbl.y, fbl.z,
172 | bbr.x, bbr.y, bbr.z,
173 | fbr.x, fbr.y, fbr.z
174 | ]);
175 |
176 | let uvs = new Float32Array([
177 | //front
178 | 0, 0,
179 | 1, 0,
180 | 0, 1,
181 | 0, 1,
182 | 1, 0,
183 | 1, 1,
184 |
185 | //right
186 | 0, 0,
187 | 1, 0,
188 | 0, 1,
189 | 0, 1,
190 | 1, 0,
191 | 1, 1,
192 |
193 | //back
194 | 0, 0,
195 | 1, 0,
196 | 0, 1,
197 | 0, 1,
198 | 1, 0,
199 | 1, 1,
200 |
201 | //left
202 | 0, 0,
203 | 1, 0,
204 | 0, 1,
205 | 0, 1,
206 | 1, 0,
207 | 1, 1,
208 |
209 | //top
210 | 0, 0,
211 | 1, 0,
212 | 0, 1,
213 | 0, 1,
214 | 1, 0,
215 | 1, 1,
216 |
217 | //bottom
218 | 0, 0,
219 | 1, 0,
220 | 0, 1,
221 | 0, 1,
222 | 1, 0,
223 | 1, 1
224 | ]);
225 |
226 | let normals = new Float32Array([
227 | // front
228 | 0, 0, 1,
229 | 0, 0, 1,
230 | 0, 0, 1,
231 | 0, 0, 1,
232 | 0, 0, 1,
233 | 0, 0, 1,
234 |
235 | // right
236 | 1, 0, 0,
237 | 1, 0, 0,
238 | 1, 0, 0,
239 | 1, 0, 0,
240 | 1, 0, 0,
241 | 1, 0, 0,
242 |
243 | // back
244 | 0, 0, -1,
245 | 0, 0, -1,
246 | 0, 0, -1,
247 | 0, 0, -1,
248 | 0, 0, -1,
249 | 0, 0, -1,
250 |
251 | // left
252 | -1, 0, 0,
253 | -1, 0, 0,
254 | -1, 0, 0,
255 | -1, 0, 0,
256 | -1, 0, 0,
257 | -1, 0, 0,
258 |
259 | // top
260 | 0, 1, 0,
261 | 0, 1, 0,
262 | 0, 1, 0,
263 | 0, 1, 0,
264 | 0, 1, 0,
265 | 0, 1, 0,
266 |
267 | // bottom
268 | 0, -1, 0,
269 | 0, -1, 0,
270 | 0, -1, 0,
271 | 0, -1, 0,
272 | 0, -1, 0,
273 | 0, -1, 0
274 | ]);
275 |
276 | return {
277 | positions,
278 | normals,
279 | uvs
280 | };
281 |
282 | }
283 |
284 | export function createSphere(options) {
285 | options = options || {};
286 |
287 | let longBands = options.longBands || 32;
288 | let latBands = options.latBands || 32;
289 | let radius = options.radius || 1;
290 | let lat_step = Math.PI / latBands;
291 | let long_step = 2 * Math.PI / longBands;
292 | let num_positions = longBands * latBands * 4;
293 | let num_indices = longBands * latBands * 6;
294 | let lat_angle, long_angle;
295 | let positions = new Float32Array(num_positions * 3);
296 | let normals = new Float32Array(num_positions * 3);
297 | let uvs = new Float32Array(num_positions * 2);
298 | let indices = new Uint32Array(num_indices);
299 | let x1, x2, x3, x4,
300 | y1, y2,
301 | z1, z2, z3, z4,
302 | u1, u2,
303 | v1, v2;
304 | let i, j;
305 | let k = 0, l = 0;
306 | let vi, ti;
307 |
308 | for (i = 0; i < latBands; i++) {
309 | lat_angle = i * lat_step;
310 | y1 = Math.cos(lat_angle);
311 | y2 = Math.cos(lat_angle + lat_step);
312 | for (j = 0; j < longBands; j++) {
313 | long_angle = j * long_step;
314 | x1 = Math.sin(lat_angle) * Math.cos(long_angle);
315 | x2 = Math.sin(lat_angle) * Math.cos(long_angle + long_step);
316 | x3 = Math.sin(lat_angle + lat_step) * Math.cos(long_angle);
317 | x4 = Math.sin(lat_angle + lat_step) * Math.cos(long_angle + long_step);
318 | z1 = Math.sin(lat_angle) * Math.sin(long_angle);
319 | z2 = Math.sin(lat_angle) * Math.sin(long_angle + long_step);
320 | z3 = Math.sin(lat_angle + lat_step) * Math.sin(long_angle);
321 | z4 = Math.sin(lat_angle + lat_step) * Math.sin(long_angle + long_step);
322 | u1 = 1 - j / longBands;
323 | u2 = 1 - (j + 1) / longBands;
324 | v1 = 1 - i / latBands;
325 | v2 = 1 - (i + 1) / latBands;
326 | vi = k * 3;
327 | ti = k * 2;
328 |
329 | positions[vi] = x1 * radius;
330 | positions[vi+1] = y1 * radius;
331 | positions[vi+2] = z1 * radius; //v0
332 |
333 | positions[vi+3] = x2 * radius;
334 | positions[vi+4] = y1 * radius;
335 | positions[vi+5] = z2 * radius; //v1
336 |
337 | positions[vi+6] = x3 * radius;
338 | positions[vi+7] = y2 * radius;
339 | positions[vi+8] = z3 * radius; // v2
340 |
341 |
342 | positions[vi+9] = x4 * radius;
343 | positions[vi+10] = y2 * radius;
344 | positions[vi+11] = z4 * radius; // v3
345 |
346 | normals[vi] = x1;
347 | normals[vi+1] = y1;
348 | normals[vi+2] = z1;
349 |
350 | normals[vi+3] = x2;
351 | normals[vi+4] = y1;
352 | normals[vi+5] = z2;
353 |
354 | normals[vi+6] = x3;
355 | normals[vi+7] = y2;
356 | normals[vi+8] = z3;
357 |
358 | normals[vi+9] = x4;
359 | normals[vi+10] = y2;
360 | normals[vi+11] = z4;
361 |
362 | uvs[ti] = u1;
363 | uvs[ti+1] = v1;
364 |
365 | uvs[ti+2] = u2;
366 | uvs[ti+3] = v1;
367 |
368 | uvs[ti+4] = u1;
369 | uvs[ti+5] = v2;
370 |
371 | uvs[ti+6] = u2;
372 | uvs[ti+7] = v2;
373 |
374 | indices[l ] = k;
375 | indices[l + 1] = k + 1;
376 | indices[l + 2] = k + 2;
377 | indices[l + 3] = k + 2;
378 | indices[l + 4] = k + 1;
379 | indices[l + 5] = k + 3;
380 |
381 | k += 4;
382 | l += 6;
383 | }
384 | }
385 |
386 | return {
387 | positions,
388 | normals,
389 | uvs,
390 | indices
391 | };
392 | }
393 |
394 | export function createQuad() {
395 | return {
396 | positions: new Float32Array([
397 | -1, -1,
398 | 1, -1,
399 | -1, 1,
400 | 1, 1,
401 | ]),
402 | normals: new Float32Array([
403 | 0, 0, 1,
404 | 0, 0, 1,
405 | 0, 0, 1,
406 | 0, 0, 1
407 | ]),
408 | uvs: new Float32Array([
409 | 0, 0,
410 | 1, 0,
411 | 0, 1,
412 | 1, 1,
413 | ])
414 | }
415 | }
416 |
417 | export function loadImageBitmaps(urls) {
418 | return Promise.all(
419 | urls.map(
420 | url =>
421 | fetch(url)
422 | .then(resp => resp.blob())
423 | .then(blob => createImageBitmap(blob, { colorSpaceConversion: 'none' }))
424 | )
425 | );
426 | }
427 |
428 | export function randomRange(min, max) {
429 | return min + Math.random() * (max - min);
430 | }
431 |
432 | export function parseAdapterInfo(info) {
433 | let gpuInfo = "";
434 | if (info.vendor) {
435 | gpuInfo = `GPU: ${info.vendor}`;
436 | if (info.architecture) {
437 | gpuInfo += ` (${info.architecture})`
438 | }
439 | }
440 |
441 | return gpuInfo;
442 | }
443 |
444 | export function getMipLevelCount(width, height = 0, depth = 0) {
445 | return 1 + Math.floor(Math.log2(Math.max(width, height, depth)));
446 | }
447 |
448 | // From: https://webgpufundamentals.org/webgpu/lessons/webgpu-importing-textures.html
449 | export const generate2DMipmap = (() => {
450 | let sampler;
451 | let module;
452 |
453 | const pipelines = {};
454 |
455 | return function generate2DMipmap(device, texture) {
456 | module ??= device.createShaderModule({
457 | label: "generateMipmap shader module",
458 | code: `
459 | struct VSOut {
460 | @builtin(position) position: vec4f,
461 | @location(0) uv: vec2f
462 | }
463 |
464 | @vertex
465 | fn vs(@builtin(vertex_index) index: u32) -> VSOut {
466 | let positions = array(
467 | vec2f(-1, -1),
468 | vec2f(1, -1),
469 | vec2f(-1, 1),
470 | vec2f(1, 1)
471 | );
472 |
473 | let pos = positions[index];
474 | let uv = pos * 0.5 + 0.5;
475 |
476 | var vsOut: VSOut;
477 | vsOut.position = vec4f(pos, 0.0, 1.0);
478 | vsOut.uv = vec2f(uv.x, 1.0 - uv.y);
479 |
480 | return vsOut;
481 | }
482 |
483 | @group(0) @binding(0) var mipmapSampler: sampler;
484 | @group(0) @binding(1) var mipmapTexture: texture_2d;
485 |
486 |
487 | @fragment
488 | fn fs(fsIn: VSOut) -> @location(0) vec4f {
489 | return textureSample(mipmapTexture, mipmapSampler, fsIn.uv);
490 | }
491 | `
492 | });
493 |
494 | sampler ??= device.createSampler({
495 | minFilter: "linear"
496 | })
497 |
498 | pipelines[texture.format] ??= device.createRenderPipeline({
499 | label: `mipmap pipeline`,
500 | layout: 'auto',
501 | vertex: {
502 | module,
503 | entryPoint: "vs"
504 | },
505 | fragment: {
506 | module,
507 | entryPoint: "fs",
508 | targets: [{
509 | format: texture.format
510 | }]
511 | },
512 | primitive: {
513 | topology: "triangle-strip"
514 | }
515 | })
516 |
517 | const pipeline = pipelines[texture.format];
518 |
519 | const encoder = device.createCommandEncoder({
520 | label: "mipmap command encoder"
521 | });
522 |
523 | let { width, height } = texture;
524 | let mipLevel = 0;
525 |
526 | while (width > 1 || height > 1) {
527 | width = Math.max(1, Math.floor(width / 2));
528 | height = Math.max(1, Math.floor(height / 2));
529 |
530 | const bindGroup = device.createBindGroup({
531 | layout: pipeline.getBindGroupLayout(0),
532 | entries: [
533 | {
534 | binding: 0,
535 | resource: sampler
536 | },
537 | {
538 | binding: 1,
539 | resource: texture.createView({
540 | baseMipLevel: mipLevel,
541 | mipLevelCount: 1
542 | })
543 | },
544 | ]
545 | })
546 |
547 | const pass = encoder.beginRenderPass({
548 | label: `mipmap level ${mipLevel} render pass`,
549 | colorAttachments: [
550 | {
551 | view: texture.createView({
552 | baseMipLevel: mipLevel + 1,
553 | mipLevelCount: 1
554 | }),
555 | loadOp: "load",
556 | storeOp: "store"
557 | }
558 | ]
559 | });
560 |
561 | pass.setPipeline(pipeline);
562 | pass.setBindGroup(0, bindGroup);
563 | pass.draw(4);
564 | pass.end();
565 |
566 | ++mipLevel;
567 | }
568 |
569 | device.queue.submit([encoder.finish()])
570 | };
571 | })()
--------------------------------------------------------------------------------
/particles.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
31 |
32 |
33 |
34 | CPU Frame Time:
35 |
36 |
37 | GPU Compute Time:
38 |
39 |
40 | GPU Draw Time:
41 |
42 |
43 |
484 |
485 |
--------------------------------------------------------------------------------
/pick.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
30 |
31 |
32 |
33 |
34 |
35 | CPU Frame Time:
36 |
37 |
38 | GPU Pick Time:
39 |
40 |
41 | GPU Draw Time:
42 |
43 |
44 |
715 |
716 |
--------------------------------------------------------------------------------
/deferred.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
30 |
31 |
32 |
33 |
34 |
35 | CPU Frame Time:
36 |
37 |
38 | GPU Lights Draw Time:
39 |
40 |
41 | GPU GBuffer Render Time:
42 |
43 |
44 | GPU Lighting Time:
45 |
46 |
47 |
1018 |
1019 |
1020 |
--------------------------------------------------------------------------------