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/trid/index.js CHANGED
@@ -11,12 +11,14 @@
11
11
 
12
12
  // Scene
13
13
  export { Scene } from "./scene.js";
14
+ export { worldObjects, worldMatrix, worldPosition, ancestors, forEachPlaced } from "./sceneGraph.js";
14
15
 
15
16
  // Shapes and meshes
16
- export { makeShape, scaleMesh } from "./render/shapes.js";
17
+ export { makeShape, scaleMesh, lathe, extrude, triangulate } from "./render/shapes.js";
18
+ export { cloneMesh, transformMesh, displaceMesh, mergeMeshes, meshBounds } from "./render/meshUtils.js";
17
19
 
18
20
  // Renderers
19
- export { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
21
+ export { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT, materialsOf } from "./render/canvasRenderer.js";
20
22
  export { WebGLRenderer, DEFAULT_FAR } from "./render/webglRenderer.js";
21
23
 
22
24
  // Effects
@@ -25,21 +27,34 @@ export {
25
27
  buildEffects, effectsKey, uniformType, SURFACE_GLSL,
26
28
  } from "./render/effects.js";
27
29
 
30
+ // Procedural textures
31
+ export { makeTexture, texturePixels, colorBytes, TEXTURE_STYLES } from "./render/proceduralTextures.js";
32
+
33
+ // Texture mapping
34
+ export { computeUVs, mapUVs, uvsFor, uvTransform, applyUVTransform, mappingOf, mappingKey, MAPPINGS } from "./render/uvMapping.js";
35
+
28
36
  // Normals (smooth shading)
29
37
  export { cornerNormals, creaseOf, newell, DEFAULT_CREASE } from "./render/normals.js";
30
38
 
31
39
  // Loading files
32
- export { parseOBJ, loadOBJ, readOBJFile, pickOBJFile, fitMesh } from "./loaders/obj.js";
33
- export { loadTexture, textureReady, textureSize, textureIsLive } from "./loaders/texture.js";
40
+ export { parseOBJ, loadOBJ, loadMaterials, readOBJFile, pickOBJFile, fitMesh } from "./loaders/obj.js";
41
+ export { parseMTL } from "./loaders/mtl.js";
42
+ export { loadTexture, textureReady, textureSize, textureIsLive, updateTexture, textureVersion } from "./loaders/texture.js";
43
+
44
+ // Exporting
45
+ export { exportOBJ } from "./export/obj.js";
46
+ export { exportMTL, objMaterials, exportModel, textureToBlob } from "./export/mtl.js";
47
+ export { screenshot, record, download } from "./export/capture.js";
34
48
 
35
49
  // Tools
36
50
  export {
37
51
  OrbitCamera, FlyCamera, DragObject, HoverHighlight, FpsMeter, ObjUploader,
38
- ToolManager, pick, screenBounds, worldPerPixel,
52
+ ViewGizmo, TransformGizmo,
53
+ ToolManager, pick, pickHit, screenBounds, worldPerPixel,
39
54
  } from "./tools/index.js";
40
55
 
41
56
  // Drawing and the frame loop
42
- export { clear, point, line, drawWireframe, drawInstance, edgeSegments } from "./render/draw.js";
57
+ export { clear, point, line, drawWireframe, drawInstance, edgeSegments, wireEdges } from "./render/draw.js";
43
58
  export { loop } from "./render/loop.js";
44
59
  export { parseColor } from "./render/color.js";
45
60
 
@@ -51,3 +66,11 @@ export {
51
66
  DEFAULT_FOV, DEFAULT_NEAR,
52
67
  } from "./math/transform.js";
53
68
  export { project, screen } from "./math/projection.js";
69
+ export { matFromEuler, eulerFromMat, matFromAxisAngle, mulMat, mulVec, applyTransformOp } from "./math/rotation.js";
70
+ export {
71
+ identityMat4, mat4FromTransform, mulMat4, invertMat4, determinantMat4,
72
+ transformPoint, transformDirection, normalMatrix, transformNormal, transformOpToLocal,
73
+ } from "./math/matrix.js";
74
+
75
+ // Random numbers and noise
76
+ export { rng, noise2, noise3, fbm2, fbm3 } from "./math/noise.js";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * MTL loading: the material file an OBJ file names with `mtllib`.
3
+ *
4
+ * Reads each `newmtl` block's diffuse color (`Kd`), see-through (`d`, or
5
+ * `Tr` = 1 - d) and diffuse texture (`map_Kd`, options like `-s 1 1 1`
6
+ * skipped). Everything else is ignored.
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} MtlMaterial
11
+ * @property {string} name
12
+ * @property {string} [fill] - "#rrggbb" from `Kd` (or "rgba(...)" when see-through)
13
+ * @property {number} [opacity] - From `d` / `Tr`, 0 to 1
14
+ * @property {string} [textureFile] - The texture's file name (`map_Kd`), relative to the .mtl file
15
+ */
16
+
17
+ /**
18
+ * Parses the text of an MTL file.
19
+ *
20
+ * @param {string} text
21
+ * @returns {Object<string, MtlMaterial>} Materials by name
22
+ */
23
+ export function parseMTL(text) {
24
+ const materials = {};
25
+ let m = null;
26
+ for (const raw of text.split(/\r?\n/)) {
27
+ const line = raw.trim();
28
+ if (line === "" || line[0] === "#") continue;
29
+ const [key, ...rest] = line.split(/\s+/);
30
+ const k = key.toLowerCase();
31
+ if (k === "newmtl") {
32
+ const name = rest.join(" ");
33
+ m = materials[name] = { name };
34
+ } else if (!m) {
35
+ continue;
36
+ } else if (k === "kd") {
37
+ const [r, g, b] = rest.map(Number);
38
+ const hex = (x) => Math.round(Math.min(1, Math.max(0, x || 0)) * 255).toString(16).padStart(2, "0");
39
+ m.fill = `#${hex(r)}${hex(g ?? r)}${hex(b ?? r)}`;
40
+ } else if (k === "d") {
41
+ m.opacity = Math.min(1, Math.max(0, Number(rest[0])));
42
+ } else if (k === "tr") {
43
+ m.opacity = Math.min(1, Math.max(0, 1 - Number(rest[0])));
44
+ } else if (k === "map_kd") {
45
+ // Skip options ("-s 1 1 1", "-bm 0.5", "-clamp on"...): the file name is what's left
46
+ const words = [...rest];
47
+ const takes = { "-s": 3, "-o": 3, "-t": 3, "-mm": 2, "-bm": 1, "-blendu": 1, "-blendv": 1, "-clamp": 1, "-texres": 1, "-imfchan": 1, "-boost": 1, "-cc": 1 };
48
+ while (words.length && words[0][0] === "-") {
49
+ const opt = words.shift().toLowerCase();
50
+ words.splice(0, takes[opt] ?? 1);
51
+ }
52
+ if (words.length) m.textureFile = words.join(" ");
53
+ }
54
+ }
55
+ // See-through fills as rgba, so materials can use them directly
56
+ for (const mat of Object.values(materials)) {
57
+ if (mat.fill && mat.opacity !== undefined && mat.opacity < 1) {
58
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(mat.fill.slice(i, i + 2), 16));
59
+ mat.fill = `rgba(${r}, ${g}, ${b}, ${mat.opacity})`;
60
+ }
61
+ }
62
+ return materials;
63
+ }
@@ -1,9 +1,13 @@
1
+ import { parseMTL } from "./mtl.js";
2
+ import { loadTexture } from "./texture.js";
3
+
1
4
  /**
2
5
  * Wavefront OBJ loading.
3
6
  *
4
7
  * Reads vertex positions (`v`), texture coordinates (`vt`), normals
5
- * (`vn`), faces (`f`) and polylines (`l`). Everything else (materials,
6
- * groups) is ignored for now.
8
+ * (`vn`), faces (`f`), polylines (`l`), material names (`usemtl`) and the
9
+ * material file's name (`mtllib`). `loadOBJ(url, { materials: true })` also
10
+ * reads that .mtl file and its textures. Groups (`g`, `o`) are ignored.
7
11
  *
8
12
  * OBJ files are right-handed with +Z toward the viewer. Trid is
9
13
  * left-handed with the camera looking down +Z, so Z is flipped on
@@ -21,10 +25,18 @@
21
25
  * @property {{u:number,v:number}[]} [uvs] - Texture coordinates from the file (`vt`), if any
22
26
  * @property {(number[]|null)[]} [faceUVs] - For each face, the texture coordinate index of
23
27
  * each corner (null when the file gives none for that face). Used by textures.
28
+ * @property {string[]} [materialNames] - Material names from `usemtl` lines, in the order they first appear
29
+ * @property {number[]} [faceMaterials] - For each face, its index in `materialNames`. Give the
30
+ * object a list of materials in the same order to color or texture each part.
31
+ * @property {string} [mtllib] - The .mtl file the OBJ names (`mtllib`), if any
32
+ * @property {Object[]} [materials] - With `loadOBJ(url, { materials: true })`: one Trid material per
33
+ * entry of `materialNames`, from the .mtl file (fill, texture). Use it as the object's `material`
24
34
  *
25
35
  * @typedef {Object} ObjOptions
26
36
  * @property {number} [fit] - If set, center the model on the origin and scale it
27
37
  * so its largest side is this long. Handy because OBJ files come in any size.
38
+ * @property {boolean} [materials=false] - `loadOBJ` only: also load the .mtl file and its
39
+ * textures, into `mesh.materials`
28
40
  */
