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