trid3d 0.4.0 → 0.6.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 +29 -12
- package/package.json +1 -1
- package/trid/index.d.ts +423 -8
- package/trid/index.js +13 -0
- package/trid/math/projection.js +8 -2
- package/trid/math/transform.js +4 -5
- package/trid/modeling/bevel.js +596 -0
- package/trid/modeling/blobs.js +465 -0
- package/trid/modeling/cleanup.js +471 -0
- package/trid/modeling/curves.js +156 -0
- package/trid/modeling/deform.js +191 -0
- package/trid/modeling/frames.js +97 -0
- package/trid/modeling/loft.js +363 -0
- package/trid/modeling/query.js +706 -0
- package/trid/modeling/subdivide.js +215 -0
- package/trid/modeling/sweep.js +169 -0
- package/trid/modeling/vec.js +35 -0
- package/trid/render/canvasRenderer.js +1 -1
- package/trid/render/draw.js +12 -2
- package/trid/render/post.js +1 -1
- package/trid/render/shapes.js +133 -26
- package/trid/render/webglRenderer.js +1 -1
- package/trid/scene.js +1 -1
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes that melt together: signed distance fields (negative inside),
|
|
3
|
+
* joined with smooth unions, subtractions and intersections, then turned
|
|
4
|
+
* into a mesh. Organic creatures, soft bodies, clay-like forms.
|
|
5
|
+
*/
|
|
6
|
+
import { rotateX, rotateY, rotateZ } from "../math/transform.js";
|
|
7
|
+
import { computeUVs } from "../render/uvMapping.js";
|
|
8
|
+
import { newell } from "../render/normals.js";
|
|
9
|
+
import { cleanMesh } from "./cleanup.js";
|
|
10
|
+
|
|
11
|
+
const len3 = (x, y, z) => Math.sqrt(x * x + y * y + z * z);
|
|
12
|
+
const num = (x, fallback) => (Number.isFinite(x) ? x : fallback);
|
|
13
|
+
const pt = (p, fallback = { x: 0, y: 0, z: 0 }) => ({ x: num(p?.x, fallback.x), y: num(p?.y, fallback.y), z: num(p?.z, fallback.z) });
|
|
14
|
+
|
|
15
|
+
/** The point in a shape's own frame: moved to its center, then turned back by its `rot` (radians, X then Y then Z). */
|
|
16
|
+
function local(p, center, rot) {
|
|
17
|
+
let q = { x: p.x - center.x, y: p.y - center.y, z: p.z - center.z };
|
|
18
|
+
if (rot && (rot.x || rot.y || rot.z)) q = rotateX(rotateY(rotateZ(q, -(rot.z || 0)), -(rot.y || 0)), -(rot.x || 0));
|
|
19
|
+
return q;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A shape, checked and with defaults: its distance function and the box
|
|
24
|
+
* around it. Unknown kinds throw.
|
|
25
|
+
*/
|
|
26
|
+
function prepare(s) {
|
|
27
|
+
const kind = s?.kind;
|
|
28
|
+
const op = s.op ?? (s.subtract ? "subtract" : "add");
|
|
29
|
+
if (!["add", "subtract", "intersect"].includes(op)) throw new Error(`Unknown blob op "${op}": use "add", "subtract" or "intersect"`);
|
|
30
|
+
const rot = s.rot;
|
|
31
|
+
const around = (c, r) => ({ min: { x: c.x - r, y: c.y - r, z: c.z - r }, max: { x: c.x + r, y: c.y + r, z: c.z + r } });
|
|
32
|
+
const between = (a, b, r) => ({
|
|
33
|
+
min: { x: Math.min(a.x, b.x) - r, y: Math.min(a.y, b.y) - r, z: Math.min(a.z, b.z) - r },
|
|
34
|
+
max: { x: Math.max(a.x, b.x) + r, y: Math.max(a.y, b.y) + r, z: Math.max(a.z, b.z) + r },
|
|
35
|
+
});
|
|
36
|
+
let dist, box;
|
|
37
|
+
switch (kind) {
|
|
38
|
+
case "sphere": {
|
|
39
|
+
const c = pt(s.center), r = Math.max(0, num(s.radius, 0.1));
|
|
40
|
+
dist = (p) => len3(p.x - c.x, p.y - c.y, p.z - c.z) - r;
|
|
41
|
+
box = around(c, r);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "ellipsoid": {
|
|
45
|
+
const c = pt(s.center), rr = pt(s.radii, { x: 0.1, y: 0.1, z: 0.1 });
|
|
46
|
+
const r = { x: Math.max(1e-9, rr.x), y: Math.max(1e-9, rr.y), z: Math.max(1e-9, rr.z) };
|
|
47
|
+
// Close to the true distance near the surface (Quilez), which is what meshing needs
|
|
48
|
+
dist = (p) => {
|
|
49
|
+
const q = local(p, c, rot);
|
|
50
|
+
const k0 = len3(q.x / r.x, q.y / r.y, q.z / r.z);
|
|
51
|
+
const k1 = len3(q.x / (r.x * r.x), q.y / (r.y * r.y), q.z / (r.z * r.z));
|
|
52
|
+
return k1 > 1e-12 ? (k0 * (k0 - 1)) / k1 : -Math.min(r.x, r.y, r.z);
|
|
53
|
+
};
|
|
54
|
+
box = rot ? around(c, Math.max(r.x, r.y, r.z)) : { min: { x: c.x - r.x, y: c.y - r.y, z: c.z - r.z }, max: { x: c.x + r.x, y: c.y + r.y, z: c.z + r.z } };
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "capsule": {
|
|
58
|
+
// A rounded cone between two points (a capsule when both radii match)
|
|
59
|
+
const a = pt(s.a), b = pt(s.b, { x: 0, y: 0.2, z: 0 });
|
|
60
|
+
const ra = Math.max(0, num(s.ra ?? s.radius, 0.05)), rb = Math.max(0, num(s.rb ?? s.radius, 0.05));
|
|
61
|
+
const bx = b.x - a.x, by = b.y - a.y, bz = b.z - a.z;
|
|
62
|
+
const l2 = bx * bx + by * by + bz * bz || 1e-12;
|
|
63
|
+
dist = (p) => {
|
|
64
|
+
const px = p.x - a.x, py = p.y - a.y, pz = p.z - a.z;
|
|
65
|
+
const t = Math.min(1, Math.max(0, (px * bx + py * by + pz * bz) / l2));
|
|
66
|
+
return len3(px - bx * t, py - by * t, pz - bz * t) - (ra + (rb - ra) * t);
|
|
67
|
+
};
|
|
68
|
+
box = between(a, b, Math.max(ra, rb));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "cylinder": {
|
|
72
|
+
// A capped cylinder between two points, with edges rounded by `round`
|
|
73
|
+
const a = pt(s.a), b = pt(s.b, { x: 0, y: 0.2, z: 0 }), r = Math.max(0, num(s.radius, 0.05));
|
|
74
|
+
const h = len3(b.x - a.x, b.y - a.y, b.z - a.z) / 2;
|
|
75
|
+
const rd = Math.max(0, Math.min(num(s.round, 0), r, h));
|
|
76
|
+
const c = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
|
|
77
|
+
const ax = h > 0 ? { x: (b.x - a.x) / (2 * h), y: (b.y - a.y) / (2 * h), z: (b.z - a.z) / (2 * h) } : { x: 0, y: 1, z: 0 };
|
|
78
|
+
dist = (p) => {
|
|
79
|
+
const qx = p.x - c.x, qy = p.y - c.y, qz = p.z - c.z;
|
|
80
|
+
const along = qx * ax.x + qy * ax.y + qz * ax.z;
|
|
81
|
+
const across = len3(qx - ax.x * along, qy - ax.y * along, qz - ax.z * along);
|
|
82
|
+
const dx = across - (r - rd), dy = Math.abs(along) - (h - rd);
|
|
83
|
+
return Math.min(Math.max(dx, dy), 0) + Math.hypot(Math.max(dx, 0), Math.max(dy, 0)) - rd;
|
|
84
|
+
};
|
|
85
|
+
box = between(a, b, r);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "box": {
|
|
89
|
+
const c = pt(s.center), size = pt(s.size, { x: 0.2, y: 0.2, z: 0.2 });
|
|
90
|
+
const hx = Math.abs(size.x) / 2, hy = Math.abs(size.y) / 2, hz = Math.abs(size.z) / 2;
|
|
91
|
+
const rd = Math.max(0, Math.min(num(s.round, 0), hx, hy, hz));
|
|
92
|
+
dist = (p) => {
|
|
93
|
+
const q = local(p, c, rot);
|
|
94
|
+
const dx = Math.abs(q.x) - hx + rd, dy = Math.abs(q.y) - hy + rd, dz = Math.abs(q.z) - hz + rd;
|
|
95
|
+
return len3(Math.max(dx, 0), Math.max(dy, 0), Math.max(dz, 0)) + Math.min(Math.max(dx, dy, dz), 0) - rd;
|
|
96
|
+
};
|
|
97
|
+
box = rot ? around(c, len3(hx, hy, hz)) : { min: { x: c.x - hx, y: c.y - hy, z: c.z - hz }, max: { x: c.x + hx, y: c.y + hy, z: c.z + hz } };
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "torus": {
|
|
101
|
+
// A ring lying flat in XZ (like makeShape("torus")), turned by `rot`
|
|
102
|
+
const c = pt(s.center), R = Math.max(0, num(s.R, 0.2)), t = Math.max(0, num(s.tube, 0.05));
|
|
103
|
+
dist = (p) => {
|
|
104
|
+
const q = local(p, c, rot);
|
|
105
|
+
return Math.hypot(Math.hypot(q.x, q.z) - R, q.y) - t;
|
|
106
|
+
};
|
|
107
|
+
box = around(c, R + t);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
throw new Error(`Unknown blob kind "${kind}": use "sphere", "ellipsoid", "capsule", "cylinder", "box" or "torus"`);
|
|
112
|
+
}
|
|
113
|
+
return { dist, box, op, blend: Number.isFinite(s.blend) ? Math.max(0, s.blend) : null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Smooth minimum (polynomial): like min, but rounded over a distance k. */
|
|
117
|
+
function smin(a, b, k) {
|
|
118
|
+
if (!(k > 0)) return Math.min(a, b);
|
|
119
|
+
const h = Math.max(k - Math.abs(a - b), 0) / k;
|
|
120
|
+
return Math.min(a, b) - (h * h * k) / 4;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The size of the box around the shapes that add matter, and that box. */
|
|
124
|
+
function extentOf(prepared) {
|
|
125
|
+
const min = { x: Infinity, y: Infinity, z: Infinity }, max = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
126
|
+
for (const s of prepared) {
|
|
127
|
+
if (s.op !== "add") continue;
|
|
128
|
+
for (const a of ["x", "y", "z"]) {
|
|
129
|
+
min[a] = Math.min(min[a], s.box.min[a]);
|
|
130
|
+
max[a] = Math.max(max[a], s.box.max[a]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return min.x <= max.x ? { min, max } : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The distance field of a list of shapes: negative inside, positive
|
|
138
|
+
* outside, about the distance to the surface. Shapes are applied in order:
|
|
139
|
+
* "add" melts into what's before it, "subtract" carves it, "intersect"
|
|
140
|
+
* keeps only the overlap. Each joins over its `blend` distance (default
|
|
141
|
+
* `opts.blend`, or a tenth of the model's size).
|
|
142
|
+
*
|
|
143
|
+
* Use it to mix shapes with your own math, then mesh it with `sdfMesh`.
|
|
144
|
+
*
|
|
145
|
+
* @param {object[]} shapes
|
|
146
|
+
* @param {{ blend?: number }} [opts]
|
|
147
|
+
* @returns {(p: {x:number,y:number,z:number}) => number}
|
|
148
|
+
*/
|
|
149
|
+
export function blobField(shapes, opts = {}) {
|
|
150
|
+
const list = (Array.isArray(shapes) ? shapes : []).filter(Boolean).map(prepare);
|
|
151
|
+
const ext = extentOf(list);
|
|
152
|
+
const size = ext ? Math.max(ext.max.x - ext.min.x, ext.max.y - ext.min.y, ext.max.z - ext.min.z) : 1;
|
|
153
|
+
const blend = Number.isFinite(opts.blend) ? Math.max(0, opts.blend) : size / 10;
|
|
154
|
+
return (p) => {
|
|
155
|
+
let d = Infinity;
|
|
156
|
+
for (const s of list) {
|
|
157
|
+
const k = s.blend ?? blend;
|
|
158
|
+
const ds = s.dist(p);
|
|
159
|
+
if (s.op === "add") d = d === Infinity ? ds : smin(d, ds, k);
|
|
160
|
+
else if (d === Infinity) continue;
|
|
161
|
+
else if (s.op === "subtract") d = -smin(-d, ds, k);
|
|
162
|
+
else d = -smin(-d, -ds, k);
|
|
163
|
+
}
|
|
164
|
+
return d;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Corners of a grid cell (bit 1 = +x, 2 = +y, 4 = +z), its 12 edges, and its 6 sides (4 corners in order round, with their edges)
|
|
169
|
+
const CORNERS = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]];
|
|
170
|
+
const EDGES = [[0, 1], [2, 3], [4, 5], [6, 7], [0, 2], [1, 3], [4, 6], [5, 7], [0, 4], [1, 5], [2, 6], [3, 7]];
|
|
171
|
+
const edgeOf = (a, b) => EDGES.findIndex(([p, q]) => (p === a && q === b) || (p === b && q === a));
|
|
172
|
+
const SIDES = [[0, 1, 3, 2], [4, 5, 7, 6], [0, 1, 5, 4], [2, 3, 7, 6], [0, 2, 6, 4], [1, 3, 7, 5]].map((c) => ({
|
|
173
|
+
corners: c,
|
|
174
|
+
edges: c.map((a, i) => edgeOf(a, c[(i + 1) % 4])),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Samples the field on a grid. Far from the surface (more than the block's
|
|
179
|
+
* size away), a block of 4×4×4 points takes the value at its middle: same
|
|
180
|
+
* sign, much less work.
|
|
181
|
+
*/
|
|
182
|
+
function sample(field, o, cell, nx, ny, nz, sparse) {
|
|
183
|
+
const values = new Float64Array(nx * ny * nz);
|
|
184
|
+
const at = (i, j, k) => i + nx * (j + ny * k);
|
|
185
|
+
const eval_ = (i, j, k) => {
|
|
186
|
+
const v = field({ x: o.x + i * cell, y: o.y + j * cell, z: o.z + k * cell });
|
|
187
|
+
// Exactly 0 counts as outside, a hair away, so every crossing is between two different values
|
|
188
|
+
return Number.isFinite(v) ? (v === 0 ? cell * 1e-9 : v) : 1e9;
|
|
189
|
+
};
|
|
190
|
+
const B = sparse ? 4 : 1;
|
|
191
|
+
const reach = ((Math.sqrt(3) * (B - 1)) / 2 + 1) * cell * 1.5;
|
|
192
|
+
for (let bk = 0; bk < nz; bk += B) for (let bj = 0; bj < ny; bj += B) for (let bi = 0; bi < nx; bi += B) {
|
|
193
|
+
const ei = Math.min(nx, bi + B), ej = Math.min(ny, bj + B), ek = Math.min(nz, bk + B);
|
|
194
|
+
if (B > 1) {
|
|
195
|
+
const mid = eval_((bi + ei - 1) / 2, (bj + ej - 1) / 2, (bk + ek - 1) / 2);
|
|
196
|
+
if (Math.abs(mid) > reach) {
|
|
197
|
+
for (let k = bk; k < ek; k++) for (let j = bj; j < ej; j++) for (let i = bi; i < ei; i++) values[at(i, j, k)] = mid;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (let k = bk; k < ek; k++) for (let j = bj; j < ej; j++) for (let i = bi; i < ei; i++) values[at(i, j, k)] = eval_(i, j, k);
|
|
202
|
+
}
|
|
203
|
+
return values;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Field gradient by central differences (points outward). */
|
|
207
|
+
function gradient(field, p, h) {
|
|
208
|
+
const g = {
|
|
209
|
+
x: field({ x: p.x + h, y: p.y, z: p.z }) - field({ x: p.x - h, y: p.y, z: p.z }),
|
|
210
|
+
y: field({ x: p.x, y: p.y + h, z: p.z }) - field({ x: p.x, y: p.y - h, z: p.z }),
|
|
211
|
+
z: field({ x: p.x, y: p.y, z: p.z + h }) - field({ x: p.x, y: p.y, z: p.z - h }),
|
|
212
|
+
};
|
|
213
|
+
return { x: g.x / (2 * h), y: g.y / (2 * h), z: g.z / (2 * h) };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The surface's pieces inside one grid cell, as loops of the cell's edges it
|
|
218
|
+
* crosses. On each side of the cell the crossings are joined in pairs; a
|
|
219
|
+
* side with 4 crossings is settled by the value at its middle (asymptotic
|
|
220
|
+
* decider), which both cells sharing the side agree on, so neighbors fit.
|
|
221
|
+
*/
|
|
222
|
+
function cellLoops(v, corner) {
|
|
223
|
+
const link = new Array(12).fill(null);
|
|
224
|
+
const join = (e1, e2) => {
|
|
225
|
+
(link[e1] ??= []).push(e2);
|
|
226
|
+
(link[e2] ??= []).push(e1);
|
|
227
|
+
};
|
|
228
|
+
for (const side of SIDES) {
|
|
229
|
+
const crossed = side.edges.filter((e) => corner[EDGES[e][0]] !== corner[EDGES[e][1]]);
|
|
230
|
+
if (crossed.length === 2) join(crossed[0], crossed[1]);
|
|
231
|
+
else if (crossed.length === 4) {
|
|
232
|
+
// Cut off the corners whose sign differs from the side's middle
|
|
233
|
+
const [c0, c1, c2, c3] = side.corners.map((c) => v[c]);
|
|
234
|
+
const den = c0 + c2 - c1 - c3;
|
|
235
|
+
const midInside = (den !== 0 ? (c0 * c2 - c1 * c3) / den : (c0 + c1 + c2 + c3) / 4) < 0;
|
|
236
|
+
for (let q = 0; q < 4; q++) if (corner[side.corners[q]] !== midInside) join(side.edges[(q + 3) % 4], side.edges[q]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const loops = [];
|
|
240
|
+
const used = new Array(12).fill(false);
|
|
241
|
+
for (let s = 0; s < 12; s++) {
|
|
242
|
+
if (!link[s] || used[s]) continue;
|
|
243
|
+
const loop = [];
|
|
244
|
+
let prev = -1, cur = s;
|
|
245
|
+
while (cur !== undefined && !used[cur]) {
|
|
246
|
+
used[cur] = true;
|
|
247
|
+
loop.push(cur);
|
|
248
|
+
const next = link[cur].find((e) => e !== prev && !used[e]) ?? link[cur].find((e) => e !== prev);
|
|
249
|
+
prev = cur;
|
|
250
|
+
cur = next;
|
|
251
|
+
}
|
|
252
|
+
if (loop.length >= 3) loops.push(loop);
|
|
253
|
+
}
|
|
254
|
+
return loops;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Reads a cell's 8 corner values and signs; returns how many are inside. */
|
|
258
|
+
function readCell(values, at, i, j, k, v, corner) {
|
|
259
|
+
let inside = 0;
|
|
260
|
+
for (let c = 0; c < 8; c++) {
|
|
261
|
+
v[c] = values[at(i + CORNERS[c][0], j + CORNERS[c][1], k + CORNERS[c][2])];
|
|
262
|
+
corner[c] = v[c] < 0;
|
|
263
|
+
if (corner[c]) inside++;
|
|
264
|
+
}
|
|
265
|
+
return inside;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Surface nets: one vertex per piece of surface in each cell (at the middle
|
|
270
|
+
* of its crossings), one quad per grid edge the surface crosses. With one
|
|
271
|
+
* vertex per piece rather than per cell, two thin layers passing through
|
|
272
|
+
* the same cell aren't stitched together, so the mesh stays watertight.
|
|
273
|
+
*/
|
|
274
|
+
function surfaceNets(values, o, cell, nx, ny, nz) {
|
|
275
|
+
const at = (i, j, k) => i + nx * (j + ny * k);
|
|
276
|
+
const cx = nx - 1, cy = ny - 1;
|
|
277
|
+
const cellAt = (i, j, k) => i + cx * (j + cy * k);
|
|
278
|
+
const edgeVertex = new Map(); // cell → for each of its 12 edges, the vertex of the piece crossing it
|
|
279
|
+
const vertices = [];
|
|
280
|
+
const v = new Float64Array(8), corner = new Array(8);
|
|
281
|
+
for (let k = 0; k < nz - 1; k++) for (let j = 0; j < ny - 1; j++) for (let i = 0; i < nx - 1; i++) {
|
|
282
|
+
const inside = readCell(values, at, i, j, k, v, corner);
|
|
283
|
+
if (inside === 0 || inside === 8) continue;
|
|
284
|
+
const ids = new Int32Array(12).fill(-1);
|
|
285
|
+
for (const loop of cellLoops(v, corner)) {
|
|
286
|
+
let sx = 0, sy = 0, sz = 0;
|
|
287
|
+
for (const e of loop) {
|
|
288
|
+
const [a, b] = EDGES[e];
|
|
289
|
+
const t = v[a] / (v[a] - v[b]);
|
|
290
|
+
sx += CORNERS[a][0] + (CORNERS[b][0] - CORNERS[a][0]) * t;
|
|
291
|
+
sy += CORNERS[a][1] + (CORNERS[b][1] - CORNERS[a][1]) * t;
|
|
292
|
+
sz += CORNERS[a][2] + (CORNERS[b][2] - CORNERS[a][2]) * t;
|
|
293
|
+
}
|
|
294
|
+
const n = loop.length;
|
|
295
|
+
const id = vertices.push({ x: o.x + (i + sx / n) * cell, y: o.y + (j + sy / n) * cell, z: o.z + (k + sz / n) * cell }) - 1;
|
|
296
|
+
for (const e of loop) ids[e] = id;
|
|
297
|
+
}
|
|
298
|
+
edgeVertex.set(cellAt(i, j, k), ids);
|
|
299
|
+
}
|
|
300
|
+
// The vertex in cell (ci, cj, ck) on its edge from corner a to corner b
|
|
301
|
+
const vertexAt = (ci, cj, ck, a, b) => edgeVertex.get(cellAt(ci, cj, ck))?.[edgeOf(a, b)] ?? -1;
|
|
302
|
+
const faces = [];
|
|
303
|
+
const quad = (a, b, c, d, inside) => {
|
|
304
|
+
if (a < 0 || b < 0 || c < 0 || d < 0) return;
|
|
305
|
+
faces.push(inside ? [a, b, c, d] : [d, c, b, a]);
|
|
306
|
+
};
|
|
307
|
+
for (let k = 1; k < nz - 1; k++) for (let j = 1; j < ny - 1; j++) for (let i = 1; i < nx - 1; i++) {
|
|
308
|
+
const here = values[at(i, j, k)] < 0;
|
|
309
|
+
// Grid edge along x from (i, j, k): in the 4 cells around it, it's the edge from corner (0, dy, dz) to (1, dy, dz)
|
|
310
|
+
if (here !== values[at(i + 1, j, k)] < 0) {
|
|
311
|
+
quad(vertexAt(i, j - 1, k - 1, 6, 7), vertexAt(i, j, k - 1, 4, 5), vertexAt(i, j, k, 0, 1), vertexAt(i, j - 1, k, 2, 3), here);
|
|
312
|
+
}
|
|
313
|
+
if (here !== values[at(i, j + 1, k)] < 0) {
|
|
314
|
+
quad(vertexAt(i - 1, j, k - 1, 5, 7), vertexAt(i - 1, j, k, 1, 3), vertexAt(i, j, k, 0, 2), vertexAt(i, j, k - 1, 4, 6), here);
|
|
315
|
+
}
|
|
316
|
+
if (here !== values[at(i, j, k + 1)] < 0) {
|
|
317
|
+
quad(vertexAt(i - 1, j - 1, k, 3, 7), vertexAt(i, j - 1, k, 2, 6), vertexAt(i, j, k, 0, 4), vertexAt(i - 1, j, k, 1, 5), here);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { vertices, faces };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Marching cubes without a table: each piece of surface in a cell (a loop
|
|
325
|
+
* of crossings, see cellLoops) is turned to face outward and cut into
|
|
326
|
+
* triangles. Vertices on grid edges are shared with the cells around.
|
|
327
|
+
*/
|
|
328
|
+
function marchingCubes(values, o, cell, nx, ny, nz) {
|
|
329
|
+
const at = (i, j, k) => i + nx * (j + ny * k);
|
|
330
|
+
const vertices = [];
|
|
331
|
+
const edgeVertex = new Map();
|
|
332
|
+
const v = new Float64Array(8), corner = new Array(8);
|
|
333
|
+
// Vertex on the grid edge from corner a to corner b of cell (i, j, k), shared with the cells around it
|
|
334
|
+
const vertexOn = (i, j, k, a, b) => {
|
|
335
|
+
const pa = CORNERS[a], pb = CORNERS[b];
|
|
336
|
+
const ia = at(i + pa[0], j + pa[1], k + pa[2]), ib = at(i + pb[0], j + pb[1], k + pb[2]);
|
|
337
|
+
const axis = pb[0] - pa[0] ? 0 : pb[1] - pa[1] ? 1 : 2;
|
|
338
|
+
const key = Math.min(ia, ib) * 3 + axis;
|
|
339
|
+
let id = edgeVertex.get(key);
|
|
340
|
+
if (id === undefined) {
|
|
341
|
+
const t = v[a] / (v[a] - v[b]);
|
|
342
|
+
id = vertices.push({
|
|
343
|
+
x: o.x + (i + pa[0] + (pb[0] - pa[0]) * t) * cell,
|
|
344
|
+
y: o.y + (j + pa[1] + (pb[1] - pa[1]) * t) * cell,
|
|
345
|
+
z: o.z + (k + pa[2] + (pb[2] - pa[2]) * t) * cell,
|
|
346
|
+
}) - 1;
|
|
347
|
+
edgeVertex.set(key, id);
|
|
348
|
+
}
|
|
349
|
+
return id;
|
|
350
|
+
};
|
|
351
|
+
const faces = [];
|
|
352
|
+
for (let k = 0; k < nz - 1; k++) for (let j = 0; j < ny - 1; j++) for (let i = 0; i < nx - 1; i++) {
|
|
353
|
+
const inside = readCell(values, at, i, j, k, v, corner);
|
|
354
|
+
if (inside === 0 || inside === 8) continue;
|
|
355
|
+
for (const loop of cellLoops(v, corner)) {
|
|
356
|
+
const ids = loop.map((e) => vertexOn(i, j, k, EDGES[e][0], EDGES[e][1]));
|
|
357
|
+
// Outward: from the inside corner to the outside one, summed over the loop
|
|
358
|
+
let ox = 0, oy = 0, oz = 0;
|
|
359
|
+
for (const e of loop) {
|
|
360
|
+
const [a, b] = EDGES[e];
|
|
361
|
+
const sg = corner[a] ? 1 : -1;
|
|
362
|
+
ox += sg * (CORNERS[b][0] - CORNERS[a][0]);
|
|
363
|
+
oy += sg * (CORNERS[b][1] - CORNERS[a][1]);
|
|
364
|
+
oz += sg * (CORNERS[b][2] - CORNERS[a][2]);
|
|
365
|
+
}
|
|
366
|
+
const nrm = newell(ids.map((id) => vertices[id]));
|
|
367
|
+
if (nrm.x * ox + nrm.y * oy + nrm.z * oz < 0) ids.reverse();
|
|
368
|
+
for (let t = 1; t + 1 < ids.length; t++) faces.push([ids[0], ids[t], ids[t + 1]]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { vertices, faces };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Turns a distance field into a mesh: the surface where `field(p)` is 0
|
|
376
|
+
* (negative inside), within `bounds`.
|
|
377
|
+
*
|
|
378
|
+
* The field should be about the distance to the surface (or less): far from
|
|
379
|
+
* it, blocks of the grid are skipped by trusting that. For a field that
|
|
380
|
+
* isn't (strong noise, a scaled distance), pass `sparse: false`.
|
|
381
|
+
*
|
|
382
|
+
* @param {(p: {x:number,y:number,z:number}) => number} field
|
|
383
|
+
* @param {{ min: {x:number,y:number,z:number}, max: {x:number,y:number,z:number} }} bounds
|
|
384
|
+
* @param {{ resolution?: number, method?: "nets"|"cubes", project?: boolean, normals?: boolean, uvs?: string|object|false, sparse?: boolean }} [opts]
|
|
385
|
+
* @returns {import("../index.d.ts").Mesh}
|
|
386
|
+
*/
|
|
387
|
+
export function sdfMesh(field, bounds, opts = {}) {
|
|
388
|
+
const { resolution = 64, method = "nets", project = true, normals = true, uvs = "box", sparse = true } = opts;
|
|
389
|
+
if (typeof field !== "function") throw new Error("sdfMesh: field must be a function of a point");
|
|
390
|
+
if (method !== "nets" && method !== "cubes") throw new Error(`Unknown method "${method}": use "nets" or "cubes"`);
|
|
391
|
+
const lo = pt(bounds?.min), hi = pt(bounds?.max);
|
|
392
|
+
const size = Math.max(hi.x - lo.x, hi.y - lo.y, hi.z - lo.z);
|
|
393
|
+
if (!(size > 0)) return { vertices: [], faces: [] };
|
|
394
|
+
const cell = size / Math.max(4, Math.min(256, Math.round(num(resolution, 64))));
|
|
395
|
+
// Two empty cells all around, so the surface closes
|
|
396
|
+
const o = { x: lo.x - 2 * cell, y: lo.y - 2 * cell, z: lo.z - 2 * cell };
|
|
397
|
+
const nx = Math.ceil((hi.x - lo.x) / cell) + 5, ny = Math.ceil((hi.y - lo.y) / cell) + 5, nz = Math.ceil((hi.z - lo.z) / cell) + 5;
|
|
398
|
+
|
|
399
|
+
const values = sample(field, o, cell, nx, ny, nz, sparse);
|
|
400
|
+
const mesh = (method === "cubes" ? marchingCubes : surfaceNets)(values, o, cell, nx, ny, nz);
|
|
401
|
+
|
|
402
|
+
// Pull each vertex onto the surface (Newton steps along the gradient, at most a cell)
|
|
403
|
+
const h = cell * 0.05;
|
|
404
|
+
if (project) {
|
|
405
|
+
for (const p of mesh.vertices) {
|
|
406
|
+
for (let it = 0; it < 2; it++) {
|
|
407
|
+
const d = field(p);
|
|
408
|
+
const g = gradient(field, p, h);
|
|
409
|
+
const g2 = g.x * g.x + g.y * g.y + g.z * g.z;
|
|
410
|
+
if (!(g2 > 1e-12) || !Number.isFinite(d)) break;
|
|
411
|
+
let k = d / g2;
|
|
412
|
+
const step = Math.abs(k) * Math.sqrt(g2);
|
|
413
|
+
if (step > cell) k *= cell / step;
|
|
414
|
+
p.x -= g.x * k; p.y -= g.y * k; p.z -= g.z * k;
|
|
415
|
+
if (Math.abs(d) < cell * 1e-4) break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
cleanMesh(mesh);
|
|
420
|
+
if (normals && mesh.faces.length) {
|
|
421
|
+
mesh.normals = mesh.vertices.map((p) => {
|
|
422
|
+
const g = gradient(field, p, h);
|
|
423
|
+
const l = len3(g.x, g.y, g.z);
|
|
424
|
+
return l > 1e-12 ? { x: g.x / l, y: g.y / l, z: g.z / l } : { x: 0, y: 1, z: 0 };
|
|
425
|
+
});
|
|
426
|
+
mesh.faceNormals = mesh.faces.map((f) => f.slice());
|
|
427
|
+
}
|
|
428
|
+
if (uvs && mesh.faces.length) Object.assign(mesh, computeUVs(mesh, uvs));
|
|
429
|
+
delete mesh.dirty;
|
|
430
|
+
return mesh;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* One smooth mesh of shapes that melt together: spheres, ellipsoids,
|
|
435
|
+
* capsules, cylinders, boxes and tori, each added, subtracted or intersected
|
|
436
|
+
* with a smooth join. Organic creatures, soft bodies, clay-like forms.
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* // A snowman: three balls melting into each other, a carrot nose
|
|
440
|
+
* const snowman = blobMesh([
|
|
441
|
+
* { kind: "sphere", center: { x: 0, y: -0.25, z: 0 }, radius: 0.3 },
|
|
442
|
+
* { kind: "sphere", center: { x: 0, y: 0.15, z: 0 }, radius: 0.22 },
|
|
443
|
+
* { kind: "sphere", center: { x: 0, y: 0.45, z: 0 }, radius: 0.15 },
|
|
444
|
+
* { kind: "capsule", a: { x: 0, y: 0.45, z: 0.12 }, b: { x: 0, y: 0.43, z: 0.3 }, ra: 0.035, rb: 0.005, blend: 0.01 },
|
|
445
|
+
* ], { blend: 0.08 });
|
|
446
|
+
*
|
|
447
|
+
* @param {object[]} shapes
|
|
448
|
+
* @param {{ blend?: number, resolution?: number, method?: "nets"|"cubes", project?: boolean, normals?: boolean, uvs?: string|object|false }} [opts]
|
|
449
|
+
* @returns {import("../index.d.ts").Mesh}
|
|
450
|
+
*/
|
|
451
|
+
export function blobMesh(shapes, opts = {}) {
|
|
452
|
+
const list = (Array.isArray(shapes) ? shapes : []).filter(Boolean);
|
|
453
|
+
const ext = extentOf(list.map(prepare));
|
|
454
|
+
if (!ext) return { vertices: [], faces: [] };
|
|
455
|
+
const size = Math.max(ext.max.x - ext.min.x, ext.max.y - ext.min.y, ext.max.z - ext.min.z) || 1;
|
|
456
|
+
const blend = Number.isFinite(opts.blend) ? Math.max(0, opts.blend) : size / 10;
|
|
457
|
+
// A smooth union swells by up to a quarter of its blend
|
|
458
|
+
const maxBlend = Math.max(blend, ...list.map((s) => (Number.isFinite(s.blend) ? s.blend : 0)));
|
|
459
|
+
const pad = maxBlend / 4;
|
|
460
|
+
const bounds = {
|
|
461
|
+
min: { x: ext.min.x - pad, y: ext.min.y - pad, z: ext.min.z - pad },
|
|
462
|
+
max: { x: ext.max.x + pad, y: ext.max.y + pad, z: ext.max.z + pad },
|
|
463
|
+
};
|
|
464
|
+
return sdfMesh(blobField(list, { blend }), bounds, { ...opts, sparse: true });
|
|
465
|
+
}
|