29
41
 
30
42
  /**
@@ -43,6 +55,10 @@ export function parseOBJ(text, { fit } = {}) {
43
55
  const faceNormals = []; // per face: normal index of each corner, or null
44
56
  const uvs = [];
45
57
  const faceUVs = []; // per face: texture coordinate index of each corner, or null
58
+ const materialNames = [];
59
+ const faceMaterials = []; // per face: index in materialNames
60
+ let material = 0;
61
+ let mtllib;
46
62
 
47
63
  const src = text.split(/\r?\n/);
48
64
 
@@ -60,6 +76,12 @@ export function parseOBJ(text, { fit } = {}) {
60
76
  uvs.push({ u: +(parts[1] ?? 0), v: +(parts[2] ?? 0) });
61
77
  } else if (kind === "vn") {
62
78
  normals.push({ x: +(parts[1] ?? 0), y: +(parts[2] ?? 0), z: -(parts[3] ?? 0) });
79
+ } else if (kind === "mtllib") {
80
+ mtllib ??= parts.slice(1).join(" ");
81
+ } else if (kind === "usemtl") {
82
+ const name = parts.slice(1).join(" ");
83
+ material = materialNames.indexOf(name);
84
+ if (material < 0) material = materialNames.push(name) - 1;
63
85
  } else if (kind === "f" || kind === "l") {
64
86
  // "7", "7/1", "7//3", "7/1/3" all point at vertex 7. Negative = counted from the end.
65
87
  const ids = parts.slice(1).map((p) => {
@@ -72,6 +94,7 @@ export function parseOBJ(text, { fit } = {}) {
72
94
  });
73
95
  if (kind === "f" && ids.length >= 3) {
74
96
  faces.push(ids.reverse()); // flipping Z flips the winding
97
+ faceMaterials.push(material);
75
98
  // "7//3" and "7/1/3": the third number is the corner's normal
76
99
  const ns = parts.slice(1).map((p) => {
77
100
  const s = p.split("/")[2];
@@ -127,6 +150,12 @@ export function parseOBJ(text, { fit } = {}) {
127
150
  mesh.uvs = uvs;
128
151
  mesh.faceUVs = faceUVs;
129
152
  }
153
+ // Material names (a list of materials on the object then colors each part)
154
+ if (materialNames.length > 0) {
155
+ mesh.materialNames = materialNames;
156
+ mesh.faceMaterials = faceMaterials;
157
+ }
158
+ if (mtllib) mesh.mtllib = mtllib;
130
159
  if (fit != null) fitMesh(mesh, fit);
131
160
  return mesh;
132
161
  }
@@ -171,10 +200,49 @@ export function fitMesh(mesh, size = 1) {
171
200
  * @param {ObjOptions} [opts]
172
201
  * @returns {Promise<ObjMesh>}
173
202
  */
