reze-engine 0.15.0 → 0.16.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 +3 -3
- package/dist/camera.d.ts +2 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +8 -3
- package/dist/engine.d.ts +19 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +294 -27
- package/dist/gpu-profile.d.ts +19 -0
- package/dist/gpu-profile.d.ts.map +1 -0
- package/dist/gpu-profile.js +120 -0
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +13 -7
- package/dist/math.d.ts +2 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +65 -46
- package/dist/model.d.ts +28 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +259 -75
- package/dist/physics/body.d.ts +2 -0
- package/dist/physics/body.d.ts.map +1 -1
- package/dist/physics/body.js +30 -0
- package/dist/physics/constraint.d.ts +13 -0
- package/dist/physics/constraint.d.ts.map +1 -1
- package/dist/physics/constraint.js +13 -0
- package/dist/physics/contact.d.ts +28 -0
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +27 -32
- package/dist/physics/physics.d.ts +3 -0
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +57 -8
- package/dist/physics/profile.d.ts +18 -0
- package/dist/physics/profile.d.ts.map +1 -0
- package/dist/physics/profile.js +44 -0
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +436 -293
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +2 -30
- package/dist/shaders/materials/body.d.ts +1 -1
- package/dist/shaders/materials/body.d.ts.map +1 -1
- package/dist/shaders/materials/body.js +3 -1
- package/dist/shaders/materials/cloth_rough.d.ts +1 -1
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
- package/dist/shaders/materials/cloth_rough.js +3 -1
- package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
- package/dist/shaders/materials/common.d.ts +2 -2
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +8 -0
- package/dist/shaders/materials/default.d.ts +1 -1
- package/dist/shaders/materials/default.d.ts.map +1 -1
- package/dist/shaders/materials/eye.d.ts +1 -1
- package/dist/shaders/materials/eye.d.ts.map +1 -1
- package/dist/shaders/materials/face.d.ts +1 -1
- package/dist/shaders/materials/face.d.ts.map +1 -1
- package/dist/shaders/materials/face.js +4 -1
- package/dist/shaders/materials/hair.d.ts +1 -1
- package/dist/shaders/materials/hair.d.ts.map +1 -1
- package/dist/shaders/materials/metal.d.ts +1 -1
- package/dist/shaders/materials/metal.d.ts.map +1 -1
- package/dist/shaders/materials/stockings.d.ts +1 -1
- package/dist/shaders/materials/stockings.d.ts.map +1 -1
- package/dist/shaders/materials/stockings.js +13 -9
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +18 -12
- package/dist/shaders/passes/morph.d.ts +2 -0
- package/dist/shaders/passes/morph.d.ts.map +1 -0
- package/dist/shaders/passes/morph.js +49 -0
- package/package.json +1 -1
- package/src/camera.ts +9 -3
- package/src/engine.ts +336 -29
- package/src/ik-solver.ts +14 -7
- package/src/math.ts +41 -51
- package/src/model.ts +288 -91
- package/src/physics/body.ts +29 -0
- package/src/physics/constraint.ts +30 -0
- package/src/physics/contact.ts +43 -30
- package/src/physics/physics.ts +58 -17
- package/src/physics/solver.ts +431 -284
- package/src/pmx-loader.ts +2 -35
- package/src/shaders/materials/body.ts +3 -1
- package/src/shaders/materials/cloth_rough.ts +3 -1
- package/src/shaders/materials/common.ts +8 -0
- package/src/shaders/materials/face.ts +4 -1
- package/src/shaders/materials/stockings.ts +13 -9
- package/src/shaders/passes/composite.ts +18 -12
- package/src/shaders/passes/morph.ts +50 -0
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
package/src/model.ts
CHANGED
|
@@ -19,6 +19,14 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const VERTEX_STRIDE = 8
|
|
21
21
|
|
|
22
|
+
// Animation-sampling scratch (applyPoseFromClip → convertVMDTranslationToLocal). These
|
|
23
|
+
// run sequentially and are not reentrant with the world-matrix scratch in math.ts, so
|
|
24
|
+
// plain module singletons are safe and let the per-bone sample path allocate nothing.
|
|
25
|
+
const _animSlerp = new Quat(0, 0, 0, 1)
|
|
26
|
+
const _animInterpT = new Vec3(0, 0, 0)
|
|
27
|
+
const _convOut = new Vec3(0, 0, 0)
|
|
28
|
+
const _convMat = new Float32Array(16)
|
|
29
|
+
|
|
22
30
|
export interface Texture {
|
|
23
31
|
path: string
|
|
24
32
|
name: string
|
|
@@ -112,7 +120,17 @@ export interface Morph {
|
|
|
112
120
|
|
|
113
121
|
export interface Morphing {
|
|
114
122
|
morphs: Morph[]
|
|
115
|
-
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// CSR inversion of vertex-morph offsets for the GPU compute pass (built once at load).
|
|
126
|
+
export interface MorphComputeData {
|
|
127
|
+
basePositions: Float32Array // vertexCount * 3
|
|
128
|
+
rowStart: Uint32Array // vertexCount + 1 (prefix offsets into the entry arrays)
|
|
129
|
+
colMorph: Uint32Array // entryCount (morph index per entry)
|
|
130
|
+
colOffset: Float32Array // entryCount * 3 (xyz offset per entry)
|
|
131
|
+
morphCount: number
|
|
132
|
+
vertexCount: number
|
|
133
|
+
entryCount: number
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
// Runtime skeleton pose state (updated each frame)
|
|
@@ -192,6 +210,18 @@ export class Model {
|
|
|
192
210
|
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
193
211
|
private vertexCount: number
|
|
194
212
|
private indexData: Uint32Array<ArrayBuffer>
|
|
213
|
+
|
|
214
|
+
// Morph state reused across frames (S1) + partial vertex-upload range tracking (S2, CPU path).
|
|
215
|
+
private morphEffectiveWeights?: Float32Array
|
|
216
|
+
private morphPrevMinVert = -1 // vertices touched by last applyMorphs (reset to base this pass)
|
|
217
|
+
private morphPrevMaxVert = -1
|
|
218
|
+
private morphPendingMinVert = -1 // accumulated range awaiting a GPU upload
|
|
219
|
+
private morphPendingMaxVert = -1
|
|
220
|
+
private morphUploadFull = true // first upload after load pushes the whole buffer once
|
|
221
|
+
// GPU morph path: when enabled (engine set up the compute buffers), applyMorphs only
|
|
222
|
+
// resolves effective weights and flags them dirty — the compute pass does the vertex work.
|
|
223
|
+
private gpuMorphEnabled = false
|
|
224
|
+
private morphWeightsDirty = false
|
|
195
225
|
private textures: Texture[] = []
|
|
196
226
|
private materials: Material[] = []
|
|
197
227
|
// Static skeleton/skinning (not necessarily serialized yet)
|
|
@@ -225,6 +255,17 @@ export class Model {
|
|
|
225
255
|
// Cached skin matrices array to avoid allocations in getSkinMatrices
|
|
226
256
|
private skinMatricesArray?: Float32Array
|
|
227
257
|
|
|
258
|
+
// Static bone traversal order (parents before children), precomputed once at load.
|
|
259
|
+
// computeWorldMatrices replays this flat instead of recursing with a per-call
|
|
260
|
+
// visited-array + closure. Order depends only on parentIndex (static), so this
|
|
261
|
+
// reproduces the old recursion's finishing order exactly. See buildDeformOrder.
|
|
262
|
+
private deformOrder!: Int32Array
|
|
263
|
+
|
|
264
|
+
// Bind-pose absolute (world) position per bone, xyz packed. Static (bindTranslation
|
|
265
|
+
// accumulated down the hierarchy). Precomputed once so convertVMDTranslationToLocal
|
|
266
|
+
// stops re-deriving it recursively (with per-ancestor allocations) every frame.
|
|
267
|
+
private bindWorldPos!: Float32Array
|
|
268
|
+
|
|
228
269
|
private tweenState!: TweenState
|
|
229
270
|
private tweenTimeMs: number = 0 // Time tracking for tweens (milliseconds)
|
|
230
271
|
|
|
@@ -338,6 +379,59 @@ export class Model {
|
|
|
338
379
|
|
|
339
380
|
this.runtimeSkeleton.ikChainInfo = ikChainInfo
|
|
340
381
|
this.runtimeSkeleton.ikSolvers = ikSolvers
|
|
382
|
+
|
|
383
|
+
this.buildDeformOrder()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Precompute the bone order that computeWorldMatrices iterates every frame: every
|
|
387
|
+
// bone appears after its parent. This is the exact finishing order the previous
|
|
388
|
+
// recursive computeWorld() produced (walk up the not-yet-emitted ancestor chain,
|
|
389
|
+
// then emit it top-down; ties broken by ascending index) — so behavior is identical,
|
|
390
|
+
// minus the per-frame closure + visited-array allocation and the recursion overhead.
|
|
391
|
+
private buildDeformOrder(): void {
|
|
392
|
+
const bones = this.skeleton.bones
|
|
393
|
+
const n = bones.length
|
|
394
|
+
const order = new Int32Array(n)
|
|
395
|
+
const done = new Uint8Array(n)
|
|
396
|
+
const stack: number[] = []
|
|
397
|
+
let k = 0
|
|
398
|
+
for (let i = 0; i < n; i++) {
|
|
399
|
+
if (done[i]) continue
|
|
400
|
+
stack.length = 0
|
|
401
|
+
let cur = i
|
|
402
|
+
// Collect the chain of not-yet-emitted ancestors up to the root (or a done one).
|
|
403
|
+
while (cur >= 0 && cur < n && !done[cur]) {
|
|
404
|
+
stack.push(cur)
|
|
405
|
+
cur = bones[cur].parentIndex
|
|
406
|
+
}
|
|
407
|
+
// Emit from the topmost ancestor down so parents precede children.
|
|
408
|
+
for (let s = stack.length - 1; s >= 0; s--) {
|
|
409
|
+
const b = stack[s]
|
|
410
|
+
done[b] = 1
|
|
411
|
+
order[k++] = b
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.deformOrder = order
|
|
415
|
+
|
|
416
|
+
// Accumulate bind-pose world positions in the same parent-before-child order and
|
|
417
|
+
// with the same add order as the old recursive computeBindPoseWorldPosition, so the
|
|
418
|
+
// downstream arithmetic stays bit-identical.
|
|
419
|
+
const bindWorld = new Float32Array(n * 3)
|
|
420
|
+
for (let idx = 0; idx < n; idx++) {
|
|
421
|
+
const i = order[idx]
|
|
422
|
+
const bt = bones[i].bindTranslation
|
|
423
|
+
const p = bones[i].parentIndex
|
|
424
|
+
if (p >= 0 && p < n) {
|
|
425
|
+
bindWorld[i * 3 + 0] = bindWorld[p * 3 + 0] + bt[0]
|
|
426
|
+
bindWorld[i * 3 + 1] = bindWorld[p * 3 + 1] + bt[1]
|
|
427
|
+
bindWorld[i * 3 + 2] = bindWorld[p * 3 + 2] + bt[2]
|
|
428
|
+
} else {
|
|
429
|
+
bindWorld[i * 3 + 0] = bt[0]
|
|
430
|
+
bindWorld[i * 3 + 1] = bt[1]
|
|
431
|
+
bindWorld[i * 3 + 2] = bt[2]
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
this.bindWorldPos = bindWorld
|
|
341
435
|
}
|
|
342
436
|
|
|
343
437
|
private initializeTweenBuffers(): void {
|
|
@@ -659,63 +753,49 @@ export class Model {
|
|
|
659
753
|
}
|
|
660
754
|
|
|
661
755
|
// VMD translation (world delta from bind pose) → bone local space; optional rotation for animation vs IK
|
|
756
|
+
// Returns a REUSED scratch Vec3 (_convOut) — callers must copy immediately (they do:
|
|
757
|
+
// .set() / destructure). Zero allocation; result is bit-identical to the previous
|
|
758
|
+
// recursive implementation (verified numerically).
|
|
662
759
|
private convertVMDTranslationToLocal(boneIdx: number, vmdRelativeTranslation: Vec3, rotation?: Quat): Vec3 {
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
parentBindPoseWorldPos = computeBindPoseWorldPosition(bone.parentIndex)
|
|
760
|
+
const bone = this.skeleton.bones[boneIdx]
|
|
761
|
+
const bindWorld = this.bindWorldPos
|
|
762
|
+
const bt = bone.bindTranslation
|
|
763
|
+
const p = bone.parentIndex
|
|
764
|
+
|
|
765
|
+
// afterBindTranslation = (bindWorld[bone] + vmd − bindWorld[parent]) − bindTranslation.
|
|
766
|
+
// (Algebraically this reduces to vmd, since bindWorld[bone] = bindWorld[parent] +
|
|
767
|
+
// bindTranslation, but the explicit form keeps the result bit-identical to before.)
|
|
768
|
+
const bi3 = boneIdx * 3
|
|
769
|
+
const targetX = bindWorld[bi3 + 0] + vmdRelativeTranslation.x
|
|
770
|
+
const targetY = bindWorld[bi3 + 1] + vmdRelativeTranslation.y
|
|
771
|
+
const targetZ = bindWorld[bi3 + 2] + vmdRelativeTranslation.z
|
|
772
|
+
const px = p >= 0 ? bindWorld[p * 3 + 0] : 0
|
|
773
|
+
const py = p >= 0 ? bindWorld[p * 3 + 1] : 0
|
|
774
|
+
const pz = p >= 0 ? bindWorld[p * 3 + 2] : 0
|
|
775
|
+
const abx = targetX - px - bt[0]
|
|
776
|
+
const aby = targetY - py - bt[1]
|
|
777
|
+
const abz = targetZ - pz - bt[2]
|
|
778
|
+
|
|
779
|
+
// Inverse rotation = conjugate + normalize (matches localRotation.clone().conjugate()
|
|
780
|
+
// .normalize()), applied via a rotation matrix (matches Mat4.fromQuat). Uses animation
|
|
781
|
+
// rotation when provided so IK-modified localRot doesn't perturb the conversion.
|
|
782
|
+
const q = rotation ?? this.runtimeSkeleton.localRotations[boneIdx]
|
|
783
|
+
const qlen = Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w)
|
|
784
|
+
let ix: number, iy: number, iz: number, iw: number
|
|
785
|
+
if (qlen === 0) {
|
|
786
|
+
ix = 0; iy = 0; iz = 0; iw = 1
|
|
691
787
|
} else {
|
|
692
|
-
|
|
788
|
+
const inv = 1 / qlen
|
|
789
|
+
ix = -q.x * inv; iy = -q.y * inv; iz = -q.z * inv; iw = q.w * inv
|
|
693
790
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const afterBindTranslation = parentSpacePos.subtract(
|
|
701
|
-
new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
// Apply inverse rotation to get local translation
|
|
705
|
-
// Use provided rotation (animation rotation) or fall back to current localRotation
|
|
706
|
-
// Using animation rotation prevents conflicts when IK modifies the rotation
|
|
707
|
-
const localRotation = rotation ?? localRot[boneIdx]
|
|
708
|
-
// Clone to avoid mutating, then conjugate and normalize
|
|
709
|
-
const invRotation = localRotation.clone().conjugate().normalize()
|
|
710
|
-
const rotationMat = Mat4.fromQuat(invRotation.x, invRotation.y, invRotation.z, invRotation.w)
|
|
711
|
-
const rm = rotationMat.values
|
|
712
|
-
const localTranslation = new Vec3(
|
|
713
|
-
rm[0] * afterBindTranslation.x + rm[4] * afterBindTranslation.y + rm[8] * afterBindTranslation.z,
|
|
714
|
-
rm[1] * afterBindTranslation.x + rm[5] * afterBindTranslation.y + rm[9] * afterBindTranslation.z,
|
|
715
|
-
rm[2] * afterBindTranslation.x + rm[6] * afterBindTranslation.y + rm[10] * afterBindTranslation.z
|
|
791
|
+
Mat4.fromQuatInto(ix, iy, iz, iw, _convMat, 0)
|
|
792
|
+
const rm = _convMat
|
|
793
|
+
_convOut.setXYZ(
|
|
794
|
+
rm[0] * abx + rm[4] * aby + rm[8] * abz,
|
|
795
|
+
rm[1] * abx + rm[5] * aby + rm[9] * abz,
|
|
796
|
+
rm[2] * abx + rm[6] * aby + rm[10] * abz
|
|
716
797
|
)
|
|
717
|
-
|
|
718
|
-
return localTranslation
|
|
798
|
+
return _convOut
|
|
719
799
|
}
|
|
720
800
|
|
|
721
801
|
getWorldMatrices(): Mat4[] {
|
|
@@ -826,15 +906,16 @@ export class Model {
|
|
|
826
906
|
}
|
|
827
907
|
|
|
828
908
|
private applyMorphs(): void {
|
|
829
|
-
// Reset vertex data to base positions
|
|
830
|
-
this.vertexData.set(this.baseVertexData)
|
|
831
|
-
|
|
832
909
|
const vertexCount = this.vertexCount
|
|
833
910
|
const morphCount = this.morphing.morphs.length
|
|
834
911
|
const weights = this.runtimeMorph.weights
|
|
835
912
|
|
|
836
|
-
//
|
|
837
|
-
|
|
913
|
+
// Effective weights (group-morph resolution + clamp). Both paths need these: the GPU
|
|
914
|
+
// path uploads them for the compute pass; the CPU path applies them below. Reused buffer.
|
|
915
|
+
if (!this.morphEffectiveWeights || this.morphEffectiveWeights.length !== morphCount) {
|
|
916
|
+
this.morphEffectiveWeights = new Float32Array(morphCount)
|
|
917
|
+
}
|
|
918
|
+
const effectiveWeights = this.morphEffectiveWeights
|
|
838
919
|
effectiveWeights.set(weights) // Start with direct weights
|
|
839
920
|
|
|
840
921
|
// Apply group morphs: group morph weight * ratio affects referenced morphs
|
|
@@ -845,20 +926,33 @@ export class Model {
|
|
|
845
926
|
if (groupWeight > 0.0001) {
|
|
846
927
|
for (const ref of morph.groupReferences) {
|
|
847
928
|
if (ref.morphIndex >= 0 && ref.morphIndex < morphCount) {
|
|
848
|
-
// Add group morph's contribution to the referenced morph
|
|
849
929
|
effectiveWeights[ref.morphIndex] += groupWeight * ref.ratio
|
|
850
930
|
}
|
|
851
931
|
}
|
|
852
932
|
}
|
|
853
933
|
}
|
|
854
934
|
}
|
|
855
|
-
|
|
856
|
-
// Clamp effective weights to [0, 1]
|
|
857
935
|
for (let i = 0; i < morphCount; i++) {
|
|
858
936
|
effectiveWeights[i] = Math.max(0, Math.min(1, effectiveWeights[i]))
|
|
859
937
|
}
|
|
860
938
|
|
|
861
|
-
//
|
|
939
|
+
// GPU path: the compute pass applies the vertex offsets from these weights.
|
|
940
|
+
if (this.gpuMorphEnabled) {
|
|
941
|
+
this.morphWeightsDirty = true
|
|
942
|
+
return
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ── CPU path ── Reset only the vertices morphed by the previous pass back to base
|
|
946
|
+
// (targeted reset; vertexData never diverges from base outside that range).
|
|
947
|
+
if (this.morphPrevMaxVert >= 0) {
|
|
948
|
+
const s = this.morphPrevMinVert * VERTEX_STRIDE
|
|
949
|
+
const e = (this.morphPrevMaxVert + 1) * VERTEX_STRIDE
|
|
950
|
+
this.vertexData.set(this.baseVertexData.subarray(s, e), s)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Apply vertex morphs, tracking the touched vertex-index range for partial upload.
|
|
954
|
+
let curMinVert = -1
|
|
955
|
+
let curMaxVert = -1
|
|
862
956
|
for (let morphIdx = 0; morphIdx < morphCount; morphIdx++) {
|
|
863
957
|
const effectiveWeight = effectiveWeights[morphIdx]
|
|
864
958
|
if (effectiveWeight === 0 || effectiveWeight < 0.0001) continue
|
|
@@ -866,28 +960,140 @@ export class Model {
|
|
|
866
960
|
const morph = this.morphing.morphs[morphIdx]
|
|
867
961
|
if (morph.type !== 1) continue // Only process vertex morphs
|
|
868
962
|
|
|
869
|
-
// For vertex morphs, iterate through vertices that have offsets
|
|
870
963
|
for (const vertexOffset of morph.vertexOffsets) {
|
|
871
964
|
const vIdx = vertexOffset.vertexIndex
|
|
872
965
|
if (vIdx < 0 || vIdx >= vertexCount) continue
|
|
873
966
|
|
|
874
|
-
// Get morph offset for this vertex
|
|
875
967
|
const offsetX = vertexOffset.positionOffset[0]
|
|
876
968
|
const offsetY = vertexOffset.positionOffset[1]
|
|
877
969
|
const offsetZ = vertexOffset.positionOffset[2]
|
|
878
|
-
|
|
879
|
-
// Skip if offset is zero
|
|
880
970
|
if (Math.abs(offsetX) < 0.0001 && Math.abs(offsetY) < 0.0001 && Math.abs(offsetZ) < 0.0001) {
|
|
881
971
|
continue
|
|
882
972
|
}
|
|
883
973
|
|
|
884
|
-
// Apply weighted offset to vertex position (positions are at stride 0, 8, 16, ...)
|
|
885
974
|
const vertexIdx = vIdx * VERTEX_STRIDE
|
|
886
975
|
this.vertexData[vertexIdx] += offsetX * effectiveWeight
|
|
887
976
|
this.vertexData[vertexIdx + 1] += offsetY * effectiveWeight
|
|
888
977
|
this.vertexData[vertexIdx + 2] += offsetZ * effectiveWeight
|
|
978
|
+
|
|
979
|
+
if (curMinVert < 0 || vIdx < curMinVert) curMinVert = vIdx
|
|
980
|
+
if (vIdx > curMaxVert) curMaxVert = vIdx
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let dirtyMin = curMinVert
|
|
985
|
+
let dirtyMax = curMaxVert
|
|
986
|
+
if (this.morphPrevMaxVert >= 0) {
|
|
987
|
+
if (dirtyMin < 0 || this.morphPrevMinVert < dirtyMin) dirtyMin = this.morphPrevMinVert
|
|
988
|
+
if (this.morphPrevMaxVert > dirtyMax) dirtyMax = this.morphPrevMaxVert
|
|
989
|
+
}
|
|
990
|
+
if (dirtyMin >= 0 && dirtyMax >= 0) {
|
|
991
|
+
this.morphPendingMinVert =
|
|
992
|
+
this.morphPendingMinVert < 0 ? dirtyMin : Math.min(this.morphPendingMinVert, dirtyMin)
|
|
993
|
+
this.morphPendingMaxVert =
|
|
994
|
+
this.morphPendingMaxVert < 0 ? dirtyMax : Math.max(this.morphPendingMaxVert, dirtyMax)
|
|
995
|
+
}
|
|
996
|
+
this.morphPrevMinVert = curMinVert
|
|
997
|
+
this.morphPrevMaxVert = curMaxVert
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── GPU morph path support ──
|
|
1001
|
+
// Called by the engine once it has created the compute buffers for this model; switches
|
|
1002
|
+
// applyMorphs to the weights-only branch.
|
|
1003
|
+
enableGpuMorphs(): void {
|
|
1004
|
+
this.gpuMorphEnabled = true
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// True (once) when morph weights changed since the last check — the engine then uploads
|
|
1008
|
+
// getEffectiveMorphWeights() and dispatches the compute pass.
|
|
1009
|
+
consumeMorphWeightsDirty(): boolean {
|
|
1010
|
+
const d = this.morphWeightsDirty
|
|
1011
|
+
this.morphWeightsDirty = false
|
|
1012
|
+
return d
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Effective (group-resolved, clamped) morph weights for GPU upload. Ensures they're
|
|
1016
|
+
// computed at least once even before the first weight change.
|
|
1017
|
+
getEffectiveMorphWeights(): Float32Array {
|
|
1018
|
+
if (!this.morphEffectiveWeights) this.applyMorphs()
|
|
1019
|
+
return this.morphEffectiveWeights ?? new Float32Array(0)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Build the CSR inversion of vertex-morph offsets for the GPU compute pass. Returns null
|
|
1023
|
+
// when the model has no vertex-morph offsets (no GPU path needed). Entries for each vertex
|
|
1024
|
+
// are emitted in ascending morph-index order, matching the CPU accumulation order.
|
|
1025
|
+
buildMorphComputeData(): MorphComputeData | null {
|
|
1026
|
+
const V = this.vertexCount
|
|
1027
|
+
const M = this.morphing.morphs.length
|
|
1028
|
+
const morphs = this.morphing.morphs
|
|
1029
|
+
const EPS = 0.0001
|
|
1030
|
+
|
|
1031
|
+
const isLive = (o: VertexMorphOffset): boolean =>
|
|
1032
|
+
o.vertexIndex >= 0 &&
|
|
1033
|
+
o.vertexIndex < V &&
|
|
1034
|
+
(Math.abs(o.positionOffset[0]) >= EPS ||
|
|
1035
|
+
Math.abs(o.positionOffset[1]) >= EPS ||
|
|
1036
|
+
Math.abs(o.positionOffset[2]) >= EPS)
|
|
1037
|
+
|
|
1038
|
+
const counts = new Uint32Array(V)
|
|
1039
|
+
for (let m = 0; m < M; m++) {
|
|
1040
|
+
const morph = morphs[m]
|
|
1041
|
+
if (morph.type !== 1) continue
|
|
1042
|
+
for (const o of morph.vertexOffsets) if (isLive(o)) counts[o.vertexIndex]++
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const rowStart = new Uint32Array(V + 1)
|
|
1046
|
+
let acc = 0
|
|
1047
|
+
for (let v = 0; v < V; v++) {
|
|
1048
|
+
rowStart[v] = acc
|
|
1049
|
+
acc += counts[v]
|
|
1050
|
+
}
|
|
1051
|
+
rowStart[V] = acc
|
|
1052
|
+
const E = acc
|
|
1053
|
+
if (E === 0) return null
|
|
1054
|
+
|
|
1055
|
+
const colMorph = new Uint32Array(E)
|
|
1056
|
+
const colOffset = new Float32Array(E * 3)
|
|
1057
|
+
const fill = new Uint32Array(V)
|
|
1058
|
+
for (let m = 0; m < M; m++) {
|
|
1059
|
+
const morph = morphs[m]
|
|
1060
|
+
if (morph.type !== 1) continue
|
|
1061
|
+
for (const o of morph.vertexOffsets) {
|
|
1062
|
+
if (!isLive(o)) continue
|
|
1063
|
+
const v = o.vertexIndex
|
|
1064
|
+
const p = rowStart[v] + fill[v]++
|
|
1065
|
+
colMorph[p] = m
|
|
1066
|
+
colOffset[p * 3] = o.positionOffset[0]
|
|
1067
|
+
colOffset[p * 3 + 1] = o.positionOffset[1]
|
|
1068
|
+
colOffset[p * 3 + 2] = o.positionOffset[2]
|
|
889
1069
|
}
|
|
890
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
const basePositions = new Float32Array(V * 3)
|
|
1073
|
+
for (let v = 0; v < V; v++) {
|
|
1074
|
+
const vi = v * VERTEX_STRIDE
|
|
1075
|
+
basePositions[v * 3] = this.baseVertexData[vi]
|
|
1076
|
+
basePositions[v * 3 + 1] = this.baseVertexData[vi + 1]
|
|
1077
|
+
basePositions[v * 3 + 2] = this.baseVertexData[vi + 2]
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return { basePositions, rowStart, colMorph, colOffset, morphCount: M, vertexCount: V, entryCount: E }
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Consume the pending morph vertex-upload range for the engine. Returns null when a
|
|
1084
|
+
// full upload is needed (first time after load, or nothing tracked), else the inclusive
|
|
1085
|
+
// [minVert, maxVert] slice that changed. Resets pending state.
|
|
1086
|
+
consumeVertexUploadRange(): { minVert: number; maxVert: number } | null {
|
|
1087
|
+
if (this.morphUploadFull || this.morphPendingMaxVert < 0) {
|
|
1088
|
+
this.morphUploadFull = false
|
|
1089
|
+
this.morphPendingMinVert = -1
|
|
1090
|
+
this.morphPendingMaxVert = -1
|
|
1091
|
+
return null
|
|
1092
|
+
}
|
|
1093
|
+
const range = { minVert: this.morphPendingMinVert, maxVert: this.morphPendingMaxVert }
|
|
1094
|
+
this.morphPendingMinVert = -1
|
|
1095
|
+
this.morphPendingMaxVert = -1
|
|
1096
|
+
return range
|
|
891
1097
|
}
|
|
892
1098
|
|
|
893
1099
|
private buildClipFromVmdKeyFrames(vmdKeyFrames: VMDKeyFrame[]): AnimationClip {
|
|
@@ -1143,13 +1349,13 @@ export class Model {
|
|
|
1143
1349
|
const interp = frameB.interpolation
|
|
1144
1350
|
|
|
1145
1351
|
const rotT = interpolateControlPoints(interp.rotation, gradient)
|
|
1146
|
-
const rotation = Quat.
|
|
1352
|
+
const rotation = Quat.slerpInto(frameA.rotation, frameB.rotation, rotT, _animSlerp)
|
|
1147
1353
|
|
|
1148
1354
|
const txWeight = interpolateControlPoints(interp.translationX, gradient)
|
|
1149
1355
|
const tyWeight = interpolateControlPoints(interp.translationY, gradient)
|
|
1150
1356
|
const tzWeight = interpolateControlPoints(interp.translationZ, gradient)
|
|
1151
1357
|
|
|
1152
|
-
const interpolatedVMDTranslation =
|
|
1358
|
+
const interpolatedVMDTranslation = _animInterpT.setXYZ(
|
|
1153
1359
|
frameA.translation.x + (frameB.translation.x - frameA.translation.x) * txWeight,
|
|
1154
1360
|
frameA.translation.y + (frameB.translation.y - frameA.translation.y) * tyWeight,
|
|
1155
1361
|
frameA.translation.z + (frameB.translation.z - frameA.translation.z) * tzWeight
|
|
@@ -1235,10 +1441,13 @@ export class Model {
|
|
|
1235
1441
|
if (!ikChainInfo) return
|
|
1236
1442
|
|
|
1237
1443
|
// Solve each IK solver sequentially, ensuring consistent state between solvers
|
|
1444
|
+
let firstSolver = true
|
|
1238
1445
|
for (const solver of ikSolvers) {
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
|
|
1446
|
+
// Each solver must see the effects of previous solvers on localRotations, so
|
|
1447
|
+
// recompute world matrices between solvers. The first solver is skipped: the
|
|
1448
|
+
// caller (update) already computed them and nothing has changed localRotations yet.
|
|
1449
|
+
if (!firstSolver) this.computeWorldMatrices()
|
|
1450
|
+
firstSolver = false
|
|
1242
1451
|
|
|
1243
1452
|
// Clear computed set for this solver's pass
|
|
1244
1453
|
this.ikComputedSet.clear()
|
|
@@ -1388,21 +1597,13 @@ export class Model {
|
|
|
1388
1597
|
|
|
1389
1598
|
if (boneCount === 0) return
|
|
1390
1599
|
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1600
|
+
// Flat traversal in precomputed order: every bone's parent is already done, so no
|
|
1601
|
+
// per-bone visited check, no recursion, and no per-call allocation. Same per-bone
|
|
1602
|
+
// math as before. Scratch slots are safe to reuse since there's no reentrancy now.
|
|
1603
|
+
const order = this.deformOrder
|
|
1604
|
+
for (let k = 0; k < boneCount; k++) {
|
|
1605
|
+
const i = order[k]
|
|
1397
1606
|
const b = bones[i]
|
|
1398
|
-
if (b.parentIndex >= boneCount) {
|
|
1399
|
-
console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// Ensure parent is computed FIRST, before we touch any scratch buffers.
|
|
1403
|
-
// Recursion may itself use scratchMat4Values[0] / scratchQuat; doing it up
|
|
1404
|
-
// front keeps the current frame's scratch slots untouched when we use them below.
|
|
1405
|
-
if (b.parentIndex >= 0 && !computed[b.parentIndex]) computeWorld(b.parentIndex)
|
|
1406
1607
|
|
|
1407
1608
|
const boneRot = localRot[i]
|
|
1408
1609
|
let fx = boneRot.x, fy = boneRot.y, fz = boneRot.z, fw = boneRot.w
|
|
@@ -1467,10 +1668,6 @@ export class Model {
|
|
|
1467
1668
|
} else {
|
|
1468
1669
|
worldMat.values.set(localMVals)
|
|
1469
1670
|
}
|
|
1470
|
-
computed[i] = true
|
|
1471
1671
|
}
|
|
1472
|
-
|
|
1473
|
-
// Process all bones (recursion handles dependencies automatically)
|
|
1474
|
-
for (let i = 0; i < boneCount; i++) computeWorld(i)
|
|
1475
1672
|
}
|
|
1476
1673
|
}
|
package/src/physics/body.ts
CHANGED
|
@@ -40,6 +40,12 @@ export class RigidBodyStore {
|
|
|
40
40
|
readonly bodyOffsetInverse: Float32Array // 16*N column-major
|
|
41
41
|
private boneOffsetsReady = false
|
|
42
42
|
|
|
43
|
+
// Flat list of (i, j) pairs that survive the static-static + group/mask
|
|
44
|
+
// filter. None of those inputs change after construction, so building this
|
|
45
|
+
// once collapses 60k pair tests/step (349 bodies) down to a few thousand.
|
|
46
|
+
// Built lazily on first access.
|
|
47
|
+
private collisionPairs: Uint16Array | null = null
|
|
48
|
+
|
|
43
49
|
constructor(rigidbodies: Rigidbody[]) {
|
|
44
50
|
const N = rigidbodies.length
|
|
45
51
|
this.count = N
|
|
@@ -247,6 +253,29 @@ export class RigidBodyStore {
|
|
|
247
253
|
isBoneOffsetsReady(): boolean {
|
|
248
254
|
return this.boneOffsetsReady
|
|
249
255
|
}
|
|
256
|
+
|
|
257
|
+
// Pair-filter inputs (invMass, group, mask) are immutable post-construction,
|
|
258
|
+
// so build the candidate-pair list once and reuse every step.
|
|
259
|
+
getCollisionPairs(): Uint16Array {
|
|
260
|
+
if (this.collisionPairs !== null) return this.collisionPairs
|
|
261
|
+
const N = this.count
|
|
262
|
+
const invMass = this.invMass
|
|
263
|
+
const group = this.collisionGroup
|
|
264
|
+
const mask = this.willCollideMask
|
|
265
|
+
const buf: number[] = []
|
|
266
|
+
for (let i = 0; i < N; i++) {
|
|
267
|
+
const gi = group[i]
|
|
268
|
+
const mi = mask[i]
|
|
269
|
+
const dynA = invMass[i] > 0
|
|
270
|
+
for (let j = i + 1; j < N; j++) {
|
|
271
|
+
if (!dynA && invMass[j] === 0) continue
|
|
272
|
+
if ((mi & group[j]) === 0 || (mask[j] & gi) === 0) continue
|
|
273
|
+
buf.push(i, j)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
this.collisionPairs = new Uint16Array(buf)
|
|
277
|
+
return this.collisionPairs
|
|
278
|
+
}
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
const _scratchA = new Float32Array(16)
|
|
@@ -26,6 +26,23 @@ export interface SixDofSpringConstraint {
|
|
|
26
26
|
springEnabled: Uint8Array // length 6
|
|
27
27
|
springStiffness: Float32Array // length 6 (k)
|
|
28
28
|
equilibriumPoint: Float32Array// length 6, baked at setup time
|
|
29
|
+
|
|
30
|
+
// Per-substep cache. Filled by solver's setup pass once before SI iters,
|
|
31
|
+
// read by the velocity-only iter loop. None of these depend on lv/av — only
|
|
32
|
+
// on pos/ori/inertia which are constant during solve.
|
|
33
|
+
cacheSkip: boolean // both bodies static — skip entirely
|
|
34
|
+
cacheLeverA: Float32Array // 3: rA = anchor − posA (world-space)
|
|
35
|
+
cacheLeverB: Float32Array // 3
|
|
36
|
+
cacheLinAxes: Float32Array // 9: 3 linear axes × xyz, world-space
|
|
37
|
+
cacheLinCrossA: Float32Array // 9: (rA × ax) per axis
|
|
38
|
+
cacheLinCrossB: Float32Array // 9
|
|
39
|
+
cacheLinJacInv: Float32Array // 3: 1/(im+im+cA²·ii+cB²·ii) per axis
|
|
40
|
+
cacheLinTargetVel: Float32Array // 3: limit ERP + spring drive, signed
|
|
41
|
+
cacheLinActive: Uint8Array // 3
|
|
42
|
+
cacheAngAxes: Float32Array // 9
|
|
43
|
+
cacheAngTargetVel: Float32Array // 3
|
|
44
|
+
cacheAngActive: Uint8Array // 3
|
|
45
|
+
cacheAngJacInv: number // 1 — same for all 3 angular axes
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
export const STOP_ERP = 0.475
|
|
@@ -102,6 +119,19 @@ export function buildConstraints(
|
|
|
102
119
|
springEnabled,
|
|
103
120
|
springStiffness,
|
|
104
121
|
equilibriumPoint: new Float32Array(6),
|
|
122
|
+
cacheSkip: false,
|
|
123
|
+
cacheLeverA: new Float32Array(3),
|
|
124
|
+
cacheLeverB: new Float32Array(3),
|
|
125
|
+
cacheLinAxes: new Float32Array(9),
|
|
126
|
+
cacheLinCrossA: new Float32Array(9),
|
|
127
|
+
cacheLinCrossB: new Float32Array(9),
|
|
128
|
+
cacheLinJacInv: new Float32Array(3),
|
|
129
|
+
cacheLinTargetVel: new Float32Array(3),
|
|
130
|
+
cacheLinActive: new Uint8Array(3),
|
|
131
|
+
cacheAngAxes: new Float32Array(9),
|
|
132
|
+
cacheAngTargetVel: new Float32Array(3),
|
|
133
|
+
cacheAngActive: new Uint8Array(3),
|
|
134
|
+
cacheAngJacInv: 0,
|
|
105
135
|
})
|
|
106
136
|
}
|
|
107
137
|
|