reze-engine 0.50.4 → 0.50.6

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/model.js CHANGED
@@ -13,6 +13,10 @@ const _animSlerp = new Quat(0, 0, 0, 1);
13
13
  const _animInterpT = new Vec3(0, 0, 0);
14
14
  const _convOut = new Vec3(0, 0, 0);
15
15
  const _convMat = new Float32Array(16);
16
+ // Scratch for the post-physics append recovery — see applyPhysicsAppend.
17
+ const _appendBasisX = new Vec3(0, 0, 0);
18
+ const _appendBasisY = new Vec3(0, 0, 0);
19
+ const _appendBasisZ = new Vec3(0, 0, 0);
16
20
  // Blend-path scratch: per-entry sample target and the crossfade's two fixed entries.
17
21
  const _blendQ = new Quat(0, 0, 0, 1);
18
22
  const _blendT = new Vec3(0, 0, 0);
@@ -118,6 +122,15 @@ export class Model {
118
122
  this.rootMatrixValues = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
119
123
  this.rootMatrixDirty = false;
120
124
  this.rootIsIdentity = true;
125
+ /** 1 where the simulation overwrites the bone's world matrix. See setPhysicsDrivenBones. */
126
+ this.physicsDriven = null;
127
+ /** Bones to recompute after a step, in deform order — null when no rig needs it. */
128
+ this.physicsAppendOrder = null;
129
+ /** Simulated bones something inherits from; their local rotation is recovered. */
130
+ this.physicsAppendSources = null;
131
+ /** Recovered post-step local rotations, kept OUT of localRotations on purpose. */
132
+ this.appendRotOverride = null;
133
+ this.appendRotOverrideSet = null;
121
134
  this.tweenTimeMs = 0; // Time tracking for tweens (milliseconds)
122
135
  // Animation: state and multiple slots (idle, walk, attack, etc.); commit/rollback for action-game style
123
136
  this.animationState = new AnimationState();
@@ -1328,11 +1341,21 @@ export class Model {
1328
1341
  const t = (f - frames[i - 1]) / (frames[i] - frames[i - 1]);
1329
1342
  return values[i - 1] + (values[i] - values[i - 1]) * t;
1330
1343
  }
1331
- exportVmd(name) {
1344
+ /**
1345
+ * The clip called `name` as VMD bytes.
1346
+ *
1347
+ * `tracks` splits the file the same way `loadVmd`'s option splits what it
1348
+ * reads: "motion" is the dance (bones + IK), "morphs" is the expression file
1349
+ * to lay over one, "all" (the default) is both in one file as MMD exports it.
1350
+ * A clip missing the half you asked for still writes — an empty motion or an
1351
+ * empty expression file is a valid VMD, and is the honest answer to "export
1352
+ * the morphs" for a clip that has none.
1353
+ */
1354
+ exportVmd(name, options) {
1332
1355
  const clip = this.animationState.getAnimationClip(name);
1333
1356
  if (!clip)
1334
1357
  throw new Error(`Animation clip "${name}" not found`);
1335
- return new VMDWriter().write(clip);
1358
+ return new VMDWriter().write(clip, options);
1336
1359
  }
1337
1360
  play(name, options) {
1338
1361
  this.clipApplySuspended = false;
@@ -2175,7 +2198,139 @@ export class Model {
2175
2198
  worldMat.values.set(localMVals);
2176
2199
  }
2177
2200
  }
2178
- computeWorldMatrices() {
2201
+ /**
2202
+ * Which bones the physics simulation overwrites, and what that costs the
2203
+ * append (付与) pass — the 胸 problem.
2204
+ *
2205
+ * PMX lets a bone inherit a fraction of another bone's rotation (付与親 /
2206
+ * append parent). The pipeline computes that inheritance inside
2207
+ * computeWorldMatrices, which runs BEFORE physics — and it reads the
2208
+ * parent's LOCAL rotation, which physics never writes: the simulation
2209
+ * publishes world matrices only. So when a rig hangs a bone off a simulated
2210
+ * one, the inheritance saw the animated pose and nothing else, and the
2211
+ * dependent bone sat still no matter how much the parent swung. Rigs that
2212
+ * drive a chest this way — a simulated bone with the visible bones
2213
+ * appending from it — produced no motion at all.
2214
+ *
2215
+ * The engine calls this once, after building the simulation, and it
2216
+ * precomputes the whole answer: WHICH bones need revisiting after a step,
2217
+ * in deform order. Everything downstream is a walk over that list.
2218
+ */
2219
+ setPhysicsDrivenBones(boneIndices) {
2220
+ const bones = this.skeleton.bones;
2221
+ const n = bones.length;
2222
+ this.physicsDriven = new Uint8Array(n);
2223
+ for (const b of boneIndices)
2224
+ if (b >= 0 && b < n)
2225
+ this.physicsDriven[b] = 1;
2226
+ // Bones to recompute after a step: anything that INHERITS from a simulated
2227
+ // bone, everything under it, and anything inheriting from those in turn.
2228
+ // Simulated bones themselves are deliberately excluded — their world matrix
2229
+ // IS the simulation's output, and recomputing it from a local pose the
2230
+ // simulation never wrote would throw the step away.
2231
+ const affected = new Uint8Array(n);
2232
+ let changed = true;
2233
+ while (changed) {
2234
+ changed = false;
2235
+ for (let k = 0; k < n; k++) {
2236
+ const i = this.deformOrder[k];
2237
+ if (affected[i] || this.physicsDriven[i])
2238
+ continue;
2239
+ const b = bones[i];
2240
+ const ap = b.appendParentIndex;
2241
+ const inheritsAffected = (b.appendRotate || b.appendMove) && ap !== undefined && ap >= 0 && ap < n && (this.physicsDriven[ap] || affected[ap]);
2242
+ const parentAffected = b.parentIndex >= 0 && affected[b.parentIndex];
2243
+ if (inheritsAffected || parentAffected) {
2244
+ affected[i] = 1;
2245
+ changed = true;
2246
+ }
2247
+ }
2248
+ }
2249
+ const order = [];
2250
+ for (let k = 0; k < n; k++) {
2251
+ const i = this.deformOrder[k];
2252
+ if (affected[i])
2253
+ order.push(i);
2254
+ }
2255
+ this.physicsAppendOrder = order.length > 0 ? Int32Array.from(order) : null;
2256
+ // The simulated bones something actually inherits from — the only ones
2257
+ // whose post-step local rotation has to be recovered below.
2258
+ const sources = new Set();
2259
+ for (const i of order) {
2260
+ const ap = bones[i].appendParentIndex;
2261
+ if (ap !== undefined && ap >= 0 && ap < n && this.physicsDriven[ap])
2262
+ sources.add(ap);
2263
+ }
2264
+ this.physicsAppendSources = sources.size > 0 ? Int32Array.from(sources) : null;
2265
+ if (this.appendRotOverride === null && this.physicsAppendOrder) {
2266
+ this.appendRotOverride = Array.from({ length: n }, () => Quat.identity());
2267
+ this.appendRotOverrideSet = new Uint8Array(n);
2268
+ }
2269
+ }
2270
+ /** The simulated bones that visible bones INHERIT from — the chest rig, in
2271
+ * one list. Empty unless setPhysicsDrivenBones found such a relationship. */
2272
+ getAppendSourceBones() {
2273
+ return this.physicsAppendSources ? Array.from(this.physicsAppendSources) : [];
2274
+ }
2275
+ /**
2276
+ * Re-run the append pass against the simulation's result. Call after a step.
2277
+ *
2278
+ * Two halves. First the simulated bones an append parent list names get their
2279
+ * post-step LOCAL rotation recovered — physics published only world matrices,
2280
+ * and the append math speaks local. The recovery is the ordinary change of
2281
+ * basis: local = parentWorld⁻¹ · world, read back as a quaternion off the
2282
+ * relative basis, which is exact for the rigid transforms these are.
2283
+ *
2284
+ * Then the dependent bones recompute, in deform order, reading that recovered
2285
+ * rotation instead of the animated one. The recovered value lives in its OWN
2286
+ * array rather than in localRotations, deliberately: localRotations is what
2287
+ * next frame's pose blends against and what the simulation reads to build
2288
+ * kinematic targets, and writing a simulation result back into it would make
2289
+ * the animation chase its own tail.
2290
+ */
2291
+ applyPhysicsAppend() {
2292
+ const order = this.physicsAppendOrder;
2293
+ const sources = this.physicsAppendSources;
2294
+ if (!order || !sources || !this.appendRotOverride || !this.appendRotOverrideSet)
2295
+ return;
2296
+ const bones = this.skeleton.bones;
2297
+ const worldMats = this.runtimeSkeleton.worldMatrices;
2298
+ this.appendRotOverrideSet.fill(0);
2299
+ for (let s = 0; s < sources.length; s++) {
2300
+ const i = sources[s];
2301
+ const w = worldMats[i].values;
2302
+ const p = bones[i].parentIndex;
2303
+ // Columns of the bone's own basis, expressed in its parent's frame. With
2304
+ // no parent the world basis already IS the local one.
2305
+ let bx0 = w[0], bx1 = w[1], bx2 = w[2];
2306
+ let by0 = w[4], by1 = w[5], by2 = w[6];
2307
+ let bz0 = w[8], bz1 = w[9], bz2 = w[10];
2308
+ if (p >= 0) {
2309
+ const pm = worldMats[p].values;
2310
+ // parentᵀ · child, the rotation half of parentWorld⁻¹ · world: the
2311
+ // parent basis is orthonormal, so its inverse is its transpose.
2312
+ const r0 = bx0, r1 = bx1, r2 = bx2;
2313
+ bx0 = pm[0] * r0 + pm[1] * r1 + pm[2] * r2;
2314
+ bx1 = pm[4] * r0 + pm[5] * r1 + pm[6] * r2;
2315
+ bx2 = pm[8] * r0 + pm[9] * r1 + pm[10] * r2;
2316
+ const g0 = by0, g1 = by1, g2 = by2;
2317
+ by0 = pm[0] * g0 + pm[1] * g1 + pm[2] * g2;
2318
+ by1 = pm[4] * g0 + pm[5] * g1 + pm[6] * g2;
2319
+ by2 = pm[8] * g0 + pm[9] * g1 + pm[10] * g2;
2320
+ const b0 = bz0, b1 = bz1, b2 = bz2;
2321
+ bz0 = pm[0] * b0 + pm[1] * b1 + pm[2] * b2;
2322
+ bz1 = pm[4] * b0 + pm[5] * b1 + pm[6] * b2;
2323
+ bz2 = pm[8] * b0 + pm[9] * b1 + pm[10] * b2;
2324
+ }
2325
+ _appendBasisX.setXYZ(bx0, bx1, bx2);
2326
+ _appendBasisY.setXYZ(by0, by1, by2);
2327
+ _appendBasisZ.setXYZ(bz0, bz1, bz2);
2328
+ Quat.fromBasisInto(_appendBasisX, _appendBasisY, _appendBasisZ, this.appendRotOverride[i]);
2329
+ this.appendRotOverrideSet[i] = 1;
2330
+ }
2331
+ this.computeWorldMatrices(order);
2332
+ }
2333
+ computeWorldMatrices(subset) {
2179
2334
  const bones = this.skeleton.bones;
2180
2335
  const localRot = this.runtimeSkeleton.localRotations;
2181
2336
  const localTrans = this.runtimeSkeleton.localTranslations;
@@ -2186,8 +2341,15 @@ export class Model {
2186
2341
  // Flat traversal in precomputed order: every bone's parent is already done, so no
2187
2342
  // per-bone visited check, no recursion, and no per-call allocation. Same per-bone
2188
2343
  // math as before. Scratch slots are safe to reuse since there's no reentrancy now.
2189
- const order = this.deformOrder;
2190
- for (let k = 0; k < boneCount; k++) {
2344
+ //
2345
+ // A SUBSET is the post-physics pass (applyPhysicsAppend): the same walk over
2346
+ // the bones that inherit from a simulated one, in the same relative order,
2347
+ // leaving every other bone — the simulated ones above all — untouched.
2348
+ const order = subset ?? this.deformOrder;
2349
+ const count = subset ? subset.length : boneCount;
2350
+ const override = this.appendRotOverride;
2351
+ const overrideSet = this.appendRotOverrideSet;
2352
+ for (let k = 0; k < count; k++) {
2191
2353
  const i = order[k];
2192
2354
  const b = bones[i];
2193
2355
  const boneRot = localRot[i];
@@ -2200,7 +2362,11 @@ export class Model {
2200
2362
  const hasRatio = Math.abs(ratio) > 1e-6;
2201
2363
  if (hasRatio) {
2202
2364
  if (b.appendRotate) {
2203
- const appendRot = localRot[appendParentIdx];
2365
+ // The simulated parent's RECOVERED rotation when there is one — the
2366
+ // whole point of the post-physics pass. localRotations still holds
2367
+ // the animated pose for that bone, which is exactly what must not
2368
+ // be inherited here.
2369
+ const appendRot = override && overrideSet && overrideSet[appendParentIdx] ? override[appendParentIdx] : localRot[appendParentIdx];
2204
2370
  let ax = appendRot.x, ay = appendRot.y, az = appendRot.z;
2205
2371
  const aw = appendRot.w;
2206
2372
  const absRatio = ratio < 0 ? -ratio : ratio;
@@ -92,6 +92,41 @@ export declare class RezePhysics {
92
92
  private carryDynamicThroughTeleport;
93
93
  private alignPinnedBodiesToBones;
94
94
  private restoreNonFiniteBodies;
95
+ /**
96
+ * Let the bodies carrying these bones swing longer, by damping them less.
97
+ *
98
+ * DAMPING, and not solver iterations, and the difference is the whole point.
99
+ * Under-converging a joint does make it swing further — it also stops it ever
100
+ * reaching equilibrium, so the body hangs visibly low at rest. Sag and swing
101
+ * come as a pair there and no amount of tuning separates them. Damping does
102
+ * separate them: for m·x″ + c·x′ + k·x = mg the rest position is mg/k, which
103
+ * c does not appear in. Less damping is a longer, larger oscillation about
104
+ * exactly the same resting height.
105
+ *
106
+ * Scoped to the bones asked for, because it is a look and not a correction —
107
+ * rigs whose visible bones inherit from a simulated one (付与親) are authored
108
+ * against an MMD that lets them move more than a faithfully damped
109
+ * simulation does. Hair and skirt keep their authored damping.
110
+ *
111
+ * `scale` multiplies the AUTHORED damping: 1 restores it, 0.5 halves it,
112
+ * 0 leaves the body undamped and ringing. Idempotent — the authored values
113
+ * are snapshotted on first use, so repeated calls set rather than compound.
114
+ */
115
+ setJiggleDamping(boneIndices: number[], scale: number): void;
116
+ /** Authored damping, kept so setJiggleDamping sets rather than compounds. */
117
+ private authoredLinDamp;
118
+ private authoredAngDamp;
119
+ /**
120
+ * Bones whose world matrix this simulation OVERWRITES each step.
121
+ *
122
+ * The same test applyDynamicsToBones runs — a Dynamic body bound to a real
123
+ * bone — exposed because the pose pipeline has to know. PMX lets a bone
124
+ * inherit rotation from an 付与親 (append parent), and when that parent is
125
+ * simulated the inheritance has to consume the SIMULATED result, not the
126
+ * animated pose the frame started with. Nothing else can answer which bones
127
+ * those are: the mapping lives in this store.
128
+ */
129
+ getPhysicsDrivenBones(): number[];
95
130
  private applyDynamicsToBones;
96
131
  }
97
132
  //# sourceMappingURL=physics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/physics/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,EAAS,KAAK,WAAW,EAAE,MAAM,SAAS,CAAA;AAYjD,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;IAChC,uEAAuE;IACvE,OAAO,CAAC,aAAa,CAAM;IAC3B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAM;IACzC;gFAC4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAI;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IAMrC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,gBAAgB,CAAc;IAQtC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,eAAe,CAAc;IAMrC,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,aAAa,CAAY;IAMjC,OAAO,CAAC,WAAW,CAAY;IAG/B,aAAa,SAAI;gBAEL,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,KAAK,EAAO;IA6D1D,OAAO,CAAC,mBAAmB;IA4B3B,OAAO,CAAC,aAAa;IAKrB,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI;IAG/B,UAAU,IAAI,IAAI;IAGlB,2EAA2E;IAC3E,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAGvC,OAAO,IAAI,WAAW,GAAG,IAAI;IAG7B,cAAc,IAAI,SAAS,EAAE;IAG7B,SAAS,IAAI,KAAK,EAAE;IAGpB,QAAQ,IAAI,cAAc;IAI1B,sBAAsB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC;IAiBnE,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IActC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,uBAAuB,EAAE,YAAY,GAAG,IAAI;IAiFxF,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,uBAAuB;IAqF/B,OAAO,CAAC,yBAAyB;IAgEjC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,sBAAsB;IA0C9B,OAAO,CAAC,2BAA2B;IA6EnC,OAAO,CAAC,wBAAwB;IA2BhC,OAAO,CAAC,sBAAsB;IAqC9B,OAAO,CAAC,oBAAoB;CA6D7B"}
1
+ {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/physics/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,EAAS,KAAK,WAAW,EAAE,MAAM,SAAS,CAAA;AAYjD,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;IAChC,uEAAuE;IACvE,OAAO,CAAC,aAAa,CAAM;IAC3B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAM;IACzC;gFAC4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAI;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IAMrC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,gBAAgB,CAAc;IAQtC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,eAAe,CAAc;IAMrC,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,aAAa,CAAY;IAMjC,OAAO,CAAC,WAAW,CAAY;IAG/B,aAAa,SAAI;gBAEL,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,KAAK,EAAO;IA6D1D,OAAO,CAAC,mBAAmB;IA4B3B,OAAO,CAAC,aAAa;IAKrB,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI;IAG/B,UAAU,IAAI,IAAI;IAGlB,2EAA2E;IAC3E,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAGvC,OAAO,IAAI,WAAW,GAAG,IAAI;IAG7B,cAAc,IAAI,SAAS,EAAE;IAG7B,SAAS,IAAI,KAAK,EAAE;IAGpB,QAAQ,IAAI,cAAc;IAI1B,sBAAsB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC;IAiBnE,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IActC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,uBAAuB,EAAE,YAAY,GAAG,IAAI;IAiFxF,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,uBAAuB;IAqF/B,OAAO,CAAC,yBAAyB;IAgEjC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,sBAAsB;IA0C9B,OAAO,CAAC,2BAA2B;IA6EnC,OAAO,CAAC,wBAAwB;IA2BhC,OAAO,CAAC,sBAAsB;IAqC9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAmB5D,6EAA6E;IAC7E,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,eAAe,CAA4B;IAEnD;;;;;;;;;OASG;IACH,qBAAqB,IAAI,MAAM,EAAE;IAUjC,OAAO,CAAC,oBAAoB;CA6D7B"}
@@ -44,6 +44,9 @@ export class RezePhysics {
44
44
  // Debug counter: frames on which a teleport (scrub/jump discontinuity)
45
45
  // was detected and settled.
46
46
  this.teleportCount = 0;
47
+ /** Authored damping, kept so setJiggleDamping sets rather than compounds. */
48
+ this.authoredLinDamp = null;
49
+ this.authoredAngDamp = null;
47
50
  this.rigidbodies = rigidbodies;
48
51
  this.joints = joints;
49
52
  // The floor is part of every world: a huge static box whose top face is
@@ -657,6 +660,67 @@ export class RezePhysics {
657
660
  // boneWorld = bodyWorld × bodyOffsetInverse.
658
661
  // The body pose is the render-interpolated pose between the previous and current
659
662
  // substep states (alpha = fraction into the next step), which removes fixed-step judder.
663
+ /**
664
+ * Let the bodies carrying these bones swing longer, by damping them less.
665
+ *
666
+ * DAMPING, and not solver iterations, and the difference is the whole point.
667
+ * Under-converging a joint does make it swing further — it also stops it ever
668
+ * reaching equilibrium, so the body hangs visibly low at rest. Sag and swing
669
+ * come as a pair there and no amount of tuning separates them. Damping does
670
+ * separate them: for m·x″ + c·x′ + k·x = mg the rest position is mg/k, which
671
+ * c does not appear in. Less damping is a longer, larger oscillation about
672
+ * exactly the same resting height.
673
+ *
674
+ * Scoped to the bones asked for, because it is a look and not a correction —
675
+ * rigs whose visible bones inherit from a simulated one (付与親) are authored
676
+ * against an MMD that lets them move more than a faithfully damped
677
+ * simulation does. Hair and skirt keep their authored damping.
678
+ *
679
+ * `scale` multiplies the AUTHORED damping: 1 restores it, 0.5 halves it,
680
+ * 0 leaves the body undamped and ringing. Idempotent — the authored values
681
+ * are snapshotted on first use, so repeated calls set rather than compound.
682
+ */
683
+ setJiggleDamping(boneIndices, scale) {
684
+ const wanted = new Set(boneIndices.filter((b) => b >= 0));
685
+ if (wanted.size === 0)
686
+ return;
687
+ if (!this.authoredLinDamp || !this.authoredAngDamp) {
688
+ this.authoredLinDamp = Float32Array.from(this.store.linearDamping);
689
+ this.authoredAngDamp = Float32Array.from(this.store.angularDamping);
690
+ }
691
+ const s = Math.max(0, Math.min(1, scale));
692
+ const boneOf = this.store.boneIndex;
693
+ for (let i = 0; i < this.store.count; i++) {
694
+ const b = boneOf[i];
695
+ if (b < 0 || !wanted.has(b))
696
+ continue;
697
+ this.store.linearDamping[i] = this.authoredLinDamp[i] * s;
698
+ this.store.angularDamping[i] = this.authoredAngDamp[i] * s;
699
+ }
700
+ // The factors are cached against dt, which has not changed.
701
+ this.world.invalidateDampingCache();
702
+ }
703
+ /**
704
+ * Bones whose world matrix this simulation OVERWRITES each step.
705
+ *
706
+ * The same test applyDynamicsToBones runs — a Dynamic body bound to a real
707
+ * bone — exposed because the pose pipeline has to know. PMX lets a bone
708
+ * inherit rotation from an 付与親 (append parent), and when that parent is
709
+ * simulated the inheritance has to consume the SIMULATED result, not the
710
+ * animated pose the frame started with. Nothing else can answer which bones
711
+ * those are: the mapping lives in this store.
712
+ */
713
+ getPhysicsDrivenBones() {
714
+ const out = [];
715
+ for (let i = 0; i < this.store.count; i++) {
716
+ if (this.store.type[i] !== RigidbodyType.Dynamic)
717
+ continue;
718
+ const b = this.store.boneIndex[i];
719
+ if (b >= 0)
720
+ out.push(b);
721
+ }
722
+ return out;
723
+ }
660
724
  applyDynamicsToBones(boneWorldMatrices, alpha) {
661
725
  const N = this.store.count;
662
726
  const inv = this.store.bodyOffsetInverse;
@@ -32,6 +32,10 @@ export declare class World {
32
32
  /** Per-pair contact impulse history behind warm starting. */
33
33
  private manifolds;
34
34
  private dampCacheDt;
35
+ /** Drop the cached damping factors. The cache is keyed on dt alone, because
36
+ * authored damping never changed — until a rig asked for softer jiggle (see
37
+ * RezePhysics.setJiggleDamping), which rewrites the store's values. */
38
+ invalidateDampingCache(): void;
35
39
  private linDampFactor;
36
40
  private angDampFactor;
37
41
  private windX;
@@ -1 +1 @@
1
- {"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/physics/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC1D,OAAO,EAAgE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEzG,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAO1D;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,SAAS,EAAE,IAAI,CAAA;IACf,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;IAChB;;;kEAG8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,gBAAgB,SAAK;IAErB,6DAA6D;IAC7D,OAAO,CAAC,SAAS,CAAsB;IAIvC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,aAAa,CAA4B;IAIjD,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,aAAa,CAAO;IAC5B;gDAC4C;IAC5C,OAAO,CAAC,SAAS,CAAI;gBAET,OAAO,EAAE,IAAI;IAIzB,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI;IAMzB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAoBvC,OAAO,IAAI,WAAW,GAAG,IAAI;IAW7B,IAAI,CACF,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,GACT,IAAI;CA4IR"}
1
+ {"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/physics/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC1D,OAAO,EAAgE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEzG,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAO1D;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,SAAS,EAAE,IAAI,CAAA;IACf,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;IAChB;;;kEAG8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,gBAAgB,SAAK;IAErB,6DAA6D;IAC7D,OAAO,CAAC,SAAS,CAAsB;IAIvC,OAAO,CAAC,WAAW,CAAK;IAExB;;4EAEwE;IACxE,sBAAsB,IAAI,IAAI;IAG9B,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,aAAa,CAA4B;IAIjD,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,aAAa,CAAO;IAC5B;gDAC4C;IAC5C,OAAO,CAAC,SAAS,CAAI;gBAET,OAAO,EAAE,IAAI;IAIzB,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI;IAMzB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAoBvC,OAAO,IAAI,WAAW,GAAG,IAAI;IAW7B,IAAI,CACF,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,GACT,IAAI;CA4IR"}
@@ -4,6 +4,12 @@ import { solveConstraints, saveContactImpulses, applySplitImpulsePush } from "./
4
4
  import { ManifoldCache } from "./manifold";
5
5
  import { findContacts } from "./contact";
6
6
  export class World {
7
+ /** Drop the cached damping factors. The cache is keyed on dt alone, because
8
+ * authored damping never changed — until a rig asked for softer jiggle (see
9
+ * RezePhysics.setJiggleDamping), which rewrites the store's values. */
10
+ invalidateDampingCache() {
11
+ this.dampCacheDt = -1;
12
+ }
7
13
  constructor(gravity) {
8
14
  this.solverIterations = 10;
9
15
  /** Per-pair contact impulse history behind warm starting. */
@@ -25,8 +25,16 @@ export interface CameraKeyframe {
25
25
  target: Vec3;
26
26
  rotation: Vec3;
27
27
  fov: number;
28
- interpolation: Uint8Array;
28
+ /** 24 bytes, contiguous per channel — see camera-animation.ts's `bez`.
29
+ * Optional so a hand-authored keyframe does not have to know the layout;
30
+ * both the sampler and the writer fall back to DEFAULT_CAMERA_INTERPOLATION.
31
+ * A parsed file always carries its own. */
32
+ interpolation?: Uint8Array;
29
33
  }
34
+ /** Linear in, linear out on all six channels. 20/107 is MMD's own linear pair
35
+ * (its bezier bytes run 0-127), so a keyframe written with this reads back as
36
+ * a straight line in MMD rather than an ease nobody asked for. */
37
+ export declare const DEFAULT_CAMERA_INTERPOLATION: Uint8Array;
30
38
  /** A VMD "IK/display" record: one moment at which chains are switched. */
31
39
  export interface IkFrame {
32
40
  frame: number;
@@ -1 +1 @@
1
- {"version":3,"file":"vmd-loader.d.ts","sourceRoot":"","sources":["../src/vmd-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,CAAA;IACd,WAAW,EAAE,IAAI,CAAA;IACjB,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;qFAEqF;AACrF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,IAAI,CAAA;IACZ,QAAQ,EAAE,IAAI,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CACjD;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAa;IAE5B,OAAO;WAWM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAKtD,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAKzD;mGAC+F;WAClF,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAK/D,MAAM,CAAC,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,cAAc,EAAE;IAOlE,OAAO,CAAC,WAAW;IA4BnB,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,EAAE;IAIvD;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IA8Bf,OAAO,CAAC,KAAK;IAgGb,OAAO,CAAC,aAAa;IAuDrB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IASlB,gFAAgF;IAChF,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,IAAI;CAMb"}
1
+ {"version":3,"file":"vmd-loader.d.ts","sourceRoot":"","sources":["../src/vmd-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,CAAA;IACd,WAAW,EAAE,IAAI,CAAA;IACjB,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;qFAEqF;AACrF,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,IAAI,CAAA;IACZ,QAAQ,EAAE,IAAI,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX;;;gDAG4C;IAC5C,aAAa,CAAC,EAAE,UAAU,CAAA;CAC3B;AAED;;mEAEmE;AACnE,eAAO,MAAM,4BAA4B,EAAE,UAUvC,CAAA;AAEJ,0EAA0E;AAC1E,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CACjD;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAa;IAE5B,OAAO;WAWM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAKtD,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAKzD;mGAC+F;WAClF,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAK/D,MAAM,CAAC,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,cAAc,EAAE;IAOlE,OAAO,CAAC,WAAW;IA4BnB,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,EAAE;IAIvD;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IA8Bf,OAAO,CAAC,KAAK;IAgGb,OAAO,CAAC,aAAa;IAuDrB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IASlB,gFAAgF;IAChF,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,IAAI;CAMb"}
@@ -1,4 +1,18 @@
1
1
  import { Quat, Vec3 } from "./math";
2
+ /** Linear in, linear out on all six channels. 20/107 is MMD's own linear pair
3
+ * (its bezier bytes run 0-127), so a keyframe written with this reads back as
4
+ * a straight line in MMD rather than an ease nobody asked for. */
5
+ export const DEFAULT_CAMERA_INTERPOLATION = (() => {
6
+ const ip = new Uint8Array(24);
7
+ for (let c = 0; c < 6; c++) {
8
+ const b = c * 4;
9
+ ip[b] = 20; // x1
10
+ ip[b + 1] = 107; // x2
11
+ ip[b + 2] = 20; // y1
12
+ ip[b + 3] = 107; // y2
13
+ }
14
+ return ip;
15
+ })();
2
16
  export class VMDLoader {
3
17
  constructor(buffer) {
4
18
  this.offset = 0;
@@ -1,5 +1,33 @@
1
1
  import { AnimationClip } from "./animation";
2
+ import { type CameraKeyframe } from "./vmd-loader";
3
+ /** Which half of a clip to write. Mirrors `Model.loadVmd`'s `tracks` option, so
4
+ * a file this writer splits out is one the loader can read straight back:
5
+ *
6
+ * "all" bone + morph (+ IK) — one file, what MMD itself exports
7
+ * "motion" bone (+ IK) only — the dance, no expressions
8
+ * "morphs" morph only — an expression file (\u8868\u60c5\u30e2\u30fc\u30b7\u30e7\u30f3) to lay over a motion
9
+ *
10
+ * IK rides with "motion" rather than "morphs" because it is bone state: which
11
+ * chains solve says nothing about a face. */
12
+ export type VmdTrackSelection = "all" | "motion" | "morphs";
2
13
  export declare class VMDWriter {
3
- write(clip: AnimationClip): ArrayBuffer;
14
+ write(clip: AnimationClip, options?: {
15
+ tracks?: VmdTrackSelection;
16
+ }): ArrayBuffer;
17
+ /**
18
+ * A camera VMD: the shot's own file, with no model motion in it.
19
+ *
20
+ * Bone and morph counts are written as zero rather than omitted — the camera
21
+ * block sits after them in the format, so a reader walking the file in order
22
+ * (including this package's own parseCamera) has to pass through both to
23
+ * reach it. Light, self-shadow and IK blocks are left off entirely; every
24
+ * reader bounds-checks past the camera block, and MMD is happy with a file
25
+ * that simply ends there.
26
+ *
27
+ * `frames` is sorted by frame on the way out: CameraAnimation binary-searches
28
+ * the track it loads, and an out-of-order file would sample wrong rather than
29
+ * fail loudly.
30
+ */
31
+ writeCamera(frames: CameraKeyframe[]): ArrayBuffer;
4
32
  }
5
33
  //# sourceMappingURL=vmd-writer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vmd-writer.d.ts","sourceRoot":"","sources":["../src/vmd-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmC,MAAM,aAAa,CAAA;AAiD5E,qBAAa,SAAS;IACpB,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,WAAW;CAwHxC"}
1
+ {"version":3,"file":"vmd-writer.d.ts","sourceRoot":"","sources":["../src/vmd-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmC,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAsDhF;;;;;;;;8CAQ8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE3D,qBAAa,SAAS;IACpB,KAAK,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,WAAW;IAiIjF;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,WAAW;CA2CnD"}
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_CAMERA_INTERPOLATION } from "./vmd-loader";
1
2
  const VMD_HEADER = "Vocaloid Motion Data 0002";
2
3
  const HEADER_SIZE = 30;
3
4
  const MODEL_NAME_SIZE = 20;
@@ -7,6 +8,11 @@ const BONE_FRAME_SIZE = BONE_NAME_SIZE + 4 + 12 + 16 + 64; // 111 bytes
7
8
  const MORPH_FRAME_SIZE = MORPH_NAME_SIZE + 4 + 4; // 23 bytes
8
9
  /** IK bone names get 20 bytes in the IK block, not the 15 bones get elsewhere. */
9
10
  const IK_NAME_SIZE = 20;
11
+ /** frame + distance + target(3) + rotation(3) + interpolation(24) + fov + perspective. */
12
+ const CAMERA_FRAME_SIZE = 4 + 4 + 12 + 12 + 24 + 4 + 1; // 61 bytes
13
+ /** What MMD stamps in the model-name field of a camera VMD. Tools sniff it to
14
+ * tell a camera file from a motion at a glance, so write what they expect. */
15
+ const CAMERA_MODEL_NAME = "\u30ab\u30e1\u30e9\u30fb\u7167\u660e";
10
16
  // Build a Unicode-to-Shift-JIS lookup by inverting the TextDecoder mapping.
11
17
  let shiftJISTable = null;
12
18
  function getShiftJISTable() {
@@ -47,20 +53,27 @@ function encodeShiftJIS(str) {
47
53
  return new Uint8Array(bytes);
48
54
  }
49
55
  export class VMDWriter {
50
- write(clip) {
56
+ write(clip, options) {
57
+ const tracks = options?.tracks ?? "all";
58
+ const wantBones = tracks !== "morphs";
59
+ const wantMorphs = tracks !== "motion";
51
60
  let totalBoneFrames = 0;
52
- for (const frames of clip.boneTracks.values()) {
53
- totalBoneFrames += frames.length;
61
+ if (wantBones) {
62
+ for (const frames of clip.boneTracks.values()) {
63
+ totalBoneFrames += frames.length;
64
+ }
54
65
  }
55
66
  let totalMorphFrames = 0;
56
- for (const frames of clip.morphTracks.values()) {
57
- totalMorphFrames += frames.length;
67
+ if (wantMorphs) {
68
+ for (const frames of clip.morphTracks.values()) {
69
+ totalMorphFrames += frames.length;
70
+ }
58
71
  }
59
72
  // IK state is stored per MOMENT, not per bone: one record lists every chain
60
73
  // and its state at that frame. So the tracks are transposed back into the
61
74
  // frames they were flattened from.
62
75
  const ikByFrame = new Map();
63
- for (const [boneName, keys] of clip.ikTracks ?? []) {
76
+ for (const [boneName, keys] of (wantBones ? clip.ikTracks : undefined) ?? []) {
64
77
  for (const key of keys) {
65
78
  const at = ikByFrame.get(key.frame);
66
79
  if (at)
@@ -92,7 +105,7 @@ export class VMDWriter {
92
105
  view.setUint32(offset, totalBoneFrames, true);
93
106
  offset += 4;
94
107
  // Bone frames
95
- for (const frames of clip.boneTracks.values()) {
108
+ for (const frames of wantBones ? clip.boneTracks.values() : []) {
96
109
  for (const kf of frames) {
97
110
  // Bone name (15 bytes, Shift-JIS)
98
111
  offset = writeFixedShiftJIS(buffer, offset, kf.boneName, BONE_NAME_SIZE);
@@ -125,7 +138,7 @@ export class VMDWriter {
125
138
  view.setUint32(offset, totalMorphFrames, true);
126
139
  offset += 4;
127
140
  // Morph frames
128
- for (const frames of clip.morphTracks.values()) {
141
+ for (const frames of wantMorphs ? clip.morphTracks.values() : []) {
129
142
  for (const kf of frames) {
130
143
  // Morph name (15 bytes, Shift-JIS)
131
144
  offset = writeFixedShiftJIS(buffer, offset, kf.morphName, MORPH_NAME_SIZE);
@@ -160,6 +173,69 @@ export class VMDWriter {
160
173
  }
161
174
  return buffer;
162
175
  }
176
+ /**
177
+ * A camera VMD: the shot's own file, with no model motion in it.
178
+ *
179
+ * Bone and morph counts are written as zero rather than omitted — the camera
180
+ * block sits after them in the format, so a reader walking the file in order
181
+ * (including this package's own parseCamera) has to pass through both to
182
+ * reach it. Light, self-shadow and IK blocks are left off entirely; every
183
+ * reader bounds-checks past the camera block, and MMD is happy with a file
184
+ * that simply ends there.
185
+ *
186
+ * `frames` is sorted by frame on the way out: CameraAnimation binary-searches
187
+ * the track it loads, and an out-of-order file would sample wrong rather than
188
+ * fail loudly.
189
+ */
190
+ writeCamera(frames) {
191
+ const sorted = [...frames].sort((a, b) => a.frame - b.frame);
192
+ const size = HEADER_SIZE + MODEL_NAME_SIZE + 4 + 4 + 4 + sorted.length * CAMERA_FRAME_SIZE;
193
+ const buffer = new ArrayBuffer(size);
194
+ const view = new DataView(buffer);
195
+ let offset = 0;
196
+ offset = writeFixedString(buffer, offset, VMD_HEADER, HEADER_SIZE);
197
+ offset = writeFixedShiftJIS(buffer, offset, CAMERA_MODEL_NAME, MODEL_NAME_SIZE);
198
+ view.setUint32(offset, 0, true); // bone frame count
199
+ offset += 4;
200
+ view.setUint32(offset, 0, true); // morph frame count
201
+ offset += 4;
202
+ view.setUint32(offset, sorted.length, true);
203
+ offset += 4;
204
+ for (const kf of sorted) {
205
+ view.setUint32(offset, kf.frame, true);
206
+ offset += 4;
207
+ view.setFloat32(offset, kf.distance, true);
208
+ offset += 4;
209
+ view.setFloat32(offset, kf.target.x, true);
210
+ offset += 4;
211
+ view.setFloat32(offset, kf.target.y, true);
212
+ offset += 4;
213
+ view.setFloat32(offset, kf.target.z, true);
214
+ offset += 4;
215
+ // Euler radians, as the loader reads them.
216
+ view.setFloat32(offset, kf.rotation.x, true);
217
+ offset += 4;
218
+ view.setFloat32(offset, kf.rotation.y, true);
219
+ offset += 4;
220
+ view.setFloat32(offset, kf.rotation.z, true);
221
+ offset += 4;
222
+ // 24 bytes, contiguous per channel — see camera-animation.ts's `bez`.
223
+ // Short or missing tables are padded with a linear default rather than
224
+ // writing junk: a hand-built keyframe should not have to know the layout.
225
+ const ip = new Uint8Array(24);
226
+ ip.set(DEFAULT_CAMERA_INTERPOLATION);
227
+ if (kf.interpolation)
228
+ ip.set(kf.interpolation.subarray(0, 24));
229
+ new Uint8Array(buffer, offset, 24).set(ip);
230
+ offset += 24;
231
+ // fov is degrees, and an integer in the file — MMD's own field is u32.
232
+ view.setUint32(offset, Math.max(0, Math.round(kf.fov)), true);
233
+ offset += 4;
234
+ view.setUint8(offset, 0); // 0 = perspective
235
+ offset += 1;
236
+ }
237
+ return buffer;
238
+ }
163
239
  }
164
240
  function writeFixedString(buffer, offset, str, maxBytes) {
165
241
  const bytes = new Uint8Array(buffer, offset, maxBytes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.50.4",
3
+ "version": "0.50.6",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",