trid3d 0.0.0-stage → 0.1.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.
@@ -0,0 +1,613 @@
1
+ import { DEFAULT_FOV, DEFAULT_NEAR } from "../math/transform.js";
2
+ import { clear, edgeSegments } from "./draw.js";
3
+ import { parseColor } from "./color.js";
4
+ import { DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./canvasRenderer.js";
5
+ import { newell, cornerNormals, creaseOf } from "./normals.js";
6
+ import { SURFACE_GLSL, buildEffects, effectsKey } from "./effects.js";
7
+ import { textureReady, textureSize, textureIsLive } from "../loaders/texture.js";
8
+
9
+ /** Farthest distance drawn by the WebGL renderer, unless the camera sets `far`. */
10
+ export const DEFAULT_FAR = 1000;
11
+
12
+ // Trid's transform pipeline, written in GLSL. It matches localToWorld,
13
+ // worldToCamera and project() exactly, so tools and picking line up.
14
+ const COMMON = `
15
+ uniform vec3 uPos, uRot, uScale;
16
+ uniform vec3 uCamPos;
17
+ uniform vec2 uCamRot; // yaw, pitch
18
+ uniform float uF, uAspect, uNear, uFar;
19
+
20
+ vec3 rotX(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x, p.y * c - p.z * s, p.y * s + p.z * c); }
21
+ vec3 rotY(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x * c - p.z * s, p.y, p.x * s + p.z * c); }
22
+ vec3 rotZ(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x * c - p.y * s, p.x * s + p.y * c, p.z); }
23
+ vec3 rotXYZ(vec3 p) { return rotZ(rotY(rotX(p, uRot.x), uRot.y), uRot.z); }
24
+ vec3 toWorld(vec3 v) { return rotXYZ(v * uScale) + uPos; }
25
+ vec3 toCamera(vec3 p) { return rotX(rotY(p - uCamPos, uCamRot.x), uCamRot.y); }
26
+ vec4 toClip(vec3 p) {
27
+ float a = (uFar + uNear) / (uFar - uNear);
28
+ float b = -2.0 * uFar * uNear / (uFar - uNear);
29
+ return vec4(p.x * uF / uAspect, p.y * uF, a * p.z + b, p.z);
30
+ }
31
+ `;
32
+
33
+ const FACE_VS = `
34
+ attribute vec3 aPos;
35
+ attribute vec3 aNormal;
36
+ attribute vec2 aUV;
37
+ varying vec3 vNormal;
38
+ varying vec3 vWorld;
39
+ varying vec3 vLocal;
40
+ varying vec2 vUV;
41
+ ${COMMON}
42
+ void main() {
43
+ vNormal = rotXYZ(aNormal / uScale); // inverse scale keeps normals right on stretched objects
44
+ vLocal = aPos;
45
+ vUV = aUV;
46
+ vWorld = toWorld(aPos);
47
+ gl_Position = toClip(toCamera(vWorld));
48
+ }
49
+ `;
50
+
51
+ /** Fragment shader for faces: Trid's lighting, then the material's effects in order. */
52
+ function faceFS(effects) {
53
+ const fx = buildEffects(effects);
54
+ const source = `
55
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
56
+ precision highp float;
57
+ #else
58
+ precision mediump float;
59
+ #endif
60
+ uniform vec4 uColor;
61
+ uniform vec3 uLight;
62
+ uniform float uAmbient;
63
+ uniform float uShade;
64
+ uniform float uTime;
65
+ uniform vec4 uBackground;
66
+ uniform vec3 uCamWorld;
67
+ uniform sampler2D uTexture;
68
+ uniform float uUseTexture;
69
+ varying vec3 vNormal;
70
+ varying vec3 vWorld;
71
+ varying vec3 vLocal;
72
+ varying vec2 vUV;
73
+ ${SURFACE_GLSL}
74
+ ${fx.code}
75
+ void main() {
76
+ vec3 n = normalize(vNormal);
77
+ if (!gl_FrontFacing) n = -n;
78
+ float diffuse = max(0.0, dot(n, uLight));
79
+ float light = mix(1.0, uAmbient + (1.0 - uAmbient) * diffuse, uShade);
80
+
81
+ // Fill color, times the texture's color when there is one
82
+ vec4 base = uColor * mix(vec4(1.0), texture2D(uTexture, vUV), uUseTexture);
83
+
84
+ Surface s;
85
+ s.base = base;
86
+ s.uv = vUV;
87
+ s.normal = n;
88
+ s.position = vWorld;
89
+ s.local = vLocal;
90
+ s.view = normalize(uCamWorld - vWorld);
91
+ s.lightDir = uLight;
92
+ s.diffuse = diffuse;
93
+ s.light = light;
94
+ s.ambient = uAmbient;
95
+ s.depth = length(uCamWorld - vWorld);
96
+ s.time = uTime;
97
+ s.background = uBackground;
98
+
99
+ vec4 color = vec4(base.rgb * light, base.a);
100
+ ${fx.calls}
101
+ color = clamp(color, 0.0, 1.0);
102
+ if (color.a < 0.004) discard; // fully see-through pixels (e.g. a PNG's empty parts) don't hide what's behind
103
+ gl_FragColor = vec4(color.rgb * color.a, color.a);
104
+ }
105
+ `;
106
+ return { source, uniforms: fx.uniforms };
107
+ }
108
+
109
+ const FACE_UNIFORMS = ["uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
110
+
111
+ // Each line segment is a thin rectangle facing the screen, `uWidth` pixels wide
112
+ const LINE_VS = `
113
+ attribute vec3 aA;
114
+ attribute vec3 aB;
115
+ attribute vec2 aCorner; // x: 0 = at A, 1 = at B; y: -1 / +1 = which side
116
+ uniform vec2 uRes;
117
+ uniform float uWidth;
118
+ ${COMMON}
119
+ void main() {
120
+ vec3 a = toCamera(toWorld(aA));
121
+ vec3 b = toCamera(toWorld(aB));
122
+ float near = uNear * 1.001;
123
+ if (a.z < near && b.z < near) { gl_Position = vec4(0.0, 0.0, -2.0, 1.0); return; }
124
+ if (a.z < near) a = mix(a, b, (near - a.z) / (b.z - a.z));
125
+ if (b.z < near) b = mix(b, a, (near - b.z) / (a.z - b.z));
126
+ vec4 ca = toClip(a), cb = toClip(b);
127
+ vec2 sa = ca.xy / ca.w * uRes * 0.5, sb = cb.xy / cb.w * uRes * 0.5;
128
+ vec2 d = sb - sa;
129
+ float len = length(d);
130
+ vec2 side = len > 1e-6 ? vec2(-d.y, d.x) / len : vec2(0.0, 1.0);
131
+ vec4 c = aCorner.x < 0.5 ? ca : cb;
132
+ c.xy += side * aCorner.y * uWidth / uRes * c.w;
133
+ gl_Position = c;
134
+ }
135
+ `;
136
+
137
+ const LINE_FS = `
138
+ precision mediump float;
139
+ uniform vec4 uColor;
140
+ void main() {
141
+ gl_FragColor = vec4(uColor.rgb * uColor.a, uColor.a);
142
+ }
143
+ `;
144
+
145
+ const UNIFORMS = ["uPos", "uRot", "uScale", "uCamPos", "uCamRot", "uF", "uAspect", "uNear", "uFar", "uColor"];
146
+
147
+ /**
148
+ * Draws a scene with WebGL, on the graphics card.
149
+ *
150
+ * Same objects, materials and light as `CanvasRenderer`, but:
151
+ * - a depth buffer decides what's in front, per pixel, so faces always
152
+ * overlap correctly (no sorting mistakes)
153
+ * - much faster on big models
154
+ *
155
+ * The scene is drawn on a hidden WebGL canvas, then copied onto the
156
+ * scene's 2D canvas. Tools draw on top as usual, the page keeps a
157
+ * single canvas, and `canvas.toDataURL()` includes everything.
158
+ *
159
+ * Meshes are sent to the graphics card once and reused. After changing
160
+ * a mesh's vertices, edges or faces in place, set `mesh.dirty = true`.
161
+ *
162
+ * @example
163
+ * const scene = new Scene(canvas, { renderer: new WebGLRenderer() });
164
+ */
165
+ export class WebGLRenderer {
166
+ /** Whether this browser can create a WebGL context. */
167
+ static isSupported() {
168
+ try {
169
+ const c = document.createElement("canvas");
170
+ return !!(c.getContext("webgl2") || c.getContext("webgl"));
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @param {Object} [opts]
178
+ * @param {boolean} [opts.antialias=true] - Smooth edges
179
+ * @throws {Error} If WebGL isn't available (check `WebGLRenderer.isSupported()` first)
180
+ */
181
+ constructor({ antialias = true } = {}) {
182
+ /** The hidden canvas WebGL draws on. */
183
+ this.canvas = document.createElement("canvas");
184
+ this._options = { antialias, alpha: true, premultipliedAlpha: true, depth: true };
185
+ this._colors = new Map();
186
+ this._lost = false;
187
+ this._start = performance.now();
188
+ this._failed = new Set(); // effect lists whose shader didn't compile (reported once)
189
+
190
+ this.canvas.addEventListener("webglcontextlost", (e) => {
191
+ e.preventDefault(); // allows the context to come back
192
+ this._lost = true;
193
+ });
194
+ this.canvas.addEventListener("webglcontextrestored", () => {
195
+ this._init();
196
+ this._lost = false;
197
+ });
198
+
199
+ this._init();
200
+ }
201
+
202
+ _init() {
203
+ const gl = this.canvas.getContext("webgl2", this._options) || this.canvas.getContext("webgl", this._options);
204
+ if (!gl) throw new Error("WebGL is not available in this browser");
205
+ this.gl = gl;
206
+ this._webgl2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
207
+ this._meshes = new WeakMap();
208
+ this._buffers = new Set();
209
+ this._textures = new WeakMap(); // image / canvas / video → GL texture
210
+ this._glTextures = new Set();
211
+ this._warned = new WeakSet();
212
+ this._frame = 0;
213
+
214
+ this._faces = new Map(); // face programs, one per combination of effects
215
+ this._line = program(gl, LINE_VS, LINE_FS, ["aA", "aB", "aCorner"], [...UNIFORMS, "uRes", "uWidth"]);
216
+
217
+ gl.enable(gl.DEPTH_TEST);
218
+ gl.depthFunc(gl.LEQUAL);
219
+ gl.frontFace(gl.CW); // Trid faces are clockwise from the front
220
+ gl.cullFace(gl.BACK);
221
+ gl.enable(gl.BLEND);
222
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
223
+ // Push faces back a little so outlines on them win the depth test
224
+ gl.polygonOffset(1, 1);
225
+ }
226
+
227
+ /**
228
+ * Draws every object of the scene, then copies the image onto the scene's canvas.
229
+ *
230
+ * @param {import("../scene.js").Scene} scene
231
+ */
232
+ render(scene) {
233
+ const { ctx, canvas, camera } = scene;
234
+ const w = canvas.width, h = canvas.height;
235
+ clear(ctx, w, h, scene.background);
236
+ if (this._lost || this._disposed) return;
237
+
238
+ const gl = this.gl;
239
+ this._frame++;
240
+ if (this.canvas.width !== w || this.canvas.height !== h) {
241
+ this.canvas.width = w;
242
+ this.canvas.height = h;
243
+ }
244
+ gl.viewport(0, 0, w, h);
245
+ gl.clearColor(0, 0, 0, 0);
246
+ gl.depthMask(true);
247
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
248
+
249
+ const view = {
250
+ ctx,
251
+ background: this._color(ctx, scene.background),
252
+ time: (performance.now() - this._start) / 1000,
253
+ camPos: [camera.x ?? 0, camera.y ?? 0, camera.z ?? 0],
254
+ camRot: [camera.yaw ?? 0, camera.pitch ?? 0],
255
+ f: 1 / Math.tan((camera.fov ?? DEFAULT_FOV) / 2),
256
+ aspect: w / h,
257
+ near: camera.near ?? DEFAULT_NEAR,
258
+ far: camera.far ?? DEFAULT_FAR,
259
+ };
260
+ const l = scene.light ?? DEFAULT_LIGHT;
261
+ const len = Math.hypot(l.x, l.y, l.z) || 1;
262
+ const light = [l.x / len, l.y / len, l.z / len];
263
+ const ambient = scene.ambient ?? DEFAULT_AMBIENT;
264
+
265
+ // Opaque things first; see-through ones after, without writing depth
266
+ const opaque = [], clear2 = [];
267
+ for (const obj of scene.objects) {
268
+ const geo = this._geometry(obj.mesh);
269
+ const lineColor = this._color(ctx, obj.color ?? scene.color);
270
+ const lineWidth = obj.lineWidth ?? scene.lineWidth;
271
+ const mat = obj.material;
272
+
273
+ if (mat) {
274
+ const fill = this._color(ctx, mat.fill ?? (mat.texture ? "#ffffff" : scene.color));
275
+ (fill[3] < 1 ? clear2 : opaque).push({ kind: "face", obj, geo, color: fill, mat });
276
+ if (mat.wireframe) {
277
+ (lineColor[3] < 1 ? clear2 : opaque).push({ kind: "line", obj, buf: geo.outlines, color: lineColor, width: lineWidth });
278
+ }
279
+ } else {
280
+ (lineColor[3] < 1 ? clear2 : opaque).push({ kind: "line", obj, buf: geo.edges, color: lineColor, width: lineWidth });
281
+ }
282
+ }
283
+
284
+ for (const d of opaque) this._draw(d, view, light, ambient, w, h);
285
+ gl.depthMask(false);
286
+ for (const d of clear2) this._draw(d, view, light, ambient, w, h);
287
+
288
+ ctx.drawImage(this.canvas, 0, 0);
289
+
290
+ // Meshes marked as changed have been sent again
291
+ for (const obj of scene.objects) if (obj.mesh.dirty) obj.mesh.dirty = false;
292
+ }
293
+
294
+ /** Frees the graphics card memory. The renderer can't be used afterwards. */
295
+ dispose() {
296
+ if (this._disposed) return;
297
+ this._disposed = true;
298
+ const gl = this.gl;
299
+ for (const b of this._buffers) gl.deleteBuffer(b);
300
+ for (const t of this._glTextures) gl.deleteTexture(t);
301
+ this._glTextures.clear();
302
+ this._buffers.clear();
303
+ for (const p of new Set(this._faces.values())) gl.deleteProgram(p.program);
304
+ gl.deleteProgram(this._line.program);
305
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
306
+ }
307
+
308
+ _draw(d, view, light, ambient, w, h) {
309
+ const gl = this.gl;
310
+ const isFace = d.kind === "face";
311
+ const effects = isFace ? d.mat.effects ?? [] : null;
312
+ const prog = isFace ? this._faceProgram(effects) : this._line;
313
+ const buf = isFace ? this._faceBuffer(d.geo, d.obj.mesh, d.mat.shade === false ? null : creaseOf(d.mat.smooth)) : d.buf;
314
+ if (!buf.count) return;
315
+
316
+ gl.useProgram(prog.program);
317
+ const u = prog.uniforms;
318
+ const { pos, rot, scale } = d.obj;
319
+ gl.uniform3f(u.uPos, pos.x, pos.y, pos.z);
320
+ gl.uniform3f(u.uRot, rot.x, rot.y, rot.z);
321
+ gl.uniform3f(u.uScale, scale.x, scale.y, scale.z);
322
+ gl.uniform3fv(u.uCamPos, view.camPos);
323
+ gl.uniform2fv(u.uCamRot, view.camRot);
324
+ gl.uniform1f(u.uF, view.f);
325
+ gl.uniform1f(u.uAspect, view.aspect);
326
+ gl.uniform1f(u.uNear, view.near);
327
+ gl.uniform1f(u.uFar, view.far);
328
+ gl.uniform4fv(u.uColor, d.color);
329
+
330
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
331
+ const a = prog.attributes;
332
+
333
+ if (isFace) {
334
+ gl.uniform3fv(u.uLight, light);
335
+ gl.uniform1f(u.uAmbient, ambient);
336
+ gl.uniform1f(u.uShade, d.mat.shade === false ? 0 : 1);
337
+ gl.uniform1f(u.uTime, view.time);
338
+ gl.uniform4fv(u.uBackground, view.background);
339
+ gl.uniform3fv(u.uCamWorld, view.camPos);
340
+ for (const e of prog.effectUniforms) {
341
+ this._setUniform(view.ctx, u[e.glslName], e.type, effects[e.effect]?.uniforms?.[e.name]);
342
+ }
343
+ if (d.mat.doubleSided) gl.disable(gl.CULL_FACE);
344
+ else gl.enable(gl.CULL_FACE);
345
+ gl.enable(gl.POLYGON_OFFSET_FILL);
346
+ gl.vertexAttribPointer(a.aPos, 3, gl.FLOAT, false, 32, 0);
347
+ if (a.aNormal >= 0) gl.vertexAttribPointer(a.aNormal, 3, gl.FLOAT, false, 32, 12);
348
+ if (a.aUV >= 0) gl.vertexAttribPointer(a.aUV, 2, gl.FLOAT, false, 32, 24);
349
+ const tex = d.mat.texture ? this._texture(d.mat.texture, !!d.mat.pixelated, d.obj.mesh) : null;
350
+ gl.activeTexture(gl.TEXTURE0);
351
+ gl.bindTexture(gl.TEXTURE_2D, tex);
352
+ gl.uniform1i(u.uTexture, 0);
353
+ gl.uniform1f(u.uUseTexture, tex ? 1 : 0);
354
+ } else {
355
+ if (a.aA < 0) return;
356
+ gl.uniform2f(u.uRes, w, h);
357
+ gl.uniform1f(u.uWidth, d.width);
358
+ gl.disable(gl.CULL_FACE);
359
+ gl.disable(gl.POLYGON_OFFSET_FILL);
360
+ gl.vertexAttribPointer(a.aA, 3, gl.FLOAT, false, 32, 0);
361
+ gl.vertexAttribPointer(a.aB, 3, gl.FLOAT, false, 32, 12);
362
+ gl.vertexAttribPointer(a.aCorner, 2, gl.FLOAT, false, 32, 24);
363
+ }
364
+ const locs = Object.values(a).filter((loc) => loc >= 0);
365
+ for (const loc of locs) gl.enableVertexAttribArray(loc);
366
+ gl.drawArrays(gl.TRIANGLES, 0, buf.count);
367
+ for (const loc of locs) gl.disableVertexAttribArray(loc);
368
+ }
369
+
370
+ /**
371
+ * The face program for a list of effects, compiled once per combination.
372
+ * A shader that doesn't compile is reported once, and the object is
373
+ * drawn without its effects.
374
+ */
375
+ _faceProgram(effects) {
376
+ const key = effectsKey(effects);
377
+ let p = this._faces.get(key);
378
+ if (p) return p;
379
+ try {
380
+ const fs = faceFS(effects);
381
+ p = program(this.gl, FACE_VS, fs.source, ["aPos", "aNormal", "aUV"],
382
+ [...UNIFORMS, ...FACE_UNIFORMS, ...fs.uniforms.map((u) => u.glslName)]);
383
+ p.effectUniforms = fs.uniforms;
384
+ } catch (err) {
385
+ if (!this._failed.has(key)) {
386
+ this._failed.add(key);
387
+ console.error("Trid: an effect's shader failed to compile, drawing without effects.\n" + err.message);
388
+ }
389
+ p = effects.length ? this._faceProgram([]) : null;
390
+ if (!p) throw err;
391
+ }
392
+ this._faces.set(key, p);
393
+ return p;
394
+ }
395
+
396
+ /**
397
+ * The GL texture for an image, canvas or video, uploaded once
398
+ * (canvases and videos: every frame). Null while it isn't ready,
399
+ * or if the mesh has no texture coordinates.
400
+ */
401
+ _texture(src, pixelated, mesh) {
402
+ const gl = this.gl;
403
+ if (!textureReady(src)) return null;
404
+ if (!mesh.faceUVs) {
405
+ if (!this._warned.has(mesh)) {
406
+ this._warned.add(mesh);
407
+ console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn");
408
+ }
409
+ return null;
410
+ }
411
+
412
+ let t = this._textures.get(src);
413
+ const live = textureIsLive(src);
414
+ if (!t || live || t.failed) {
415
+ if (t?.failed) return null;
416
+ const { w, h } = textureSize(src);
417
+ if (!t) {
418
+ t = { tex: gl.createTexture(), frame: -1 };
419
+ this._textures.set(src, t);
420
+ this._glTextures.add(t.tex);
421
+ }
422
+ if (t.frame !== this._frame) {
423
+ t.frame = this._frame;
424
+ gl.bindTexture(gl.TEXTURE_2D, t.tex);
425
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // images go top-down, v goes up
426
+ try {
427
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
428
+ } catch (err) {
429
+ // Usually an image from another site without permission (CORS)
430
+ console.warn("Trid: can't use this image as a texture: " + err.message);
431
+ t.failed = true;
432
+ return null;
433
+ } finally {
434
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
435
+ }
436
+ // WebGL 1 can only repeat and mipmap power-of-two sizes
437
+ const pot = (w & (w - 1)) === 0 && (h & (h - 1)) === 0;
438
+ const full = this._webgl2 || pot;
439
+ t.mipmaps = full && !live;
440
+ if (t.mipmaps) gl.generateMipmap(gl.TEXTURE_2D);
441
+ const wrap = full ? gl.REPEAT : gl.CLAMP_TO_EDGE;
442
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
443
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
444
+ }
445
+ }
446
+
447
+ gl.bindTexture(gl.TEXTURE_2D, t.tex);
448
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, pixelated ? gl.NEAREST : gl.LINEAR);
449
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
450
+ pixelated ? (t.mipmaps ? gl.NEAREST_MIPMAP_NEAREST : gl.NEAREST) : (t.mipmaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR));
451
+ return t.tex;
452
+ }
453
+
454
+ /** Sends one effect setting to the shader, converting it to its GLSL type. */
455
+ _setUniform(ctx, loc, type, value) {
456
+ const gl = this.gl;
457
+ if (loc == null || value == null) return;
458
+ if (type === "float") gl.uniform1f(loc, Number(value));
459
+ else if (typeof value === "string") gl.uniform4fv(loc, this._color(ctx, value));
460
+ else if (Array.isArray(value)) gl[`uniform${value.length}fv`](loc, value);
461
+ else gl.uniform3f(loc, value.x, value.y, value.z);
462
+ }
463
+
464
+ /** Graphics card buffers for a mesh, built once and rebuilt when it changes. */
465
+ _geometry(mesh) {
466
+ let geo = this._meshes.get(mesh);
467
+ const stale = !geo || mesh.dirty || geo.vertices !== mesh.vertices || geo.faceList !== mesh.faces
468
+ || geo.edgeList !== mesh.edges || geo.counts !== countsOf(mesh);
469
+ if (!stale) return geo;
470
+
471
+ if (geo) {
472
+ for (const b of [geo.outlines, geo.edges, ...geo.faceBuffers.values()]) this._free(b);
473
+ }
474
+ const v = mesh.vertices;
475
+ const faces = mesh.faces ?? [];
476
+
477
+ // Outlines of the faces, each edge once
478
+ const outline = new Set();
479
+ const outlinePairs = [];
480
+ for (const f of faces) {
481
+ for (let k = 0; k < f.length; k++) {
482
+ const a = f[k], b = f[(k + 1) % f.length];
483
+ const key = a < b ? a + "," + b : b + "," + a;
484
+ if (!outline.has(key)) {
485
+ outline.add(key);
486
+ outlinePairs.push([a, b]);
487
+ }
488
+ }
489
+ }
490
+
491
+ geo = {
492
+ vertices: mesh.vertices,
493
+ faceList: mesh.faces,
494
+ edgeList: mesh.edges,
495
+ counts: countsOf(mesh),
496
+ faceBuffers: new Map(), // one per shading mode, built when first needed
497
+ outlines: this._upload(lineData(v, outlinePairs), 8),
498
+ edges: this._upload(lineData(v, edgeSegments(mesh.edges ?? [])), 8),
499
+ };
500
+ this._meshes.set(mesh, geo);
501
+ return geo;
502
+ }
503
+
504
+ /**
505
+ * Triangles of a mesh's faces, each corner with its own position and
506
+ * normal. Flat shading uses the face's normal at every corner; smooth
507
+ * shading uses the corner normals.
508
+ */
509
+ _faceBuffer(geo, mesh, crease) {
510
+ const key = crease ?? "flat";
511
+ let buf = geo.faceBuffers.get(key);
512
+ if (buf) return buf;
513
+
514
+ const v = mesh.vertices;
515
+ const faces = mesh.faces ?? [];
516
+ const corners = crease ? cornerNormals(mesh, crease) : null;
517
+ const uvs = mesh.uvs, faceUVs = mesh.faceUVs;
518
+ const tri = [];
519
+ faces.forEach((f, fi) => {
520
+ if (f.length < 3) return;
521
+ let ns;
522
+ if (corners) {
523
+ ns = corners[fi];
524
+ } else {
525
+ const n = newell(f.map((i) => v[i]));
526
+ const len = Math.hypot(n.x, n.y, n.z) || 1;
527
+ const flat = { x: n.x / len, y: n.y / len, z: n.z / len };
528
+ ns = f.map(() => flat);
529
+ }
530
+ const ts = uvs && faceUVs?.[fi];
531
+ for (let k = 1; k < f.length - 1; k++) {
532
+ for (const c of [0, k, k + 1]) {
533
+ const p = v[f[c]], n = ns[c];
534
+ const t = ts ? uvs[ts[c]] : null;
535
+ tri.push(p.x, p.y, p.z, n.x, n.y, n.z, t ? t.u : 0, t ? t.v : 0);
536
+ }
537
+ }
538
+ });
539
+
540
+ buf = this._upload(tri, 8);
541
+ geo.faceBuffers.set(key, buf);
542
+ return buf;
543
+ }
544
+
545
+ _upload(data, floatsPerVertex) {
546
+ const gl = this.gl;
547
+ const buffer = gl.createBuffer();
548
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
549
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
550
+ this._buffers.add(buffer);
551
+ return { buffer, count: data.length / floatsPerVertex };
552
+ }
553
+
554
+ _free(b) {
555
+ this.gl.deleteBuffer(b.buffer);
556
+ this._buffers.delete(b.buffer);
557
+ }
558
+
559
+ /** [r, g, b, a] in 0..1 for a CSS color, cached. */
560
+ _color(ctx, css) {
561
+ let c = this._colors.get(css);
562
+ if (!c) {
563
+ const [r, g, b, a] = parseColor(ctx, css);
564
+ c = [r / 255, g / 255, b / 255, a];
565
+ this._colors.set(css, c);
566
+ }
567
+ return c;
568
+ }
569
+ }
570
+
571
+ /** A short string that changes when a mesh's arrays grow or shrink. */
572
+ function countsOf(mesh) {
573
+ return `${mesh.vertices.length}/${mesh.edges?.length ?? 0}/${mesh.faces?.length ?? 0}`;
574
+ }
575
+
576
+ /** Six corners (two triangles) per segment: A, B, then which corner. */
577
+ function lineData(v, pairs) {
578
+ const out = [];
579
+ const corners = [[0, -1], [1, -1], [1, 1], [0, -1], [1, 1], [0, 1]];
580
+ for (const [i, j] of pairs) {
581
+ const a = v[i], b = v[j];
582
+ for (const [cx, cy] of corners) out.push(a.x, a.y, a.z, b.x, b.y, b.z, cx, cy);
583
+ }
584
+ return out;
585
+ }
586
+
587
+ function program(gl, vsSource, fsSource, attributes, uniforms) {
588
+ const compile = (type, src) => {
589
+ const s = gl.createShader(type);
590
+ gl.shaderSource(s, src);
591
+ gl.compileShader(s);
592
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS) && !gl.isContextLost()) {
593
+ throw new Error("Trid shader error: " + gl.getShaderInfoLog(s));
594
+ }
595
+ return s;
596
+ };
597
+ const p = gl.createProgram();
598
+ const vs = compile(gl.VERTEX_SHADER, vsSource);
599
+ const fs = compile(gl.FRAGMENT_SHADER, fsSource);
600
+ gl.attachShader(p, vs);
601
+ gl.attachShader(p, fs);
602
+ gl.linkProgram(p);
603
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS) && !gl.isContextLost()) {
604
+ throw new Error("Trid shader error: " + gl.getProgramInfoLog(p));
605
+ }
606
+ gl.deleteShader(vs);
607
+ gl.deleteShader(fs);
608
+
609
+ const out = { program: p, attributes: {}, uniforms: {} };
610
+ for (const name of attributes) out.attributes[name] = gl.getAttribLocation(p, name);
611
+ for (const name of uniforms) out.uniforms[name] = gl.getUniformLocation(p, name);
612
+ return out;
613
+ }