@es3d/es3d 0.0.0-stage → 1.0.0

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.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +86 -3
  3. package/docs/REFERENCE.md +581 -0
  4. package/docs/SPEC.md +417 -0
  5. package/package.json +50 -4
  6. package/scripts/es3d-cli.mjs +377 -0
  7. package/src/cache/geometry-store.js +392 -0
  8. package/src/cache/mesh-bundle.js +123 -0
  9. package/src/cache/sha256.js +73 -0
  10. package/src/format/ast.js +238 -0
  11. package/src/format/colors.js +93 -0
  12. package/src/format/compact-parser.js +171 -0
  13. package/src/format/compact-tokenizer.js +165 -0
  14. package/src/format/errors.js +247 -0
  15. package/src/format/json.js +55 -0
  16. package/src/format/limits.js +14 -0
  17. package/src/format/parse.js +92 -0
  18. package/src/format/schema.js +617 -0
  19. package/src/format/serializer.js +333 -0
  20. package/src/format/validator.js +330 -0
  21. package/src/format/value-error.js +12 -0
  22. package/src/format/value-format.js +314 -0
  23. package/src/format/values.js +459 -0
  24. package/src/format/xml-parser.js +136 -0
  25. package/src/format/xml-tokenizer.js +237 -0
  26. package/src/geometry/contour.js +86 -0
  27. package/src/geometry/extrude.js +175 -0
  28. package/src/geometry/gear.js +85 -0
  29. package/src/geometry/geometry-cache.js +159 -0
  30. package/src/geometry/lod-error.js +178 -0
  31. package/src/geometry/mesh.js +458 -0
  32. package/src/geometry/primitives.js +299 -0
  33. package/src/geometry/registry.js +645 -0
  34. package/src/geometry/revolve.js +136 -0
  35. package/src/geometry/sweep.js +441 -0
  36. package/src/index.js +47 -0
  37. package/src/io/compression.js +67 -0
  38. package/src/io/convert.js +236 -0
  39. package/src/io/download.js +13 -0
  40. package/src/io/external-scene.js +127 -0
  41. package/src/io/file-loader.js +28 -0
  42. package/src/io/gltf.js +630 -0
  43. package/src/io/mesh-import.js +104 -0
  44. package/src/io/obj.js +352 -0
  45. package/src/io/step-brep.js +439 -0
  46. package/src/io/step-exact.js +1149 -0
  47. package/src/io/step.js +1418 -0
  48. package/src/io/stl.js +193 -0
  49. package/src/io/svg-import.js +787 -0
  50. package/src/material/materials.js +154 -0
  51. package/src/math/mat4.js +475 -0
  52. package/src/math/random.js +30 -0
  53. package/src/math/trig.js +65 -0
  54. package/src/math/vec3.js +81 -0
  55. package/src/optimize/optimizer.js +579 -0
  56. package/src/path/path-flatten.js +322 -0
  57. package/src/path/path-parser.js +332 -0
  58. package/src/path/shapes.js +129 -0
  59. package/src/path/triangulate.js +517 -0
  60. package/src/pipeline.js +74 -0
  61. package/src/render/camera.js +168 -0
  62. package/src/render/frame-builder.js +362 -0
  63. package/src/render/geometry-pool.js +149 -0
  64. package/src/render/lighting.js +149 -0
  65. package/src/render/orbit-controls.js +237 -0
  66. package/src/render/renderer.js +249 -0
  67. package/src/render/scroll-timeline.js +104 -0
  68. package/src/render/shaders/glsl.js +285 -0
  69. package/src/render/shaders/wgsl.js +252 -0
  70. package/src/render/stage.js +352 -0
  71. package/src/render/texture-cache.js +40 -0
  72. package/src/render/textures.js +223 -0
  73. package/src/render/webgl2/webgl2-backend.js +532 -0
  74. package/src/render/webgpu/webgpu-backend.js +480 -0
  75. package/src/scene/animation.js +171 -0
  76. package/src/scene/camera-constraints.js +117 -0
  77. package/src/scene/flatten.js +754 -0
  78. package/src/scene/picking.js +161 -0
  79. package/src/scene/scene-graph.js +320 -0
  80. package/src/transform/transform.js +211 -0
  81. package/src/wasm/kernels-binary.js +3 -0
  82. package/src/wasm/kernels.js +385 -0
