reze-engine 0.36.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/camera.d.ts +17 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +25 -0
- package/dist/engine.d.ts +99 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +259 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/locomotion.d.ts +7 -0
- package/dist/locomotion.d.ts.map +1 -1
- package/dist/locomotion.js +27 -0
- package/dist/model.d.ts +107 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +250 -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-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +84 -44
- package/package.json +1 -1
- package/src/camera.ts +26 -0
- package/src/engine.ts +314 -18
- package/src/index.ts +12 -1
- package/src/locomotion.ts +28 -0
- package/src/model.ts +295 -1
- package/src/pmx-loader.ts +84 -44
package/src/model.ts
CHANGED
|
@@ -32,6 +32,10 @@ const _convMat = new Float32Array(16)
|
|
|
32
32
|
// Blend-path scratch: per-entry sample target and the crossfade's two fixed entries.
|
|
33
33
|
const _blendQ = new Quat(0, 0, 0, 1)
|
|
34
34
|
const _blendT = new Vec3(0, 0, 0)
|
|
35
|
+
|
|
36
|
+
// Bone-morph scratch: the weighted rotation, slerped out of identity.
|
|
37
|
+
const _boneMorphQ = new Quat(0, 0, 0, 1)
|
|
38
|
+
const _boneMorphIdentity = new Quat(0, 0, 0, 1)
|
|
35
39
|
export interface ClipEventInfo {
|
|
36
40
|
clip: string
|
|
37
41
|
/** The registered event time, seconds. */
|
|
@@ -40,6 +44,15 @@ export interface ClipEventInfo {
|
|
|
40
44
|
weight: number
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
/** A clip's authored horizontal root path, lifted off センター by
|
|
48
|
+
* Model.extractRootMotion: per-key offsets from the FIRST key, raw clip
|
|
49
|
+
* units, clip space (rest facing -Z). `frames` are VMD frames (30fps). */
|
|
50
|
+
export interface RootMotionProfile {
|
|
51
|
+
frames: number[]
|
|
52
|
+
x: number[]
|
|
53
|
+
z: number[]
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
const _fadeEntries: BlendEntry[] = [
|
|
44
57
|
{ name: "", time: 0, weight: 0 },
|
|
45
58
|
{ name: "", time: 0, weight: 0 },
|
|
@@ -131,12 +144,56 @@ export interface GroupMorphReference {
|
|
|
131
144
|
ratio: number
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
// Bone morph offset data (type 2). A stage's doors, platforms and hatches are
|
|
148
|
+
// posed with these — there is no VMD to drive them, so the weight IS the pose.
|
|
149
|
+
export interface BoneMorphOffset {
|
|
150
|
+
boneIndex: number
|
|
151
|
+
translation: [number, number, number]
|
|
152
|
+
/** Rotation quaternion (x, y, z, w). */
|
|
153
|
+
rotation: [number, number, number, number]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** PMX material-morph blend mode. Multiply lerps toward base*morph; add offsets from base. */
|
|
157
|
+
export const MATERIAL_MORPH_MULTIPLY = 0
|
|
158
|
+
export const MATERIAL_MORPH_ADD = 1
|
|
159
|
+
|
|
160
|
+
// Material morph offset data (type 8). Stage artists ship colour and on/off
|
|
161
|
+
// switches this way: alpha to 0 hides a part, a diffuse tint recolours a set.
|
|
162
|
+
export interface MaterialMorphOffset {
|
|
163
|
+
/** -1 targets EVERY material in the model, per the PMX spec. */
|
|
164
|
+
materialIndex: number
|
|
165
|
+
/** MATERIAL_MORPH_MULTIPLY | MATERIAL_MORPH_ADD */
|
|
166
|
+
offsetType: number
|
|
167
|
+
diffuse: [number, number, number, number]
|
|
168
|
+
specular: [number, number, number]
|
|
169
|
+
shininess: number
|
|
170
|
+
ambient: [number, number, number]
|
|
171
|
+
edgeColor: [number, number, number, number]
|
|
172
|
+
edgeSize: number
|
|
173
|
+
textureCoeff: [number, number, number, number]
|
|
174
|
+
sphereCoeff: [number, number, number, number]
|
|
175
|
+
toonCoeff: [number, number, number, number]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// UV morph offset data (types 3–7). Type 3 is the base UV channel; 4–7 are the
|
|
179
|
+
// additional UV channels, which this engine does not carry — kept so the panel
|
|
180
|
+
// can tell "unsupported" from "absent".
|
|
181
|
+
export interface UvMorphOffset {
|
|
182
|
+
vertexIndex: number
|
|
183
|
+
/** (u, v, z, w) — only u/v apply to the base channel. */
|
|
184
|
+
offset: [number, number, number, number]
|
|
185
|
+
}
|
|
186
|
+
|
|
134
187
|
// Morph definition
|
|
135
188
|
export interface Morph {
|
|
136
189
|
name: string
|
|
137
|
-
|
|
190
|
+
/** 0=group, 1=vertex, 2=bone, 3–7=UV, 8=material, 9=flip, 10=impulse. */
|
|
191
|
+
type: number
|
|
138
192
|
vertexOffsets: VertexMorphOffset[] // Only for type 1 (vertex morph)
|
|
139
193
|
groupReferences?: GroupMorphReference[] // Only for type 0 (group morph)
|
|
194
|
+
boneOffsets?: BoneMorphOffset[] // Only for type 2
|
|
195
|
+
materialOffsets?: MaterialMorphOffset[] // Only for type 8
|
|
196
|
+
uvOffsets?: UvMorphOffset[] // Only for types 3–7
|
|
140
197
|
}
|
|
141
198
|
|
|
142
199
|
export interface Morphing {
|
|
@@ -286,6 +343,24 @@ export class Model {
|
|
|
286
343
|
private runtimeMorph!: MorphRuntime
|
|
287
344
|
private morphsDirty: boolean = false // Flag indicating if morphs need to be applied
|
|
288
345
|
|
|
346
|
+
// Bone morphs (type 2), flattened once at load so the per-frame pass is a
|
|
347
|
+
// straight walk with no allocation. Empty for models without them, which is
|
|
348
|
+
// most characters — a stage's doors and platforms are the common case.
|
|
349
|
+
private boneMorphPlan: { morphIndex: number; boneIndex: number; translation: Vec3; rotation: Quat }[] = []
|
|
350
|
+
// Bones any bone morph touches, plus their locals as they were BEFORE the last
|
|
351
|
+
// application. A pose source rewrites the locals it owns every frame, but a
|
|
352
|
+
// stage usually has no clip at all — nothing would reset these, and the offset
|
|
353
|
+
// would compound frame after frame into a slow drift.
|
|
354
|
+
private boneMorphBones: number[] = []
|
|
355
|
+
private boneMorphRestoreR: Quat[] = []
|
|
356
|
+
private boneMorphRestoreT: Vec3[] = []
|
|
357
|
+
private boneMorphApplied = false
|
|
358
|
+
/** A full pose pass has run, so the world matrices are real. See isIdle. */
|
|
359
|
+
private posedOnce = false
|
|
360
|
+
// Set whenever effective weights change, for material/UV consumers that live
|
|
361
|
+
// outside this class. Vertex morphs use morphWeightsDirty (GPU) instead.
|
|
362
|
+
private auxMorphDirty = true
|
|
363
|
+
|
|
289
364
|
// Root transform — model's placement in world space, independent of bones.
|
|
290
365
|
// Folded into skin matrices (see getSkinMatrices) so every pass (main VS,
|
|
291
366
|
// shadow VS, any future skinned pass) sees it without per-shader plumbing.
|
|
@@ -567,6 +642,119 @@ export class Model {
|
|
|
567
642
|
}, {} as Record<string, number>),
|
|
568
643
|
weights: new Float32Array(morphCount),
|
|
569
644
|
}
|
|
645
|
+
const boneCount = this.skeleton.bones.length
|
|
646
|
+
this.boneMorphPlan = []
|
|
647
|
+
for (let i = 0; i < morphCount; i++) {
|
|
648
|
+
const morph = this.morphing.morphs[i]
|
|
649
|
+
if (morph.type !== 2 || !morph.boneOffsets) continue
|
|
650
|
+
for (const off of morph.boneOffsets) {
|
|
651
|
+
if (off.boneIndex < 0 || off.boneIndex >= boneCount) continue
|
|
652
|
+
this.boneMorphPlan.push({
|
|
653
|
+
morphIndex: i,
|
|
654
|
+
boneIndex: off.boneIndex,
|
|
655
|
+
translation: new Vec3(off.translation[0], off.translation[1], off.translation[2]),
|
|
656
|
+
rotation: new Quat(off.rotation[0], off.rotation[1], off.rotation[2], off.rotation[3]),
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const touched = new Set(this.boneMorphPlan.map((e) => e.boneIndex))
|
|
661
|
+
this.boneMorphBones = [...touched]
|
|
662
|
+
this.boneMorphRestoreR = this.boneMorphBones.map(() => Quat.identity())
|
|
663
|
+
this.boneMorphRestoreT = this.boneMorphBones.map(() => Vec3.zeros())
|
|
664
|
+
this.boneMorphApplied = false
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Bone morphs (type 2) compose over whatever the pose sources produced, the
|
|
669
|
+
* same way boneRotationOffsets do — they are an offset on the animated local
|
|
670
|
+
* transform, not a replacement for it. Re-applied every frame because each
|
|
671
|
+
* pose source rewrites the locals it touches.
|
|
672
|
+
*
|
|
673
|
+
* Stages are the reason this exists: a door or a lift is rigged as a bone
|
|
674
|
+
* morph and there is no VMD anywhere that drives it.
|
|
675
|
+
*/
|
|
676
|
+
private applyBoneMorphs(): void {
|
|
677
|
+
if (this.boneMorphPlan.length === 0) return
|
|
678
|
+
|
|
679
|
+
// Undo the previous application first. With a clip running this is a no-op
|
|
680
|
+
// in effect — the pose source already overwrote these bones — but a stage
|
|
681
|
+
// has no clip, so without it the same offset would be re-added every frame.
|
|
682
|
+
if (this.boneMorphApplied) {
|
|
683
|
+
for (let i = 0; i < this.boneMorphBones.length; i++) {
|
|
684
|
+
const b = this.boneMorphBones[i]
|
|
685
|
+
this.runtimeSkeleton.localRotations[b].set(this.boneMorphRestoreR[i])
|
|
686
|
+
this.runtimeSkeleton.localTranslations[b].set(this.boneMorphRestoreT[i])
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
for (let i = 0; i < this.boneMorphBones.length; i++) {
|
|
690
|
+
const b = this.boneMorphBones[i]
|
|
691
|
+
this.boneMorphRestoreR[i].set(this.runtimeSkeleton.localRotations[b])
|
|
692
|
+
this.boneMorphRestoreT[i].set(this.runtimeSkeleton.localTranslations[b])
|
|
693
|
+
}
|
|
694
|
+
this.boneMorphApplied = true
|
|
695
|
+
|
|
696
|
+
const weights = this.getEffectiveMorphWeights()
|
|
697
|
+
for (const entry of this.boneMorphPlan) {
|
|
698
|
+
const w = weights[entry.morphIndex]
|
|
699
|
+
if (w < 0.0001) continue
|
|
700
|
+
const t = this.runtimeSkeleton.localTranslations[entry.boneIndex]
|
|
701
|
+
t.x += entry.translation.x * w
|
|
702
|
+
t.y += entry.translation.y * w
|
|
703
|
+
t.z += entry.translation.z * w
|
|
704
|
+
// Scale the rotation by weight the way MMD does — slerp out of identity,
|
|
705
|
+
// then compose. Multiplying components would not stay a unit quaternion.
|
|
706
|
+
const r = this.runtimeSkeleton.localRotations[entry.boneIndex]
|
|
707
|
+
Quat.slerpInto(_boneMorphIdentity, entry.rotation, w, _boneMorphQ)
|
|
708
|
+
Quat.multiplyInto(r, _boneMorphQ, r)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** True (once) when effective morph weights changed — the engine re-derives
|
|
713
|
+
* material-morph uniforms from it. Separate from the GPU vertex path's flag
|
|
714
|
+
* so both can consume the same change. */
|
|
715
|
+
consumeAuxMorphDirty(): boolean {
|
|
716
|
+
const d = this.auxMorphDirty
|
|
717
|
+
this.auxMorphDirty = false
|
|
718
|
+
return d
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Morph indices this model can actually act on, so a UI never offers a control
|
|
723
|
+
* that moves nothing.
|
|
724
|
+
*
|
|
725
|
+
* Driven directly: vertex (1), bone (2), material (8). Excluded: UV (3–7),
|
|
726
|
+
* which are parsed and kept but not yet applied; flip (9) and impulse (10),
|
|
727
|
+
* which are PMX 2.1 and would need the rigidbody solver.
|
|
728
|
+
*
|
|
729
|
+
* A group morph (0) is only as alive as what it points at — one referencing
|
|
730
|
+
* nothing but UV morphs is just as dead as the UV morphs themselves, so it is
|
|
731
|
+
* resolved rather than assumed.
|
|
732
|
+
*/
|
|
733
|
+
getSupportedMorphIndices(): number[] {
|
|
734
|
+
const morphs = this.morphing.morphs
|
|
735
|
+
const drivable = (t: number) => t === 1 || t === 2 || t === 8
|
|
736
|
+
// Groups can reference groups, so walk with a seen-set rather than recursing.
|
|
737
|
+
const resolves = (start: number): boolean => {
|
|
738
|
+
const seen = new Set<number>()
|
|
739
|
+
const stack = [start]
|
|
740
|
+
while (stack.length > 0) {
|
|
741
|
+
const i = stack.pop()!
|
|
742
|
+
if (seen.has(i) || i < 0 || i >= morphs.length) continue
|
|
743
|
+
seen.add(i)
|
|
744
|
+
const m = morphs[i]
|
|
745
|
+
if (drivable(m.type)) return true
|
|
746
|
+
if (m.type === 0 && m.groupReferences) {
|
|
747
|
+
for (const ref of m.groupReferences) stack.push(ref.morphIndex)
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return false
|
|
751
|
+
}
|
|
752
|
+
const out: number[] = []
|
|
753
|
+
for (let i = 0; i < morphs.length; i++) {
|
|
754
|
+
const t = morphs[i].type
|
|
755
|
+
if (drivable(t) || (t === 0 && resolves(i))) out.push(i)
|
|
756
|
+
}
|
|
757
|
+
return out
|
|
570
758
|
}
|
|
571
759
|
|
|
572
760
|
// Tween update - processes all tweens together with a single time reference
|
|
@@ -983,6 +1171,11 @@ export class Model {
|
|
|
983
1171
|
this.runtimeMorph.weights[idx] = clampedWeight
|
|
984
1172
|
this.tweenState.morphActive[idx] = 0
|
|
985
1173
|
this.applyMorphs()
|
|
1174
|
+
// Vertex and material morphs are done by applyMorphs alone, but a bone
|
|
1175
|
+
// morph lands in the pose pass — and a model with no clip (every stage)
|
|
1176
|
+
// reports idle, so without this the pass never runs and the switch does
|
|
1177
|
+
// nothing. Costs one redundant applyMorphs on the next frame.
|
|
1178
|
+
if (this.boneMorphPlan.length > 0) this.morphsDirty = true
|
|
986
1179
|
try {
|
|
987
1180
|
Engine.getInstance().markVertexBufferDirty(this)
|
|
988
1181
|
} catch {
|
|
@@ -1047,6 +1240,11 @@ export class Model {
|
|
|
1047
1240
|
effectiveWeights[i] = Math.max(0, Math.min(1, effectiveWeights[i]))
|
|
1048
1241
|
}
|
|
1049
1242
|
|
|
1243
|
+
// Bone morphs read these every frame, but material/UV consumers live outside
|
|
1244
|
+
// this class and need telling. Set on BOTH paths — a model whose only morphs
|
|
1245
|
+
// are material morphs never enables the GPU vertex path at all.
|
|
1246
|
+
this.auxMorphDirty = true
|
|
1247
|
+
|
|
1050
1248
|
// GPU path: the compute pass applies the vertex offsets from these weights.
|
|
1051
1249
|
if (this.gpuMorphEnabled) {
|
|
1052
1250
|
this.morphWeightsDirty = true
|
|
@@ -1338,6 +1536,61 @@ export class Model {
|
|
|
1338
1536
|
return this.animationState.getAnimationClip(name)
|
|
1339
1537
|
}
|
|
1340
1538
|
|
|
1539
|
+
/** Lift a clip's authored horizontal root path off センター so the host can
|
|
1540
|
+
* drive the MODEL ROOT along it — game-style root motion. The clip keeps
|
|
1541
|
+
* its vertical bob but its horizontal センター flattens to `rest` (default
|
|
1542
|
+
* 0,0): a displaced first frame — common in per-pose game exports — would
|
|
1543
|
+
* otherwise ride the whole clip as a constant and visibly slide off during
|
|
1544
|
+
* the exit crossfade. Pass the pack's NEUTRAL standing offset (the idle
|
|
1545
|
+
* clip's first センター key) as `rest` so the flattened pose blends into
|
|
1546
|
+
* the surrounding states without even a micro-slide. The removed path
|
|
1547
|
+
* returns RELATIVE TO THE FIRST KEY, raw clip units, clip space (rest
|
|
1548
|
+
* facing -Z). Leg-IK target position tracks lose the same path so feet
|
|
1549
|
+
* keep oscillating around the body. Call once per clip, after
|
|
1550
|
+
* loadVmd/loadClip; null if the clip or its センター track doesn't exist. */
|
|
1551
|
+
extractRootMotion(name: string, rest?: { x?: number; z?: number }): RootMotionProfile | null {
|
|
1552
|
+
const clip = this.animationState.getAnimationClip(name)
|
|
1553
|
+
const track = clip?.boneTracks.get("センター")
|
|
1554
|
+
if (!clip || !track || track.length === 0) return null
|
|
1555
|
+
const restX = rest?.x ?? 0
|
|
1556
|
+
const restZ = rest?.z ?? 0
|
|
1557
|
+
const frames: number[] = []
|
|
1558
|
+
const x: number[] = []
|
|
1559
|
+
const z: number[] = []
|
|
1560
|
+
const pathX: number[] = [] // horizontal removed (rel. rest), for the IK tracks
|
|
1561
|
+
const pathZ: number[] = []
|
|
1562
|
+
const x0 = track[0].translation.x
|
|
1563
|
+
const z0 = track[0].translation.z
|
|
1564
|
+
for (const kf of track) {
|
|
1565
|
+
frames.push(kf.frame)
|
|
1566
|
+
x.push(kf.translation.x - x0)
|
|
1567
|
+
z.push(kf.translation.z - z0)
|
|
1568
|
+
pathX.push(kf.translation.x - restX)
|
|
1569
|
+
pathZ.push(kf.translation.z - restZ)
|
|
1570
|
+
kf.translation.x = restX
|
|
1571
|
+
kf.translation.z = restZ
|
|
1572
|
+
}
|
|
1573
|
+
for (const [bone, kfs] of clip.boneTracks) {
|
|
1574
|
+
if (!bone.includes("IK")) continue
|
|
1575
|
+
for (const kf of kfs) {
|
|
1576
|
+
kf.translation.x -= Model.sampleTrack(frames, pathX, kf.frame)
|
|
1577
|
+
kf.translation.z -= Model.sampleTrack(frames, pathZ, kf.frame)
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return { frames, x, z }
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/** Linear sample of a keyed scalar track at fractional frame f. */
|
|
1584
|
+
private static sampleTrack(frames: number[], values: number[], f: number): number {
|
|
1585
|
+
const n = frames.length
|
|
1586
|
+
if (f <= frames[0]) return values[0]
|
|
1587
|
+
if (f >= frames[n - 1]) return values[n - 1]
|
|
1588
|
+
let i = 1
|
|
1589
|
+
while (frames[i] < f) i++
|
|
1590
|
+
const t = (f - frames[i - 1]) / (frames[i] - frames[i - 1])
|
|
1591
|
+
return values[i - 1] + (values[i] - values[i - 1]) * t
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1341
1594
|
exportVmd(name: string): ArrayBuffer {
|
|
1342
1595
|
const clip = this.animationState.getAnimationClip(name)
|
|
1343
1596
|
if (!clip) throw new Error(`Animation clip "${name}" not found`)
|
|
@@ -1913,6 +2166,42 @@ export class Model {
|
|
|
1913
2166
|
// within that. A host driving bones directly — motion capture writing FK
|
|
1914
2167
|
// rotations every frame with no clip playing — turns it off wholesale, because
|
|
1915
2168
|
// there is no motion present to carry the per-chain answer.
|
|
2169
|
+
/**
|
|
2170
|
+
* Nothing can have moved this frame: no clip, no blend, no live tween, no
|
|
2171
|
+
* morph weight change.
|
|
2172
|
+
*
|
|
2173
|
+
* Environment geometry is in this state almost every frame, so the engine
|
|
2174
|
+
* skips the whole pose pass — sampling, world matrices, and the skin-matrix
|
|
2175
|
+
* upload — for a stage that reports idle. A stage is usually the heaviest mesh
|
|
2176
|
+
* in the scene and the one that never moves; paying a full pose pass for it
|
|
2177
|
+
* every frame is the thing worth not doing.
|
|
2178
|
+
*/
|
|
2179
|
+
isIdle(): boolean {
|
|
2180
|
+
// Never idle before the first pose pass. The constructor leaves the world
|
|
2181
|
+
// matrices identity, so skin = world × inverseBind collapses every vertex
|
|
2182
|
+
// into bone-local space — the mesh piles up at the origin. A cast member is
|
|
2183
|
+
// saved by running update() on frame 1 regardless; a stage that reported
|
|
2184
|
+
// idle immediately would render as a heap and never recover.
|
|
2185
|
+
if (!this.posedOnce) return false
|
|
2186
|
+
return (
|
|
2187
|
+
!this.morphsDirty &&
|
|
2188
|
+
this.oneShot === null &&
|
|
2189
|
+
this.crossfade === null &&
|
|
2190
|
+
(this.blendEntries === null || this.blendEntries.length === 0) &&
|
|
2191
|
+
this.animationState.getCurrentClip() === null &&
|
|
2192
|
+
!this.hasActiveTweens()
|
|
2193
|
+
)
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/** Any live rotation / translation / morph tween. */
|
|
2197
|
+
private hasActiveTweens(): boolean {
|
|
2198
|
+
const s = this.tweenState
|
|
2199
|
+
for (let i = 0; i < s.rotActive.length; i++) if (s.rotActive[i] === 1) return true
|
|
2200
|
+
for (let i = 0; i < s.transActive.length; i++) if (s.transActive[i] === 1) return true
|
|
2201
|
+
for (let i = 0; i < s.morphActive.length; i++) if (s.morphActive[i] === 1) return true
|
|
2202
|
+
return false
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1916
2205
|
update(deltaTime: number, ikEnabled = true): boolean {
|
|
1917
2206
|
// Update tween time (in milliseconds)
|
|
1918
2207
|
this.tweenTimeMs += deltaTime * 1000
|
|
@@ -1955,8 +2244,13 @@ export class Model {
|
|
|
1955
2244
|
this.morphsDirty = false
|
|
1956
2245
|
}
|
|
1957
2246
|
|
|
2247
|
+
// After the pose sources and the constant offsets, before the world pass —
|
|
2248
|
+
// bone morphs must survive into the world matrices IK then reads.
|
|
2249
|
+
this.applyBoneMorphs()
|
|
2250
|
+
|
|
1958
2251
|
// Compute world matrices (needed for IK solving to read bone positions)
|
|
1959
2252
|
this.computeWorldMatrices()
|
|
2253
|
+
this.posedOnce = true
|
|
1960
2254
|
|
|
1961
2255
|
// Solve IK chains (modifies localRotations with final IK rotations). Chains
|
|
1962
2256
|
// the clip switched off are skipped inside.
|
package/src/pmx-loader.ts
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
Morphing,
|
|
10
10
|
VertexMorphOffset,
|
|
11
11
|
GroupMorphReference,
|
|
12
|
+
BoneMorphOffset,
|
|
13
|
+
MaterialMorphOffset,
|
|
14
|
+
UvMorphOffset,
|
|
12
15
|
IKLink,
|
|
13
16
|
} from "./model"
|
|
14
17
|
import { Mat4, Vec3 } from "./math"
|
|
@@ -533,6 +536,11 @@ export class PmxLoader {
|
|
|
533
536
|
vertexOffsets: [],
|
|
534
537
|
groupReferences: [],
|
|
535
538
|
}
|
|
539
|
+
// Allocated only for the type that uses them, so `morph.boneOffsets`
|
|
540
|
+
// being undefined means "not that kind of morph" rather than "empty".
|
|
541
|
+
if (morphType === 2) morph.boneOffsets = []
|
|
542
|
+
else if (morphType >= 3 && morphType <= 7) morph.uvOffsets = []
|
|
543
|
+
else if (morphType === 8) morph.materialOffsets = []
|
|
536
544
|
|
|
537
545
|
// Parse vertex morphs (type 1)
|
|
538
546
|
if (morphType === 1) {
|
|
@@ -569,60 +577,92 @@ export class PmxLoader {
|
|
|
569
577
|
}
|
|
570
578
|
}
|
|
571
579
|
} else {
|
|
572
|
-
//
|
|
580
|
+
// Bone, UV and material morphs are kept; flip and impulse (PMX 2.1)
|
|
581
|
+
// are still read past — nothing in the wild uses them, and impulse
|
|
582
|
+
// would need to drive rigidbodies.
|
|
573
583
|
for (let j = 0; j < offsetCount; j++) {
|
|
574
584
|
if (this.offset >= this.view.buffer.byteLength) {
|
|
575
585
|
break
|
|
576
586
|
}
|
|
577
587
|
if (morphType === 2) {
|
|
578
588
|
// Bone morph
|
|
579
|
-
this.getNonVertexIndex(this.boneIndexSize)
|
|
580
|
-
this.getFloat32()
|
|
581
|
-
this.getFloat32()
|
|
582
|
-
this.getFloat32()
|
|
583
|
-
this.getFloat32()
|
|
584
|
-
this.getFloat32()
|
|
585
|
-
this.getFloat32()
|
|
586
|
-
this.getFloat32()
|
|
589
|
+
const boneIndex = this.getNonVertexIndex(this.boneIndexSize)
|
|
590
|
+
const tx = this.getFloat32()
|
|
591
|
+
const ty = this.getFloat32()
|
|
592
|
+
const tz = this.getFloat32()
|
|
593
|
+
const rx = this.getFloat32()
|
|
594
|
+
const ry = this.getFloat32()
|
|
595
|
+
const rz = this.getFloat32()
|
|
596
|
+
const rw = this.getFloat32()
|
|
597
|
+
if (boneIndex >= 0) {
|
|
598
|
+
const offset: BoneMorphOffset = {
|
|
599
|
+
boneIndex,
|
|
600
|
+
translation: [tx, ty, tz],
|
|
601
|
+
rotation: [rx, ry, rz, rw],
|
|
602
|
+
}
|
|
603
|
+
morph.boneOffsets!.push(offset)
|
|
604
|
+
}
|
|
587
605
|
} else if (morphType >= 3 && morphType <= 7) {
|
|
588
606
|
// UV morph + additional UV channels 1-4; offset is always a Float4 per spec
|
|
589
|
-
this.getIndex(this.vertexIndexSize)
|
|
590
|
-
this.getFloat32()
|
|
591
|
-
this.getFloat32()
|
|
592
|
-
this.getFloat32()
|
|
593
|
-
this.getFloat32()
|
|
607
|
+
const vertexIndex = this.getIndex(this.vertexIndexSize)
|
|
608
|
+
const x = this.getFloat32()
|
|
609
|
+
const y = this.getFloat32()
|
|
610
|
+
const z = this.getFloat32()
|
|
611
|
+
const w = this.getFloat32()
|
|
612
|
+
if (vertexIndex >= 0 && vertexIndex < this.vertexCount) {
|
|
613
|
+
const offset: UvMorphOffset = { vertexIndex, offset: [x, y, z, w] }
|
|
614
|
+
morph.uvOffsets!.push(offset)
|
|
615
|
+
}
|
|
594
616
|
} else if (morphType === 8) {
|
|
595
617
|
// Material morph
|
|
596
|
-
this.getNonVertexIndex(this.materialIndexSize)
|
|
597
|
-
this.getUint8()
|
|
598
|
-
this.getFloat32()
|
|
599
|
-
this.getFloat32()
|
|
600
|
-
this.getFloat32()
|
|
601
|
-
this.getFloat32()
|
|
602
|
-
this.getFloat32()
|
|
603
|
-
this.getFloat32()
|
|
604
|
-
this.getFloat32()
|
|
605
|
-
this.getFloat32()
|
|
606
|
-
this.getFloat32()
|
|
607
|
-
this.getFloat32()
|
|
608
|
-
this.getFloat32()
|
|
609
|
-
this.getFloat32()
|
|
610
|
-
this.getFloat32()
|
|
611
|
-
this.getFloat32()
|
|
612
|
-
this.getFloat32()
|
|
613
|
-
this.getFloat32()
|
|
614
|
-
this.getFloat32()
|
|
615
|
-
this.getFloat32()
|
|
616
|
-
this.getFloat32()
|
|
617
|
-
this.getFloat32()
|
|
618
|
-
this.getFloat32()
|
|
619
|
-
this.getFloat32()
|
|
620
|
-
this.getFloat32()
|
|
621
|
-
this.getFloat32()
|
|
622
|
-
this.getFloat32()
|
|
623
|
-
this.getFloat32()
|
|
624
|
-
this.getFloat32()
|
|
625
|
-
this.getFloat32()
|
|
618
|
+
const materialIndex = this.getNonVertexIndex(this.materialIndexSize)
|
|
619
|
+
const offsetType = this.getUint8()
|
|
620
|
+
const diffuseR = this.getFloat32()
|
|
621
|
+
const diffuseG = this.getFloat32()
|
|
622
|
+
const diffuseB = this.getFloat32()
|
|
623
|
+
const diffuseA = this.getFloat32()
|
|
624
|
+
const specularR = this.getFloat32()
|
|
625
|
+
const specularG = this.getFloat32()
|
|
626
|
+
const specularB = this.getFloat32()
|
|
627
|
+
const shininess = this.getFloat32()
|
|
628
|
+
const ambientR = this.getFloat32()
|
|
629
|
+
const ambientG = this.getFloat32()
|
|
630
|
+
const ambientB = this.getFloat32()
|
|
631
|
+
const edgeR = this.getFloat32()
|
|
632
|
+
const edgeG = this.getFloat32()
|
|
633
|
+
const edgeB = this.getFloat32()
|
|
634
|
+
const edgeA = this.getFloat32()
|
|
635
|
+
const edgeSize = this.getFloat32()
|
|
636
|
+
const texR = this.getFloat32()
|
|
637
|
+
const texG = this.getFloat32()
|
|
638
|
+
const texB = this.getFloat32()
|
|
639
|
+
const texA = this.getFloat32()
|
|
640
|
+
const sphR = this.getFloat32()
|
|
641
|
+
const sphG = this.getFloat32()
|
|
642
|
+
const sphB = this.getFloat32()
|
|
643
|
+
const sphA = this.getFloat32()
|
|
644
|
+
const toonR = this.getFloat32()
|
|
645
|
+
const toonG = this.getFloat32()
|
|
646
|
+
const toonB = this.getFloat32()
|
|
647
|
+
const toonA = this.getFloat32()
|
|
648
|
+
// -1 is legal and means "every material" — kept as-is so the
|
|
649
|
+
// applier can fan it out without a second convention.
|
|
650
|
+
if (materialIndex >= -1) {
|
|
651
|
+
const offset: MaterialMorphOffset = {
|
|
652
|
+
materialIndex,
|
|
653
|
+
offsetType,
|
|
654
|
+
diffuse: [diffuseR, diffuseG, diffuseB, diffuseA],
|
|
655
|
+
specular: [specularR, specularG, specularB],
|
|
656
|
+
shininess,
|
|
657
|
+
ambient: [ambientR, ambientG, ambientB],
|
|
658
|
+
edgeColor: [edgeR, edgeG, edgeB, edgeA],
|
|
659
|
+
edgeSize,
|
|
660
|
+
textureCoeff: [texR, texG, texB, texA],
|
|
661
|
+
sphereCoeff: [sphR, sphG, sphB, sphA],
|
|
662
|
+
toonCoeff: [toonR, toonG, toonB, toonA],
|
|
663
|
+
}
|
|
664
|
+
morph.materialOffsets!.push(offset)
|
|
665
|
+
}
|
|
626
666
|
} else if (morphType === 9) {
|
|
627
667
|
// Flip morph (PMX 2.1): same layout as group morph
|
|
628
668
|
this.getNonVertexIndex(this.morphIndexSize) // morphIndex
|