trid3d 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -9
- package/package.json +2 -1
- package/trid/export/capture.js +150 -0
- package/trid/export/mtl.js +119 -0
- package/trid/export/obj.js +227 -0
- package/trid/index.d.ts +630 -16
- package/trid/index.js +29 -6
- package/trid/loaders/mtl.js +63 -0
- package/trid/loaders/obj.js +72 -4
- package/trid/loaders/texture.js +47 -6
- package/trid/math/matrix.js +200 -0
- package/trid/math/noise.js +162 -0
- package/trid/math/rotation.js +152 -0
- package/trid/render/canvasRenderer.js +132 -82
- package/trid/render/draw.js +21 -6
- package/trid/render/meshUtils.js +257 -0
- package/trid/render/proceduralTextures.js +211 -0
- package/trid/render/shapes.js +353 -23
- package/trid/render/uvMapping.js +225 -0
- package/trid/render/webglRenderer.js +240 -75
- package/trid/scene.js +43 -9
- package/trid/sceneGraph.js +131 -0
- package/trid/tools/dragObject.js +33 -9
- package/trid/tools/helpers.js +109 -10
- package/trid/tools/hoverHighlight.js +6 -3
- package/trid/tools/index.js +3 -1
- package/trid/tools/orbitCamera.js +27 -0
- package/trid/tools/transformGizmo.js +316 -0
- package/trid/tools/viewGizmo.js +178 -0
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { DEFAULT_FOV, DEFAULT_NEAR } from "../math/transform.js";
|
|
2
|
-
import {
|
|
2
|
+
import { normalMatrix, determinantMat4 } from "../math/matrix.js";
|
|
3
|
+
import { worldObjects } from "../sceneGraph.js";
|
|
4
|
+
import { clear, edgeSegments, wireEdges } from "./draw.js";
|
|
3
5
|
import { parseColor } from "./color.js";
|
|
4
|
-
import { DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./canvasRenderer.js";
|
|
6
|
+
import { DEFAULT_LIGHT, DEFAULT_AMBIENT, materialsOf } from "./canvasRenderer.js";
|
|
7
|
+
import { uvsFor, uvTransform, mappingKey } from "./uvMapping.js";
|
|
5
8
|
import { newell, cornerNormals, creaseOf } from "./normals.js";
|
|
6
9
|
import { SURFACE_GLSL, buildEffects, effectsKey } from "./effects.js";
|
|
7
|
-
import { textureReady, textureSize, textureIsLive } from "../loaders/texture.js";
|
|
10
|
+
import { textureReady, textureSize, textureIsLive, textureVersion } from "../loaders/texture.js";
|
|
8
11
|
|
|
9
12
|
/** Farthest distance drawn by the WebGL renderer, unless the camera sets `far`. */
|
|
10
13
|
export const DEFAULT_FAR = 1000;
|
|
11
14
|
|
|
12
|
-
// Trid's transform pipeline, written in GLSL. It matches
|
|
13
|
-
// worldToCamera and project() exactly, so tools and picking line up.
|
|
15
|
+
// Trid's transform pipeline, written in GLSL. It matches the objects' world
|
|
16
|
+
// matrices, worldToCamera and project() exactly, so tools and picking line up.
|
|
14
17
|
const COMMON = `
|
|
15
|
-
uniform
|
|
18
|
+
uniform mat4 uModel; // the object's world matrix (its parents' transforms included)
|
|
16
19
|
uniform vec3 uCamPos;
|
|
17
20
|
uniform vec2 uCamRot; // yaw, pitch
|
|
18
21
|
uniform float uF, uAspect, uNear, uFar;
|
|
19
22
|
|
|
20
23
|
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
24
|
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
|
|
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 toWorld(vec3 v) { return (uModel * vec4(v, 1.0)).xyz; }
|
|
25
26
|
vec3 toCamera(vec3 p) { return rotX(rotY(p - uCamPos, uCamRot.x), uCamRot.y); }
|
|
26
27
|
vec4 toClip(vec3 p) {
|
|
27
28
|
float a = (uFar + uNear) / (uFar - uNear);
|
|
@@ -38,11 +39,13 @@ varying vec3 vNormal;
|
|
|
38
39
|
varying vec3 vWorld;
|
|
39
40
|
varying vec3 vLocal;
|
|
40
41
|
varying vec2 vUV;
|
|
42
|
+
uniform mat3 uUVTransform; // textureRepeat, textureOffset, textureRotation
|
|
43
|
+
uniform mat3 uNormalMatrix; // keeps normals right on stretched and mirrored objects
|
|
41
44
|
${COMMON}
|
|
42
45
|
void main() {
|
|
43
|
-
vNormal =
|
|
46
|
+
vNormal = uNormalMatrix * aNormal;
|
|
44
47
|
vLocal = aPos;
|
|
45
|
-
vUV = aUV;
|
|
48
|
+
vUV = (uUVTransform * vec3(aUV, 1.0)).xy;
|
|
46
49
|
vWorld = toWorld(aPos);
|
|
47
50
|
gl_Position = toClip(toCamera(vWorld));
|
|
48
51
|
}
|
|
@@ -106,7 +109,7 @@ ${fx.calls}
|
|
|
106
109
|
return { source, uniforms: fx.uniforms };
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
const FACE_UNIFORMS = ["uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
|
|
112
|
+
const FACE_UNIFORMS = ["uUVTransform", "uNormalMatrix", "uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
|
|
110
113
|
|
|
111
114
|
// Each line segment is a thin rectangle facing the screen, `uWidth` pixels wide
|
|
112
115
|
const LINE_VS = `
|
|
@@ -142,7 +145,7 @@ void main() {
|
|
|
142
145
|
}
|
|
143
146
|
`;
|
|
144
147
|
|
|
145
|
-
const UNIFORMS = ["
|
|
148
|
+
const UNIFORMS = ["uModel", "uCamPos", "uCamRot", "uF", "uAspect", "uNear", "uFar", "uColor"];
|
|
146
149
|
|
|
147
150
|
/**
|
|
148
151
|
* Draws a scene with WebGL, on the graphics card.
|
|
@@ -158,6 +161,12 @@ const UNIFORMS = ["uPos", "uRot", "uScale", "uCamPos", "uCamRot", "uF", "uAspect
|
|
|
158
161
|
*
|
|
159
162
|
* Meshes are sent to the graphics card once and reused. After changing
|
|
160
163
|
* a mesh's vertices, edges or faces in place, set `mesh.dirty = true`.
|
|
164
|
+
* Textures are sent once too (videos every frame): after redrawing a canvas,
|
|
165
|
+
* call `updateTexture(canvas)`.
|
|
166
|
+
*
|
|
167
|
+
* Meshes and textures that haven't been drawn for `releaseAfter` frames
|
|
168
|
+
* are freed from the graphics card, so replacing them (`obj.mesh = newMesh`)
|
|
169
|
+
* doesn't fill its memory. They're sent again if they come back.
|
|
161
170
|
*
|
|
162
171
|
* @example
|
|
163
172
|
* const scene = new Scene(canvas, { renderer: new WebGLRenderer() });
|
|
@@ -176,9 +185,13 @@ export class WebGLRenderer {
|
|
|
176
185
|
/**
|
|
177
186
|
* @param {Object} [opts]
|
|
178
187
|
* @param {boolean} [opts.antialias=true] - Smooth edges
|
|
188
|
+
* @param {number} [opts.releaseAfter=300] - Free meshes and textures not drawn for this many frames
|
|
189
|
+
* (about 5 s at 60 fps). `Infinity` keeps everything until `dispose()`
|
|
179
190
|
* @throws {Error} If WebGL isn't available (check `WebGLRenderer.isSupported()` first)
|
|
180
191
|
*/
|
|
181
|
-
constructor({ antialias = true } = {}) {
|
|
192
|
+
constructor({ antialias = true, releaseAfter = 300 } = {}) {
|
|
193
|
+
/** Frames a mesh or texture can go undrawn before it's freed (Infinity: never). */
|
|
194
|
+
this.releaseAfter = releaseAfter;
|
|
182
195
|
/** The hidden canvas WebGL draws on. */
|
|
183
196
|
this.canvas = document.createElement("canvas");
|
|
184
197
|
this._options = { antialias, alpha: true, premultipliedAlpha: true, depth: true };
|
|
@@ -208,6 +221,9 @@ export class WebGLRenderer {
|
|
|
208
221
|
this._buffers = new Set();
|
|
209
222
|
this._textures = new WeakMap(); // image / canvas / video → GL texture
|
|
210
223
|
this._glTextures = new Set();
|
|
224
|
+
// Last frame each mesh / texture source was drawn, to free the ones no longer used
|
|
225
|
+
this._meshUse = new Map();
|
|
226
|
+
this._textureUse = new Map();
|
|
211
227
|
this._warned = new WeakSet();
|
|
212
228
|
this._frame = 0;
|
|
213
229
|
|
|
@@ -264,20 +280,34 @@ export class WebGLRenderer {
|
|
|
264
280
|
|
|
265
281
|
// Opaque things first; see-through ones after, without writing depth
|
|
266
282
|
const opaque = [], clear2 = [];
|
|
267
|
-
|
|
283
|
+
// Every object with a mesh, groups' children included, with its world matrix
|
|
284
|
+
const drawn = worldObjects(scene).filter((o) => o.mesh);
|
|
285
|
+
for (const obj of drawn) {
|
|
268
286
|
const geo = this._geometry(obj.mesh);
|
|
269
287
|
const lineColor = this._color(ctx, obj.color ?? scene.color);
|
|
270
288
|
const lineWidth = obj.lineWidth ?? scene.lineWidth;
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
289
|
+
const mats = materialsOf(obj);
|
|
290
|
+
|
|
291
|
+
if (mats) {
|
|
292
|
+
// Normals turn with the object; a mirrored one has its faces' winding reversed
|
|
293
|
+
const place = { normals: normalMatrix(obj.world), mirrored: determinantMat4(obj.world) < 0 };
|
|
294
|
+
// One draw per group of faces sharing a material (mesh.faceMaterials)
|
|
295
|
+
const groups = geo.groups.map((g) => {
|
|
296
|
+
const k = g.index >= 0 && g.index < mats.length && mats[g.index] ? g.index : 0;
|
|
297
|
+
return { ...g, mat: mats[k] };
|
|
298
|
+
});
|
|
299
|
+
for (const g of groups) {
|
|
300
|
+
const fill = this._color(ctx, g.mat.fill ?? (g.mat.texture ? "#ffffff" : scene.color));
|
|
301
|
+
(fill[3] < 1 ? clear2 : opaque).push({ kind: "face", obj, geo, color: fill, mat: g.mat, first: g.first, count: g.count, place });
|
|
278
302
|
}
|
|
303
|
+
const lines = (buf) => (lineColor[3] < 1 ? clear2 : opaque).push({ kind: "line", obj, buf, color: lineColor, width: lineWidth });
|
|
304
|
+
if (groups.every((g) => g.mat.wireframe)) lines(this._lineBuffer(geo, "outlines", () => outlinePairs(geo.faceList ?? [])));
|
|
305
|
+
else for (const g of groups) if (g.mat.wireframe) lines(this._outlineBuffer(geo, obj.mesh, g));
|
|
279
306
|
} else {
|
|
280
|
-
(lineColor[3] < 1 ? clear2 : opaque).push({
|
|
307
|
+
(lineColor[3] < 1 ? clear2 : opaque).push({
|
|
308
|
+
kind: "line", obj, color: lineColor, width: lineWidth,
|
|
309
|
+
buf: this._lineBuffer(geo, "edges", () => edgeSegments(wireEdges(obj.mesh))),
|
|
310
|
+
});
|
|
281
311
|
}
|
|
282
312
|
}
|
|
283
313
|
|
|
@@ -288,7 +318,49 @@ export class WebGLRenderer {
|
|
|
288
318
|
ctx.drawImage(this.canvas, 0, 0);
|
|
289
319
|
|
|
290
320
|
// Meshes marked as changed have been sent again
|
|
291
|
-
for (const obj of
|
|
321
|
+
for (const obj of drawn) if (obj.mesh.dirty) obj.mesh.dirty = false;
|
|
322
|
+
|
|
323
|
+
if (this._frame % 60 === 0) this._sweep();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* What the renderer keeps on the graphics card right now.
|
|
328
|
+
*
|
|
329
|
+
* @returns {{ meshes:number, buffers:number, textures:number }}
|
|
330
|
+
*/
|
|
331
|
+
get memory() {
|
|
332
|
+
return { meshes: this._meshUse.size, buffers: this._buffers.size, textures: this._glTextures.size };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Frees a mesh's buffers or a texture's image on the graphics card now,
|
|
337
|
+
* instead of waiting for `releaseAfter`. If it's drawn again, it's sent again.
|
|
338
|
+
*
|
|
339
|
+
* @param {Object} item - A mesh, or a texture source (image, canvas, video)
|
|
340
|
+
*/
|
|
341
|
+
release(item) {
|
|
342
|
+
if (!item || this._disposed) return;
|
|
343
|
+
const geo = this._meshes.get(item);
|
|
344
|
+
if (geo) {
|
|
345
|
+
for (const b of buffersOf(geo)) this._free(b);
|
|
346
|
+
this._meshes.delete(item);
|
|
347
|
+
}
|
|
348
|
+
this._meshUse.delete(item);
|
|
349
|
+
const t = this._textures.get(item);
|
|
350
|
+
if (t) {
|
|
351
|
+
this.gl.deleteTexture(t.tex);
|
|
352
|
+
this._glTextures.delete(t.tex);
|
|
353
|
+
this._textures.delete(item);
|
|
354
|
+
}
|
|
355
|
+
this._textureUse.delete(item);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Frees what hasn't been drawn for `releaseAfter` frames. */
|
|
359
|
+
_sweep() {
|
|
360
|
+
if (!(this.releaseAfter < Infinity)) return;
|
|
361
|
+
const oldest = this._frame - this.releaseAfter;
|
|
362
|
+
for (const [item, frame] of this._meshUse) if (frame < oldest) this.release(item);
|
|
363
|
+
for (const [item, frame] of this._textureUse) if (frame < oldest) this.release(item);
|
|
292
364
|
}
|
|
293
365
|
|
|
294
366
|
/** Frees the graphics card memory. The renderer can't be used afterwards. */
|
|
@@ -300,6 +372,8 @@ export class WebGLRenderer {
|
|
|
300
372
|
for (const t of this._glTextures) gl.deleteTexture(t);
|
|
301
373
|
this._glTextures.clear();
|
|
302
374
|
this._buffers.clear();
|
|
375
|
+
this._meshUse.clear();
|
|
376
|
+
this._textureUse.clear();
|
|
303
377
|
for (const p of new Set(this._faces.values())) gl.deleteProgram(p.program);
|
|
304
378
|
gl.deleteProgram(this._line.program);
|
|
305
379
|
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
@@ -310,15 +384,15 @@ export class WebGLRenderer {
|
|
|
310
384
|
const isFace = d.kind === "face";
|
|
311
385
|
const effects = isFace ? d.mat.effects ?? [] : null;
|
|
312
386
|
const prog = isFace ? this._faceProgram(effects) : this._line;
|
|
313
|
-
const buf = isFace
|
|
314
|
-
|
|
387
|
+
const buf = isFace
|
|
388
|
+
? this._faceBuffer(d.geo, d.obj.mesh, d.mat.shade === false ? null : creaseOf(d.mat.smooth), d.mat.mapping)
|
|
389
|
+
: d.buf;
|
|
390
|
+
const first = d.first ?? 0, count = d.count ?? buf.count;
|
|
391
|
+
if (!count) return;
|
|
315
392
|
|
|
316
393
|
gl.useProgram(prog.program);
|
|
317
394
|
const u = prog.uniforms;
|
|
318
|
-
|
|
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);
|
|
395
|
+
gl.uniformMatrix4fv(u.uModel, false, d.obj.world);
|
|
322
396
|
gl.uniform3fv(u.uCamPos, view.camPos);
|
|
323
397
|
gl.uniform2fv(u.uCamRot, view.camRot);
|
|
324
398
|
gl.uniform1f(u.uF, view.f);
|
|
@@ -331,6 +405,10 @@ export class WebGLRenderer {
|
|
|
331
405
|
const a = prog.attributes;
|
|
332
406
|
|
|
333
407
|
if (isFace) {
|
|
408
|
+
const m = uvTransform(d.mat) ?? [1, 0, 0, 1, 0, 0];
|
|
409
|
+
gl.uniformMatrix3fv(u.uUVTransform, false, [m[0], m[1], 0, m[2], m[3], 0, m[4], m[5], 1]);
|
|
410
|
+
gl.uniformMatrix3fv(u.uNormalMatrix, false, d.place.normals);
|
|
411
|
+
gl.frontFace(d.place.mirrored ? gl.CCW : gl.CW);
|
|
334
412
|
gl.uniform3fv(u.uLight, light);
|
|
335
413
|
gl.uniform1f(u.uAmbient, ambient);
|
|
336
414
|
gl.uniform1f(u.uShade, d.mat.shade === false ? 0 : 1);
|
|
@@ -346,7 +424,9 @@ export class WebGLRenderer {
|
|
|
346
424
|
gl.vertexAttribPointer(a.aPos, 3, gl.FLOAT, false, 32, 0);
|
|
347
425
|
if (a.aNormal >= 0) gl.vertexAttribPointer(a.aNormal, 3, gl.FLOAT, false, 32, 12);
|
|
348
426
|
if (a.aUV >= 0) gl.vertexAttribPointer(a.aUV, 2, gl.FLOAT, false, 32, 24);
|
|
349
|
-
const tex = d.mat.texture
|
|
427
|
+
const tex = d.mat.texture
|
|
428
|
+
? this._texture(d.mat.texture, d.mat, d.obj.mesh, !!uvsFor(d.obj.mesh, d.mat.mapping).faceUVs)
|
|
429
|
+
: null;
|
|
350
430
|
gl.activeTexture(gl.TEXTURE0);
|
|
351
431
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
352
432
|
gl.uniform1i(u.uTexture, 0);
|
|
@@ -363,7 +443,7 @@ export class WebGLRenderer {
|
|
|
363
443
|
}
|
|
364
444
|
const locs = Object.values(a).filter((loc) => loc >= 0);
|
|
365
445
|
for (const loc of locs) gl.enableVertexAttribArray(loc);
|
|
366
|
-
gl.drawArrays(gl.TRIANGLES,
|
|
446
|
+
gl.drawArrays(gl.TRIANGLES, first, count);
|
|
367
447
|
for (const loc of locs) gl.disableVertexAttribArray(loc);
|
|
368
448
|
}
|
|
369
449
|
|
|
@@ -398,33 +478,53 @@ export class WebGLRenderer {
|
|
|
398
478
|
* (canvases and videos: every frame). Null while it isn't ready,
|
|
399
479
|
* or if the mesh has no texture coordinates.
|
|
400
480
|
*/
|
|
401
|
-
_texture(src,
|
|
481
|
+
_texture(src, mat, mesh, hasUVs) {
|
|
402
482
|
const gl = this.gl;
|
|
483
|
+
const pixelated = !!mat.pixelated;
|
|
403
484
|
if (!textureReady(src)) return null;
|
|
404
|
-
if (!
|
|
485
|
+
if (!hasUVs) {
|
|
405
486
|
if (!this._warned.has(mesh)) {
|
|
406
487
|
this._warned.add(mesh);
|
|
407
|
-
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn");
|
|
488
|
+
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn. Set material.mapping to make some.");
|
|
408
489
|
}
|
|
409
490
|
return null;
|
|
410
491
|
}
|
|
411
492
|
|
|
412
493
|
let t = this._textures.get(src);
|
|
413
|
-
const live = textureIsLive(src);
|
|
414
|
-
|
|
494
|
+
const live = textureIsLive(src, mat);
|
|
495
|
+
const version = textureVersion(src);
|
|
496
|
+
this._textureUse.set(src, this._frame);
|
|
497
|
+
// Sent once, then again after updateTexture(src), or every frame when live
|
|
498
|
+
if (!t || live || t.version !== version || t.failed) {
|
|
415
499
|
if (t?.failed) return null;
|
|
416
|
-
|
|
500
|
+
let { w, h } = textureSize(src);
|
|
417
501
|
if (!t) {
|
|
418
|
-
t = { tex: gl.createTexture(), frame: -1 };
|
|
502
|
+
t = { tex: gl.createTexture(), frame: -1, version: -1 };
|
|
419
503
|
this._textures.set(src, t);
|
|
420
504
|
this._glTextures.add(t.tex);
|
|
421
505
|
}
|
|
422
506
|
if (t.frame !== this._frame) {
|
|
507
|
+
const changed = t.version !== version;
|
|
423
508
|
t.frame = this._frame;
|
|
509
|
+
t.version = version;
|
|
424
510
|
gl.bindTexture(gl.TEXTURE_2D, t.tex);
|
|
511
|
+
// Bigger than the graphics card allows: upload a smaller copy
|
|
512
|
+
let pixels = src;
|
|
513
|
+
const max = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
514
|
+
if (w > max || h > max) {
|
|
515
|
+
const k = max / Math.max(w, h);
|
|
516
|
+
if (!t.small || live || changed) t.small = shrink(src, Math.floor(w * k), Math.floor(h * k), t.small);
|
|
517
|
+
if (!t.warned) {
|
|
518
|
+
t.warned = true;
|
|
519
|
+
console.warn(`Trid: a ${w} × ${h} texture is bigger than this graphics card allows (${max}), so it's drawn at ${t.small.width} × ${t.small.height}`);
|
|
520
|
+
}
|
|
521
|
+
pixels = t.small;
|
|
522
|
+
w = t.small.width;
|
|
523
|
+
h = t.small.height;
|
|
524
|
+
}
|
|
425
525
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // images go top-down, v goes up
|
|
426
526
|
try {
|
|
427
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
|
|
527
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
428
528
|
} catch (err) {
|
|
429
529
|
// Usually an image from another site without permission (CORS)
|
|
430
530
|
console.warn("Trid: can't use this image as a texture: " + err.message);
|
|
@@ -463,61 +563,88 @@ export class WebGLRenderer {
|
|
|
463
563
|
|
|
464
564
|
/** Graphics card buffers for a mesh, built once and rebuilt when it changes. */
|
|
465
565
|
_geometry(mesh) {
|
|
566
|
+
this._meshUse.set(mesh, this._frame);
|
|
466
567
|
let geo = this._meshes.get(mesh);
|
|
467
568
|
const stale = !geo || mesh.dirty || geo.vertices !== mesh.vertices || geo.faceList !== mesh.faces
|
|
468
|
-
|| geo.edgeList !== mesh.edges || geo.counts !== countsOf(mesh);
|
|
569
|
+
|| geo.edgeList !== mesh.edges || geo.faceMats !== mesh.faceMaterials || geo.counts !== countsOf(mesh);
|
|
469
570
|
if (!stale) return geo;
|
|
470
571
|
|
|
471
|
-
if (geo)
|
|
472
|
-
for (const b of [geo.outlines, geo.edges, ...geo.faceBuffers.values()]) this._free(b);
|
|
473
|
-
}
|
|
474
|
-
const v = mesh.vertices;
|
|
572
|
+
if (geo) for (const b of buffersOf(geo)) this._free(b);
|
|
475
573
|
const faces = mesh.faces ?? [];
|
|
476
574
|
|
|
477
|
-
//
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
575
|
+
// Faces grouped by material index, in the order the face buffers store them
|
|
576
|
+
const byIndex = new Map();
|
|
577
|
+
faces.forEach((f, fi) => {
|
|
578
|
+
if (f.length < 3) return;
|
|
579
|
+
const k = mesh.faceMaterials?.[fi] ?? 0;
|
|
580
|
+
if (!byIndex.has(k)) byIndex.set(k, []);
|
|
581
|
+
byIndex.get(k).push(fi);
|
|
582
|
+
});
|
|
583
|
+
const groups = [];
|
|
584
|
+
let first = 0;
|
|
585
|
+
for (const k of [...byIndex.keys()].sort((a, b) => a - b)) {
|
|
586
|
+
const list = byIndex.get(k);
|
|
587
|
+
const count = list.reduce((n, fi) => n + (faces[fi].length - 2) * 3, 0);
|
|
588
|
+
groups.push({ index: k, faces: list, first, count });
|
|
589
|
+
first += count;
|
|
489
590
|
}
|
|
490
591
|
|
|
491
592
|
geo = {
|
|
492
593
|
vertices: mesh.vertices,
|
|
493
594
|
faceList: mesh.faces,
|
|
494
595
|
edgeList: mesh.edges,
|
|
596
|
+
faceMats: mesh.faceMaterials,
|
|
495
597
|
counts: countsOf(mesh),
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
598
|
+
groups,
|
|
599
|
+
groupOutlines: new Map(), // outlines of one group, when only some materials have `wireframe`
|
|
600
|
+
faceBuffers: new Map(), // one per shading mode and mapping, built when first needed
|
|
601
|
+
lines: new Map(), // "outlines" and "edges", built when first drawn (they're big on big models)
|
|
499
602
|
};
|
|
500
603
|
this._meshes.set(mesh, geo);
|
|
501
604
|
return geo;
|
|
502
605
|
}
|
|
503
606
|
|
|
607
|
+
/** A line buffer of the mesh, built the first time it's drawn. */
|
|
608
|
+
_lineBuffer(geo, name, pairs) {
|
|
609
|
+
let buf = geo.lines.get(name);
|
|
610
|
+
if (!buf) {
|
|
611
|
+
buf = this._upload(lineData(geo.vertices, pairs()), 8);
|
|
612
|
+
geo.lines.set(name, buf);
|
|
613
|
+
}
|
|
614
|
+
return buf;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Outlines of one group of faces (see `_geometry`), built when first needed. */
|
|
618
|
+
_outlineBuffer(geo, mesh, group) {
|
|
619
|
+
let buf = geo.groupOutlines.get(group.index);
|
|
620
|
+
if (!buf) {
|
|
621
|
+
const faces = mesh.faces ?? [];
|
|
622
|
+
buf = this._upload(lineData(mesh.vertices, outlinePairs(group.faces.map((fi) => faces[fi]))), 8);
|
|
623
|
+
geo.groupOutlines.set(group.index, buf);
|
|
624
|
+
}
|
|
625
|
+
return buf;
|
|
626
|
+
}
|
|
627
|
+
|
|
504
628
|
/**
|
|
505
|
-
* Triangles of a mesh's faces, each corner with its own position
|
|
506
|
-
* normal
|
|
507
|
-
* shading uses the corner
|
|
629
|
+
* Triangles of a mesh's faces, each corner with its own position,
|
|
630
|
+
* normal and texture coordinate, grouped by material (`geo.groups`).
|
|
631
|
+
* Flat shading uses the face's normal at every corner; smooth shading
|
|
632
|
+
* uses the corner normals. `mapping` picks the texture coordinates.
|
|
508
633
|
*/
|
|
509
|
-
_faceBuffer(geo, mesh, crease) {
|
|
510
|
-
const key = crease ?? "flat";
|
|
634
|
+
_faceBuffer(geo, mesh, crease, mapping) {
|
|
635
|
+
const key = (crease ?? "flat") + "|" + mappingKey(mapping);
|
|
511
636
|
let buf = geo.faceBuffers.get(key);
|
|
512
637
|
if (buf) return buf;
|
|
513
638
|
|
|
514
639
|
const v = mesh.vertices;
|
|
515
640
|
const faces = mesh.faces ?? [];
|
|
516
641
|
const corners = crease ? cornerNormals(mesh, crease) : null;
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
642
|
+
const { uvs, faceUVs } = uvsFor(mesh, mapping);
|
|
643
|
+
const total = geo.groups.reduce((n, g) => n + g.count, 0);
|
|
644
|
+
const tri = new Float32Array(total * 8);
|
|
645
|
+
let o = 0;
|
|
646
|
+
for (const fi of geo.groups.flatMap((g) => g.faces)) {
|
|
647
|
+
const f = faces[fi];
|
|
521
648
|
let ns;
|
|
522
649
|
if (corners) {
|
|
523
650
|
ns = corners[fi];
|
|
@@ -532,10 +659,12 @@ export class WebGLRenderer {
|
|
|
532
659
|
for (const c of [0, k, k + 1]) {
|
|
533
660
|
const p = v[f[c]], n = ns[c];
|
|
534
661
|
const t = ts ? uvs[ts[c]] : null;
|
|
535
|
-
tri
|
|
662
|
+
tri[o++] = p.x; tri[o++] = p.y; tri[o++] = p.z;
|
|
663
|
+
tri[o++] = n.x; tri[o++] = n.y; tri[o++] = n.z;
|
|
664
|
+
tri[o++] = t ? t.u : 0; tri[o++] = t ? t.v : 0;
|
|
536
665
|
}
|
|
537
666
|
}
|
|
538
|
-
}
|
|
667
|
+
}
|
|
539
668
|
|
|
540
669
|
buf = this._upload(tri, 8);
|
|
541
670
|
geo.faceBuffers.set(key, buf);
|
|
@@ -546,7 +675,7 @@ export class WebGLRenderer {
|
|
|
546
675
|
const gl = this.gl;
|
|
547
676
|
const buffer = gl.createBuffer();
|
|
548
677
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
549
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
|
|
678
|
+
gl.bufferData(gl.ARRAY_BUFFER, data instanceof Float32Array ? data : new Float32Array(data), gl.STATIC_DRAW);
|
|
550
679
|
this._buffers.add(buffer);
|
|
551
680
|
return { buffer, count: data.length / floatsPerVertex };
|
|
552
681
|
}
|
|
@@ -568,18 +697,54 @@ export class WebGLRenderer {
|
|
|
568
697
|
}
|
|
569
698
|
}
|
|
570
699
|
|
|
700
|
+
/** Every graphics card buffer of a mesh's geometry. */
|
|
701
|
+
function buffersOf(geo) {
|
|
702
|
+
return [...geo.lines.values(), ...geo.faceBuffers.values(), ...geo.groupOutlines.values()];
|
|
703
|
+
}
|
|
704
|
+
|
|
571
705
|
/** A short string that changes when a mesh's arrays grow or shrink. */
|
|
572
706
|
function countsOf(mesh) {
|
|
573
707
|
return `${mesh.vertices.length}/${mesh.edges?.length ?? 0}/${mesh.faces?.length ?? 0}`;
|
|
574
708
|
}
|
|
575
709
|
|
|
710
|
+
/** A smaller copy of an image, drawn on a canvas (reused when given one). */
|
|
711
|
+
function shrink(src, w, h, canvas = document.createElement("canvas")) {
|
|
712
|
+
canvas.width = w;
|
|
713
|
+
canvas.height = h;
|
|
714
|
+
canvas.getContext("2d").drawImage(src, 0, 0, w, h);
|
|
715
|
+
return canvas;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Each edge of the faces once, as [a, b] pairs. */
|
|
719
|
+
function outlinePairs(faces) {
|
|
720
|
+
const seen = new Set();
|
|
721
|
+
const pairs = [];
|
|
722
|
+
for (const f of faces) {
|
|
723
|
+
for (let k = 0; k < f.length; k++) {
|
|
724
|
+
const a = f[k], b = f[(k + 1) % f.length];
|
|
725
|
+
const key = a < b ? a * 4294967296 + b : b * 4294967296 + a; // one number per edge
|
|
726
|
+
if (!seen.has(key)) {
|
|
727
|
+
seen.add(key);
|
|
728
|
+
pairs.push([a, b]);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return pairs;
|
|
733
|
+
}
|
|
734
|
+
|
|
576
735
|
/** Six corners (two triangles) per segment: A, B, then which corner. */
|
|
577
|
-
function lineData(v,
|
|
578
|
-
const
|
|
736
|
+
function lineData(v, segments) {
|
|
737
|
+
const pairs = Array.isArray(segments) ? segments : [...segments]; // edgeSegments is a generator
|
|
738
|
+
const out = new Float32Array(pairs.length * 48);
|
|
579
739
|
const corners = [[0, -1], [1, -1], [1, 1], [0, -1], [1, 1], [0, 1]];
|
|
740
|
+
let o = 0;
|
|
580
741
|
for (const [i, j] of pairs) {
|
|
581
742
|
const a = v[i], b = v[j];
|
|
582
|
-
for (const [cx, cy] of corners)
|
|
743
|
+
for (const [cx, cy] of corners) {
|
|
744
|
+
out[o++] = a.x; out[o++] = a.y; out[o++] = a.z;
|
|
745
|
+
out[o++] = b.x; out[o++] = b.y; out[o++] = b.z;
|
|
746
|
+
out[o++] = cx; out[o++] = cy;
|
|
747
|
+
}
|
|
583
748
|
}
|
|
584
749
|
return out;
|
|
585
750
|
}
|
package/trid/scene.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
|
|
2
2
|
import { loop } from "./render/loop.js";
|
|
3
3
|
import { ToolManager } from "./tools/manager.js";
|
|
4
|
+
import { linkTree } from "./sceneGraph.js";
|
|
5
|
+
import { screenshot, record } from "./export/capture.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Holds a canvas, a camera and a list of objects, and draws them.
|
|
@@ -70,27 +72,37 @@ export class Scene {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
* Adds an object to the scene, filling in missing transform fields
|
|
75
|
+
* Adds an object to the scene, filling in missing transform fields
|
|
76
|
+
* (down its `children` too).
|
|
74
77
|
*
|
|
75
|
-
*
|
|
78
|
+
* An object can be a group: give it `children`, objects placed relative
|
|
79
|
+
* to it, and leave out `mesh` if it's only there to hold them. Moving it
|
|
80
|
+
* moves them all. See `trid/sceneGraph.js`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const car = scene.add({ children: [{ mesh: body }, { mesh: wheel, pos: { x: 0.4, y: -0.2, z: 0 } }] });
|
|
84
|
+
* car.pos.x += 0.1; // the body and the wheel move together
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} obj - An object: `mesh` and/or `children`, plus pos, rot, scale, material...
|
|
76
87
|
* @returns The same object, so it can be kept and animated
|
|
77
88
|
*/
|
|
78
89
|
add(obj) {
|
|
79
|
-
obj
|
|
80
|
-
obj.rot ??= { x: 0, y: 0, z: 0 };
|
|
81
|
-
obj.scale ??= { x: 1, y: 1, z: 1 };
|
|
90
|
+
linkTree(obj, null);
|
|
82
91
|
this.objects.push(obj);
|
|
83
92
|
return obj;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/**
|
|
87
|
-
* Removes an object from the scene
|
|
96
|
+
* Removes an object: from the scene's top level, or from its group's
|
|
97
|
+
* `children`. Its own children go with it. Does nothing if it isn't there.
|
|
88
98
|
*
|
|
89
|
-
* @param {Object} obj - Object previously
|
|
99
|
+
* @param {Object} obj - Object previously added (or a child of one)
|
|
90
100
|
*/
|
|
91
101
|
remove(obj) {
|
|
92
|
-
const
|
|
93
|
-
|
|
102
|
+
const list = obj.parent ? obj.parent.children : this.objects;
|
|
103
|
+
const i = list?.indexOf(obj) ?? -1;
|
|
104
|
+
if (i !== -1) list.splice(i, 1);
|
|
105
|
+
if (obj.parent) obj.parent = null;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/**
|
|
@@ -150,6 +162,28 @@ export class Scene {
|
|
|
150
162
|
this._stop = null;
|
|
151
163
|
}
|
|
152
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Draws a new frame and returns it as an image file (PNG by default).
|
|
167
|
+
* The page's canvas doesn't change. Save it with `download`.
|
|
168
|
+
*
|
|
169
|
+
* @param {import("./export/capture.js").ScreenshotOptions} [opts]
|
|
170
|
+
* @returns {Promise<Blob>}
|
|
171
|
+
*/
|
|
172
|
+
screenshot(opts) {
|
|
173
|
+
return screenshot(this, opts);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Starts recording the canvas as a video. Call `stop()` on the result,
|
|
178
|
+
* or pass `duration`, then save `await recording.done` with `download`.
|
|
179
|
+
*
|
|
180
|
+
* @param {import("./export/capture.js").RecordOptions} [opts]
|
|
181
|
+
* @returns {import("./export/capture.js").Recording}
|
|
182
|
+
*/
|
|
183
|
+
record(opts) {
|
|
184
|
+
return record(this, opts);
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
/**
|
|
154
188
|
* Stops the loop, removes every tool (and its event listeners) and
|
|
155
189
|
* frees the renderer's resources. Use it when the scene is no longer needed.
|