trid3d 0.2.0 → 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 +8 -7
- package/package.json +1 -1
- package/trid/export/mtl.js +119 -0
- package/trid/export/obj.js +107 -21
- package/trid/index.d.ts +417 -8
- package/trid/index.js +19 -3
- package/trid/loaders/mtl.js +63 -0
- package/trid/loaders/obj.js +58 -6
- 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 +40 -23
- package/trid/render/draw.js +6 -2
- package/trid/render/meshUtils.js +257 -0
- package/trid/render/proceduralTextures.js +211 -0
- package/trid/render/shapes.js +95 -27
- package/trid/render/webglRenderer.js +98 -27
- package/trid/scene.js +20 -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 +4 -2
- package/trid/tools/index.js +3 -1
- package/trid/tools/orbitCamera.js +9 -0
- package/trid/tools/transformGizmo.js +316 -0
- package/trid/tools/viewGizmo.js +178 -0
package/trid/loaders/obj.js
CHANGED
|
@@ -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`), polylines (`l`)
|
|
6
|
-
*
|
|
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
|
|
@@ -24,10 +28,15 @@
|
|
|
24
28
|
* @property {string[]} [materialNames] - Material names from `usemtl` lines, in the order they first appear
|
|
25
29
|
* @property {number[]} [faceMaterials] - For each face, its index in `materialNames`. Give the
|
|
26
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`
|
|
27
34
|
*
|
|
28
35
|
* @typedef {Object} ObjOptions
|
|
29
36
|
* @property {number} [fit] - If set, center the model on the origin and scale it
|
|
30
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`
|
|
31
40
|
*/
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -49,6 +58,7 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
49
58
|
const materialNames = [];
|
|
50
59
|
const faceMaterials = []; // per face: index in materialNames
|
|
51
60
|
let material = 0;
|
|
61
|
+
let mtllib;
|
|
52
62
|
|
|
53
63
|
const src = text.split(/\r?\n/);
|
|
54
64
|
|
|
@@ -66,6 +76,8 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
66
76
|
uvs.push({ u: +(parts[1] ?? 0), v: +(parts[2] ?? 0) });
|
|
67
77
|
} else if (kind === "vn") {
|
|
68
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(" ");
|
|
69
81
|
} else if (kind === "usemtl") {
|
|
70
82
|
const name = parts.slice(1).join(" ");
|
|
71
83
|
material = materialNames.indexOf(name);
|
|
@@ -138,11 +150,12 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
138
150
|
mesh.uvs = uvs;
|
|
139
151
|
mesh.faceUVs = faceUVs;
|
|
140
152
|
}
|
|
141
|
-
//
|
|
142
|
-
if (materialNames.length >
|
|
153
|
+
// Material names (a list of materials on the object then colors each part)
|
|
154
|
+
if (materialNames.length > 0) {
|
|
143
155
|
mesh.materialNames = materialNames;
|
|
144
156
|
mesh.faceMaterials = faceMaterials;
|
|
145
157
|
}
|
|
158
|
+
if (mtllib) mesh.mtllib = mtllib;
|
|
146
159
|
if (fit != null) fitMesh(mesh, fit);
|
|
147
160
|
return mesh;
|
|
148
161
|
}
|
|
@@ -187,10 +200,49 @@ export function fitMesh(mesh, size = 1) {
|
|
|
187
200
|
* @param {ObjOptions} [opts]
|
|
188
201
|
* @returns {Promise<ObjMesh>}
|
|
189
202
|
*/
|
|
190
|
-
export async function loadOBJ(url, opts) {
|
|
203
|
+
export async function loadOBJ(url, opts = {}) {
|
|
191
204
|
const res = await fetch(url);
|
|
192
205
|
if (!res.ok) throw new Error(`Could not load ${url} (${res.status})`);
|
|
193
|
-
|
|
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
|
+
}));
|
|
194
246
|
}
|
|
195
247
|
|
|
196
248
|
/**
|
package/trid/loaders/texture.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { rotateXYZ } from "./transform.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rotation matrices and move / rotate / scale operations on an object's
|
|
5
|
+
* transform ({ pos, rot, scale }, applied scale → rotate X, Y, Z → move).
|
|
6
|
+
*
|
|
7
|
+
* Matrices are 3×3, row-major (`m[row][col]`), applied as M · v. They're
|
|
8
|
+
* built from `rotateXYZ` itself, so they always match what the renderers draw.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {[[number,number,number],[number,number,number],[number,number,number]]} Mat3
|
|
11
|
+
* @typedef {{x:number, y:number, z:number}} Vec3
|
|
12
|
+
* @typedef {{ pos:Vec3, rot:Vec3, scale:Vec3 }} Transform
|
|
13
|
+
*
|
|
14
|
+
* @typedef {{ mode:"move", delta:Vec3 }
|
|
15
|
+
* | { mode:"rotate", axis:Vec3, angle:number, center:Vec3 }
|
|
16
|
+
* | { mode:"scale", axis:Vec3|null, factor:number, center:Vec3 }} TransformOp
|
|
17
|
+
* An operation in world space. Rotate: a right-handed turn of `angle`
|
|
18
|
+
* radians around the unit `axis`, about `center`. Scale: `factor` about
|
|
19
|
+
* `center`, uniform when `axis` is null, otherwise along that unit axis.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
|
|
23
|
+
const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z });
|
|
24
|
+
const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y, z: a.z - b.z });
|
|
25
|
+
const mul = (a, k) => ({ x: a.x * k, y: a.y * k, z: a.z * k });
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The matrix `rotateXYZ(p, rot)` applies.
|
|
29
|
+
*
|
|
30
|
+
* @param {Vec3} rot - Angles in radians (X, then Y, then Z)
|
|
31
|
+
* @returns {Mat3}
|
|
32
|
+
*/
|
|
33
|
+
export function matFromEuler(rot) {
|
|
34
|
+
const cx = rotateXYZ({ x: 1, y: 0, z: 0 }, rot);
|
|
35
|
+
const cy = rotateXYZ({ x: 0, y: 1, z: 0 }, rot);
|
|
36
|
+
const cz = rotateXYZ({ x: 0, y: 0, z: 1 }, rot);
|
|
37
|
+
return [
|
|
38
|
+
[cx.x, cy.x, cz.x],
|
|
39
|
+
[cx.y, cy.y, cz.y],
|
|
40
|
+
[cx.z, cy.z, cz.z],
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Angles for `rotateXYZ` (an object's `rot`) from a rotation matrix: the
|
|
46
|
+
* inverse of `matFromEuler`. Looking straight along Y (gimbal lock), X and
|
|
47
|
+
* Z turn the same way, so the whole turn goes into Z.
|
|
48
|
+
*
|
|
49
|
+
* @param {Mat3} m - A rotation matrix
|
|
50
|
+
* @returns {Vec3} Angles in radians
|
|
51
|
+
*/
|
|
52
|
+
export function eulerFromMat(m) {
|
|
53
|
+
// rotateXYZ's matrix is Rz·Ry·Rx: m[2][0] = sin(y), m[2][1] = cos(y)·sin(x), m[1][0] = sin(z)·cos(y)
|
|
54
|
+
const sy = Math.max(-1, Math.min(1, m[2][0]));
|
|
55
|
+
const y = Math.asin(sy);
|
|
56
|
+
if (Math.abs(sy) < 0.99999) {
|
|
57
|
+
return { x: Math.atan2(m[2][1], m[2][2]), y, z: Math.atan2(m[1][0], m[0][0]) };
|
|
58
|
+
}
|
|
59
|
+
return { x: 0, y, z: Math.atan2(-m[0][1], m[1][1]) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A right-handed turn of `angle` radians around a unit `axis` (Rodrigues).
|
|
64
|
+
*
|
|
65
|
+
* @param {Vec3} axis - Unit vector
|
|
66
|
+
* @param {number} angle - Radians
|
|
67
|
+
* @returns {Mat3}
|
|
68
|
+
*/
|
|
69
|
+
export function matFromAxisAngle(axis, angle) {
|
|
70
|
+
const { x, y, z } = axis, c = Math.cos(angle), s = Math.sin(angle), t = 1 - c;
|
|
71
|
+
return [
|
|
72
|
+
[t * x * x + c, t * x * y - s * z, t * x * z + s * y],
|
|
73
|
+
[t * x * y + s * z, t * y * y + c, t * y * z - s * x],
|
|
74
|
+
[t * x * z - s * y, t * y * z + s * x, t * z * z + c],
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Matrix product a · b (b is applied first).
|
|
80
|
+
*
|
|
81
|
+
* @param {Mat3} a
|
|
82
|
+
* @param {Mat3} b
|
|
83
|
+
* @returns {Mat3}
|
|
84
|
+
*/
|
|
85
|
+
export function mulMat(a, b) {
|
|
86
|
+
return [0, 1, 2].map((i) => [0, 1, 2].map((j) => a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Matrix times vector, m · v.
|
|
91
|
+
*
|
|
92
|
+
* @param {Mat3} m
|
|
93
|
+
* @param {Vec3} v
|
|
94
|
+
* @returns {Vec3}
|
|
95
|
+
*/
|
|
96
|
+
export function mulVec(m, v) {
|
|
97
|
+
return {
|
|
98
|
+
x: m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z,
|
|
99
|
+
y: m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z,
|
|
100
|
+
z: m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Applies a world-space operation to a transform and returns the new one
|
|
106
|
+
* (the input isn't changed). `TransformGizmo` reports these.
|
|
107
|
+
*
|
|
108
|
+
* - move: adds `delta` to `pos`
|
|
109
|
+
* - rotate: turns the object around `center`: its position orbits and its
|
|
110
|
+
* orientation turns, both by the same rotation
|
|
111
|
+
* - scale: scales about `center`. Stretching a rotated object along a world
|
|
112
|
+
* axis can't be stored as `scale` (it would shear), so it stretches the
|
|
113
|
+
* object's own axis that points most along it
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* Object.assign(obj, applyTransformOp(obj, { mode: "rotate", axis: { x: 0, y: 1, z: 0 }, angle: Math.PI / 2, center: { x: 0, y: 0, z: 0 } }));
|
|
117
|
+
*
|
|
118
|
+
* @param {Transform} t - The transform before the operation
|
|
119
|
+
* @param {TransformOp} op
|
|
120
|
+
* @returns {Transform}
|
|
121
|
+
*/
|
|
122
|
+
export function applyTransformOp(t, op) {
|
|
123
|
+
switch (op.mode) {
|
|
124
|
+
case "move":
|
|
125
|
+
return { pos: add(t.pos, op.delta), rot: { ...t.rot }, scale: { ...t.scale } };
|
|
126
|
+
case "rotate": {
|
|
127
|
+
const r = matFromAxisAngle(op.axis, op.angle);
|
|
128
|
+
return {
|
|
129
|
+
pos: add(op.center, mulVec(r, sub(t.pos, op.center))),
|
|
130
|
+
rot: eulerFromMat(mulMat(r, matFromEuler(t.rot))),
|
|
131
|
+
scale: { ...t.scale },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "scale": {
|
|
135
|
+
const f = op.factor;
|
|
136
|
+
if (!op.axis) {
|
|
137
|
+
return { pos: add(op.center, mul(sub(t.pos, op.center), f)), rot: { ...t.rot }, scale: mul(t.scale, f) };
|
|
138
|
+
}
|
|
139
|
+
const a = op.axis;
|
|
140
|
+
const pos = add(t.pos, mul(a, (f - 1) * dot(sub(t.pos, op.center), a)));
|
|
141
|
+
// The object's own axis most aligned with the stretch (columns of its rotation)
|
|
142
|
+
const m = matFromEuler(t.rot);
|
|
143
|
+
const along = [0, 1, 2].map((j) => Math.abs(m[0][j] * a.x + m[1][j] * a.y + m[2][j] * a.z));
|
|
144
|
+
const j = along.indexOf(Math.max(...along));
|
|
145
|
+
const scale = { ...t.scale };
|
|
146
|
+
scale["xyz"[j]] *= f;
|
|
147
|
+
return { pos, rot: { ...t.rot }, scale };
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
throw new Error("applyTransformOp: unknown mode " + op?.mode);
|
|
151
|
+
}
|
|
152
|
+
}
|