@yali-engine/physics 0.0.0-stage → 0.4.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.
@@ -0,0 +1,706 @@
1
+ // Cloth assets for Kanduka's GPU cloth (ADR-0101): a triangle mesh becomes the solver's
2
+ // topology (stretch constraints on its edges, isometric bending stencils on its interior edges,
3
+ // vertex → triangle adjacency) and its constraints are graph-coloured, so each colour can be
4
+ // solved in parallel with no two constraints sharing a vertex. Pure: tested in Node.
5
+ import { YaliError } from '@yali-engine/core';
6
+ /** Marks a constraint's unused vertex slots. */
7
+ export const NO_VERTEX = 0xffffffff;
8
+ /** Words per packed constraint (`solve` in shaders/kanduka/cloth.wesl). */
9
+ export const CONSTRAINT_WORDS = 12;
10
+ /** Most colours a cloth may need (128 × u32 bit masks per vertex while colouring). */
11
+ const MAX_COLOURS = 128;
12
+ /** gf·cm²/cm in N·m (a bending rigidity per width). */
13
+ const GF_CM = 9.80665e-5;
14
+ /** gf/cm in N/m. */
15
+ const GF_PER_CM = 0.980665;
16
+ /**
17
+ * The isometric stencils' energy over a flat grid rolled to curvature k is
18
+ * `Σ ½ |Σ w x|² / compliance = ½ (r / compliance) k² × area`, where r depends on the fold's angle to
19
+ * the grid: 2.92 along the warp or weft, 4.46 along a cell diagonal (the grid's own anisotropy),
20
+ * 3.69 averaged over every angle (at 5 mm–2 cm cells). A drape folds at every angle, so a plate's
21
+ * bending rigidity B (N·m) is the compliance STENCIL_BEND / B with the average (ADR-0120; a test
22
+ * re-measures it).
23
+ */
24
+ export const STENCIL_BEND = 3.69;
25
+ const pair = (v) => (typeof v === 'number' ? [v, v] : v);
26
+ /**
27
+ * A fabric from KES-F measurements (ADR-0120): density from W; XPBD's bending compliance from B
28
+ * (warp and weft averaged; `STENCIL_BEND`); VBD's shear modulus from G (N/m per radian); warp and
29
+ * weft stiffness, if measured, as the linear spring storing the tensile test's energy at EMT
30
+ * (`LT × load / EMT`), and XPBD's stretch compliance their geometric mean's inverse. What KES-F does
31
+ * not measure (damping, friction against colliders, collision thickness, the air, Poisson's ratio)
32
+ * comes from `base`; compression keeps `base`'s ratio to stretch.
33
+ *
34
+ * @example
35
+ * const habotai = fabricFromKes({ weight: 72.1, bending: [0.019, 0.063], shear: [0.23, 0.26], tensile: { emt: [7.11, 1.72], lt: [0.554, 0.867] } }, FABRICS.silk);
36
+ */
37
+ export function fabricFromKes(kes, base) {
38
+ const [b1, b2] = pair(kes.bending);
39
+ const [g1, g2] = pair(kes.shear);
40
+ for (const [path, v] of [['weight', kes.weight], ['bending', b1], ['bending', b2], ['shear', g1], ['shear', g2]]) {
41
+ if (!(v > 0 && Number.isFinite(v)))
42
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `KES-F ${path} ${v}.`, 'Give the measured value, above 0, in KES-F units (W g/m², B gf·cm²/cm, G gf/cm·deg).'), { path });
43
+ }
44
+ const bend = STENCIL_BEND / (((b1 + b2) / 2) * GF_CM);
45
+ const shear = ((g1 + g2) / 2) * GF_PER_CM * (180 / Math.PI);
46
+ const out = { ...base, density: kes.weight / 1000, bend, shear };
47
+ if (!kes.tensile)
48
+ return out;
49
+ const [e1, e2] = pair(kes.tensile.emt);
50
+ const [l1, l2] = pair(kes.tensile.lt);
51
+ const load = (kes.tensile.load ?? 500) * GF_PER_CM;
52
+ for (const [path, v] of [['tensile.emt', e1], ['tensile.emt', e2], ['tensile.lt', l1], ['tensile.lt', l2]]) {
53
+ if (!(v > 0 && Number.isFinite(v)))
54
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `KES-F ${path} ${v}.`, 'Give EMT in % and LT (0–1), both above 0.'), { path });
55
+ }
56
+ const warp = (l1 * load) / (e1 / 100);
57
+ const weft = (l2 * load) / (e2 / 100);
58
+ const stretch = 1 / Math.sqrt(warp * weft);
59
+ return { ...out, warp, weft, stretch, compression: (base.compression / base.stretch) * stretch };
60
+ }
61
+ /**
62
+ * Published KES-F measurements behind the presets (ADR-0120, ADR-0131). `canvas` is data only: built
63
+ * from it, the preset drapes 0.51 against its source's 0.91 (XPBD has no shear rigidity, which is
64
+ * what holds a stiff weave up), so the canvas preset stays fitted. `leather` is data only too: built from it
65
+ * the preset drapes 0.80 against the fitted 0.98, and its source measured no drape to referee (ADR-0138).
66
+ */
67
+ export const KES = {
68
+ /** Habotai, plain weave, 72.1 g/m² (sample 8): Hu X. et al., "Comparison of silk fabric dynamic drapability…", Industria Textila 71(1), 2020, pp. 55–61, Tables 1–5 (doi:10.35530/IT.071.01.1536). Drape coefficient 42.7% (YG811, 24 cm). */
69
+ silk: { weight: 72.067, bending: [0.019, 0.063], shear: [0.23, 0.26], tensile: { emt: [7.11, 1.723], lt: [0.554, 0.867] } },
70
+ /** 100% cotton plain weave, Ne 30, 70 ppi, 138.6 g/m² (sample 6): Alam M.S., Majumdar A., Ghosh A., Indian J. Fibre Text. Res. 44, 2019, pp. 9–15, Table 2. No tensile data. */
71
+ cotton: { weight: 138.6, bending: 0.07, shear: 1.75 },
72
+ /** 100% cotton indigo denim (sample A), 246 g/m² and 0.62 mm (Table 2; Table 5 prints W 104 mg/cm², which contradicts it): Kumari A., Khurana K., Journal of Textiles 2016, 4614168, Table 5 (doi:10.1155/2016/4614168). */
73
+ denim: { weight: 246, bending: 0.172, shear: 2.18, tensile: { emt: 5.515, lt: 0.712 } },
74
+ /** A heavy 100% cotton plain weave, 279 g/m², 16 × 23 threads/cm, 0.88 mm (sample C3; no source measures a fabric sold as canvas, ADR-0131): Kuijpers A., "Evaluation of physical and virtual fabric drape created from objective fabric properties", MPhil thesis, University of Manchester, 2017, Table 3.1 and Appendix 7 (KES-FB). Drape coefficient 91% (Cusick, 30 cm on 18 cm, Table 5.1). */
75
+ canvas: { weight: 279, bending: [0.33, 0.275], shear: [4.58, 4.41], tensile: { emt: [4.16, 8.03], lt: [0.905, 0.773] } },
76
+ /** Cowhide grain leather for bags (the butt), the mean of nine samples C1–C9, 1.21 mm: Roh E.K., "Mechanical properties and preferences of natural and artificial leathers…", J. Eng. Fibers Fabr. 15, 2020, Tables 1 and 3 (doi:10.1177/1558925020968825, CC BY). KES-FB: W 706.5 g/m², B 2.67 gf·cm²/cm (2 × 2 cm specimens), G 12.52 gf/cm·deg; the tensile test is reported as WT, RT and EM 4.13% without LT (none here); no drape measured. */
77
+ leather: { weight: 706.47, bending: 2.67, shear: 12.52 },
78
+ };
79
+ /**
80
+ * Fabric presets. Silk, cotton and denim are fitted to published KES-F measurements (`KES`,
81
+ * `fabricFromKes`, ADR-0120); their drape matches the published drape coefficients when the solve
82
+ * converges. Canvas is fitted to a published drape (ADR-0131: its KES-F values alone drape far too
83
+ * softly under XPBD); leather is fitted by eye (ADR-0101, ADR-0105) and stays so beside `KES.leather` (ADR-0138).
84
+ * Cotton's warp / weft stiffness is fitted too (its source has no tensile test). Woven fabrics are
85
+ * stiffer along the warp than the weft and far softer in shear; leather has no weave.
86
+ */
87
+ export const FABRICS = {
88
+ silk: fabricFromKes(KES.silk, { density: 0.08, stretch: 2e-4, compression: 0.005, bend: 6e4, damping: 0.05, friction: 0.25, thickness: 0.006, drag: 1.2, skinDrag: 0.02, warp: 5000, weft: 4000, shear: 300, poisson: 0.2 }),
89
+ cotton: fabricFromKes(KES.cotton, { density: 0.16, stretch: 1e-4, compression: 0.002, bend: 1.5e4, damping: 0.05, friction: 0.45, thickness: 0.008, drag: 1.2, skinDrag: 0.03, warp: 10000, weft: 8000, shear: 800, poisson: 0.2 }),
90
+ // Fitted (ADR-0131): drapes 0.96 against the measured heavy cotton plain weave's 0.91 (`KES.canvas`).
91
+ canvas: { density: 0.22, stretch: 5e-5, compression: 0.002, bend: 5e3, damping: 0.05, friction: 0.5, thickness: 0.008, drag: 1.2, skinDrag: 0.03, warp: 20000, weft: 18000, shear: 2500, poisson: 0.2 },
92
+ denim: fabricFromKes(KES.denim, { density: 0.4, stretch: 3e-5, compression: 0.001, bend: 1.5e3, damping: 0.08, friction: 0.6, thickness: 0.01, drag: 1.1, skinDrag: 0.04, warp: 33000, weft: 24000, shear: 3000, poisson: 0.25 }),
93
+ leather: { density: 0.9, stretch: 1e-5, compression: 5e-4, bend: 300, damping: 0.1, friction: 0.7, thickness: 0.012, drag: 1.0, skinDrag: 0.05, warp: 100000, weft: 100000, shear: 35000, poisson: 0.4 },
94
+ };
95
+ /**
96
+ * A rectangular cloth centred on its entity's origin, its cells cut along alternating diagonals
97
+ * (no preferred shear direction).
98
+ *
99
+ * @example
100
+ * const flag = clothGrid({ width: 3, height: 2, segments: [60, 40], pin: 'left' });
101
+ */
102
+ export function clothGrid(o) {
103
+ if (!(o.width > 0 && o.height > 0)) {
104
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `A ${o.width} × ${o.height} m cloth grid.`, 'Give a width and height above 0.'), { path: 'width' });
105
+ }
106
+ const nx = Math.max(2, Math.round(o.segments?.[0] ?? o.width * 32));
107
+ const ny = Math.max(2, Math.round(o.segments?.[1] ?? o.height * 32));
108
+ const flat = o.plane === 'xz';
109
+ const n = (nx + 1) * (ny + 1);
110
+ const positions = new Float32Array(n * 3);
111
+ const uvs = new Float32Array(n * 2);
112
+ const pinned = [];
113
+ const pin = o.pin ?? 'none';
114
+ for (let j = 0; j <= ny; j++) {
115
+ for (let i = 0; i <= nx; i++) {
116
+ const v = j * (nx + 1) + i;
117
+ const u = i / nx;
118
+ const w = j / ny;
119
+ const x = (u - 0.5) * o.width;
120
+ const y = (0.5 - w) * o.height;
121
+ positions.set(flat ? [x, 0, -y] : [x, y, 0], v * 3);
122
+ uvs.set([u, w], v * 2);
123
+ const hold = typeof pin === 'function' ? pin(u, w) : pin === 'top' ? j === 0 : pin === 'left' ? i === 0 : pin === 'corners' ? j === 0 && (i === 0 || i === nx) : false;
124
+ if (hold)
125
+ pinned.push(v);
126
+ }
127
+ }
128
+ const indices = new Uint32Array(nx * ny * 6);
129
+ let k = 0;
130
+ for (let j = 0; j < ny; j++) {
131
+ for (let i = 0; i < nx; i++) {
132
+ const a = j * (nx + 1) + i;
133
+ const b = a + 1;
134
+ const c = a + nx + 1;
135
+ const d = c + 1;
136
+ // Counter-clockwise seen from the front (+Z, or +Y lying flat): a top-left, b top-right, c and d below.
137
+ if ((i + j) % 2 === 0)
138
+ indices.set([a, c, d, a, d, b], k);
139
+ else
140
+ indices.set([a, c, b, b, c, d], k);
141
+ k += 6;
142
+ }
143
+ }
144
+ return { positions, indices, uvs, pinned };
145
+ }
146
+ /**
147
+ * Builds a mesh's solver topology: a stretch constraint per edge, a bending stencil per edge two
148
+ * triangles share (isometric bending, Bergou et al., *A Quadratic Bending Model for Inextensible
149
+ * Surfaces*, 2006; it assumes a flat rest shape), adjacency, per-vertex areas, and a greedy
150
+ * colouring of all constraints together.
151
+ */
152
+ export function clothTopology(mesh) {
153
+ const p = mesh.positions;
154
+ const idx = mesh.indices;
155
+ const n = p.length / 3;
156
+ const t = idx.length / 3;
157
+ if (n < 3 || t < 1 || idx.length % 3 !== 0) {
158
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `A cloth mesh of ${n} vertices and ${idx.length} indices.`, 'Give at least one triangle (indices in threes).'), { path: 'indices' });
159
+ }
160
+ for (let i = 0; i < idx.length; i++) {
161
+ if (idx[i] >= n)
162
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `Cloth index ${idx[i]} at ${i} is past the ${n} vertices.`, 'Index the positions given.'), { path: `indices[${i}]` });
163
+ }
164
+ const pinned = new Uint8Array(n);
165
+ for (const v of Array.from(mesh.pinned ?? [])) {
166
+ if (!(v >= 0 && v < n))
167
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `Pinned vertex ${v} is not one of the ${n}.`, 'Pin vertex indices of the mesh.'), { path: 'pinned' });
168
+ pinned[v] = 1;
169
+ }
170
+ const area = new Float64Array(n);
171
+ const adjCount = new Uint32Array(n + 1);
172
+ // Edges: key (lo, hi) → the vertices opposite it, one per triangle.
173
+ const edges = new Map();
174
+ for (let f = 0; f < t; f++) {
175
+ const a = idx[f * 3];
176
+ const b = idx[f * 3 + 1];
177
+ const c = idx[f * 3 + 2];
178
+ const tri = triArea(p, a, b, c);
179
+ for (const v of [a, b, c]) {
180
+ area[v] += tri / 3;
181
+ adjCount[v + 1]++;
182
+ }
183
+ for (const [u, w, o] of [[a, b, c], [b, c, a], [c, a, b]]) {
184
+ const key = Math.min(u, w) * n + Math.max(u, w);
185
+ const opp = edges.get(key);
186
+ if (opp)
187
+ opp.push(o);
188
+ else
189
+ edges.set(key, [o]);
190
+ }
191
+ }
192
+ for (let v = 0; v < n; v++)
193
+ adjCount[v + 1] += adjCount[v];
194
+ const adjOffsets = adjCount;
195
+ const adjacency = new Uint32Array(t * 3);
196
+ const fill = adjOffsets.slice(0, n);
197
+ for (let f = 0; f < t; f++)
198
+ for (let k = 0; k < 3; k++)
199
+ adjacency[fill[idx[f * 3 + k]]++] = f;
200
+ // Constraints: every edge stretches; an edge with exactly two triangles also bends.
201
+ const kinds = [];
202
+ const ids = [];
203
+ const rest = [];
204
+ const bends = [];
205
+ for (const [key, opp] of edges) {
206
+ const u = Math.floor(key / n);
207
+ const w = key - u * n;
208
+ kinds.push(0);
209
+ ids.push(u, w, NO_VERTEX, NO_VERTEX);
210
+ rest.push(dist(p, u, w), 0, 0, 0);
211
+ if (opp.length === 2)
212
+ bends.push([u, w, opp[0], opp[1]]);
213
+ }
214
+ for (const s of bends) {
215
+ const k = bendWeights(p, s[0], s[1], s[2], s[3]);
216
+ if (!k)
217
+ continue;
218
+ kinds.push(1);
219
+ ids.push(...s);
220
+ rest.push(...k);
221
+ }
222
+ const m = kinds.length;
223
+ const vertexIds = new Uint32Array(ids);
224
+ // Greedy colouring of the stretch constraints: each takes the lowest colour none of its vertices
225
+ // has yet (8 on a grid). The bending stencils all go in one last range after them: the GPU
226
+ // solves them together, Jacobi (ADR-0110), and the CPU walks every range in order anyway.
227
+ const used = new Uint32Array(n * (MAX_COLOURS / 32));
228
+ const colour = new Uint8Array(m);
229
+ let colours = 0;
230
+ for (let c = 0; c < m; c++) {
231
+ if (kinds[c] !== 0)
232
+ continue;
233
+ const vs = [vertexIds[c * 4], vertexIds[c * 4 + 1], vertexIds[c * 4 + 2], vertexIds[c * 4 + 3]].filter((v) => v !== NO_VERTEX);
234
+ let k = 0;
235
+ while (k < MAX_COLOURS && vs.some((v) => used[v * 4 + (k >> 5)] & (1 << (k & 31))))
236
+ k++;
237
+ if (k === MAX_COLOURS) {
238
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `A cloth vertex is in too many constraints to colour within ${MAX_COLOURS} colours.`, 'Remesh the cloth: no vertex should join more than ~20 triangles.'), { path: 'indices' });
239
+ }
240
+ for (const v of vs)
241
+ used[v * 4 + (k >> 5)] |= 1 << (k & 31);
242
+ colour[c] = k;
243
+ colours = Math.max(colours, k + 1);
244
+ }
245
+ const stretchColours = colours;
246
+ if (kinds.some((k) => k === 1)) {
247
+ for (let c = 0; c < m; c++)
248
+ if (kinds[c] === 1)
249
+ colour[c] = stretchColours;
250
+ colours++;
251
+ }
252
+ const colourOffsets = new Uint32Array(colours + 1);
253
+ for (let c = 0; c < m; c++)
254
+ colourOffsets[colour[c] + 1]++;
255
+ for (let k = 0; k < colours; k++)
256
+ colourOffsets[k + 1] += colourOffsets[k];
257
+ const order = new Uint32Array(m);
258
+ const at = colourOffsets.slice(0, colours);
259
+ for (let c = 0; c < m; c++)
260
+ order[at[colour[c]]++] = c;
261
+ const { vertexOrder, vertexColourOffsets } = colourVertices(n, idx, vertexIds, kinds, pinned);
262
+ const restFrames = membraneFrames(p, idx, mesh.uvs);
263
+ const { tetherAnchor, tetherLength } = tethers(n, vertexIds, kinds, rest, pinned);
264
+ return { mesh, vertices: n, triangles: t, pinned, area, adjOffsets, adjacency, kinds: Uint8Array.from(kinds), vertexIds, rest: Float32Array.from(rest), order, colourOffsets, stretchColours, vertexOrder, vertexColourOffsets, restFrames, tetherAnchor, tetherLength };
265
+ }
266
+ /**
267
+ * VBD's vertex colouring (ADR-0105): a vertex reads every vertex it shares a triangle or a bending
268
+ * stencil with, so those take different colours; pinned vertices are never solved and take none.
269
+ */
270
+ function colourVertices(n, idx, ids, kinds, pinned) {
271
+ const near = Array.from({ length: n }, () => []);
272
+ const link = (vs) => {
273
+ for (const a of vs)
274
+ for (const b of vs)
275
+ if (a !== b && !pinned[a] && !pinned[b])
276
+ near[a].push(b);
277
+ };
278
+ for (let f = 0; f < idx.length; f += 3)
279
+ link([idx[f], idx[f + 1], idx[f + 2]]);
280
+ for (let c = 0; c < kinds.length; c++)
281
+ if (kinds[c] === 1)
282
+ link([ids[c * 4], ids[c * 4 + 1], ids[c * 4 + 2], ids[c * 4 + 3]]);
283
+ const colour = new Int16Array(n).fill(-1);
284
+ const taken = new Uint8Array(MAX_COLOURS);
285
+ let colours = 0;
286
+ for (let v = 0; v < n; v++) {
287
+ if (pinned[v])
288
+ continue;
289
+ taken.fill(0);
290
+ for (const u of near[v])
291
+ if (colour[u] >= 0)
292
+ taken[colour[u]] = 1;
293
+ let k = 0;
294
+ while (k < MAX_COLOURS && taken[k])
295
+ k++;
296
+ if (k === MAX_COLOURS) {
297
+ throw Object.assign(new YaliError('PHYSICS_INVALID', `A cloth vertex has too many neighbours to colour within ${MAX_COLOURS} colours.`, 'Remesh the cloth: no vertex should join more than ~20 triangles.'), { path: 'indices' });
298
+ }
299
+ colour[v] = k;
300
+ colours = Math.max(colours, k + 1);
301
+ }
302
+ const vertexColourOffsets = new Uint32Array(colours + 1);
303
+ for (let v = 0; v < n; v++)
304
+ if (colour[v] >= 0)
305
+ vertexColourOffsets[colour[v] + 1]++;
306
+ for (let k = 0; k < colours; k++)
307
+ vertexColourOffsets[k + 1] += vertexColourOffsets[k];
308
+ const vertexOrder = new Uint32Array(vertexColourOffsets[colours]);
309
+ const at = vertexColourOffsets.slice(0, colours);
310
+ for (let v = 0; v < n; v++)
311
+ if (colour[v] >= 0)
312
+ vertexOrder[at[colour[v]]++] = v;
313
+ return { vertexOrder, vertexColourOffsets };
314
+ }
315
+ /**
316
+ * Each triangle's membrane rest frame (ADR-0105): its edges in a 2D frame whose first axis is the
317
+ * warp (the direction of increasing u, in the triangle's plane; the first edge where the uvs are
318
+ * degenerate), inverted; then its area.
319
+ */
320
+ function membraneFrames(p, idx, uvs) {
321
+ const t = idx.length / 3;
322
+ const out = new Float32Array(t * 5);
323
+ const x = (i) => [p[i * 3], p[i * 3 + 1], p[i * 3 + 2]];
324
+ for (let f = 0; f < t; f++) {
325
+ const [i0, i1, i2] = [idx[f * 3], idx[f * 3 + 1], idx[f * 3 + 2]];
326
+ const e1 = sub(x(i1), x(i0));
327
+ const e2 = sub(x(i2), x(i0));
328
+ const nrm = cross(e1, e2);
329
+ const a2 = len(nrm);
330
+ if (!(a2 > 1e-12))
331
+ continue; // degenerate: zero area, no membrane
332
+ const nn = nrm.map((v) => v / a2);
333
+ let axis = e1;
334
+ if (uvs) {
335
+ const du1 = uvs[i1 * 2] - uvs[i0 * 2];
336
+ const dv1 = uvs[i1 * 2 + 1] - uvs[i0 * 2 + 1];
337
+ const du2 = uvs[i2 * 2] - uvs[i0 * 2];
338
+ const dv2 = uvs[i2 * 2 + 1] - uvs[i0 * 2 + 1];
339
+ const det = du1 * dv2 - du2 * dv1;
340
+ // ∂p/∂u from the uv parametrisation.
341
+ if (Math.abs(det) > 1e-12)
342
+ axis = [0, 1, 2].map((k) => (dv2 * e1[k] - dv1 * e2[k]) / det);
343
+ }
344
+ const inPlane = sub(axis, nn.map((v) => v * dot(axis, nn)));
345
+ const la = len(inPlane);
346
+ const a = la > 1e-12 ? inPlane.map((v) => v / la) : e1.map((v) => v / len(e1));
347
+ const b = cross(nn, a);
348
+ const [m00, m01, m10, m11] = [dot(e1, a), dot(e2, a), dot(e1, b), dot(e2, b)];
349
+ const det = m00 * m11 - m01 * m10;
350
+ out.set([m11 / det, -m01 / det, -m10 / det, m00 / det, 0.5 * a2], f * 5);
351
+ }
352
+ return out;
353
+ }
354
+ /**
355
+ * Tethers (ADR-0105): for each free vertex, the pinned vertex nearest along the mesh's edges and
356
+ * that distance (Dijkstra from every pinned vertex at once). No pins: no tethers.
357
+ */
358
+ function tethers(n, ids, kinds, rest, pinned) {
359
+ const tetherAnchor = new Uint32Array(n).fill(NO_VERTEX);
360
+ const tetherLength = new Float32Array(n);
361
+ const dist = new Float64Array(n).fill(Infinity);
362
+ const nb = Array.from({ length: n }, () => []);
363
+ for (let c = 0; c < kinds.length; c++) {
364
+ if (kinds[c] !== 0)
365
+ continue;
366
+ const [u, w, l] = [ids[c * 4], ids[c * 4 + 1], rest[c * 4]];
367
+ nb[u].push([w, l]);
368
+ nb[w].push([u, l]);
369
+ }
370
+ // A binary heap of (distance, vertex).
371
+ const heap = [];
372
+ const push = (d, v) => {
373
+ heap.push([d, v]);
374
+ for (let i = heap.length - 1; i > 0;) {
375
+ const pa = (i - 1) >> 1;
376
+ if (heap[pa][0] <= heap[i][0])
377
+ break;
378
+ [heap[pa], heap[i]] = [heap[i], heap[pa]];
379
+ i = pa;
380
+ }
381
+ };
382
+ const pop = () => {
383
+ const top = heap[0];
384
+ const last = heap.pop();
385
+ if (heap.length > 0) {
386
+ heap[0] = last;
387
+ for (let i = 0;;) {
388
+ const l = i * 2 + 1;
389
+ const r = l + 1;
390
+ let s = i;
391
+ if (l < heap.length && heap[l][0] < heap[s][0])
392
+ s = l;
393
+ if (r < heap.length && heap[r][0] < heap[s][0])
394
+ s = r;
395
+ if (s === i)
396
+ break;
397
+ [heap[s], heap[i]] = [heap[i], heap[s]];
398
+ i = s;
399
+ }
400
+ }
401
+ return top;
402
+ };
403
+ for (let v = 0; v < n; v++) {
404
+ if (!pinned[v])
405
+ continue;
406
+ dist[v] = 0;
407
+ tetherAnchor[v] = v;
408
+ push(0, v);
409
+ }
410
+ while (heap.length > 0) {
411
+ const [d, v] = pop();
412
+ if (d > dist[v])
413
+ continue;
414
+ for (const [u, l] of nb[v]) {
415
+ if (d + l < dist[u]) {
416
+ dist[u] = d + l;
417
+ tetherAnchor[u] = tetherAnchor[v];
418
+ push(dist[u], u);
419
+ }
420
+ }
421
+ }
422
+ for (let v = 0; v < n; v++) {
423
+ if (pinned[v] || !Number.isFinite(dist[v]))
424
+ tetherAnchor[v] = NO_VERTEX;
425
+ else
426
+ tetherLength[v] = dist[v];
427
+ }
428
+ return { tetherAnchor, tetherLength };
429
+ }
430
+ /**
431
+ * The VBD section's header (ADR-0105), words from its start: where the triangles' rest frames, the
432
+ * vertex colours, the per-vertex bending and edge lists and the tethers are (words from the cloth's
433
+ * base; tethers 0: none), the vertex colour count, then the membrane's c11, c22, c12, c33 and the
434
+ * compression scale (f32); word 11: where the per-vertex skinned anchors are (ADR-0108; 0: none);
435
+ * word 12: where the compact tear flags are, one word per constraint in colour order, which the
436
+ * solver sets with the constraint's own flag so a readback is small (vertex splitting, ADR-0108);
437
+ * word 13: how many colour ranges are stretch constraints (the bending range follows, ADR-0110).
438
+ */
439
+ export const VBD_HEADER = 16;
440
+ /**
441
+ * Packs a topology for the GPU with a fabric at a uniform scale (the entity's): the rest
442
+ * positions (unscaled, the inverse masses in w), adjacency, triangles, colour ranges and the
443
+ * constraints in colour order with their compliances (shaders/kanduka/cloth.wesl `solve`).
444
+ */
445
+ export function packCloth(topo, fabric, scale, tether = 0, anchors = null) {
446
+ const n = topo.vertices;
447
+ const m = topo.kinds.length;
448
+ const ncolours = topo.colourOffsets.length - 1;
449
+ const adj = n * 4;
450
+ const tris = adj + n + 1 + topo.adjacency.length;
451
+ const colours = tris + topo.triangles * 3;
452
+ const cons = colours + ncolours + 1;
453
+ const triEdges = cons + m * CONSTRAINT_WORDS;
454
+ // VBD (ADR-0105): the header, rest frames, vertex colours, per-vertex bending and edge lists, tethers.
455
+ let bendEntries = 0;
456
+ for (let c = 0; c < m; c++)
457
+ bendEntries += topo.kinds[c] === 1 ? 4 : 0;
458
+ const stretchRefs = (m - bendEntries / 4) * 2;
459
+ const nvc = topo.vertexColourOffsets.length - 1;
460
+ const vbd = triEdges + topo.triangles * 3;
461
+ const trest = vbd + VBD_HEADER;
462
+ const vcol = trest + topo.triangles * 5;
463
+ const bcsr = vcol + nvc + 1 + topo.vertexOrder.length;
464
+ const ecsr = bcsr + n + 1 + bendEntries;
465
+ const teth = tether > 0 ? ecsr + n + 1 + stretchRefs : 0;
466
+ const vbdEnd = (teth || ecsr + n + 1 + stretchRefs) + (teth ? n * 2 : 0);
467
+ // Skinned anchors (ADR-0108): per vertex its anchor record's index among the cloth's (NO_VERTEX: none).
468
+ const anch = anchors ? vbdEnd : 0;
469
+ const tornList = vbdEnd + (anchors ? n : 0);
470
+ const words = new Uint32Array(tornList + m);
471
+ const f = new Float32Array(words.buffer);
472
+ const invMass = new Float32Array(n);
473
+ const p = topo.mesh.positions;
474
+ for (let v = 0; v < n; v++) {
475
+ const mass = fabric.density * topo.area[v] * scale * scale;
476
+ invMass[v] = topo.pinned[v] || !(mass > 0) ? 0 : 1 / mass;
477
+ f.set([p[v * 3], p[v * 3 + 1], p[v * 3 + 2], invMass[v]], v * 4);
478
+ }
479
+ for (let v = 0; v <= n; v++)
480
+ words[adj + v] = n + 1 + topo.adjOffsets[v];
481
+ words.set(topo.adjacency, adj + n + 1);
482
+ words.set(topo.mesh.indices, tris);
483
+ words.set(topo.colourOffsets, colours);
484
+ // Each edge's stretch constraint, by (lo, hi): tearing breaks it, and its triangles and bending go with it.
485
+ const edgeAt = new Map();
486
+ for (let j = 0; j < m; j++) {
487
+ const c = topo.order[j];
488
+ if (topo.kinds[c] !== 0)
489
+ continue;
490
+ const [u, v] = [topo.vertexIds[c * 4], topo.vertexIds[c * 4 + 1]];
491
+ edgeAt.set(Math.min(u, v) * n + Math.max(u, v), cons + j * CONSTRAINT_WORDS);
492
+ }
493
+ const edge = (u, v) => edgeAt.get(Math.min(u, v) * n + Math.max(u, v)) ?? 0;
494
+ for (let j = 0; j < m; j++) {
495
+ const c = topo.order[j];
496
+ const o = cons + j * CONSTRAINT_WORDS;
497
+ words.set(topo.vertexIds.subarray(c * 4, c * 4 + 4), o);
498
+ if (topo.kinds[c] === 0) {
499
+ f[o + 4] = topo.rest[c * 4] * scale;
500
+ f[o + 5] = fabric.stretch;
501
+ f[o + 6] = fabric.compression;
502
+ }
503
+ else {
504
+ // √κ scales as 1 / scale (κ = 3 / area); the weights themselves are scale-free.
505
+ for (let k = 0; k < 4; k++)
506
+ f[o + 4 + k] = topo.rest[c * 4 + k] / scale;
507
+ f[o + 8] = fabric.bend;
508
+ words[o + 9] = edge(topo.vertexIds[c * 4], topo.vertexIds[c * 4 + 1]);
509
+ }
510
+ }
511
+ const ix = topo.mesh.indices;
512
+ for (let t = 0; t < topo.triangles; t++) {
513
+ const [a, b, c] = [ix[t * 3], ix[t * 3 + 1], ix[t * 3 + 2]];
514
+ words.set([edge(a, b), edge(b, c), edge(c, a)], triEdges + t * 3);
515
+ }
516
+ // VBD's header and the membrane (orthotropic StVK; ADR-0105).
517
+ const c11 = fabric.warp ?? 1 / Math.max(fabric.stretch, 1e-12);
518
+ const c22 = fabric.weft ?? c11;
519
+ const c33 = fabric.shear ?? c11 * 0.1;
520
+ const c12 = (fabric.poisson ?? 0.2) * Math.sqrt(c11 * c22);
521
+ words.set([trest, vcol, nvc, bcsr, ecsr, teth], vbd);
522
+ words[vbd + 11] = anch;
523
+ words[vbd + 12] = tornList;
524
+ words[vbd + 13] = topo.stretchColours;
525
+ if (anchors)
526
+ words.set(anchors, anch);
527
+ f.set([c11, c22, c12, c33, Math.min(1, fabric.stretch / Math.max(fabric.compression, 1e-12))], vbd + 6);
528
+ const rf = topo.restFrames;
529
+ for (let t = 0; t < topo.triangles; t++) {
530
+ // The inverse rest matrix scales as 1 / scale, the area as scale².
531
+ for (let k = 0; k < 4; k++)
532
+ f[trest + t * 5 + k] = rf[t * 5 + k] / scale;
533
+ f[trest + t * 5 + 4] = rf[t * 5 + 4] * scale * scale;
534
+ }
535
+ words.set(topo.vertexColourOffsets, vcol);
536
+ words.set(topo.vertexOrder, vcol + nvc + 1);
537
+ // Per-vertex lists, CSR like the adjacency: bending entries are (constraint word << 2 | slot),
538
+ // edge entries the stretch constraint's word.
539
+ const bendCount = new Uint32Array(n + 1);
540
+ const edgeCount = new Uint32Array(n + 1);
541
+ for (let j = 0; j < m; j++) {
542
+ const c = topo.order[j];
543
+ const vs = topo.vertexIds.subarray(c * 4, c * 4 + (topo.kinds[c] === 1 ? 4 : 2));
544
+ for (const v of vs)
545
+ (topo.kinds[c] === 1 ? bendCount : edgeCount)[v + 1]++;
546
+ }
547
+ for (let v = 0; v < n; v++) {
548
+ bendCount[v + 1] += bendCount[v];
549
+ edgeCount[v + 1] += edgeCount[v];
550
+ }
551
+ for (let v = 0; v <= n; v++) {
552
+ words[bcsr + v] = n + 1 + bendCount[v];
553
+ words[ecsr + v] = n + 1 + edgeCount[v];
554
+ }
555
+ const bf = bendCount.slice(0, n);
556
+ const ef = edgeCount.slice(0, n);
557
+ for (let j = 0; j < m; j++) {
558
+ const c = topo.order[j];
559
+ const o = cons + j * CONSTRAINT_WORDS;
560
+ if (topo.kinds[c] === 1)
561
+ for (let s = 0; s < 4; s++)
562
+ words[bcsr + n + 1 + bf[topo.vertexIds[c * 4 + s]]++] = (o << 2) | s;
563
+ else
564
+ for (let s = 0; s < 2; s++)
565
+ words[ecsr + n + 1 + ef[topo.vertexIds[c * 4 + s]]++] = o;
566
+ }
567
+ if (teth) {
568
+ for (let v = 0; v < n; v++) {
569
+ words[teth + v * 2] = topo.tetherAnchor[v];
570
+ f[teth + v * 2 + 1] = topo.tetherLength[v] * scale * (1 + tether);
571
+ }
572
+ }
573
+ return { words, tris, adj, colours, ncolours, cons, invMass, triEdges, vbd };
574
+ }
575
+ /**
576
+ * Splits a cloth mesh along torn edges (ADR-0108): around each vertex on a torn edge its triangles
577
+ * fall into pieces that meet only across torn edges (or not at all); the first piece keeps the
578
+ * vertex, each other piece gets a copy of it (appended, same rest position, uv and pin), and its
579
+ * triangles are re-indexed to it. The torn edges become two boundary edges, one per side, so the
580
+ * pieces separate cleanly. Null when nothing splits. `source[k]` is the vertex copy k came from.
581
+ */
582
+ export function splitTorn(mesh, torn) {
583
+ const n = mesh.positions.length / 3;
584
+ const idx = mesh.indices.slice();
585
+ const cut = new Set();
586
+ const key = (a, b) => Math.min(a, b) * n + Math.max(a, b);
587
+ for (const [a, b] of torn)
588
+ cut.add(key(a, b));
589
+ if (cut.size === 0)
590
+ return null;
591
+ // A triangle torn on all three edges is gone (degenerate, as tearing drew it), not confetti: it
592
+ // would cost a new vertex per corner. The rest go in their vertices' fans.
593
+ const fan = Array.from({ length: n }, () => []);
594
+ let dropped = 0;
595
+ for (let f = 0; f < idx.length / 3; f++) {
596
+ const [a, b, c] = [idx[f * 3], idx[f * 3 + 1], idx[f * 3 + 2]];
597
+ if (a !== b && cut.has(key(a, b)) && cut.has(key(b, c)) && cut.has(key(c, a))) {
598
+ idx[f * 3 + 1] = idx[f * 3 + 2] = a;
599
+ dropped++;
600
+ continue;
601
+ }
602
+ if (a === b && b === c)
603
+ continue;
604
+ for (let k = 0; k < 3; k++)
605
+ fan[idx[f * 3 + k]].push(f);
606
+ }
607
+ const onCut = new Set();
608
+ for (const e of cut) {
609
+ const a = Math.floor(e / n);
610
+ onCut.add(a).add(e - a * n);
611
+ }
612
+ const source = [];
613
+ for (const v of onCut) {
614
+ const tris = fan[v];
615
+ if (tris.length < 2)
616
+ continue;
617
+ // Union-find over v's triangles: two join across an edge (v, w) both share that is not torn.
618
+ const parent = tris.map((_, i) => i);
619
+ const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i])));
620
+ const others = (f) => [idx[f * 3], idx[f * 3 + 1], idx[f * 3 + 2]].filter((x) => x !== v);
621
+ for (let i = 0; i < tris.length; i++) {
622
+ for (let j = i + 1; j < tris.length; j++) {
623
+ const shared = others(tris[i]).filter((w) => others(tris[j]).includes(w));
624
+ if (shared.some((w) => !cut.has(key(v, w))))
625
+ parent[find(i)] = find(j);
626
+ }
627
+ }
628
+ const pieces = new Map();
629
+ tris.forEach((f, i) => {
630
+ const r = find(i);
631
+ const list = pieces.get(r);
632
+ if (list)
633
+ list.push(f);
634
+ else
635
+ pieces.set(r, [f]);
636
+ });
637
+ let first = true;
638
+ for (const list of pieces.values()) {
639
+ if (first) {
640
+ first = false;
641
+ continue;
642
+ }
643
+ const copy = n + source.length;
644
+ source.push(v);
645
+ for (const f of list)
646
+ for (let k = 0; k < 3; k++)
647
+ if (idx[f * 3 + k] === v)
648
+ idx[f * 3 + k] = copy;
649
+ }
650
+ }
651
+ if (source.length === 0 && dropped === 0)
652
+ return null;
653
+ const total = n + source.length;
654
+ const positions = new Float32Array(total * 3);
655
+ positions.set(mesh.positions);
656
+ const uvs = mesh.uvs ? new Float32Array(total * 2) : undefined;
657
+ uvs?.set(mesh.uvs);
658
+ const pinnedIn = new Set(Array.from(mesh.pinned ?? []));
659
+ const pinned = Array.from(pinnedIn);
660
+ source.forEach((v, k) => {
661
+ positions.set(mesh.positions.subarray(v * 3, v * 3 + 3), (n + k) * 3);
662
+ if (uvs)
663
+ uvs.set(mesh.uvs.subarray(v * 2, v * 2 + 2), (n + k) * 2);
664
+ if (pinnedIn.has(v))
665
+ pinned.push(n + k);
666
+ });
667
+ return { mesh: { positions, indices: idx, uvs, pinned }, source: Uint32Array.from(source) };
668
+ }
669
+ /**
670
+ * Bergou's bending weights for the stencil (edge x0–x1, opposite vertices x2 and x3), scaled by
671
+ * √κ, κ = 3 / (A0 + A1): `Σ k_i x_i` is the stencil's discrete mean-curvature vector, zero when
672
+ * the two triangles are flat. Null for a degenerate pair.
673
+ */
674
+ export function bendWeights(p, i0, i1, i2, i3) {
675
+ const x = (i) => [p[i * 3], p[i * 3 + 1], p[i * 3 + 2]];
676
+ const [x0, x1, x2, x3] = [x(i0), x(i1), x(i2), x(i3)];
677
+ const e0 = sub(x1, x0);
678
+ const e1 = sub(x2, x0);
679
+ const e2 = sub(x3, x0);
680
+ const e3 = sub(x2, x1);
681
+ const e4 = sub(x3, x1);
682
+ const a0 = 0.5 * len(cross(e0, e1));
683
+ const a1 = 0.5 * len(cross(e0, e2));
684
+ if (!(a0 > 1e-12 && a1 > 1e-12))
685
+ return null;
686
+ const c01 = cot(e0, e1);
687
+ const c02 = cot(e0, e2);
688
+ const c03 = cot(neg(e0), e3);
689
+ const c04 = cot(neg(e0), e4);
690
+ const s = Math.sqrt(3 / (a0 + a1));
691
+ return [(c03 + c04) * s, (c01 + c02) * s, (-c01 - c03) * s, (-c02 - c04) * s];
692
+ }
693
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
694
+ const neg = (a) => [-a[0], -a[1], -a[2]];
695
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
696
+ const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
697
+ const len = (a) => Math.hypot(a[0], a[1], a[2]);
698
+ const cot = (a, b) => dot(a, b) / len(cross(a, b));
699
+ function dist(p, a, b) {
700
+ return Math.hypot(p[a * 3] - p[b * 3], p[a * 3 + 1] - p[b * 3 + 1], p[a * 3 + 2] - p[b * 3 + 2]);
701
+ }
702
+ function triArea(p, a, b, c) {
703
+ const x = (i) => [p[i * 3], p[i * 3 + 1], p[i * 3 + 2]];
704
+ return 0.5 * len(cross(sub(x(b), x(a)), sub(x(c), x(a))));
705
+ }
706
+ //# sourceMappingURL=cloth-asset.js.map