reze-engine 0.54.16 → 0.55.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.
Files changed (49) hide show
  1. package/dist/engine.d.ts +210 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +1063 -11
  4. package/dist/graph/presets/pool_floor.d.ts +3 -0
  5. package/dist/graph/presets/pool_floor.d.ts.map +1 -0
  6. package/dist/graph/presets/pool_floor.js +42 -0
  7. package/dist/graph/presets/water.d.ts +3 -0
  8. package/dist/graph/presets/water.d.ts.map +1 -0
  9. package/dist/graph/presets/water.js +31 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +8 -0
  13. package/dist/model.d.ts.map +1 -1
  14. package/dist/model.js +10 -0
  15. package/dist/overlay.d.ts +200 -0
  16. package/dist/overlay.d.ts.map +1 -0
  17. package/dist/overlay.js +806 -0
  18. package/dist/physics/autofit.d.ts +147 -0
  19. package/dist/physics/autofit.d.ts.map +1 -0
  20. package/dist/physics/autofit.js +501 -0
  21. package/dist/pmx-document.d.ts +218 -0
  22. package/dist/pmx-document.d.ts.map +1 -0
  23. package/dist/pmx-document.js +685 -0
  24. package/dist/shaders/passes/field-blit.d.ts +26 -0
  25. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  26. package/dist/shaders/passes/field-blit.js +65 -0
  27. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  28. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  29. package/dist/shaders/passes/ground-noise.js +88 -0
  30. package/dist/shaders/passes/overlay.d.ts +3 -0
  31. package/dist/shaders/passes/overlay.d.ts.map +1 -0
  32. package/dist/shaders/passes/overlay.js +164 -0
  33. package/dist/shaders/passes/sim.d.ts +34 -0
  34. package/dist/shaders/passes/sim.d.ts.map +1 -0
  35. package/dist/shaders/passes/sim.js +169 -0
  36. package/dist/shaders/passes/wireframe.d.ts +2 -0
  37. package/dist/shaders/passes/wireframe.d.ts.map +1 -0
  38. package/dist/shaders/passes/wireframe.js +107 -0
  39. package/dist/shaders/score-api.d.ts +10 -0
  40. package/dist/shaders/score-api.d.ts.map +1 -0
  41. package/dist/shaders/score-api.js +114 -0
  42. package/package.json +1 -1
  43. package/src/engine.ts +1147 -13
  44. package/src/index.ts +50 -0
  45. package/src/model.ts +9 -0
  46. package/src/overlay.ts +1000 -0
  47. package/src/pmx-document.ts +882 -0
  48. package/src/shaders/passes/overlay.ts +166 -0
  49. package/src/shaders/passes/wireframe.ts +108 -0
