@es3d/es3d 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
|
@@ -1,480 +1,482 @@
|
|
|
1
|
-
// WebGPU rendering backend.
|
|
2
|
-
//
|
|
3
|
-
// Same architecture as the WebGL2 backend: one shared vertex buffer, one
|
|
4
|
-
// shared uint32 index buffer, one instance buffer, explicit bind group
|
|
5
|
-
// layouts and a cache of pipeline variants (shader specialised per material
|
|
6
|
-
// features and light setup, cull mode, winding, blending, topology). Each
|
|
7
|
-
// batch is one drawIndexed call using firstInstance.
|
|
8
|
-
|
|
9
|
-
import { meshShader, LINE_WGSL } from '../shaders/wgsl.js';
|
|
10
|
-
import { FRAME_BYTES, MATERIAL_FLOATS, packMaterial, packOverlay, lightTypes } from '../lighting.js';
|
|
11
|
-
import { VERTEX_BYTES } from '../geometry-pool.js';
|
|
12
|
-
import { buildMips } from '../textures.js';
|
|
13
|
-
|
|
14
|
-
const SAMPLE_COUNT = 4;
|
|
15
|
-
|
|
16
|
-
const VERTEX_LAYOUTS = [
|
|
17
|
-
{
|
|
18
|
-
arrayStride: VERTEX_BYTES,
|
|
19
|
-
stepMode: 'vertex',
|
|
20
|
-
attributes: [
|
|
21
|
-
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
22
|
-
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
23
|
-
{ shaderLocation: 2, offset: 24, format: 'float32x2' },
|
|
24
|
-
],
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
arrayStride: 64,
|
|
28
|
-
stepMode: 'instance',
|
|
29
|
-
attributes: [
|
|
30
|
-
{ shaderLocation: 3, offset: 0, format: 'float32x4' },
|
|
31
|
-
{ shaderLocation: 4, offset: 16, format: 'float32x4' },
|
|
32
|
-
{ shaderLocation: 5, offset: 32, format: 'float32x4' },
|
|
33
|
-
{ shaderLocation: 6, offset: 48, format: 'float32x4' },
|
|
34
|
-
],
|
|
35
|
-
},
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const pow2 = (n, min) => {
|
|
39
|
-
let c = min;
|
|
40
|
-
while (c < n) c *= 2;
|
|
41
|
-
return c;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export class WebGPUBackend {
|
|
45
|
-
/**
|
|
46
|
-
* @param {HTMLCanvasElement} canvas
|
|
47
|
-
* @param {{present?: 'canvas'|'readback'}} [opts] 'readback' renders offscreen and
|
|
48
|
-
* copies pixels into a 2D canvas (headless testing, screenshots without a swap chain)
|
|
49
|
-
*/
|
|
50
|
-
constructor(canvas, opts = {}) {
|
|
51
|
-
this.canvas = canvas;
|
|
52
|
-
this.present = opts.present || 'canvas';
|
|
53
|
-
this.kind = 'webgpu';
|
|
54
|
-
this.zeroToOne = true;
|
|
55
|
-
this.pipelines = new Map();
|
|
56
|
-
this.meshModules = new Map();
|
|
57
|
-
this.materialBindings = new Map();
|
|
58
|
-
this.overlayBindings = new Map();
|
|
59
|
-
this.poolGeneration = -1;
|
|
60
|
-
this.vertexBuffer = null;
|
|
61
|
-
this.indexBuffer = null;
|
|
62
|
-
this.instanceBuffer = null;
|
|
63
|
-
this.gpuBytes = 0;
|
|
64
|
-
this.lost = false;
|
|
65
|
-
this.info = '';
|
|
66
|
-
this.errors = [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async init() {
|
|
70
|
-
if (!navigator.gpu) throw new Error('WebGPU is not available in this browser.');
|
|
71
|
-
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
72
|
-
if (!adapter) throw new Error('No WebGPU adapter is available.');
|
|
73
|
-
this.adapter = adapter;
|
|
74
|
-
const info = adapter.info || {};
|
|
75
|
-
this.info = [info.vendor, info.architecture, info.description].filter(Boolean).join(' ') || 'WebGPU adapter';
|
|
76
|
-
const device = await adapter.requestDevice();
|
|
77
|
-
this.device = device;
|
|
78
|
-
device.lost.then((i) => {
|
|
79
|
-
this.lost = true;
|
|
80
|
-
this.errors.push(`WebGPU device lost: ${i.message}`);
|
|
81
|
-
});
|
|
82
|
-
device.onuncapturederror = (e) => {
|
|
83
|
-
this.errors.push(e.error && e.error.message ? e.error.message : String(e));
|
|
84
|
-
};
|
|
85
|
-
if (this.present === 'readback') {
|
|
86
|
-
this.format = 'rgba8unorm';
|
|
87
|
-
this.ctx2d = this.canvas.getContext('2d');
|
|
88
|
-
if (!this.ctx2d) throw new Error('Cannot create a 2D context for WebGPU readback presentation.');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ binding:
|
|
107
|
-
{ binding:
|
|
108
|
-
{ binding:
|
|
109
|
-
{ binding:
|
|
110
|
-
{ binding:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (this.
|
|
209
|
-
this.msaa
|
|
210
|
-
this.depth
|
|
211
|
-
this.
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.offscreen
|
|
217
|
-
this.
|
|
218
|
-
this.
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.device.
|
|
249
|
-
this.
|
|
250
|
-
this.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this.vertexBuffer
|
|
264
|
-
this.indexBuffer
|
|
265
|
-
this.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
{ binding:
|
|
304
|
-
{ binding:
|
|
305
|
-
{ binding:
|
|
306
|
-
{ binding:
|
|
307
|
-
{ binding:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
this.
|
|
387
|
-
d
|
|
388
|
-
this.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
this.device.
|
|
466
|
-
this.
|
|
467
|
-
|
|
468
|
-
this.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
1
|
+
// WebGPU rendering backend.
|
|
2
|
+
//
|
|
3
|
+
// Same architecture as the WebGL2 backend: one shared vertex buffer, one
|
|
4
|
+
// shared uint32 index buffer, one instance buffer, explicit bind group
|
|
5
|
+
// layouts and a cache of pipeline variants (shader specialised per material
|
|
6
|
+
// features and light setup, cull mode, winding, blending, topology). Each
|
|
7
|
+
// batch is one drawIndexed call using firstInstance.
|
|
8
|
+
|
|
9
|
+
import { meshShader, LINE_WGSL } from '../shaders/wgsl.js';
|
|
10
|
+
import { FRAME_BYTES, MATERIAL_FLOATS, packMaterial, packOverlay, lightTypes } from '../lighting.js';
|
|
11
|
+
import { VERTEX_BYTES } from '../geometry-pool.js';
|
|
12
|
+
import { buildMips } from '../textures.js';
|
|
13
|
+
|
|
14
|
+
const SAMPLE_COUNT = 4;
|
|
15
|
+
|
|
16
|
+
const VERTEX_LAYOUTS = [
|
|
17
|
+
{
|
|
18
|
+
arrayStride: VERTEX_BYTES,
|
|
19
|
+
stepMode: 'vertex',
|
|
20
|
+
attributes: [
|
|
21
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
22
|
+
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
23
|
+
{ shaderLocation: 2, offset: 24, format: 'float32x2' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
arrayStride: 64,
|
|
28
|
+
stepMode: 'instance',
|
|
29
|
+
attributes: [
|
|
30
|
+
{ shaderLocation: 3, offset: 0, format: 'float32x4' },
|
|
31
|
+
{ shaderLocation: 4, offset: 16, format: 'float32x4' },
|
|
32
|
+
{ shaderLocation: 5, offset: 32, format: 'float32x4' },
|
|
33
|
+
{ shaderLocation: 6, offset: 48, format: 'float32x4' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const pow2 = (n, min) => {
|
|
39
|
+
let c = min;
|
|
40
|
+
while (c < n) c *= 2;
|
|
41
|
+
return c;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export class WebGPUBackend {
|
|
45
|
+
/**
|
|
46
|
+
* @param {HTMLCanvasElement} canvas
|
|
47
|
+
* @param {{present?: 'canvas'|'readback'}} [opts] 'readback' renders offscreen and
|
|
48
|
+
* copies pixels into a 2D canvas (headless testing, screenshots without a swap chain)
|
|
49
|
+
*/
|
|
50
|
+
constructor(canvas, opts = {}) {
|
|
51
|
+
this.canvas = canvas;
|
|
52
|
+
this.present = opts.present || 'canvas';
|
|
53
|
+
this.kind = 'webgpu';
|
|
54
|
+
this.zeroToOne = true;
|
|
55
|
+
this.pipelines = new Map();
|
|
56
|
+
this.meshModules = new Map();
|
|
57
|
+
this.materialBindings = new Map();
|
|
58
|
+
this.overlayBindings = new Map();
|
|
59
|
+
this.poolGeneration = -1;
|
|
60
|
+
this.vertexBuffer = null;
|
|
61
|
+
this.indexBuffer = null;
|
|
62
|
+
this.instanceBuffer = null;
|
|
63
|
+
this.gpuBytes = 0;
|
|
64
|
+
this.lost = false;
|
|
65
|
+
this.info = '';
|
|
66
|
+
this.errors = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async init() {
|
|
70
|
+
if (!navigator.gpu) throw new Error('WebGPU is not available in this browser.');
|
|
71
|
+
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
72
|
+
if (!adapter) throw new Error('No WebGPU adapter is available.');
|
|
73
|
+
this.adapter = adapter;
|
|
74
|
+
const info = adapter.info || {};
|
|
75
|
+
this.info = [info.vendor, info.architecture, info.description].filter(Boolean).join(' ') || 'WebGPU adapter';
|
|
76
|
+
const device = await adapter.requestDevice();
|
|
77
|
+
this.device = device;
|
|
78
|
+
device.lost.then((i) => {
|
|
79
|
+
this.lost = true;
|
|
80
|
+
this.errors.push(`WebGPU device lost: ${i.message}`);
|
|
81
|
+
});
|
|
82
|
+
device.onuncapturederror = (e) => {
|
|
83
|
+
this.errors.push(e.error && e.error.message ? e.error.message : String(e));
|
|
84
|
+
};
|
|
85
|
+
if (this.present === 'readback') {
|
|
86
|
+
this.format = 'rgba8unorm';
|
|
87
|
+
this.ctx2d = this.canvas.getContext('2d');
|
|
88
|
+
if (!this.ctx2d) throw new Error('Cannot create a 2D context for WebGPU readback presentation.');
|
|
89
|
+
this.canvasUsed = true;
|
|
90
|
+
} else {
|
|
91
|
+
const ctx = this.canvas.getContext('webgpu');
|
|
92
|
+
if (!ctx) throw new Error('Cannot create a WebGPU canvas context.');
|
|
93
|
+
this.canvasUsed = true;
|
|
94
|
+
this.context = ctx;
|
|
95
|
+
this.format = navigator.gpu.getPreferredCanvasFormat();
|
|
96
|
+
// colours are written already encoded as sRGB and tone mapped to 0..1: pin the
|
|
97
|
+
// colour space and standard range so HDR and wide-gamut screens show them unchanged
|
|
98
|
+
ctx.configure({ device, format: this.format, alphaMode: 'opaque', colorSpace: 'srgb', toneMapping: { mode: 'standard' } });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.frameLayout = device.createBindGroupLayout({
|
|
102
|
+
entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }],
|
|
103
|
+
});
|
|
104
|
+
this.materialLayout = device.createBindGroupLayout({
|
|
105
|
+
entries: [
|
|
106
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
107
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
108
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
109
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
110
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
111
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
112
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
this.pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [this.frameLayout, this.materialLayout] });
|
|
116
|
+
device.pushErrorScope('validation');
|
|
117
|
+
this.lineModule = device.createShaderModule({ code: LINE_WGSL, label: 'es3d lines' });
|
|
118
|
+
for (const [name, mod] of [['mesh', this.meshModule(0, '0')], ['lines', this.lineModule]]) {
|
|
119
|
+
const info2 = await mod.getCompilationInfo();
|
|
120
|
+
const errs = info2.messages.filter((m) => m.type === 'error');
|
|
121
|
+
if (errs.length) throw new Error(`WGSL ${name} shader error: ${errs.map((m) => `${m.lineNum}:${m.linePos} ${m.message}`).join('; ')}`);
|
|
122
|
+
}
|
|
123
|
+
const err = await device.popErrorScope();
|
|
124
|
+
if (err) throw new Error(`WebGPU initialisation error: ${err.message}`);
|
|
125
|
+
|
|
126
|
+
this.frameBuffer = device.createBuffer({ size: FRAME_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
127
|
+
this.frameBindGroup = device.createBindGroup({ layout: this.frameLayout, entries: [{ binding: 0, resource: { buffer: this.frameBuffer } }] });
|
|
128
|
+
this.sampler = device.createSampler({
|
|
129
|
+
addressModeU: 'repeat',
|
|
130
|
+
addressModeV: 'repeat',
|
|
131
|
+
magFilter: 'linear',
|
|
132
|
+
minFilter: 'linear',
|
|
133
|
+
mipmapFilter: 'linear',
|
|
134
|
+
maxAnisotropy: 8,
|
|
135
|
+
});
|
|
136
|
+
this.whiteTex = this.createTexture({ width: 1, height: 1, data: new Uint8Array([255, 255, 255, 255]) }, true);
|
|
137
|
+
this.flatNormalTex = this.createTexture({ width: 1, height: 1, data: new Uint8Array([128, 128, 255, 255]) }, false);
|
|
138
|
+
this.matData = new Float32Array(MATERIAL_FLOATS);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
createTexture(px, srgb) {
|
|
142
|
+
const levels = px.width > 1 || px.height > 1 ? buildMips(px) : [px];
|
|
143
|
+
const tex = this.device.createTexture({
|
|
144
|
+
size: [px.width, px.height],
|
|
145
|
+
format: srgb ? 'rgba8unorm-srgb' : 'rgba8unorm',
|
|
146
|
+
mipLevelCount: levels.length,
|
|
147
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
148
|
+
});
|
|
149
|
+
levels.forEach((lv, i) => {
|
|
150
|
+
const data = lv.data instanceof Uint8Array ? lv.data : new Uint8Array(lv.data.buffer, lv.data.byteOffset, lv.data.byteLength);
|
|
151
|
+
this.device.queue.writeTexture({ texture: tex, mipLevel: i }, data, { bytesPerRow: lv.width * 4, rowsPerImage: lv.height }, [lv.width, lv.height]);
|
|
152
|
+
this.gpuBytes += lv.width * lv.height * 4;
|
|
153
|
+
});
|
|
154
|
+
return { texture: tex, view: tex.createView() };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
destroyTexture(t) {
|
|
158
|
+
t.texture.destroy();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The shader module for a material's feature flags under the frame's lights, compiled on first use. */
|
|
162
|
+
meshModule(flags, lights) {
|
|
163
|
+
const key = `${flags}|${lights}`;
|
|
164
|
+
let m = this.meshModules.get(key);
|
|
165
|
+
if (!m) {
|
|
166
|
+
m = this.device.createShaderModule({ code: meshShader(flags, lights), label: `es3d mesh ${key}` });
|
|
167
|
+
this.meshModules.set(key, m);
|
|
168
|
+
}
|
|
169
|
+
return m;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pipeline(kind, { cull = 'back', frontFace = 'ccw', blend = false, depthWrite = true, depthCompare = 'less-equal', flags = 0, lights = '' } = {}) {
|
|
173
|
+
const isMesh = kind === 'mesh';
|
|
174
|
+
const key = `${kind}|${cull}|${frontFace}|${blend}|${depthWrite}|${depthCompare}${isMesh ? `|${flags}|${lights}` : ''}`;
|
|
175
|
+
let p = this.pipelines.get(key);
|
|
176
|
+
if (p) return p;
|
|
177
|
+
const module = isMesh ? this.meshModule(flags, lights) : this.lineModule;
|
|
178
|
+
const entry = isMesh ? 'vs_main' : kind === 'normals' ? 'vs_normals' : 'vs_line';
|
|
179
|
+
p = this.device.createRenderPipeline({
|
|
180
|
+
label: key,
|
|
181
|
+
layout: this.pipelineLayout,
|
|
182
|
+
vertex: { module, entryPoint: entry, buffers: VERTEX_LAYOUTS },
|
|
183
|
+
fragment: {
|
|
184
|
+
module,
|
|
185
|
+
entryPoint: isMesh ? 'fs_main' : 'fs_line',
|
|
186
|
+
targets: [
|
|
187
|
+
{
|
|
188
|
+
format: this.format,
|
|
189
|
+
blend: blend
|
|
190
|
+
? {
|
|
191
|
+
color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
192
|
+
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
|
|
193
|
+
}
|
|
194
|
+
: undefined,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
primitive: { topology: isMesh ? 'triangle-list' : 'line-list', cullMode: isMesh ? cull : 'none', frontFace },
|
|
199
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: depthWrite, depthCompare },
|
|
200
|
+
multisample: { count: SAMPLE_COUNT },
|
|
201
|
+
});
|
|
202
|
+
this.pipelines.set(key, p);
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ensureTargets() {
|
|
207
|
+
const w = this.canvas.width, h = this.canvas.height;
|
|
208
|
+
if (this.targetW === w && this.targetH === h && this.msaa) return;
|
|
209
|
+
if (this.msaa) this.msaa.destroy();
|
|
210
|
+
if (this.depth) this.depth.destroy();
|
|
211
|
+
this.msaa = this.device.createTexture({ size: [w, h], sampleCount: SAMPLE_COUNT, format: this.format, usage: GPUTextureUsage.RENDER_ATTACHMENT });
|
|
212
|
+
this.depth = this.device.createTexture({ size: [w, h], sampleCount: SAMPLE_COUNT, format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT });
|
|
213
|
+
this.msaaView = this.msaa.createView();
|
|
214
|
+
this.depthView = this.depth.createView();
|
|
215
|
+
if (this.present === 'readback') {
|
|
216
|
+
if (this.offscreen) this.offscreen.destroy();
|
|
217
|
+
if (this.readBuffer && !this.readPending) this.readBuffer.destroy();
|
|
218
|
+
this.offscreen = this.device.createTexture({ size: [w, h], format: this.format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
|
|
219
|
+
this.offscreenView = this.offscreen.createView();
|
|
220
|
+
this.readStride = Math.ceil((w * 4) / 256) * 256;
|
|
221
|
+
this.readBuffer = this.device.createBuffer({ size: this.readStride * h, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
222
|
+
}
|
|
223
|
+
this.targetW = w;
|
|
224
|
+
this.targetH = h;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Copy the offscreen frame into the 2D canvas (readback presentation). */
|
|
228
|
+
readback(encoder) {
|
|
229
|
+
if (this.readPending) return false;
|
|
230
|
+
const w = this.targetW, h = this.targetH;
|
|
231
|
+
encoder.copyTextureToBuffer({ texture: this.offscreen }, { buffer: this.readBuffer, bytesPerRow: this.readStride }, [w, h]);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
startReadback() {
|
|
236
|
+
const buf = this.readBuffer;
|
|
237
|
+
const w = this.targetW, h = this.targetH, stride = this.readStride;
|
|
238
|
+
this.readPending = buf.mapAsync(GPUMapMode.READ).then(() => {
|
|
239
|
+
const raw = new Uint8Array(buf.getMappedRange());
|
|
240
|
+
const px = new Uint8ClampedArray(w * h * 4);
|
|
241
|
+
for (let y = 0; y < h; y++) px.set(raw.subarray(y * stride, y * stride + w * 4), y * w * 4);
|
|
242
|
+
buf.unmap();
|
|
243
|
+
this.ctx2d.putImageData(new ImageData(px, w, h), 0, 0);
|
|
244
|
+
this.readPending = null;
|
|
245
|
+
// A frame drawn while this copy was in flight was not copied. A page that
|
|
246
|
+
// draws only on change may draw nothing more, so show that frame now.
|
|
247
|
+
if (!this.lastFrameCopied && this.offscreen && !this.lost) {
|
|
248
|
+
const encoder = this.device.createCommandEncoder();
|
|
249
|
+
this.readback(encoder);
|
|
250
|
+
this.device.queue.submit([encoder.finish()]);
|
|
251
|
+
this.lastFrameCopied = true;
|
|
252
|
+
this.startReadback();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
syncPool(pool) {
|
|
258
|
+
const d = this.device;
|
|
259
|
+
const vBytes = pool.vertices.byteLength;
|
|
260
|
+
const iBytes = pool.indices.byteLength;
|
|
261
|
+
let uploaded = 0;
|
|
262
|
+
if (pool.generation !== this.poolGeneration || !this.vertexBuffer || this.vertexBuffer.size < vBytes || this.indexBuffer.size < iBytes) {
|
|
263
|
+
if (this.vertexBuffer) this.vertexBuffer.destroy();
|
|
264
|
+
if (this.indexBuffer) this.indexBuffer.destroy();
|
|
265
|
+
this.vertexBuffer = d.createBuffer({ size: pow2(vBytes, 1 << 16), usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
266
|
+
this.indexBuffer = d.createBuffer({ size: pow2(iBytes, 1 << 16), usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
|
|
267
|
+
this.poolGeneration = pool.generation;
|
|
268
|
+
pool.uploadedVertices = 0;
|
|
269
|
+
pool.uploadedIndices = 0;
|
|
270
|
+
}
|
|
271
|
+
const p = pool.pending();
|
|
272
|
+
if (p.vertexEnd > p.vertexStart) {
|
|
273
|
+
const src = pool.vertices.subarray(p.vertexStart * 8, p.vertexEnd * 8);
|
|
274
|
+
d.queue.writeBuffer(this.vertexBuffer, p.vertexStart * VERTEX_BYTES, src.buffer, src.byteOffset, src.byteLength);
|
|
275
|
+
uploaded += src.byteLength;
|
|
276
|
+
}
|
|
277
|
+
if (p.indexEnd > p.indexStart) {
|
|
278
|
+
const src = pool.indices.subarray(p.indexStart, p.indexEnd);
|
|
279
|
+
d.queue.writeBuffer(this.indexBuffer, p.indexStart * 4, src.buffer, src.byteOffset, src.byteLength);
|
|
280
|
+
uploaded += src.byteLength;
|
|
281
|
+
}
|
|
282
|
+
pool.markUploaded();
|
|
283
|
+
return uploaded;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
uploadInstances(data, count) {
|
|
287
|
+
const bytes = Math.max(64, count * 64);
|
|
288
|
+
if (!this.instanceBuffer || this.instanceBuffer.size < bytes) {
|
|
289
|
+
if (this.instanceBuffer) this.instanceBuffer.destroy();
|
|
290
|
+
this.instanceBuffer = this.device.createBuffer({ size: pow2(bytes, 64 * 256), usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
291
|
+
}
|
|
292
|
+
if (count) this.device.queue.writeBuffer(this.instanceBuffer, 0, data.buffer, data.byteOffset, count * 64);
|
|
293
|
+
return count * 64;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
readyTexture(entry, srgb) {
|
|
297
|
+
if (entry && entry.status === 'ready' && !entry.gpu) entry.gpu = this.createTexture(entry.pixels, srgb);
|
|
298
|
+
return entry && entry.gpu ? entry.gpu : null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
materialEntries(buffer, map, normal, mr, occlusion, emissive) {
|
|
302
|
+
return [
|
|
303
|
+
{ binding: 0, resource: { buffer } },
|
|
304
|
+
{ binding: 1, resource: this.sampler },
|
|
305
|
+
{ binding: 2, resource: map.view },
|
|
306
|
+
{ binding: 3, resource: normal.view },
|
|
307
|
+
{ binding: 4, resource: mr.view },
|
|
308
|
+
{ binding: 5, resource: occlusion.view },
|
|
309
|
+
{ binding: 6, resource: emissive.view },
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
materialBinding(material, textures, doubleSided) {
|
|
314
|
+
const id = doubleSided ? 'ds' : 'ss';
|
|
315
|
+
let rec = this.materialBindings.get(material);
|
|
316
|
+
if (!rec) {
|
|
317
|
+
rec = {};
|
|
318
|
+
this.materialBindings.set(material, rec);
|
|
319
|
+
}
|
|
320
|
+
const mapGpu = this.readyTexture(material.map ? textures.get(material.map, 'color') : null, true);
|
|
321
|
+
const nrmGpu = this.readyTexture(material.normalMap ? textures.get(material.normalMap, 'normal') : null, false);
|
|
322
|
+
const mrGpu = this.readyTexture(material.metallicRoughnessMap ? textures.get(material.metallicRoughnessMap, 'linear') : null, false);
|
|
323
|
+
const occGpu = this.readyTexture(material.occlusionMap ? textures.get(material.occlusionMap, 'linear') : null, false);
|
|
324
|
+
const emGpu = this.readyTexture(material.emissiveMap ? textures.get(material.emissiveMap, 'color') : null, true);
|
|
325
|
+
const hasMap = !!mapGpu;
|
|
326
|
+
const hasNormalMap = !!nrmGpu;
|
|
327
|
+
const hasMrMap = !!mrGpu;
|
|
328
|
+
const hasOcclusionMap = !!occGpu;
|
|
329
|
+
const hasEmissiveMap = !!emGpu;
|
|
330
|
+
const key = `${hasMap}|${hasNormalMap}|${hasMrMap}|${hasOcclusionMap}|${hasEmissiveMap}`;
|
|
331
|
+
let b = rec[id];
|
|
332
|
+
if (!b || b.key !== key) {
|
|
333
|
+
if (b) b.buffer.destroy();
|
|
334
|
+
packMaterial(this.matData, material, { hasMap, hasNormalMap, hasMrMap, hasOcclusionMap, hasEmissiveMap, doubleSided: doubleSided || material.doubleSided });
|
|
335
|
+
const buffer = this.device.createBuffer({ size: MATERIAL_FLOATS * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
336
|
+
this.device.queue.writeBuffer(buffer, 0, this.matData);
|
|
337
|
+
const bindGroup = this.device.createBindGroup({
|
|
338
|
+
layout: this.materialLayout,
|
|
339
|
+
entries: this.materialEntries(buffer, mapGpu || this.whiteTex, nrmGpu || this.flatNormalTex, mrGpu || this.whiteTex, occGpu || this.whiteTex, emGpu || this.whiteTex),
|
|
340
|
+
});
|
|
341
|
+
b = rec[id] = { key, buffer, bindGroup, flags: this.matData[10] };
|
|
342
|
+
}
|
|
343
|
+
return b;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
overlayBinding(name, rgba) {
|
|
347
|
+
let b = this.overlayBindings.get(name);
|
|
348
|
+
if (b) return b;
|
|
349
|
+
packOverlay(this.matData, rgba);
|
|
350
|
+
const buffer = this.device.createBuffer({ size: MATERIAL_FLOATS * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
351
|
+
this.device.queue.writeBuffer(buffer, 0, this.matData);
|
|
352
|
+
b = this.device.createBindGroup({
|
|
353
|
+
layout: this.materialLayout,
|
|
354
|
+
entries: this.materialEntries(buffer, this.whiteTex, this.flatNormalTex, this.whiteTex, this.whiteTex, this.whiteTex),
|
|
355
|
+
});
|
|
356
|
+
this.overlayBindings.set(name, b);
|
|
357
|
+
return b;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
releaseMaterials(keep) {
|
|
361
|
+
for (const [m, rec] of this.materialBindings) {
|
|
362
|
+
if (keep.has(m)) continue;
|
|
363
|
+
for (const b of Object.values(rec)) b.buffer.destroy();
|
|
364
|
+
this.materialBindings.delete(m);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** What this backend renders with (Renderer.caps). */
|
|
369
|
+
get caps() {
|
|
370
|
+
return {
|
|
371
|
+
msaaSamples: SAMPLE_COUNT,
|
|
372
|
+
anisotropy: 8,
|
|
373
|
+
maxTextureSize: this.device ? this.device.limits.maxTextureDimension2D : 0,
|
|
374
|
+
colorSpace: 'srgb',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
resize(width, height) {
|
|
379
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
380
|
+
this.canvas.width = width;
|
|
381
|
+
this.canvas.height = height;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
render(draw, frameData, ctx) {
|
|
386
|
+
if (this.lost) return;
|
|
387
|
+
const d = this.device;
|
|
388
|
+
this.ensureTargets();
|
|
389
|
+
d.queue.writeBuffer(this.frameBuffer, 0, frameData.buffer, frameData.byteOffset, frameData.byteLength);
|
|
390
|
+
this.uploadInstances(draw.instanceData, draw.instanceCount);
|
|
391
|
+
const encoder = d.createCommandEncoder();
|
|
392
|
+
const pass = encoder.beginRenderPass({
|
|
393
|
+
colorAttachments: [
|
|
394
|
+
{
|
|
395
|
+
view: this.msaaView,
|
|
396
|
+
resolveTarget: this.present === 'readback' ? this.offscreenView : this.context.getCurrentTexture().createView(),
|
|
397
|
+
clearValue: { r: ctx.background[0], g: ctx.background[1], b: ctx.background[2], a: 1 },
|
|
398
|
+
loadOp: 'clear',
|
|
399
|
+
storeOp: 'discard',
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
depthStencilAttachment: { view: this.depthView, depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'discard' },
|
|
403
|
+
});
|
|
404
|
+
pass.setBindGroup(0, this.frameBindGroup);
|
|
405
|
+
if (this.vertexBuffer) {
|
|
406
|
+
pass.setVertexBuffer(0, this.vertexBuffer);
|
|
407
|
+
pass.setIndexBuffer(this.indexBuffer, 'uint32');
|
|
408
|
+
}
|
|
409
|
+
pass.setVertexBuffer(1, this.instanceBuffer);
|
|
410
|
+
let currentPipeline = null;
|
|
411
|
+
const setPipe = (p) => {
|
|
412
|
+
if (p !== currentPipeline) {
|
|
413
|
+
pass.setPipeline(p);
|
|
414
|
+
currentPipeline = p;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
if (this.vertexBuffer) {
|
|
418
|
+
let boundGroup = null;
|
|
419
|
+
const lights = lightTypes(frameData);
|
|
420
|
+
for (const b of draw.batches) {
|
|
421
|
+
const lines = b.kind === 'lines';
|
|
422
|
+
const binding = this.materialBinding(b.material, ctx.textures, b.doubleSided && !b.material.doubleSided);
|
|
423
|
+
const pipe = lines
|
|
424
|
+
? this.pipeline('lines', { blend: b.transparent, depthWrite: !b.transparent })
|
|
425
|
+
: this.pipeline('mesh', {
|
|
426
|
+
cull: b.doubleSided ? 'none' : 'back',
|
|
427
|
+
frontFace: b.mirrored ? 'cw' : 'ccw',
|
|
428
|
+
blend: b.transparent,
|
|
429
|
+
depthWrite: !b.transparent,
|
|
430
|
+
flags: binding.flags,
|
|
431
|
+
lights,
|
|
432
|
+
});
|
|
433
|
+
setPipe(pipe);
|
|
434
|
+
const group = binding.bindGroup;
|
|
435
|
+
if (group !== boundGroup) {
|
|
436
|
+
pass.setBindGroup(1, group);
|
|
437
|
+
boundGroup = group;
|
|
438
|
+
}
|
|
439
|
+
pass.drawIndexed(b.range.indexCount, b.count, b.range.firstIndex, 0, b.firstInstance);
|
|
440
|
+
}
|
|
441
|
+
const ov = draw.overlays;
|
|
442
|
+
const drawLines = (kind, name, color, list, depthCompare = 'less-equal') => {
|
|
443
|
+
if (!list.length) return;
|
|
444
|
+
setPipe(this.pipeline(kind, { blend: true, depthWrite: false, depthCompare }));
|
|
445
|
+
pass.setBindGroup(1, this.overlayBinding(name, color));
|
|
446
|
+
for (const o of list) pass.drawIndexed(o.range.indexCount, o.count, o.range.firstIndex, 0, o.firstInstance);
|
|
447
|
+
};
|
|
448
|
+
drawLines('lines', 'wireframe', ctx.overlayColors.wireframe, ov.wireframe);
|
|
449
|
+
drawLines('normals', 'normals', ctx.overlayColors.normals, ov.normals);
|
|
450
|
+
if (ov.bbox && ov.bbox.count) drawLines('lines', 'bbox', ctx.overlayColors.bbox, [{ ...ov.bbox, range: ctx.cubeRange }]);
|
|
451
|
+
if (ov.selection && ov.selection.count) drawLines('lines', 'selection', ctx.overlayColors.selection, [{ ...ov.selection, range: ctx.cubeRange }], 'always');
|
|
452
|
+
}
|
|
453
|
+
pass.end();
|
|
454
|
+
const copied = this.present === 'readback' && this.readback(encoder);
|
|
455
|
+
d.queue.submit([encoder.finish()]);
|
|
456
|
+
if (copied) this.startReadback();
|
|
457
|
+
this.lastFrameCopied = copied;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async finish() {
|
|
461
|
+
await this.device.queue.onSubmittedWorkDone();
|
|
462
|
+
if (this.readPending) await this.readPending;
|
|
463
|
+
if (this.present === 'readback' && !this.lastFrameCopied && this.offscreen) {
|
|
464
|
+
// the latest frame was rendered while an older readback was in flight
|
|
465
|
+
const encoder = this.device.createCommandEncoder();
|
|
466
|
+
this.readback(encoder);
|
|
467
|
+
this.device.queue.submit([encoder.finish()]);
|
|
468
|
+
this.startReadback();
|
|
469
|
+
await this.readPending;
|
|
470
|
+
this.lastFrameCopied = true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
dispose() {
|
|
475
|
+
if (!this.device) return;
|
|
476
|
+
for (const b of [this.vertexBuffer, this.indexBuffer, this.instanceBuffer, this.frameBuffer]) if (b) b.destroy();
|
|
477
|
+
for (const rec of this.materialBindings.values()) for (const b of Object.values(rec)) b.buffer.destroy();
|
|
478
|
+
if (this.msaa) this.msaa.destroy();
|
|
479
|
+
if (this.depth) this.depth.destroy();
|
|
480
|
+
this.device.destroy();
|
|
481
|
+
}
|
|
482
|
+
}
|