@es3d/es3d 1.0.0 → 1.0.1

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 (79) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +86 -86
  3. package/docs/SPEC.md +417 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +377 -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 +92 -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.js +630 -630
  42. package/src/io/mesh-import.js +104 -104
  43. package/src/io/obj.js +352 -352
  44. package/src/io/step-brep.js +439 -439
  45. package/src/io/step-exact.js +1149 -1149
  46. package/src/io/step.js +1418 -1418
  47. package/src/io/stl.js +193 -193
  48. package/src/io/svg-import.js +787 -787
  49. package/src/material/materials.js +154 -154
  50. package/src/math/mat4.js +475 -475
  51. package/src/math/random.js +30 -30
  52. package/src/math/trig.js +65 -65
  53. package/src/math/vec3.js +81 -81
  54. package/src/optimize/optimizer.js +579 -579
  55. package/src/path/path-flatten.js +322 -322
  56. package/src/path/path-parser.js +332 -332
  57. package/src/path/shapes.js +129 -129
  58. package/src/path/triangulate.js +517 -517
  59. package/src/pipeline.js +74 -74
  60. package/src/render/camera.js +168 -168
  61. package/src/render/frame-builder.js +362 -362
  62. package/src/render/geometry-pool.js +149 -149
  63. package/src/render/lighting.js +149 -149
  64. package/src/render/orbit-controls.js +237 -237
  65. package/src/render/renderer.js +258 -249
  66. package/src/render/scroll-timeline.js +104 -104
  67. package/src/render/shaders/glsl.js +285 -285
  68. package/src/render/shaders/wgsl.js +252 -252
  69. package/src/render/stage.js +364 -352
  70. package/src/render/texture-cache.js +40 -40
  71. package/src/render/textures.js +223 -223
  72. package/src/render/webgl2/webgl2-backend.js +586 -532
  73. package/src/render/webgpu/webgpu-backend.js +482 -480
  74. package/src/scene/flatten.js +754 -754
  75. package/src/scene/picking.js +161 -161
  76. package/src/scene/scene-graph.js +320 -320
  77. package/src/transform/transform.js +211 -211
  78. package/src/wasm/kernels-binary.js +3 -3
  79. 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
+ }