@@ -0,0 +1,532 @@
1
+ // WebGL2 rendering backend.
2
+ //
3
+ // One VAO, one shared vertex buffer and one shared 32-bit index buffer (from
4
+ // the GeometryPool), one instance buffer per frame. Each batch is a single
5
+ // drawElementsInstanced call; the per-instance attribute pointers are moved to
6
+ // the batch's first instance (WebGL2 has no firstInstance parameter). Mesh
7
+ // programs are specialised per material feature set and light setup
8
+ // (meshFragmentShader) and compiled on first use.
9
+ //
10
+ // Frames are drawn with 4x multisampling, as in the WebGPU backend, so edges
11
+ // look the same everywhere. The canvas is asked for antialiasing; when the
12
+ // browser grants at least 4 samples (the usual case) frames go straight into
13
+ // it. The request is only a hint, though: with fewer samples, frames are drawn
14
+ // into our own 4x framebuffer and resolved into the canvas, which costs two
15
+ // full-screen copies per frame.
16
+
17
+ import { meshVertexShader, meshFragmentShader, TEXTURE_FLAGS, LINE_VS, NORMAL_VS, LINE_FS } from '../shaders/glsl.js';
18
+ import { FRAME_BYTES, MATERIAL_FLOATS, packMaterial, packOverlay, lightTypes } from '../lighting.js';
19
+ import { VERTEX_BYTES } from '../geometry-pool.js';
20
+
21
+ export const MSAA_SAMPLES = 4;
22
+ const ANISOTROPY = 8;
23
+
24
+ function compile(gl, type, src, label) {
25
+ const sh = gl.createShader(type);
26
+ gl.shaderSource(sh, src);
27
+ gl.compileShader(sh);
28
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
29
+ const log = gl.getShaderInfoLog(sh);
30
+ gl.deleteShader(sh);
31
+ throw new Error(`WebGL2 ${label} shader compile error: ${log}`);
32
+ }
33
+ return sh;
34
+ }
35
+
36
+ function program(gl, vs, fs, label) {
37
+ const p = gl.createProgram();
38
+ gl.attachShader(p, compile(gl, gl.VERTEX_SHADER, vs, label + ' vertex'));
39
+ gl.attachShader(p, compile(gl, gl.FRAGMENT_SHADER, fs, label + ' fragment'));
40
+ gl.linkProgram(p);
41
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(`WebGL2 ${label} link error: ${gl.getProgramInfoLog(p)}`);
42
+ const frameIdx = gl.getUniformBlockIndex(p, 'Frame');
43
+ if (frameIdx !== gl.INVALID_INDEX) gl.uniformBlockBinding(p, frameIdx, 0);
44
+ const matIdx = gl.getUniformBlockIndex(p, 'Material');
45
+ if (matIdx !== gl.INVALID_INDEX) gl.uniformBlockBinding(p, matIdx, 1);
46
+ gl.useProgram(p);
47
+ const bindSampler = (name, unit) => {
48
+ const loc = gl.getUniformLocation(p, name);
49
+ if (loc) gl.uniform1i(loc, unit);
50
+ };
51
+ bindSampler('uMap', 0);
52
+ bindSampler('uNormalMap', 1);
53
+ bindSampler('uMrMap', 2);
54
+ bindSampler('uOcclusionMap', 3);
55
+ bindSampler('uEmissiveMap', 4);
56
+ return p;
57
+ }
58
+
59
+ export class WebGL2Backend {
60
+ constructor(canvas) {
61
+ this.canvas = canvas;
62
+ this.kind = 'webgl2';
63
+ this.zeroToOne = false;
64
+ this.gl = null;
65
+ this.vertexCapacity = 0;
66
+ this.indexCapacity = 0;
67
+ this.instanceCapacity = 0;
68
+ this.poolGeneration = -1;
69
+ this.materialUbos = new Map();
70
+ this.overlayUbos = new Map();
71
+ this.gpuBytes = 0;
72
+ this.lost = false;
73
+ this.info = '';
74
+ }
75
+
76
+ async init() {
77
+ const gl = this.canvas.getContext('webgl2', {
78
+ antialias: true,
79
+ alpha: false,
80
+ depth: true,
81
+ premultipliedAlpha: false,
82
+ preserveDrawingBuffer: false,
83
+ powerPreference: 'high-performance',
84
+ });
85
+ if (!gl) throw new Error('WebGL2 is not available in this browser.');
86
+ this.gl = gl;
87
+ // colours are written already encoded as sRGB; say so, so wide-gamut screens do not stretch them
88
+ if ('drawingBufferColorSpace' in gl) gl.drawingBufferColorSpace = 'srgb';
89
+ const granted = gl.getParameter(gl.SAMPLES) || 0;
90
+ this.direct = granted >= MSAA_SAMPLES;
91
+ this.samples = this.direct ? granted : Math.min(MSAA_SAMPLES, gl.getParameter(gl.MAX_SAMPLES) || 0);
92
+ if (!this.direct) {
93
+ this.fbo = gl.createFramebuffer();
94
+ this.colorRb = gl.createRenderbuffer();
95
+ this.depthRb = gl.createRenderbuffer();
96
+ // A multisample resolve must blit between identical formats, and the
97
+ // canvas of an opaque context is RGB8, RGBA8 or BGRA8 depending on the
98
+ // driver. So samples are resolved into a single-sample RGBA8 buffer, and
99
+ // that is copied to the canvas (a single-sample copy may convert formats).
100
+ this.resolveFbo = gl.createFramebuffer();
101
+ this.resolveRb = gl.createRenderbuffer();
102
+ }
103
+ this.targetSize = [0, 0];
104
+ this.canvas.addEventListener('webglcontextlost', (e) => {
105
+ e.preventDefault();
106
+ this.lost = true;
107
+ });
108
+ const dbg = gl.getExtension('WEBGL_debug_renderer_info');
109
+ this.info = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
110
+ this.aniso = gl.getExtension('EXT_texture_filter_anisotropic');
111
+
112
+ this.meshPrograms = new Map();
113
+ this.lineProgram = program(gl, LINE_VS, LINE_FS, 'line');
114
+ this.normalProgram = program(gl, NORMAL_VS, LINE_FS, 'normals');
115
+
116
+ this.vertexBuffer = gl.createBuffer();
117
+ this.indexBuffer = gl.createBuffer();
118
+ this.instanceBuffer = gl.createBuffer();
119
+ this.frameUbo = gl.createBuffer();
120
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.frameUbo);
121
+ gl.bufferData(gl.UNIFORM_BUFFER, FRAME_BYTES, gl.DYNAMIC_DRAW);
122
+
123
+ this.vao = gl.createVertexArray();
124
+ gl.bindVertexArray(this.vao);
125
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
126
+ gl.enableVertexAttribArray(0);
127
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, VERTEX_BYTES, 0);
128
+ gl.enableVertexAttribArray(1);
129
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, VERTEX_BYTES, 12);
130
+ gl.enableVertexAttribArray(2);
131
+ gl.vertexAttribPointer(2, 2, gl.FLOAT, false, VERTEX_BYTES, 24);
132
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
133
+ for (let k = 0; k < 4; k++) {
134
+ gl.enableVertexAttribArray(3 + k);
135
+ gl.vertexAttribPointer(3 + k, 4, gl.FLOAT, false, 64, k * 16);
136
+ gl.vertexAttribDivisor(3 + k, 1);
137
+ }
138
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
139
+ gl.bindVertexArray(null);
140
+
141
+ this.whiteTex = this.createTexture({ width: 1, height: 1, data: new Uint8Array([255, 255, 255, 255]) }, true);
142
+ this.flatNormalTex = this.createTexture({ width: 1, height: 1, data: new Uint8Array([128, 128, 255, 255]) }, false);
143
+ this.matData = new Float32Array(MATERIAL_FLOATS);
144
+ }
145
+
146
+ createTexture(px, srgb) {
147
+ const gl = this.gl;
148
+ const t = gl.createTexture();
149
+ gl.bindTexture(gl.TEXTURE_2D, t);
150
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
151
+ gl.texImage2D(gl.TEXTURE_2D, 0, srgb ? gl.SRGB8_ALPHA8 : gl.RGBA8, px.width, px.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, px.data instanceof Uint8Array ? px.data : new Uint8Array(px.data.buffer, px.data.byteOffset, px.data.byteLength));
152
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
153
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
154
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
155
+ if (px.width > 1) {
156
+ gl.generateMipmap(gl.TEXTURE_2D);
157
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
158
+ if (this.aniso) gl.texParameterf(gl.TEXTURE_2D, this.aniso.TEXTURE_MAX_ANISOTROPY_EXT, ANISOTROPY);
159
+ } else {
160
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
161
+ }
162
+ this.gpuBytes += px.width * px.height * 4 * (px.width > 1 ? 1.33 : 1);
163
+ return t;
164
+ }
165
+
166
+ destroyTexture(t) {
167
+ this.gl.deleteTexture(t);
168
+ }
169
+
170
+ /** Upload geometry appended to the pool since the last sync. */
171
+ syncPool(pool) {
172
+ const gl = this.gl;
173
+ const vBytes = pool.vertices.byteLength;
174
+ const iBytes = pool.indices.byteLength;
175
+ const realloc = pool.generation !== this.poolGeneration || vBytes !== this.vertexCapacity || iBytes !== this.indexCapacity;
176
+ let uploaded = 0;
177
+ if (realloc) {
178
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
179
+ gl.bufferData(gl.ARRAY_BUFFER, vBytes, gl.STATIC_DRAW);
180
+ gl.bindVertexArray(this.vao);
181
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
182
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iBytes, gl.STATIC_DRAW);
183
+ gl.bindVertexArray(null);
184
+ this.vertexCapacity = vBytes;
185
+ this.indexCapacity = iBytes;
186
+ this.poolGeneration = pool.generation;
187
+ pool.uploadedVertices = 0;
188
+ pool.uploadedIndices = 0;
189
+ }
190
+ const p = pool.pending();
191
+ if (p.vertexEnd > p.vertexStart) {
192
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
193
+ const f = 8;
194
+ gl.bufferSubData(gl.ARRAY_BUFFER, p.vertexStart * VERTEX_BYTES, pool.vertices.subarray(p.vertexStart * f, p.vertexEnd * f));
195
+ uploaded += (p.vertexEnd - p.vertexStart) * VERTEX_BYTES;
196
+ }
197
+ if (p.indexEnd > p.indexStart) {
198
+ gl.bindVertexArray(this.vao);
199
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
200
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, p.indexStart * 4, pool.indices.subarray(p.indexStart, p.indexEnd));
201
+ gl.bindVertexArray(null);
202
+ uploaded += (p.indexEnd - p.indexStart) * 4;
203
+ }
204
+ pool.markUploaded();
205
+ return uploaded;
206
+ }
207
+
208
+ uploadInstances(data, count) {
209
+ const gl = this.gl;
210
+ const bytes = count * 64;
211
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
212
+ if (bytes > this.instanceCapacity) {
213
+ let cap = Math.max(this.instanceCapacity, 64 * 256);
214
+ while (cap < bytes) cap *= 2;
215
+ gl.bufferData(gl.ARRAY_BUFFER, cap, gl.DYNAMIC_DRAW);
216
+ this.instanceCapacity = cap;
217
+ }
218
+ if (bytes) gl.bufferSubData(gl.ARRAY_BUFFER, 0, data.subarray(0, count * 16));
219
+ return bytes;
220
+ }
221
+
222
+ readyTexture(entry, srgb) {
223
+ if (entry && entry.status === 'ready' && !entry.gpu) entry.gpu = this.createTexture(entry.pixels, srgb);
224
+ return entry && entry.gpu ? entry.gpu : null;
225
+ }
226
+
227
+ materialBinding(material, textures) {
228
+ const gl = this.gl;
229
+ let b = this.materialUbos.get(material);
230
+ if (!b) {
231
+ b = { ubo: gl.createBuffer(), map: null, normalMap: null, mrMap: null, occlusionMap: null, emissiveMap: null, key: '' };
232
+ this.materialUbos.set(material, b);
233
+ }
234
+ // texture state may change asynchronously (downloads)
235
+ const mapGpu = this.readyTexture(material.map ? textures.get(material.map, 'color') : null, true);
236
+ const nrmGpu = this.readyTexture(material.normalMap ? textures.get(material.normalMap, 'normal') : null, false);
237
+ const mrGpu = this.readyTexture(material.metallicRoughnessMap ? textures.get(material.metallicRoughnessMap, 'linear') : null, false);
238
+ const occGpu = this.readyTexture(material.occlusionMap ? textures.get(material.occlusionMap, 'linear') : null, false);
239
+ const emGpu = this.readyTexture(material.emissiveMap ? textures.get(material.emissiveMap, 'color') : null, true);
240
+ const hasMap = !!mapGpu;
241
+ const hasNormalMap = !!nrmGpu;
242
+ const hasMrMap = !!mrGpu;
243
+ const hasOcclusionMap = !!occGpu;
244
+ const hasEmissiveMap = !!emGpu;
245
+ const key = `${hasMap}|${hasNormalMap}|${hasMrMap}|${hasOcclusionMap}|${hasEmissiveMap}`;
246
+ if (b.key !== key) {
247
+ packMaterial(this.matData, material, { hasMap, hasNormalMap, hasMrMap, hasOcclusionMap, hasEmissiveMap, doubleSided: material.doubleSided });
248
+ gl.bindBuffer(gl.UNIFORM_BUFFER, b.ubo);
249
+ gl.bufferData(gl.UNIFORM_BUFFER, this.matData, gl.STATIC_DRAW);
250
+ b.key = key;
251
+ b.flags = this.matData[10];
252
+ }
253
+ b.map = mapGpu || this.whiteTex;
254
+ b.normalMap = nrmGpu || this.flatNormalTex;
255
+ b.mrMap = mrGpu || this.whiteTex;
256
+ b.occlusionMap = occGpu || this.whiteTex;
257
+ b.emissiveMap = emGpu || this.whiteTex;
258
+ return b;
259
+ }
260
+
261
+ /** Per-batch uniform buffer for double-sided geometry that uses a single-sided material. */
262
+ doubleSidedBinding(material, textures) {
263
+ const base = this.materialBinding(material, textures);
264
+ let ds = base.doubleSidedUbo;
265
+ const key = base.key;
266
+ if (!ds || base.dsKey !== key) {
267
+ if (!ds) ds = base.doubleSidedUbo = this.gl.createBuffer();
268
+ const hasMap = base.map !== this.whiteTex;
269
+ const hasNormalMap = base.normalMap !== this.flatNormalTex;
270
+ const hasMrMap = base.mrMap !== this.whiteTex;
271
+ const hasOcclusionMap = base.occlusionMap !== this.whiteTex;
272
+ const hasEmissiveMap = base.emissiveMap !== this.whiteTex;
273
+ packMaterial(this.matData, material, { hasMap, hasNormalMap, hasMrMap, hasOcclusionMap, hasEmissiveMap, doubleSided: true });
274
+ this.gl.bindBuffer(this.gl.UNIFORM_BUFFER, ds);
275
+ this.gl.bufferData(this.gl.UNIFORM_BUFFER, this.matData, this.gl.STATIC_DRAW);
276
+ base.dsKey = key;
277
+ base.dsFlags = this.matData[10];
278
+ }
279
+ // one object per material, so the draw loop can tell when nothing needs rebinding
280
+ const out = base.dsBinding || (base.dsBinding = {});
281
+ out.ubo = ds;
282
+ out.flags = base.dsFlags;
283
+ out.map = base.map;
284
+ out.normalMap = base.normalMap;
285
+ out.mrMap = base.mrMap;
286
+ out.occlusionMap = base.occlusionMap;
287
+ out.emissiveMap = base.emissiveMap;
288
+ return out;
289
+ }
290
+
291
+ overlayUbo(name, rgba) {
292
+ let u = this.overlayUbos.get(name);
293
+ if (!u) {
294
+ u = this.gl.createBuffer();
295
+ this.overlayUbos.set(name, u);
296
+ packOverlay(this.matData, rgba);
297
+ this.gl.bindBuffer(this.gl.UNIFORM_BUFFER, u);
298
+ this.gl.bufferData(this.gl.UNIFORM_BUFFER, this.matData, this.gl.STATIC_DRAW);
299
+ }
300
+ return u;
301
+ }
302
+
303
+ /** Free uniform buffers of materials that are no longer used. */
304
+ releaseMaterials(keep) {
305
+ for (const [m, b] of this.materialUbos) {
306
+ if (keep.has(m)) continue;
307
+ this.gl.deleteBuffer(b.ubo);
308
+ if (b.doubleSidedUbo) this.gl.deleteBuffer(b.doubleSidedUbo);
309
+ this.materialUbos.delete(m);
310
+ }
311
+ }
312
+
313
+ setInstanceOffset(first) {
314
+ const gl = this.gl;
315
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
316
+ const base = first * 64;
317
+ for (let k = 0; k < 4; k++) gl.vertexAttribPointer(3 + k, 4, gl.FLOAT, false, 64, base + k * 16);
318
+ }
319
+
320
+ resize(width, height) {
321
+ if (this.canvas.width !== width || this.canvas.height !== height) {
322
+ this.canvas.width = width;
323
+ this.canvas.height = height;
324
+ }
325
+ }
326
+
327
+ /** What this backend renders with (Renderer.caps). */
328
+ get caps() {
329
+ const gl = this.gl;
330
+ return {
331
+ msaaSamples: this.samples,
332
+ anisotropy: this.aniso ? Math.min(ANISOTROPY, gl.getParameter(this.aniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT)) : 1,
333
+ maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
334
+ colorSpace: gl.drawingBufferColorSpace || 'srgb',
335
+ };
336
+ }
337
+
338
+ /** The program for a material's feature flags under the frame's lights, compiled on first use. */
339
+ meshProgram(flags, lights) {
340
+ const key = `${flags}|${lights}`;
341
+ let p = this.meshPrograms.get(key);
342
+ if (!p) {
343
+ p = program(this.gl, meshVertexShader((flags & TEXTURE_FLAGS) !== 0), meshFragmentShader(flags, lights), 'mesh');
344
+ this.meshPrograms.set(key, p);
345
+ }
346
+ return p;
347
+ }
348
+
349
+ /** The multisampled framebuffer frames are drawn into without a multisampled canvas. */
350
+ ensureTarget() {
351
+ if (this.direct) return;
352
+ const gl = this.gl;
353
+ const w = this.canvas.width, h = this.canvas.height;
354
+ if (this.targetSize[0] === w && this.targetSize[1] === h) return;
355
+ this.targetSize = [w, h];
356
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.colorRb);
357
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, this.samples, gl.RGBA8, w, h);
358
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthRb);
359
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, this.samples, gl.DEPTH_COMPONENT24, w, h);
360
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
361
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
362
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, this.colorRb);
363
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthRb);
364
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
365
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.resolveRb);
366
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, w, h);
367
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
368
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.resolveFbo);
369
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, this.resolveRb);
370
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
371
+ }
372
+
373
+ /** Resolve the multisampled frame into the canvas. */
374
+ present(w, h) {
375
+ if (this.direct) return;
376
+ const gl = this.gl;
377
+ const blit = (from, to) => {
378
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, from);
379
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, to);
380
+ gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, gl.COLOR_BUFFER_BIT, gl.NEAREST);
381
+ };
382
+ blit(this.fbo, this.resolveFbo);
383
+ blit(this.resolveFbo, null);
384
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
385
+ }
386
+
387
+ /**
388
+ * @param {object} draw result of FrameBuilder.build
389
+ * @param {Float32Array} frameData packed frame uniforms
390
+ * @param {object} ctx {background:[r,g,b], textures, overlayColors, cubeRange}
391
+ */
392
+ render(draw, frameData, ctx) {
393
+ const gl = this.gl;
394
+ if (this.lost) return;
395
+ const w = this.canvas.width, h = this.canvas.height;
396
+ this.ensureTarget();
397
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.direct ? null : this.fbo);
398
+ gl.viewport(0, 0, w, h);
399
+ gl.clearColor(ctx.background[0], ctx.background[1], ctx.background[2], 1);
400
+ gl.clearDepth(1);
401
+ gl.depthMask(true);
402
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
403
+ gl.enable(gl.DEPTH_TEST);
404
+ gl.depthFunc(gl.LEQUAL);
405
+ gl.disable(gl.BLEND);
406
+
407
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.frameUbo);
408
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, frameData);
409
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, this.frameUbo);
410
+
411
+ this.uploadInstances(draw.instanceData, draw.instanceCount);
412
+ gl.bindVertexArray(this.vao);
413
+ const lights = lightTypes(frameData);
414
+
415
+ let current = null;
416
+ const use = (p) => {
417
+ if (current !== p) {
418
+ gl.useProgram(p);
419
+ current = p;
420
+ }
421
+ };
422
+ let blending = false;
423
+ // Batches arrive sorted by pipeline state, then material, so most of
424
+ // this state carries over from the previous batch and is not set again.
425
+ let cull = -1; // 0 none, 1 back faces of CCW triangles, 2 of CW (mirrored)
426
+ let bound = null;
427
+ gl.cullFace(gl.BACK);
428
+ for (const b of draw.batches) {
429
+ const lines = b.kind === 'lines';
430
+ const binding = b.doubleSided && !b.material.doubleSided ? this.doubleSidedBinding(b.material, ctx.textures) : this.materialBinding(b.material, ctx.textures);
431
+ use(lines ? this.lineProgram : this.meshProgram(binding.flags, lights));
432
+ const c = b.doubleSided || lines ? 0 : b.mirrored ? 2 : 1;
433
+ if (c !== cull) {
434
+ if (c === 0) gl.disable(gl.CULL_FACE);
435
+ else {
436
+ if (cull <= 0) gl.enable(gl.CULL_FACE);
437
+ gl.frontFace(c === 2 ? gl.CW : gl.CCW);
438
+ }
439
+ cull = c;
440
+ }
441
+ if (b.transparent !== blending) {
442
+ blending = b.transparent;
443
+ if (blending) {
444
+ gl.enable(gl.BLEND);
445
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
446
+ gl.depthMask(false);
447
+ } else {
448
+ gl.disable(gl.BLEND);
449
+ gl.depthMask(true);
450
+ }
451
+ }
452
+ if (binding !== bound) {
453
+ bound = binding;
454
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, binding.ubo);
455
+ gl.activeTexture(gl.TEXTURE0);
456
+ gl.bindTexture(gl.TEXTURE_2D, binding.map);
457
+ gl.activeTexture(gl.TEXTURE1);
458
+ gl.bindTexture(gl.TEXTURE_2D, binding.normalMap);
459
+ gl.activeTexture(gl.TEXTURE2);
460
+ gl.bindTexture(gl.TEXTURE_2D, binding.mrMap);
461
+ gl.activeTexture(gl.TEXTURE3);
462
+ gl.bindTexture(gl.TEXTURE_2D, binding.occlusionMap);
463
+ gl.activeTexture(gl.TEXTURE4);
464
+ gl.bindTexture(gl.TEXTURE_2D, binding.emissiveMap);
465
+ }
466
+ this.setInstanceOffset(b.firstInstance);
467
+ gl.drawElementsInstanced(lines ? gl.LINES : gl.TRIANGLES, b.range.indexCount, gl.UNSIGNED_INT, b.range.firstIndex * 4, b.count);
468
+ }
469
+
470
+ // overlays
471
+ const ov = draw.overlays;
472
+ gl.disable(gl.CULL_FACE);
473
+ gl.enable(gl.BLEND);
474
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
475
+ gl.depthMask(false);
476
+ const drawLines = (prog, uboName, color, list) => {
477
+ if (!list.length) return;
478
+ use(prog);
479
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, this.overlayUbo(uboName, color));
480
+ for (const o of list) {
481
+ this.setInstanceOffset(o.firstInstance);
482
+ gl.drawElementsInstanced(gl.LINES, o.range.indexCount, gl.UNSIGNED_INT, o.range.firstIndex * 4, o.count);
483
+ }
484
+ };
485
+ drawLines(this.lineProgram, 'wireframe', ctx.overlayColors.wireframe, ov.wireframe);
486
+ drawLines(this.normalProgram, 'normals', ctx.overlayColors.normals, ov.normals);
487
+ if (ov.bbox && ov.bbox.count) drawLines(this.lineProgram, 'bbox', ctx.overlayColors.bbox, [{ ...ov.bbox, range: ctx.cubeRange }]);
488
+ if (ov.selection && ov.selection.count) {
489
+ gl.disable(gl.DEPTH_TEST);
490
+ drawLines(this.lineProgram, 'selection', ctx.overlayColors.selection, [{ ...ov.selection, range: ctx.cubeRange }]);
491
+ gl.enable(gl.DEPTH_TEST);
492
+ }
493
+ gl.depthMask(true);
494
+ gl.disable(gl.BLEND);
495
+ gl.bindVertexArray(null);
496
+
497
+ this.present(w, h);
498
+ }
499
+
500
+ /** Block until the GPU has finished (benchmarks). */
501
+ async finish() {
502
+ this.gl.finish();
503
+ // a 1-pixel read forces completion on implementations where finish() is a no-op
504
+ const px = new Uint8Array(4);
505
+ this.gl.readPixels(0, 0, 1, 1, this.gl.RGBA, this.gl.UNSIGNED_BYTE, px);
506
+ }
507
+
508
+ dispose() {
509
+ const gl = this.gl;
510
+ if (!gl) return;
511
+ gl.deleteBuffer(this.vertexBuffer);
512
+ gl.deleteBuffer(this.indexBuffer);
513
+ gl.deleteBuffer(this.instanceBuffer);
514
+ gl.deleteBuffer(this.frameUbo);
515
+ for (const b of this.materialUbos.values()) {
516
+ gl.deleteBuffer(b.ubo);
517
+ if (b.doubleSidedUbo) gl.deleteBuffer(b.doubleSidedUbo);
518
+ }
519
+ for (const u of this.overlayUbos.values()) gl.deleteBuffer(u);
520
+ for (const p of this.meshPrograms.values()) gl.deleteProgram(p);
521
+ gl.deleteVertexArray(this.vao);
522
+ if (!this.direct) {
523
+ gl.deleteFramebuffer(this.fbo);
524
+ gl.deleteRenderbuffer(this.colorRb);
525
+ gl.deleteRenderbuffer(this.depthRb);
526
+ gl.deleteFramebuffer(this.resolveFbo);
527
+ gl.deleteRenderbuffer(this.resolveRb);
528
+ }
529
+ const ext = gl.getExtension('WEBGL_lose_context');
530
+ if (ext) ext.loseContext();
531
+ }
532
+ }