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.
- package/dist/engine.d.ts +210 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1063 -11
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +10 -0
- package/dist/overlay.d.ts +200 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +806 -0
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/pmx-document.d.ts +218 -0
- package/dist/pmx-document.d.ts.map +1 -0
- package/dist/pmx-document.js +685 -0
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/overlay.d.ts +3 -0
- package/dist/shaders/passes/overlay.d.ts.map +1 -0
- package/dist/shaders/passes/overlay.js +164 -0
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/wireframe.d.ts +2 -0
- package/dist/shaders/passes/wireframe.d.ts.map +1 -0
- package/dist/shaders/passes/wireframe.js +107 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/package.json +1 -1
- package/src/engine.ts +1147 -13
- package/src/index.ts +50 -0
- package/src/model.ts +9 -0
- package/src/overlay.ts +1000 -0
- package/src/pmx-document.ts +882 -0
- package/src/shaders/passes/overlay.ts +166 -0
- package/src/shaders/passes/wireframe.ts +108 -0
package/dist/overlay.js
ADDED
|
@@ -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
|
+
}
|