@@ -0,0 +1,806 @@
1
+ // The data behind the overlay pass: what an overlay primitive IS, the unit
2
+ // wireframes the pass draws, and the three builders that turn a posed model into
3
+ // a list of them.
4
+ //
5
+ // The builders are pure — (model, physics, options) -> OverlayPrimitive[] — so a
6
+ // host can call them itself, print the result, diff two poses, or hand a list
7
+ // straight to setOverlay without the engine's live layers in the way. The pass
8
+ // is one caller of them.
9
+ //
10
+ // Everything they emit is WORLD space, root transform included, so a list is
11
+ // meaningful on its own.
12
+ import { Quat, Vec3 } from "./math";
13
+ import { RigidbodyShape, RigidbodyType } from "./physics/types";
14
+ export const OVERLAY_SHAPES = [
15
+ "line",
16
+ "dashedLine",
17
+ "axes",
18
+ "circle",
19
+ "dot",
20
+ "link",
21
+ "sphere",
22
+ "box",
23
+ "capsule",
24
+ "solidSphere",
25
+ "solidBox",
26
+ "solidCapsule",
27
+ ];
28
+ /** The shapes that are solid geometry rather than stroked outlines. They draw
29
+ * through their own pipeline: no depth write, no culling, so a translucent
30
+ * volume reads as a volume and never hides the rig behind it. */
31
+ export const OVERLAY_SOLID_SHAPES = new Set([
32
+ "solidSphere",
33
+ "solidBox",
34
+ "solidCapsule",
35
+ ]);
36
+ /** Floats per instance in the pass's instance stream. */
37
+ export const OVERLAY_INSTANCE_FLOATS = 16;
38
+ /** Interleaved pos(3), dir(3), caps(2), side(1), t(1), mode(1) per vertex. */
39
+ export const OVERLAY_VERTEX_FLOATS = 11;
40
+ /** Radius of the marker's solid centre, as a fraction of the ring's. */
41
+ const DOT_RADIUS = 0.42;
42
+ /** Segments in a marker's ring and centre. They are small, always face the
43
+ * camera, and there are only a few hundred, so smoothness is nearly free. */
44
+ const MARKER_SEG = 48;
45
+ /** Segments around a sphere's great circles and a capsule's rings. */
46
+ const RING_SEG = 32;
47
+ /** Segments in one half-circle of a capsule cap (ring → pole → ring). */
48
+ const CAP_SEG = 16;
49
+ /** Dashes in a dashed unit line. */
50
+ const DASHES = 7;
51
+ /** Builds every unit wireframe back to back in one line-list vertex buffer. */
52
+ export function buildOverlayShapes() {
53
+ const v = [];
54
+ const ranges = {};
55
+ // Placement mode for the shape being emitted — see `placeWorld` in the shader.
56
+ let b = 0;
57
+ // A stroked segment: 6 vertices carrying each endpoint, the offset to the
58
+ // other, both cap signs, which side of the quad it is on, and its parameter
59
+ // along the segment. c0/c1 are the capsule cap signs — see the shader.
60
+ const seg = (p0, p1, c0 = 0, c1 = 0) => {
61
+ const dx = p1[0] - p0[0], dy = p1[1] - p0[1], dz = p1[2] - p0[2];
62
+ // prettier-ignore
63
+ v.push(p0[0], p0[1], p0[2], dx, dy, dz, c0, c1, -1, 0, b, p0[0], p0[1], p0[2], dx, dy, dz, c0, c1, 1, 0, b, p1[0], p1[1], p1[2], -dx, -dy, -dz, c1, c0, -1, 1, b, p0[0], p0[1], p0[2], dx, dy, dz, c0, c1, 1, 0, b, p1[0], p1[1], p1[2], -dx, -dy, -dz, c1, c0, 1, 1, b, p1[0], p1[1], p1[2], -dx, -dy, -dz, c1, c0, -1, 1, b);
64
+ };
65
+ // A filled triangle, for the shapes that are geometry rather than strokes.
66
+ const tri = (p0, p1, p2) => {
67
+ for (const p of [p0, p1, p2])
68
+ v.push(p[0], p[1], p[2], 0, 0, 0, 0, 0, 0, 0, b);
69
+ };
70
+ // A filled triangle whose vertices carry their own cap signs — the solid
71
+ // capsule needs it, since one quad of its wall straddles both hemispheres.
72
+ const triC = (p0, c0, p1, c1, p2, c2) => {
73
+ v.push(p0[0], p0[1], p0[2], 0, 0, 0, c0, 0, 0, 0, b);
74
+ v.push(p1[0], p1[1], p1[2], 0, 0, 0, c1, 0, 0, 0, b);
75
+ v.push(p2[0], p2[1], p2[2], 0, 0, 0, c2, 0, 0, 0, b);
76
+ };
77
+ // One rim vertex of the marker's ring: a direction, and which rim it is on.
78
+ const rim = (d, side) => {
79
+ v.push(d[0], d[1], d[2], 0, 0, 0, 0, 0, side, 0, b);
80
+ };
81
+ const open = (shape) => {
82
+ ranges[shape] = { first: v.length / OVERLAY_VERTEX_FLOATS, count: 0 };
83
+ };
84
+ const close = (shape) => {
85
+ ranges[shape].count = v.length / OVERLAY_VERTEX_FLOATS - ranges[shape].first;
86
+ };
87
+ // line — origin to +Y. scale.y is its length; point it with the rotation.
88
+ open("line");
89
+ seg([0, 0, 0], [0, 1, 0]);
90
+ close("line");
91
+ // dashedLine — the same run, broken up. A dash is geometry here rather than a
92
+ // fragment test, which is what a line pass can express without a shader for it.
93
+ open("dashedLine");
94
+ for (let i = 0; i < DASHES; i++) {
95
+ const a = i / DASHES;
96
+ const b = (i + 0.5) / DASHES;
97
+ seg([0, a, 0], [0, b, 0]);
98
+ }
99
+ close("dashedLine");
100
+ // axes — a cross through the origin, one segment per axis, ±scale.
101
+ open("axes");
102
+ seg([-1, 0, 0], [1, 0, 0]);
103
+ seg([0, -1, 0], [0, 1, 0]);
104
+ seg([0, 0, -1], [0, 0, 1]);
105
+ close("axes");
106
+ // circle — the bone marker's ring, facing the camera. Triangles between an
107
+ // inner and an outer rim, which the shader offsets radially in SCREEN space:
108
+ // the radius is world, the stroke is the same pixel width as the links, and
109
+ // the two meet without a step. `side` names the rim, -1 inner and +1 outer.
110
+ b = 4;
111
+ open("circle");
112
+ for (let i = 0; i < MARKER_SEG; i++) {
113
+ const a0 = (i / MARKER_SEG) * Math.PI * 2;
114
+ const a1 = ((i + 1) / MARKER_SEG) * Math.PI * 2;
115
+ const d0 = [Math.cos(a0), Math.sin(a0), 0];
116
+ const d1 = [Math.cos(a1), Math.sin(a1), 0];
117
+ rim(d0, -1);
118
+ rim(d0, 1);
119
+ rim(d1, 1);
120
+ rim(d0, -1);
121
+ rim(d1, 1);
122
+ rim(d1, -1);
123
+ }
124
+ close("circle");
125
+ // dot — the marker's solid centre. A plain camera-facing fan: mode 3, NOT the
126
+ // ring's 4, whose radial offset is undefined at a fan's centre vertex.
127
+ b = 3;
128
+ open("dot");
129
+ for (let i = 0; i < MARKER_SEG; i++) {
130
+ const a0 = (i / MARKER_SEG) * Math.PI * 2;
131
+ const a1 = ((i + 1) / MARKER_SEG) * Math.PI * 2;
132
+ tri([0, 0, 0], [Math.cos(a0), Math.sin(a0), 0], [Math.cos(a1), Math.sin(a1), 0]);
133
+ }
134
+ close("dot");
135
+ b = 2;
136
+ // link — the two lines from a marker's edge to its child. scale.x is the
137
+ // half-width at the base (the marker's radius), scale.y the distance to the
138
+ // child, and the rotation points +Y at it. Single segments, so there are no
139
+ // joins: the stroke can be as thick as it likes.
140
+ open("link");
141
+ seg([-1, 0, 0], [0, 1, 0]);
142
+ seg([1, 0, 0], [0, 1, 0]);
143
+ close("link");
144
+ b = 0;
145
+ // sphere — three great circles of radius 1.
146
+ open("sphere");
147
+ for (let plane = 0; plane < 3; plane++) {
148
+ for (let i = 0; i < RING_SEG; i++) {
149
+ const a0 = (i / RING_SEG) * Math.PI * 2;
150
+ const a1 = ((i + 1) / RING_SEG) * Math.PI * 2;
151
+ const c0 = Math.cos(a0), s0 = Math.sin(a0);
152
+ const c1 = Math.cos(a1), s1 = Math.sin(a1);
153
+ if (plane === 0)
154
+ seg([0, c0, s0], [0, c1, s1]);
155
+ else if (plane === 1)
156
+ seg([s0, 0, c0], [s1, 0, c1]);
157
+ else
158
+ seg([c0, s0, 0], [c1, s1, 0]);
159
+ }
160
+ }
161
+ close("sphere");
162
+ // box — the 12 edges of a cube of half-extent 1.
163
+ open("box");
164
+ for (let axis = 0; axis < 3; axis++) {
165
+ for (let a = -1; a <= 1; a += 2) {
166
+ for (let b = -1; b <= 1; b += 2) {
167
+ const p0 = [0, 0, 0];
168
+ const p1 = [0, 0, 0];
169
+ p0[axis] = -1;
170
+ p1[axis] = 1;
171
+ p0[(axis + 1) % 3] = p1[(axis + 1) % 3] = a;
172
+ p0[(axis + 2) % 3] = p1[(axis + 2) % 3] = b;
173
+ seg(p0, p1);
174
+ }
175
+ }
176
+ }
177
+ close("box");
178
+ // capsule — radius 1, with both hemispheres AT the origin and the cap sign
179
+ // saying which way `extent` pushes them. Nothing here is scaled along Y, so a
180
+ // long thin body keeps round caps.
181
+ open("capsule");
182
+ for (const capSign of [1, -1]) {
183
+ for (let i = 0; i < RING_SEG; i++) {
184
+ const a0 = (i / RING_SEG) * Math.PI * 2;
185
+ const a1 = ((i + 1) / RING_SEG) * Math.PI * 2;
186
+ seg([Math.cos(a0), 0, Math.sin(a0)], [Math.cos(a1), 0, Math.sin(a1)], capSign, capSign);
187
+ }
188
+ for (let plane = 0; plane < 2; plane++) {
189
+ for (let i = 0; i < CAP_SEG; i++) {
190
+ const a0 = (i / CAP_SEG) * Math.PI;
191
+ const a1 = ((i + 1) / CAP_SEG) * Math.PI;
192
+ const c0 = Math.cos(a0), s0 = Math.sin(a0) * capSign;
193
+ const c1 = Math.cos(a1), s1 = Math.sin(a1) * capSign;
194
+ if (plane === 0)
195
+ seg([c0, s0, 0], [c1, s1, 0], capSign, capSign);
196
+ else
197
+ seg([0, s0, c0], [0, s1, c1], capSign, capSign);
198
+ }
199
+ }
200
+ }
201
+ // The cylinder's four side lines. Both endpoints are the same point on the
202
+ // ring — the cap signs are what separate them, one pushed to +extent and one
203
+ // to -extent, so the line is exactly as long as the body is.
204
+ for (const [x, z] of [
205
+ [1, 0],
206
+ [0, 1],
207
+ [-1, 0],
208
+ [0, -1],
209
+ ]) {
210
+ seg([x, 0, z], [x, 0, z], 1, -1);
211
+ }
212
+ close("capsule");
213
+ // ── Solid volumes ────────────────────────────────────────────────
214
+ // Mode 5: placed in world like their wireframe twins, but filled, so they
215
+ // skip the stroke extrusion. Same size conventions — half-extents for the
216
+ // box, radius for the sphere, radius plus `extent` for the capsule.
217
+ b = 5;
218
+ const LAT = 10;
219
+ const LON = 24;
220
+ open("solidSphere");
221
+ for (let y = 0; y < LAT; y++) {
222
+ const p0 = (y / LAT) * Math.PI - Math.PI / 2;
223
+ const p1 = ((y + 1) / LAT) * Math.PI - Math.PI / 2;
224
+ for (let x = 0; x < LON; x++) {
225
+ const t0 = (x / LON) * Math.PI * 2;
226
+ const t1 = ((x + 1) / LON) * Math.PI * 2;
227
+ const at = (phi, theta) => [
228
+ Math.cos(phi) * Math.cos(theta),
229
+ Math.sin(phi),
230
+ Math.cos(phi) * Math.sin(theta),
231
+ ];
232
+ tri(at(p0, t0), at(p1, t0), at(p1, t1));
233
+ tri(at(p0, t0), at(p1, t1), at(p0, t1));
234
+ }
235
+ }
236
+ close("solidSphere");
237
+ open("solidBox");
238
+ for (let axis = 0; axis < 3; axis++) {
239
+ for (const face of [-1, 1]) {
240
+ const corner = (u, w) => {
241
+ const q = [0, 0, 0];
242
+ q[axis] = face;
243
+ q[(axis + 1) % 3] = u;
244
+ q[(axis + 2) % 3] = w;
245
+ return q;
246
+ };
247
+ tri(corner(-1, -1), corner(1, -1), corner(1, 1));
248
+ tri(corner(-1, -1), corner(1, 1), corner(-1, 1));
249
+ }
250
+ }
251
+ close("solidBox");
252
+ open("solidCapsule");
253
+ for (let x = 0; x < LON; x++) {
254
+ const t0 = (x / LON) * Math.PI * 2;
255
+ const t1 = ((x + 1) / LON) * Math.PI * 2;
256
+ const r0 = [Math.cos(t0), 0, Math.sin(t0)];
257
+ const r1 = [Math.cos(t1), 0, Math.sin(t1)];
258
+ // The wall: one quad from the top cap's rim to the bottom cap's.
259
+ triC(r0, 1, r0, -1, r1, -1);
260
+ triC(r0, 1, r1, -1, r1, 1);
261
+ // The two hemispheres, each entirely on its own cap.
262
+ for (const capSign of [1, -1]) {
263
+ for (let y = 0; y < LAT; y++) {
264
+ const p0 = (y / LAT) * (Math.PI / 2);
265
+ const p1 = ((y + 1) / LAT) * (Math.PI / 2);
266
+ const at = (phi, theta) => [
267
+ Math.cos(phi) * Math.cos(theta),
268
+ Math.sin(phi) * capSign,
269
+ Math.cos(phi) * Math.sin(theta),
270
+ ];
271
+ triC(at(p0, t0), capSign, at(p1, t0), capSign, at(p1, t1), capSign);
272
+ triC(at(p0, t0), capSign, at(p1, t1), capSign, at(p0, t1), capSign);
273
+ }
274
+ }
275
+ }
276
+ close("solidCapsule");
277
+ b = 0;
278
+ return { vertices: new Float32Array(v), ranges };
279
+ }
280
+ /** Packs one primitive into the pass's instance layout at `offset` floats. */
281
+ export function writeOverlayInstance(p, out, offset) {
282
+ const r = p.rotation ?? IDENTITY_ROT;
283
+ const s = p.scale ?? UNIT_SCALE;
284
+ out[offset + 0] = r[0];
285
+ out[offset + 1] = r[1];
286
+ out[offset + 2] = r[2];
287
+ out[offset + 3] = r[3];
288
+ out[offset + 4] = p.position[0];
289
+ out[offset + 5] = p.position[1];
290
+ out[offset + 6] = p.position[2];
291
+ out[offset + 7] = p.extent ?? 0;
292
+ out[offset + 8] = s[0];
293
+ out[offset + 9] = s[1];
294
+ out[offset + 10] = s[2];
295
+ out[offset + 11] = p.thickness ?? OVERLAY_STYLE.strokePx;
296
+ out[offset + 12] = p.color[0];
297
+ out[offset + 13] = p.color[1];
298
+ out[offset + 14] = p.color[2];
299
+ out[offset + 15] = p.color[3];
300
+ }
301
+ const IDENTITY_ROT = [0, 0, 0, 1];
302
+ const UNIT_SCALE = [1, 1, 1];
303
+ /**
304
+ * Every bone's marker position in world space, root transform included.
305
+ *
306
+ * THE SAME points boneOverlay puts markers at, which is the whole reason it is a
307
+ * function rather than a loop inside the picker: a hit test that recomputes
308
+ * where a marker "should" be is a hit test that drifts away from the one drawn.
309
+ */
310
+ export function boneMarkerPositions(model, out) {
311
+ const world = model.getWorldMatrices();
312
+ const dst = out && out.length >= world.length * 3 ? out : new Float32Array(world.length * 3);
313
+ const root = rootTransform(model);
314
+ const local = new Vec3(0, 0, 0);
315
+ for (let i = 0; i < world.length; i++) {
316
+ const m = world[i].values;
317
+ if (root.identity) {
318
+ dst[i * 3] = m[12];
319
+ dst[i * 3 + 1] = m[13];
320
+ dst[i * 3 + 2] = m[14];
321
+ continue;
322
+ }
323
+ local.setXYZ(m[12] * root.scale, m[13] * root.scale, m[14] * root.scale);
324
+ const r = Quat.rotateVec(root.rotation, local);
325
+ dst[i * 3] = r.x + root.position.x;
326
+ dst[i * 3 + 1] = r.y + root.position.y;
327
+ dst[i * 3 + 2] = r.z + root.position.z;
328
+ }
329
+ return dst;
330
+ }
331
+ /**
332
+ * How big this model is, from its bone positions — the reference every default
333
+ * size here is a fraction of, so one set of numbers suits a 1-unit model and a
334
+ * 100-unit one.
335
+ */
336
+ function skeletonExtent(world) {
337
+ let minY = Infinity, maxY = -Infinity, minX = Infinity, maxX = -Infinity;
338
+ for (const m of world) {
339
+ const v = m.values;
340
+ if (v[12] < minX)
341
+ minX = v[12];
342
+ if (v[12] > maxX)
343
+ maxX = v[12];
344
+ if (v[13] < minY)
345
+ minY = v[13];
346
+ if (v[13] > maxY)
347
+ maxY = v[13];
348
+ }
349
+ return Math.max(maxY - minY, maxX - minX, 1e-4);
350
+ }
351
+ /**
352
+ * The overlay's colours, and the only place they live.
353
+ *
354
+ * NOT an option a host passes. An overlay means the same thing in every product
355
+ * that draws one — orange is a bone the solver drives, whichever app you are
356
+ * looking at — and a colour each app picks for itself is a colour that drifts.
357
+ * A host that wants its own look builds its own list through setOverlay.
358
+ *
359
+ * Every value is one step of the same ramp (Tailwind's 500), so the classes read
360
+ * as a set and hue is the only thing that separates them.
361
+ *
362
+ * The rig is one colour and selection is another, and that is the whole scheme.
363
+ * Every class that had a hue of its own lost it: on a 147-bone skeleton the
364
+ * distinctions were spent on bones nobody was hunting for, and the panel is
365
+ * where those belong. The classes remain so a panel can filter on them.
366
+ *
367
+ * Red is spent entirely on `selected`, the one distinction that must never be
368
+ * ambiguous, and green is not used at all — red-green being the confusion most
369
+ * colour blindness causes.
370
+ *
371
+ * A physics-driven bone and a dynamic body share orange on purpose. They are the
372
+ * same fact seen twice — the solver owns this — and colouring them apart would
373
+ * invent a difference that is not there.
374
+ *
375
+ * The CATEGORIES are MMD's, straight out of the PMX file — IK chains, 付与親,
376
+ * 軸制限, a body's physics mode. The colours are not: MMD has no canonical
377
+ * palette for them.
378
+ */
379
+ /**
380
+ * Every number the overlay's LOOK depends on, in one place beside the colours,
381
+ * and for the same reason: an overlay should read the same in every product that
382
+ * draws one, so none of this is an option a host passes.
383
+ *
384
+ * A host that wants its own look builds its own list through setOverlay.
385
+ */
386
+ export const OVERLAY_STYLE = {
387
+ /** Stroke width for the rig, in DEVICE pixels — half this on screen at 2x. */
388
+ strokePx: 2,
389
+ /** Stroke width for the mesh wireframe. Thinner: there are tens of thousands
390
+ * of edges and they are a ground for the rig, not the subject. */
391
+ meshStrokePx: 2,
392
+ /** How much heavier the selected item draws, on both stroke and marker. */
393
+ selectedScale: 1.6,
394
+ /** Bone marker radius for a bone that governs the whole model, as a fraction
395
+ * of the skeleton's extent — so one number suits any model scale. */
396
+ markerSize: 0.005,
397
+ /** The reach a full-size marker corresponds to, as a fraction of the extent. */
398
+ markerReference: 0.2,
399
+ /** Marker radius is held between these multiples of markerSize. */
400
+ markerMin: 0.55,
401
+ markerMax: 1.0,
402
+ /** Floor on a leaf's reach, world units, so a tip still has a marker. */
403
+ minReach: 0.25,
404
+ /** Joint cross arm length, as a fraction of the skeleton's extent. */
405
+ jointSize: 0.006,
406
+ /** Stroke for the material borders drawn over the mesh wireframe. */
407
+ seamStrokePx: 3,
408
+ /** Stroke for the material under the pointer — the same self-occluding reveal
409
+ * a pick draws, layered over the section-wide view rather than replacing it,
410
+ * and heavier than either a pick's own edges or a seam so it still pops out
411
+ * of a view that already has two other things drawn in it. */
412
+ hoverStrokePx: 4,
413
+ /** How strongly the triangulation draws under those borders.
414
+ *
415
+ * Both at full strength is one undifferentiated field of yellow — forty
416
+ * thousand edges saying what the model is made of the way a page of hex says
417
+ * what a file contains. The borders are the answer and the triangulation is
418
+ * the ground it reads against, so the ground steps back. */
419
+ meshAlpha: 0.55,
420
+ };
421
+ export const DEFAULT_BONE_PALETTE = {
422
+ /** Nothing in particular drives it — most of the skeleton, so it is the colour
423
+ * the whole overlay reads as. */
424
+ plain: [0.231, 0.51, 0.965, 1], // blue-500
425
+ /** Has an IK chain (ikLinks): the leg IK bones and friends. Blue, the same as
426
+ * plain — a colour of its own bought nothing here. Leg IK sits on the floor
427
+ * and reaches almost nothing, so these are the smallest markers in the rig
428
+ * and mostly overlapped by the foot bones they share a place with; no hue
429
+ * rescues that. If they need to stand out later it is their SIZE that has to
430
+ * change, since a control bone's length means nothing to begin with. */
431
+ ik: [0.231, 0.51, 0.965, 1], // blue-500
432
+ /** A dynamic rigidbody moves it — hair, skirt, anything the solver owns. Blue,
433
+ * the same as plain: which bones the solver owns is the rigidbody layer's
434
+ * question, and it already answers it. Kept as a class so a panel can pick it
435
+ * out, spending no colour on it here. */
436
+ physics: [0.231, 0.51, 0.965, 1], // blue-500
437
+ /** The one you picked. Red is spent entirely here and used nowhere else, so
438
+ * nothing on screen can be mistaken for a selection. */
439
+ selected: [0.937, 0.267, 0.267, 1], // red-500
440
+ };
441
+ /**
442
+ * The skeleton, MMD's way: a ring with a solid centre at every bone, and two
443
+ * lines from that ring converging on the child's.
444
+ *
445
+ * Ring and centre are filled geometry facing the camera, so a marker is a circle
446
+ * from any angle and can be heavy without a stroke wide enough to tear itself
447
+ * apart. The two link lines leave the ring at its edge — the taper is what says
448
+ * which way the bone points, where a single line between centres would not.
449
+ */
450
+ export function boneOverlay(model, options = {}) {
451
+ const bones = model.getSkeleton().bones;
452
+ const world = model.getWorldMatrices();
453
+ const palette = DEFAULT_BONE_PALETTE;
454
+ const include = options.include ? new Set(options.include) : null;
455
+ const extent = skeletonExtent(world);
456
+ const markerSize = extent * OVERLAY_STYLE.markerSize;
457
+ const physicsBones = new Set();
458
+ for (const rb of model.getRigidbodies()) {
459
+ if (rb.type !== RigidbodyType.Static && rb.boneIndex >= 0)
460
+ physicsBones.add(rb.boneIndex);
461
+ }
462
+ // Marker radius per bone, from its REACH: the longest chain still hanging off
463
+ // it, head to furthest descendant.
464
+ //
465
+ // Not the bone's own length, which is what this measured before. A bone's own
466
+ // length says nothing about its place in the rig — every segment of a hair
467
+ // strand is the same short segment, so a strand's ROOT scored no larger than
468
+ // its tip, and nothing stopped a child's segment being longer than its
469
+ // parent's and drawing bigger. Reach cannot do either: a parent's reach is its
470
+ // child's plus the step between them, so a marker never grows going down a
471
+ // chain, and a bone that governs an arm, a strand or a whole side of the body
472
+ // is marked for what it carries rather than for the gap to its first child.
473
+ const depth = new Int32Array(bones.length);
474
+ for (let i = 0; i < bones.length; i++) {
475
+ let d = 0;
476
+ let c = bones[i].parentIndex;
477
+ // Guarded: a malformed file can cycle, and this must not hang on one.
478
+ while (c >= 0 && c < bones.length && d < bones.length) {
479
+ d++;
480
+ c = bones[c].parentIndex;
481
+ }
482
+ depth[i] = d;
483
+ }
484
+ const deepestFirst = Array.from(bones.keys()).sort((a, b) => depth[b] - depth[a]);
485
+ const reach = new Float32Array(bones.length);
486
+ for (const i of deepestFirst) {
487
+ const p = bones[i].parentIndex;
488
+ if (p < 0 || p >= world.length || i >= world.length)
489
+ continue;
490
+ const m = world[p].values;
491
+ const cm = world[i].values;
492
+ const step = Math.hypot(cm[12] - m[12], cm[13] - m[13], cm[14] - m[14]);
493
+ if (reach[i] + step > reach[p])
494
+ reach[p] = reach[i] + step;
495
+ }
496
+ // Square-rooted, so the spread stays legible: without it the root bone's reach
497
+ // is the whole model and everything below it collapses into the floor.
498
+ const sizes = new Float32Array(bones.length);
499
+ const referenceReach = Math.max(extent * OVERLAY_STYLE.markerReference, 1e-6);
500
+ for (let i = 0; i < bones.length; i++) {
501
+ const r = Math.sqrt(Math.max(reach[i], OVERLAY_STYLE.minReach) / referenceReach);
502
+ sizes[i] = markerSize * Math.min(Math.max(r, OVERLAY_STYLE.markerMin), OVERLAY_STYLE.markerMax);
503
+ }
504
+ // MMD stacks control bones on one point — 全ての親, センター and グルーブ all sit
505
+ // at the hips. Sized per bone they draw as a nest of rings, so a position gets
506
+ // ONE marker, the largest asked for there. Links are still drawn for every
507
+ // bone: it is the markers that overlap, not the connections.
508
+ const grid = Math.max(extent * 1e-4, 1e-6);
509
+ const key = (m) => `${Math.round(m[12] / grid)},${Math.round(m[13] / grid)},${Math.round(m[14] / grid)}`;
510
+ const markerAt = new Map();
511
+ for (let i = 0; i < bones.length && i < world.length; i++) {
512
+ if (include && !include.has(bones[i].name))
513
+ continue;
514
+ const k = key(world[i].values);
515
+ const held = markerAt.get(k);
516
+ if (held === undefined || sizes[i] > sizes[held])
517
+ markerAt.set(k, i);
518
+ }
519
+ const root = rootTransform(model);
520
+ const out = [];
521
+ const head = new Vec3(0, 0, 0);
522
+ const tail = new Vec3(0, 0, 0);
523
+ const dir = new Vec3(0, 0, 0);
524
+ for (let i = 0; i < bones.length && i < world.length; i++) {
525
+ const bone = bones[i];
526
+ if (include && !include.has(bone.name))
527
+ continue;
528
+ const m = world[i].values;
529
+ head.setXYZ(m[12], m[13], m[14]);
530
+ const selected = options.selected != null && bone.name === options.selected;
531
+ const cls = selected
532
+ ? "selected"
533
+ : bone.ikLinks && bone.ikLinks.length > 0
534
+ ? "ik"
535
+ : physicsBones.has(i)
536
+ ? "physics"
537
+ : "plain";
538
+ const color = palette[cls];
539
+ const size = selected ? sizes[i] * OVERLAY_STYLE.selectedScale : sizes[i];
540
+ const stroke = selected ? OVERLAY_STYLE.strokePx * OVERLAY_STYLE.selectedScale : OVERLAY_STYLE.strokePx;
541
+ // One link per bone, from its PARENT down to it — so every parent-child
542
+ // edge is drawn exactly once and branches all show.
543
+ //
544
+ // Not "from this bone to its first child", which is what this did and what
545
+ // made a thigh point at the wrong place: children are ordered by bone INDEX,
546
+ // not by anatomy, so 左足's children[0] is 左腿物理, a physics bone hanging
547
+ // off the thigh, and 左ひざ — the actual knee — came second. There is no
548
+ // ordering of children that is reliably the continuation. Drawing all of
549
+ // them removes the need to guess.
550
+ const parent = bone.parentIndex;
551
+ if (parent >= 0 && parent < world.length) {
552
+ const pm = world[parent].values;
553
+ tail.setXYZ(pm[12], pm[13], pm[14]);
554
+ const span = Math.hypot(head.x - tail.x, head.y - tail.y, head.z - tail.z);
555
+ if (span > 1e-5) {
556
+ dir.setXYZ((head.x - tail.x) / span, (head.y - tail.y) / span, (head.z - tail.z) / span);
557
+ const rot = Quat.fromUnitVectors(UP, dir);
558
+ out.push(applyRoot(root, {
559
+ shape: "link",
560
+ // The taper starts at the PARENT's marker and narrows onto this one,
561
+ // so its base half-width is the parent's radius.
562
+ position: [tail.x, tail.y, tail.z],
563
+ rotation: [rot.x, rot.y, rot.z, rot.w],
564
+ scale: [sizes[parent], span, sizes[parent]],
565
+ color,
566
+ thickness: stroke,
567
+ }));
568
+ }
569
+ }
570
+ // Only the bone that owns this position draws the marker.
571
+ if (markerAt.get(key(m)) !== i)
572
+ continue;
573
+ out.push(applyRoot(root, {
574
+ shape: "circle",
575
+ position: [head.x, head.y, head.z],
576
+ scale: [size, size, size],
577
+ color,
578
+ // The same stroke the links carry, so they read as one line.
579
+ thickness: stroke,
580
+ }));
581
+ out.push(applyRoot(root, {
582
+ shape: "dot",
583
+ position: [head.x, head.y, head.z],
584
+ scale: [size * DOT_RADIUS, size * DOT_RADIUS, size * DOT_RADIUS],
585
+ color,
586
+ }));
587
+ }
588
+ return out;
589
+ }
590
+ /** Solid volumes, so alpha does the work a stroke used to: low enough to see the
591
+ * body through them and the rig behind them. Cool follows its bone, warm is
592
+ * driven by the solver. */
593
+ export const DEFAULT_RIGIDBODY_PALETTE = {
594
+ static: [0.055, 0.647, 0.914, 0.28], // sky-500
595
+ dynamic: [0.976, 0.451, 0.086, 0.38], // orange-500
596
+ selected: [0.937, 0.267, 0.267, 0.55], // red-500
597
+ };
598
+ /** The joint cross, the dashed lines to the bodies it holds, and its selection. */
599
+ export const DEFAULT_JOINT_PALETTE = {
600
+ cross: [0.055, 0.647, 0.914, 1], // sky-500
601
+ link: [0.055, 0.647, 0.914, 0.5], // sky-500
602
+ selected: [0.937, 0.267, 0.267, 1], // red-500
603
+ };
604
+ /** The mesh wireframe. Yellow, because it has to carry over pale skin and dark
605
+ * cloth alike and the rig owns the blues and greens. */
606
+ // yellow-400, opaque. yellow-500 measured correct on the canvas and still read
607
+ // as gold: a 2px stroke through 4x MSAA is mostly partial-coverage pixels, so a
608
+ // dense mesh of lines lands well under the flat swatch it is cut from. The step
609
+ // up pays that back rather than the pipeline being wrong.
610
+ export const DEFAULT_VERTEX_COLOR = [0.98, 0.8, 0.082, 1];
611
+ /**
612
+ * The sphere, box or capsule each rigidbody actually is, where it actually is.
613
+ *
614
+ * With physics running the transforms come from the simulation, so a body drawn
615
+ * here is the body the solver collides — which is the point of drawing them
616
+ * while tuning cloth. Without it they fall back to the bone-driven bind pose.
617
+ */
618
+ export function rigidbodyOverlay(model, physics, options = {}) {
619
+ const bodies = model.getRigidbodies();
620
+ const palette = DEFAULT_RIGIDBODY_PALETTE;
621
+ const include = options.include ? new Set(options.include) : null;
622
+ const root = rootTransform(model);
623
+ const pos = new Vec3(0, 0, 0);
624
+ const rot = new Quat(0, 0, 0, 1);
625
+ const out = [];
626
+ for (let i = 0; i < bodies.length; i++) {
627
+ const rb = bodies[i];
628
+ if (include && !include.has(rb.name))
629
+ continue;
630
+ resolveBodyTransform(model, physics, i, pos, rot);
631
+ const selected = options.selected != null && rb.name === options.selected;
632
+ const cls = selected ? "selected" : rb.type === RigidbodyType.Static ? "static" : "dynamic";
633
+ const color = palette[cls];
634
+ if (rb.shape === RigidbodyShape.Sphere) {
635
+ const r = rb.size.x;
636
+ out.push(applyRoot(root, {
637
+ shape: "solidSphere",
638
+ position: [pos.x, pos.y, pos.z],
639
+ rotation: [rot.x, rot.y, rot.z, rot.w],
640
+ scale: [r, r, r],
641
+ color,
642
+ }));
643
+ }
644
+ else if (rb.shape === RigidbodyShape.Box) {
645
+ out.push(applyRoot(root, {
646
+ shape: "solidBox",
647
+ position: [pos.x, pos.y, pos.z],
648
+ rotation: [rot.x, rot.y, rot.z, rot.w],
649
+ // PMX box size is half-extents already.
650
+ scale: [rb.size.x, rb.size.y, rb.size.z],
651
+ color,
652
+ }));
653
+ }
654
+ else {
655
+ const r = rb.size.x;
656
+ out.push(applyRoot(root, {
657
+ shape: "solidCapsule",
658
+ position: [pos.x, pos.y, pos.z],
659
+ rotation: [rot.x, rot.y, rot.z, rot.w],
660
+ scale: [r, r, r],
661
+ // PMX capsule size.y is the CYLINDER's length, caps not included.
662
+ extent: rb.size.y * 0.5,
663
+ color,
664
+ }));
665
+ }
666
+ }
667
+ return out;
668
+ }
669
+ /**
670
+ * An axis cross at each joint, plus a dashed line to each body it constrains.
671
+ *
672
+ * The cross is where and how the constraint frame sits; the two dashed lines
673
+ * are the pair it holds together, which is the question actually being asked of
674
+ * a skirt with two hundred of them. Dashed, because a relationship should not
675
+ * read as a thing.
676
+ */
677
+ export function jointOverlay(model, physics, options = {}) {
678
+ const joints = model.getJoints();
679
+ const bodies = model.getRigidbodies();
680
+ const extent = skeletonExtent(model.getWorldMatrices());
681
+ const size = extent * OVERLAY_STYLE.jointSize;
682
+ const color = DEFAULT_JOINT_PALETTE.cross;
683
+ const linkColor = DEFAULT_JOINT_PALETTE.link;
684
+ const selectedColor = DEFAULT_JOINT_PALETTE.selected;
685
+ const links = options.links ?? true;
686
+ const include = options.include ? new Set(options.include) : null;
687
+ const root = rootTransform(model);
688
+ const jointPos = new Vec3(0, 0, 0);
689
+ const bodyPos = new Vec3(0, 0, 0);
690
+ const bodyRot = new Quat(0, 0, 0, 1);
691
+ const out = [];
692
+ for (const joint of joints) {
693
+ if (include && !include.has(joint.name))
694
+ continue;
695
+ const selected = options.selected != null && joint.name === options.selected;
696
+ const crossColor = selected ? selectedColor : color;
697
+ // A joint's frame is authored in bind pose, so drawing it there would leave
698
+ // every joint on a simulating skirt behind. Carry it on body A instead:
699
+ // local = bindA⁻¹ · bindJoint, posed = poseA · local.
700
+ jointPos.setXYZ(joint.position.x, joint.position.y, joint.position.z);
701
+ const rot = Quat.fromEuler(joint.rotation.x, joint.rotation.y, joint.rotation.z);
702
+ const a = joint.rigidbodyIndexA;
703
+ if (a >= 0 && a < bodies.length) {
704
+ resolveBodyTransform(model, physics, a, bodyPos, bodyRot);
705
+ const bind = bodies[a];
706
+ const bindRot = Quat.fromEuler(bind.shapeRotation.x, bind.shapeRotation.y, bind.shapeRotation.z);
707
+ const localPos = Quat.rotateVecInv(bindRot, new Vec3(joint.position.x - bind.shapePosition.x, joint.position.y - bind.shapePosition.y, joint.position.z - bind.shapePosition.z));
708
+ const localRot = Quat.conjugateInto(bindRot, new Quat(0, 0, 0, 1)).multiply(rot);
709
+ const carried = Quat.rotateVec(bodyRot, localPos);
710
+ jointPos.setXYZ(bodyPos.x + carried.x, bodyPos.y + carried.y, bodyPos.z + carried.z);
711
+ rot.set(bodyRot.multiply(localRot).normalize());
712
+ }
713
+ out.push(applyRoot(root, {
714
+ shape: "axes",
715
+ position: [jointPos.x, jointPos.y, jointPos.z],
716
+ rotation: [rot.x, rot.y, rot.z, rot.w],
717
+ scale: [size, size, size],
718
+ color: crossColor,
719
+ thickness: selected ? OVERLAY_STYLE.strokePx * OVERLAY_STYLE.selectedScale : OVERLAY_STYLE.strokePx,
720
+ }));
721
+ if (!links)
722
+ continue;
723
+ for (const idx of [joint.rigidbodyIndexA, joint.rigidbodyIndexB]) {
724
+ if (idx < 0 || idx >= bodies.length)
725
+ continue;
726
+ resolveBodyTransform(model, physics, idx, bodyPos, bodyRot);
727
+ const line = lineBetween([jointPos.x, jointPos.y, jointPos.z], [bodyPos.x, bodyPos.y, bodyPos.z], selected ? selectedColor : linkColor, true);
728
+ if (line) {
729
+ line.thickness = OVERLAY_STYLE.strokePx;
730
+ out.push(applyRoot(root, line));
731
+ }
732
+ }
733
+ }
734
+ return out;
735
+ }
736
+ /** A segment from `a` to `b`, solid or dashed. Null when the two coincide. */
737
+ export function lineBetween(a, b, color, dashed = false) {
738
+ const dx = b[0] - a[0], dy = b[1] - a[1], dz = b[2] - a[2];
739
+ const length = Math.hypot(dx, dy, dz);
740
+ if (length < 1e-6)
741
+ return null;
742
+ const dir = new Vec3(dx / length, dy / length, dz / length);
743
+ const rot = Quat.fromUnitVectors(UP, dir);
744
+ return {
745
+ shape: dashed ? "dashedLine" : "line",
746
+ position: a,
747
+ rotation: [rot.x, rot.y, rot.z, rot.w],
748
+ scale: [1, length, 1],
749
+ color,
750
+ };
751
+ }
752
+ const UP = new Vec3(0, 1, 0);
753
+ // ──────────────────────────────────────────────────────────────────
754
+ // Shared
755
+ /**
756
+ * Where body `index` is right now: the simulation's answer when physics is
757
+ * running, the bone-driven bind transform when it is not.
758
+ */
759
+ function resolveBodyTransform(model, physics, index, outPos, outRot) {
760
+ if (physics) {
761
+ // RigidBodyStore is [...model bodies, floor], so a model index is a store
762
+ // index and needs no mapping.
763
+ const store = physics.getStore();
764
+ if (index < store.count) {
765
+ const i3 = index * 3;
766
+ const i4 = index * 4;
767
+ outPos.setXYZ(store.positions[i3], store.positions[i3 + 1], store.positions[i3 + 2]);
768
+ outRot.setXYZW(store.orientations[i4], store.orientations[i4 + 1], store.orientations[i4 + 2], store.orientations[i4 + 3]);
769
+ return;
770
+ }
771
+ }
772
+ const rb = model.getRigidbodies()[index];
773
+ const world = model.getWorldMatrices();
774
+ if (rb.bodyOffsetMatrix && rb.boneIndex >= 0 && rb.boneIndex < world.length) {
775
+ const m = world[rb.boneIndex].multiply(rb.bodyOffsetMatrix);
776
+ outPos.setXYZ(m.values[12], m.values[13], m.values[14]);
777
+ outRot.set(m.toQuat().normalize());
778
+ return;
779
+ }
780
+ outPos.setXYZ(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z);
781
+ outRot.set(Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z));
782
+ }
783
+ /** The model's scene placement, which bone and body transforms do NOT carry —
784
+ * getSkinMatrices applies it on the way to the GPU, so an overlay has to too. */
785
+ function rootTransform(model) {
786
+ const p = model.position;
787
+ const r = model.rotation;
788
+ const s = model.scale;
789
+ const identity = p.x === 0 && p.y === 0 && p.z === 0 && r.x === 0 && r.y === 0 && r.z === 0 && r.w === 1 && s === 1;
790
+ return { identity, position: p, rotation: r, scale: s };
791
+ }
792
+ function applyRoot(root, p) {
793
+ if (root.identity)
794
+ return p;
795
+ const local = new Vec3(p.position[0] * root.scale, p.position[1] * root.scale, p.position[2] * root.scale);
796
+ const rotated = Quat.rotateVec(root.rotation, local);
797
+ const rot = p.rotation ?? IDENTITY_ROT;
798
+ const worldRot = root.rotation.multiply(new Quat(rot[0], rot[1], rot[2], rot[3])).normalize();
799
+ const s = p.scale ?? UNIT_SCALE;
800
+ p.position = [rotated.x + root.position.x, rotated.y + root.position.y, rotated.z + root.position.z];
801
+ p.rotation = [worldRot.x, worldRot.y, worldRot.z, worldRot.w];
802
+ p.scale = [s[0] * root.scale, s[1] * root.scale, s[2] * root.scale];
803
+ if (p.extent)
804
+ p.extent = p.extent * root.scale;
805
+ return p;
806
+ }