174
- export async function loadOBJ(url, opts) {
203
+ export async function loadOBJ(url, opts = {}) {
175
204
  const res = await fetch(url);
176
205
  if (!res.ok) throw new Error(`Could not load ${url} (${res.status})`);
177
- return parseOBJ(await res.text(), opts);
206
+ const mesh = parseOBJ(await res.text(), opts);
207
+ if (opts.materials) mesh.materials = await loadMaterials(mesh, new URL(url, location.href));
208
+ return mesh;
209
+ }
210
+
211
+ /**
212
+ * The materials of a loaded OBJ, from its .mtl file: one per name in
213
+ * `mesh.materialNames`, with the texture images loaded. Missing files only
214
+ * give a warning (the part keeps a plain fill).
215
+ *
216
+ * @param {ObjMesh} mesh - Parsed with `parseOBJ`
217
+ * @param {URL|string} base - Where the OBJ file is (file names are relative to it)
218
+ * @returns {Promise<Object[]>}
219
+ */
220
+ export async function loadMaterials(mesh, base) {
221
+ const names = mesh.materialNames ?? [];
222
+ if (!mesh.mtllib) return names.map((name) => ({ name, fill: "#cccccc" }));
223
+ let lib = {};
224
+ const mtlURL = new URL(mesh.mtllib, base);
225
+ try {
226
+ const res = await fetch(mtlURL);
227
+ if (!res.ok) throw new Error(`${res.status}`);
228
+ lib = parseMTL(await res.text());
229
+ } catch (err) {
230
+ console.warn(`Trid: couldn't load the material file ${mtlURL} (${err.message})`);
231
+ }
232
+ return Promise.all(names.map(async (name) => {
233
+ const m = lib[name] ?? {};
234
+ const material = { name, fill: m.fill ?? (m.textureFile ? "#ffffff" : "#cccccc") };
235
+ if (m.textureFile) {
236
+ const file = new URL(m.textureFile.replace(/\\/g, "/"), mtlURL);
237
+ try {
238
+ material.texture = await loadTexture(file.href);
239
+ material.fill = "#ffffff"; // MTL's Kd under a texture is usually a tint nobody meant
240
+ } catch (err) {
241
+ console.warn(`Trid: couldn't load the texture ${file} (${err.message})`);
242
+ }
243
+ }
244
+ return material;
245
+ }));
178
246
  }
179
247
 
180
248
  /**
@@ -3,9 +3,15 @@
3
3
  *
4
4
  * A material's `texture` can be anything the canvas can draw:
5
5
  * an `<img>`, a `<canvas>`, a `<video>` or an `ImageBitmap`.
6
- * Canvases and videos are read again every frame, so they can change.
6
+ *
7
+ * Videos are read again every frame. Everything else, canvases included,
8
+ * is read once: after redrawing a canvas, call `updateTexture(canvas)`, or
9
+ * give its material `liveTexture: true` to read it every frame.
7
10
  */
8
11
 
12
+ /** How many times each source was updated with `updateTexture` (0 when never). */
13
+ const versions = new WeakMap();
14
+
9
15
  /**
10
16
  * Loads an image to use as a texture.
11
17
  *
@@ -48,9 +54,44 @@ export function textureSize(src) {
48
54
  };
49
55
  }
50
56
 
51
- /** Whether a source can change over time (canvas, video), so it's read every frame. */
52
- export function textureIsLive(src) {
53
- return (typeof HTMLCanvasElement !== "undefined" && src instanceof HTMLCanvasElement)
54
- || (typeof HTMLVideoElement !== "undefined" && src instanceof HTMLVideoElement)
55
- || (typeof OffscreenCanvas !== "undefined" && src instanceof OffscreenCanvas);
57
+ /**
58
+ * Whether a texture is read again every frame: videos, and any source whose
59
+ * material has `liveTexture: true`. `liveTexture: false` freezes a video.
60
+ *
61
+ * @param {CanvasImageSource} src
62
+ * @param {{ liveTexture?: boolean }} [material] - The material using it
63
+ * @returns {boolean}
64
+ */
65
+ export function textureIsLive(src, material) {
66
+ return material?.liveTexture ?? (typeof HTMLVideoElement !== "undefined" && src instanceof HTMLVideoElement);
67
+ }
68
+
69
+ /**
70
+ * Tells the renderers a texture source changed: call it after drawing on a
71
+ * canvas used as a texture. It's read again (once) on the next frame, by
72
+ * every renderer and every material using it.
73
+ *
74
+ * Canvases are read only once otherwise, which keeps them fast and sharp
75
+ * (they get mipmaps). For a canvas redrawn every frame, set
76
+ * `liveTexture: true` on its material instead.
77
+ *
78
+ * @example
79
+ * paintClouds(canvas, seed);
80
+ * updateTexture(canvas);
81
+ *
82
+ * @param {CanvasImageSource} src - The canvas (or image) that changed
83
+ */
84
+ export function updateTexture(src) {
85
+ if (src && typeof src === "object") versions.set(src, (versions.get(src) ?? 0) + 1);
86
+ }
87
+
88
+ /**
89
+ * How many times `updateTexture` was called for a source (0 when never).
90
+ * Renderers compare it with the version they read last.
91
+ *
92
+ * @param {CanvasImageSource} src
93
+ * @returns {number}
94
+ */
95
+ export function textureVersion(src) {
96
+ return versions.get(src) ?? 0;
56
97
  }
@@ -0,0 +1,200 @@
1
+ import { matFromEuler } from "./rotation.js";
2
+
3
+ /**
4
+ * 4×4 transform matrices, for objects inside groups: an object's world
5
+ * matrix is its parent's world matrix times its own.
6
+ *
7
+ * A matrix is an array of 16 numbers in column-major order (the order
8
+ * WebGL uses): `m[col * 4 + row]`, translation in `m[12..14]`. They're
9
+ * built with `matFromEuler`, so they match `localToWorld` exactly.
10
+ *
11
+ * @typedef {number[]} Mat4
12
+ * @typedef {{x:number, y:number, z:number}} Vec3
13
+ */
14
+
15
+ /** @returns {Mat4} The identity matrix (changes nothing). */
16
+ export function identityMat4() {
17
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
18
+ }
19
+
20
+ /**
21
+ * The matrix of a transform: scale, then rotate (X, then Y, then Z), then
22
+ * move. Parts left out change nothing.
23
+ *
24
+ * @param {{ pos?:Vec3, rot?:Vec3, scale?:Vec3 }} t - A transform, or a scene object
25
+ * @returns {Mat4}
26
+ */
27
+ export function mat4FromTransform({ pos, rot, scale } = {}) {
28
+ const r = rot ? matFromEuler(rot) : [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
29
+ const sx = scale?.x ?? 1, sy = scale?.y ?? 1, sz = scale?.z ?? 1;
30
+ return [
31
+ r[0][0] * sx, r[1][0] * sx, r[2][0] * sx, 0,
32
+ r[0][1] * sy, r[1][1] * sy, r[2][1] * sy, 0,
33
+ r[0][2] * sz, r[1][2] * sz, r[2][2] * sz, 0,
34
+ pos?.x ?? 0, pos?.y ?? 0, pos?.z ?? 0, 1,
35
+ ];
36
+ }
37
+
38
+ /**
39
+ * Matrix product a · b: b is applied first. A child's world matrix is
40
+ * `mulMat4(parentWorld, childLocal)`.
41
+ *
42
+ * @param {Mat4} a
43
+ * @param {Mat4} b
44
+ * @returns {Mat4}
45
+ */
46
+ export function mulMat4(a, b) {
47
+ const out = new Array(16);
48
+ for (let c = 0; c < 4; c++) {
49
+ for (let r = 0; r < 4; r++) {
50
+ out[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /**
57
+ * Inverse of a transform matrix (scale, rotation and move; no perspective).
58
+ * Turns world positions into an object's local ones.
59
+ *
60
+ * @param {Mat4} m
61
+ * @returns {Mat4} The inverse, or the identity if `m` flattens space (a zero scale)
62
+ */
63
+ export function invertMat4(m) {
64
+ const [a, b, c, , d, e, f, , g, h, i] = m;
65
+ // Inverse of the 3×3 part by cofactors
66
+ const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
67
+ const det = a * A + b * B + c * C;
68
+ if (Math.abs(det) < 1e-12) return identityMat4();
69
+ const k = 1 / det;
70
+ const inv = [
71
+ A * k, -(b * i - c * h) * k, (b * f - c * e) * k,
72
+ B * k, (a * i - c * g) * k, -(a * f - c * d) * k,
73
+ C * k, -(a * h - b * g) * k, (a * e - b * d) * k,
74
+ ];
75
+ const tx = m[12], ty = m[13], tz = m[14];
76
+ return [
77
+ inv[0], inv[1], inv[2], 0,
78
+ inv[3], inv[4], inv[5], 0,
79
+ inv[6], inv[7], inv[8], 0,
80
+ -(inv[0] * tx + inv[3] * ty + inv[6] * tz),
81
+ -(inv[1] * tx + inv[4] * ty + inv[7] * tz),
82
+ -(inv[2] * tx + inv[5] * ty + inv[8] * tz),
83
+ 1,
84
+ ];
85
+ }
86
+
87
+ /**
88
+ * Determinant of the 3×3 part: < 0 when the matrix mirrors (an odd number
89
+ * of negative scales), which turns faces inside out.
90
+ *
91
+ * @param {Mat4} m
92
+ * @returns {number}
93
+ */
94
+ export function determinantMat4(m) {
95
+ return m[0] * (m[5] * m[10] - m[6] * m[9]) - m[4] * (m[1] * m[10] - m[2] * m[9]) + m[8] * (m[1] * m[6] - m[2] * m[5]);
96
+ }
97
+
98
+ /**
99
+ * A point through a matrix (scaled, turned and moved).
100
+ *
101
+ * @param {Mat4} m
102
+ * @param {Vec3} p
103
+ * @returns {Vec3}
104
+ */
105
+ export function transformPoint(m, p) {
106
+ return {
107
+ x: m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
108
+ y: m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
109
+ z: m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14],
110
+ };
111
+ }
112
+
113
+ /**
114
+ * A direction or offset through a matrix: scaled and turned, not moved.
115
+ *
116
+ * @param {Mat4} m
117
+ * @param {Vec3} v
118
+ * @returns {Vec3}
119
+ */
120
+ export function transformDirection(m, v) {
121
+ return {
122
+ x: m[0] * v.x + m[4] * v.y + m[8] * v.z,
123
+ y: m[1] * v.x + m[5] * v.y + m[9] * v.z,
124
+ z: m[2] * v.x + m[6] * v.y + m[10] * v.z,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * The 3×3 matrix that turns surface normals with an object (the inverse
130
+ * transpose of its matrix), column-major. It keeps normals perpendicular to
131
+ * stretched faces, and pointing outward on mirrored ones.
132
+ *
133
+ * @param {Mat4} m
134
+ * @returns {number[]} 9 numbers, column-major (ready for `gl.uniformMatrix3fv`)
135
+ */
136
+ export function normalMatrix(m) {
137
+ const inv = invertMat4(m);
138
+ // Transpose of the inverse's 3×3 part
139
+ return [inv[0], inv[4], inv[8], inv[1], inv[5], inv[9], inv[2], inv[6], inv[10]];
140
+ }
141
+
142
+ /**
143
+ * A surface normal through a normal matrix (from `normalMatrix`), unit length.
144
+ *
145
+ * @param {number[]} nm - 3×3 column-major
146
+ * @param {Vec3} n
147
+ * @returns {Vec3}
148
+ */
149
+ export function transformNormal(nm, n) {
150
+ const x = nm[0] * n.x + nm[3] * n.y + nm[6] * n.z;
151
+ const y = nm[1] * n.x + nm[4] * n.y + nm[7] * n.z;
152
+ const z = nm[2] * n.x + nm[5] * n.y + nm[8] * n.z;
153
+ const len = Math.hypot(x, y, z) || 1;
154
+ return { x: x / len, y: y / len, z: z / len };
155
+ }
156
+
157
+ /**
158
+ * Turns a world-space `TransformOp` (what `TransformGizmo` reports) into the
159
+ * space of an object's parent, so `applyTransformOp(child, op)` works on an
160
+ * object inside a group. Exact when the parents scale evenly (the same on
161
+ * every axis); with uneven scales, the turn is the closest a plain rotation
162
+ * can get.
163
+ *
164
+ * @example
165
+ * const local = obj.parent ? transformOpToLocal(op, worldMatrix(obj.parent)) : op;
166
+ * Object.assign(obj, applyTransformOp(saved, local));
167
+ *
168
+ * @param {Object} op - `{ mode: "move", delta }`, `{ mode: "rotate", axis, angle, center }`
169
+ * or `{ mode: "scale", axis, factor, center }`
170
+ * @param {Mat4} parentWorld - The parent's world matrix
171
+ * @returns {Object} The same operation, in the parent's space
172
+ */
173
+ export function transformOpToLocal(op, parentWorld) {
174
+ const inv = invertMat4(parentWorld);
175
+ const unit = (v) => {
176
+ const len = Math.hypot(v.x, v.y, v.z) || 1;
177
+ return { x: v.x / len, y: v.y / len, z: v.z / len };
178
+ };
179
+ switch (op.mode) {
180
+ case "move":
181
+ return { mode: "move", delta: transformDirection(inv, op.delta) };
182
+ case "rotate":
183
+ // A mirrored parent turns the other way
184
+ return {
185
+ mode: "rotate",
186
+ axis: unit(transformDirection(inv, op.axis)),
187
+ angle: determinantMat4(parentWorld) < 0 ? -op.angle : op.angle,
188
+ center: transformPoint(inv, op.center),
189
+ };
190
+ case "scale":
191
+ return {
192
+ mode: "scale",
193
+ axis: op.axis ? unit(transformDirection(inv, op.axis)) : null,
194
+ factor: op.factor,
195
+ center: transformPoint(inv, op.center),
196
+ };
197
+ default:
198
+ return op;
199
+ }
200
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Seeded randomness and smooth noise, for procedural shapes and textures.
3
+ * The same seed always gives the same result, on every browser.
4
+ */
5
+
6
+ /**
7
+ * A seeded random number generator (mulberry32): call the function it
8
+ * returns for the next number in [0, 1). Use it instead of Math.random when
9
+ * the same seed must build the same model again.
10
+ *
11
+ * @example
12
+ * const random = rng(42);
13
+ * const x = random(), y = random(); // the same two numbers every time
14
+ *
15
+ * @param {number} [seed=0] - Any whole number
16
+ * @returns {() => number}
17
+ */
18
+ export function rng(seed = 0) {
19
+ let a = seed >>> 0;
20
+ return () => {
21
+ a = (a + 0x6d2b79f5) >>> 0;
22
+ let t = a;
23
+ t = Math.imul(t ^ (t >>> 15), t | 1);
24
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
25
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
26
+ };
27
+ }
28
+
29
+ /** Hash of lattice coordinates and a seed, in [0, 1). */
30
+ function hash3(x, y, z, seed) {
31
+ let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 2147483647) ^ Math.imul(seed, 1442695041);
32
+ h = Math.imul(h ^ (h >>> 13), 1274126177);
33
+ h ^= h >>> 16;
34
+ return (h >>> 0) / 4294967296;
35
+ }
36
+
37
+ const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
38
+ const lerp = (a, b, t) => a + (b - a) * t;
39
+ /** Wraps lattice coordinate n when the noise repeats every `period` (0: no wrap) */
40
+ const wrap = (n, period) => (period > 0 ? ((n % period) + period) % period : n);
41
+ /** A usable period: a whole number, or 0 for none */
42
+ const periodOf = (p) => (p > 0 ? Math.max(1, Math.round(p)) : 0);
43
+
44
+ function value2(x, y, seed, period) {
45
+ const xi = Math.floor(x), yi = Math.floor(y);
46
+ const fx = fade(x - xi), fy = fade(y - yi);
47
+ const x0 = wrap(xi, period), x1 = wrap(xi + 1, period);
48
+ const y0 = wrap(yi, period), y1 = wrap(yi + 1, period);
49
+ return lerp(
50
+ lerp(hash3(x0, y0, 0, seed), hash3(x1, y0, 0, seed), fx),
51
+ lerp(hash3(x0, y1, 0, seed), hash3(x1, y1, 0, seed), fx),
52
+ fy,
53
+ );
54
+ }
55
+
56
+ function value3(x, y, z, seed, period) {
57
+ const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
58
+ const fx = fade(x - xi), fy = fade(y - yi), fz = fade(z - zi);
59
+ const h = (dx, dy, dz) => hash3(wrap(xi + dx, period), wrap(yi + dy, period), wrap(zi + dz, period), seed);
60
+ return lerp(
61
+ lerp(lerp(h(0, 0, 0), h(1, 0, 0), fx), lerp(h(0, 1, 0), h(1, 1, 0), fx), fy),
62
+ lerp(lerp(h(0, 0, 1), h(1, 0, 1), fx), lerp(h(0, 1, 1), h(1, 1, 1), fx), fy),
63
+ fz,
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Smooth 2D noise (value noise) in [0, 1): random at whole coordinates,
69
+ * blended smoothly in between. Multiply the coordinates to change the size
70
+ * of the blobs.
71
+ *
72
+ * With `period`, the noise repeats every `period` units on both axes, so a
73
+ * texture painted with it tiles with no seam (map the texture's width to
74
+ * `period` units).
75
+ *
76
+ * @param {number} x
77
+ * @param {number} y
78
+ * @param {Object} [opts]
79
+ * @param {number} [opts.seed=0] - Whole number; another seed gives other noise
80
+ * @param {number} [opts.period=0] - Repeat every this many units (a whole number; 0: never)
81
+ * @returns {number}
82
+ */
83
+ export function noise2(x, y, { seed = 0, period = 0 } = {}) {
84
+ return value2(x, y, seed, periodOf(period));
85
+ }
86
+
87
+ /**
88
+ * Smooth 3D noise in [0, 1). Sample it at a mesh's vertex positions to
89
+ * displace a sphere with no seam. `period` repeats it on all three axes.
90
+ *
91
+ * @param {number} x
92
+ * @param {number} y
93
+ * @param {number} z
94
+ * @param {Object} [opts]
95
+ * @param {number} [opts.seed=0] - Whole number; another seed gives other noise
96
+ * @param {number} [opts.period=0] - Repeat every this many units (a whole number; 0: never)
97
+ * @returns {number}
98
+ */
99
+ export function noise3(x, y, z, { seed = 0, period = 0 } = {}) {
100
+ return value3(x, y, z, seed, periodOf(period));
101
+ }
102
+
103
+ /**
104
+ * Fractal 2D noise (fBm): `octaves` layers of noise2, each twice as fine and
105
+ * `gain` times as strong as the one before. Gives natural detail: terrain,
106
+ * clouds, rock, marble. Result in about [0, 1), averaging 0.5.
107
+ *
108
+ * @example
109
+ * // A tiling cloud texture
110
+ * const p = 4; // 4 blobs across
111
+ * for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
112
+ * const n = fbm2((x / size) * p, (y / size) * p, { period: p });
113
+ * ctx.fillStyle = `rgb(${n * 255}, ${n * 255}, 255)`;
114
+ * ctx.fillRect(x, y, 1, 1);
115
+ * }
116
+ *
117
+ * @param {number} x
118
+ * @param {number} y
119
+ * @param {Object} [opts]
120
+ * @param {number} [opts.octaves=5] - Layers of detail
121
+ * @param {number} [opts.gain=0.5] - Strength of each layer compared to the one before
122
+ * @param {number} [opts.seed=0] - Whole number; another seed gives other noise
123
+ * @param {number} [opts.period=0] - Repeat every this many units (a whole number; 0: never)
124
+ * @returns {number}
125
+ */
126
+ export function fbm2(x, y, { octaves = 5, gain = 0.5, seed = 0, period = 0 } = {}) {
127
+ const p = periodOf(period);
128
+ let sum = 0, amp = 1, norm = 0, freq = 1;
129
+ for (let i = 0; i < octaves; i++) {
130
+ sum += amp * value2(x * freq, y * freq, seed + i * 31, p * freq);
131
+ norm += amp;
132
+ amp *= gain;
133
+ freq *= 2;
134
+ }
135
+ return norm > 0 ? sum / norm : 0;
136
+ }
137
+
138
+ /**
139
+ * Fractal 3D noise (fBm), like `fbm2` in three dimensions. Result in about
140
+ * [0, 1), averaging 0.5.
141
+ *
142
+ * @param {number} x
143
+ * @param {number} y
144
+ * @param {number} z
145
+ * @param {Object} [opts]
146
+ * @param {number} [opts.octaves=5] - Layers of detail
147
+ * @param {number} [opts.gain=0.5] - Strength of each layer compared to the one before
148
+ * @param {number} [opts.seed=0] - Whole number; another seed gives other noise
149
+ * @param {number} [opts.period=0] - Repeat every this many units (a whole number; 0: never)
150
+ * @returns {number}
151
+ */
152
+ export function fbm3(x, y, z, { octaves = 5, gain = 0.5, seed = 0, period = 0 } = {}) {
153
+ const p = periodOf(period);
154
+ let sum = 0, amp = 1, norm = 0, freq = 1;
155
+ for (let i = 0; i < octaves; i++) {
156
+ sum += amp * value3(x * freq, y * freq, z * freq, seed + i * 31, p * freq);
157
+ norm += amp;
158
+ amp *= gain;
159
+ freq *= 2;
160
+ }
161
+ return norm > 0 ? sum / norm : 0;
162
+ }