reze-engine 0.2.19 → 0.3.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 +67 -66
- package/dist/bezier-interpolate.d.ts +15 -0
- package/dist/bezier-interpolate.d.ts.map +1 -0
- package/dist/bezier-interpolate.js +40 -0
- package/dist/engine.d.ts +7 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +252 -143
- package/dist/ik-solver.d.ts +26 -0
- package/dist/ik-solver.d.ts.map +1 -0
- package/dist/ik-solver.js +372 -0
- package/dist/math.d.ts +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +8 -0
- package/dist/model.d.ts +46 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +201 -3
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +57 -36
- package/dist/vmd-loader.d.ts +11 -1
- package/dist/vmd-loader.d.ts.map +1 -1
- package/dist/vmd-loader.js +91 -15
- package/package.json +1 -1
- package/src/bezier-interpolate.ts +47 -0
- package/src/camera.ts +358 -358
- package/src/engine.ts +275 -164
- package/src/ik-solver.ts +488 -0
- package/src/math.ts +555 -546
- package/src/model.ts +284 -3
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1173 -1145
- package/src/vmd-loader.ts +276 -179
package/src/model.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Mat4, Quat, easeInOut } from "./math"
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut } from "./math"
|
|
2
2
|
import { Rigidbody, Joint } from "./physics"
|
|
3
|
+
import { IKSolverSystem } from "./ik-solver"
|
|
3
4
|
|
|
4
5
|
const VERTEX_STRIDE = 8
|
|
5
6
|
|
|
@@ -37,6 +38,53 @@ export interface Bone {
|
|
|
37
38
|
appendRatio?: number // 0..1
|
|
38
39
|
appendRotate?: boolean
|
|
39
40
|
appendMove?: boolean
|
|
41
|
+
ikTargetIndex?: number // IK target bone index (if this bone is an IK effector)
|
|
42
|
+
ikIteration?: number // IK iteration count
|
|
43
|
+
ikLimitAngle?: number // IK rotation constraint (radians)
|
|
44
|
+
ikLinks?: IKLink[] // IK chain links
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// IK link with angle constraints
|
|
48
|
+
export interface IKLink {
|
|
49
|
+
boneIndex: number
|
|
50
|
+
hasLimit: boolean
|
|
51
|
+
minAngle?: Vec3 // Minimum Euler angles (radians)
|
|
52
|
+
maxAngle?: Vec3 // Maximum Euler angles (radians)
|
|
53
|
+
rotationOrder?: EulerRotationOrder // YXZ, ZYX, or XZY
|
|
54
|
+
solveAxis?: SolveAxis // None, Fixed, X, Y, or Z
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Euler rotation order for angle constraints
|
|
58
|
+
export enum EulerRotationOrder {
|
|
59
|
+
YXZ = 0,
|
|
60
|
+
ZYX = 1,
|
|
61
|
+
XZY = 2,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Solve axis optimization
|
|
65
|
+
export enum SolveAxis {
|
|
66
|
+
None = 0,
|
|
67
|
+
Fixed = 1,
|
|
68
|
+
X = 2,
|
|
69
|
+
Y = 3,
|
|
70
|
+
Z = 4,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// IK solver definition
|
|
74
|
+
export interface IKSolver {
|
|
75
|
+
index: number
|
|
76
|
+
ikBoneIndex: number // Effector bone (the bone that should reach the target)
|
|
77
|
+
targetBoneIndex: number // Target bone
|
|
78
|
+
iterationCount: number
|
|
79
|
+
limitAngle: number // Max rotation per iteration (radians)
|
|
80
|
+
links: IKLink[] // Chain bones from effector to root
|
|
81
|
+
canSkipWhenPhysicsEnabled: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// IK chain info per bone (runtime state)
|
|
85
|
+
export interface IKChainInfo {
|
|
86
|
+
ikRotation: Quat // Accumulated IK rotation
|
|
87
|
+
localRotation: Quat // Cached local rotation before IK
|
|
40
88
|
}
|
|
41
89
|
|
|
42
90
|
export interface Skeleton {
|
|
@@ -81,6 +129,8 @@ export interface SkeletonRuntime {
|
|
|
81
129
|
localTranslations: Float32Array // vec3 per bone length = boneCount*3
|
|
82
130
|
worldMatrices: Float32Array // mat4 per bone length = boneCount*16
|
|
83
131
|
computedBones: boolean[] // length = boneCount
|
|
132
|
+
ikChainInfo?: IKChainInfo[] // IK chain info per bone (only for IK chain bones)
|
|
133
|
+
ikSolvers?: IKSolver[] // All IK solvers in the model
|
|
84
134
|
}
|
|
85
135
|
|
|
86
136
|
// Runtime morph state
|
|
@@ -107,6 +157,15 @@ interface MorphWeightTweenState {
|
|
|
107
157
|
durationMs: Float32Array // one float per morph (ms)
|
|
108
158
|
}
|
|
109
159
|
|
|
160
|
+
// Translation tween state per bone
|
|
161
|
+
interface TranslationTweenState {
|
|
162
|
+
active: Uint8Array // 0/1 per bone
|
|
163
|
+
startVec: Float32Array // vec3 per bone (x,y,z)
|
|
164
|
+
targetVec: Float32Array // vec3 per bone (x,y,z)
|
|
165
|
+
startTimeMs: Float32Array // one float per bone (ms)
|
|
166
|
+
durationMs: Float32Array // one float per bone (ms)
|
|
167
|
+
}
|
|
168
|
+
|
|
110
169
|
export class Model {
|
|
111
170
|
private vertexData: Float32Array<ArrayBuffer>
|
|
112
171
|
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
@@ -136,6 +195,7 @@ export class Model {
|
|
|
136
195
|
private cachedIdentityMat2 = Mat4.identity()
|
|
137
196
|
|
|
138
197
|
private rotTweenState!: RotationTweenState
|
|
198
|
+
private transTweenState!: TranslationTweenState
|
|
139
199
|
private morphTweenState!: MorphWeightTweenState
|
|
140
200
|
|
|
141
201
|
constructor(
|
|
@@ -168,6 +228,7 @@ export class Model {
|
|
|
168
228
|
|
|
169
229
|
this.initializeRuntimeSkeleton()
|
|
170
230
|
this.initializeRotTweenBuffers()
|
|
231
|
+
this.initializeTransTweenBuffers()
|
|
171
232
|
this.initializeRuntimeMorph()
|
|
172
233
|
this.initializeMorphTweenBuffers()
|
|
173
234
|
this.applyMorphs() // Apply initial morphs (all weights are 0, so no change)
|
|
@@ -197,6 +258,58 @@ export class Model {
|
|
|
197
258
|
rotations[qi + 3] = 1
|
|
198
259
|
}
|
|
199
260
|
}
|
|
261
|
+
|
|
262
|
+
// Initialize IK runtime state
|
|
263
|
+
this.initializeIKRuntime()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private initializeIKRuntime(): void {
|
|
267
|
+
const boneCount = this.skeleton.bones.length
|
|
268
|
+
const bones = this.skeleton.bones
|
|
269
|
+
|
|
270
|
+
// Initialize IK chain info for all bones (will be populated for IK chain bones)
|
|
271
|
+
const ikChainInfo: IKChainInfo[] = new Array(boneCount)
|
|
272
|
+
for (let i = 0; i < boneCount; i++) {
|
|
273
|
+
ikChainInfo[i] = {
|
|
274
|
+
ikRotation: new Quat(0, 0, 0, 1),
|
|
275
|
+
localRotation: new Quat(0, 0, 0, 1),
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Build IK solvers from bone data
|
|
280
|
+
const ikSolvers: IKSolver[] = []
|
|
281
|
+
let solverIndex = 0
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < boneCount; i++) {
|
|
284
|
+
const bone = bones[i]
|
|
285
|
+
if (bone.ikTargetIndex !== undefined && bone.ikLinks && bone.ikLinks.length > 0) {
|
|
286
|
+
// Check if all links are affected by physics (for optimization)
|
|
287
|
+
let canSkipWhenPhysicsEnabled = true
|
|
288
|
+
for (const link of bone.ikLinks) {
|
|
289
|
+
// For now, assume no bones are physics-controlled (can be enhanced later)
|
|
290
|
+
// If a bone has a rigidbody attached, it's physics-controlled
|
|
291
|
+
const hasPhysics = this.rigidbodies.some((rb) => rb.boneIndex === link.boneIndex)
|
|
292
|
+
if (!hasPhysics) {
|
|
293
|
+
canSkipWhenPhysicsEnabled = false
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const solver: IKSolver = {
|
|
299
|
+
index: solverIndex++,
|
|
300
|
+
ikBoneIndex: i,
|
|
301
|
+
targetBoneIndex: bone.ikTargetIndex,
|
|
302
|
+
iterationCount: bone.ikIteration ?? 1,
|
|
303
|
+
limitAngle: bone.ikLimitAngle ?? Math.PI,
|
|
304
|
+
links: bone.ikLinks,
|
|
305
|
+
canSkipWhenPhysicsEnabled,
|
|
306
|
+
}
|
|
307
|
+
ikSolvers.push(solver)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.runtimeSkeleton.ikChainInfo = ikChainInfo
|
|
312
|
+
this.runtimeSkeleton.ikSolvers = ikSolvers
|
|
200
313
|
}
|
|
201
314
|
|
|
202
315
|
private initializeRotTweenBuffers(): void {
|
|
@@ -210,6 +323,17 @@ export class Model {
|
|
|
210
323
|
}
|
|
211
324
|
}
|
|
212
325
|
|
|
326
|
+
private initializeTransTweenBuffers(): void {
|
|
327
|
+
const n = this.skeleton.bones.length
|
|
328
|
+
this.transTweenState = {
|
|
329
|
+
active: new Uint8Array(n),
|
|
330
|
+
startVec: new Float32Array(n * 3),
|
|
331
|
+
targetVec: new Float32Array(n * 3),
|
|
332
|
+
startTimeMs: new Float32Array(n),
|
|
333
|
+
durationMs: new Float32Array(n),
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
213
337
|
private initializeMorphTweenBuffers(): void {
|
|
214
338
|
const n = this.morphing.morphs.length
|
|
215
339
|
this.morphTweenState = {
|
|
@@ -270,6 +394,29 @@ export class Model {
|
|
|
270
394
|
}
|
|
271
395
|
}
|
|
272
396
|
|
|
397
|
+
private updateTranslationTweens(): void {
|
|
398
|
+
const state = this.transTweenState
|
|
399
|
+
const now = performance.now()
|
|
400
|
+
const translations = this.runtimeSkeleton.localTranslations
|
|
401
|
+
const boneCount = this.skeleton.bones.length
|
|
402
|
+
|
|
403
|
+
for (let i = 0; i < boneCount; i++) {
|
|
404
|
+
if (state.active[i] !== 1) continue
|
|
405
|
+
|
|
406
|
+
const startMs = state.startTimeMs[i]
|
|
407
|
+
const durMs = Math.max(1, state.durationMs[i])
|
|
408
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
409
|
+
const e = easeInOut(t)
|
|
410
|
+
|
|
411
|
+
const ti = i * 3
|
|
412
|
+
translations[ti] = state.startVec[ti] + (state.targetVec[ti] - state.startVec[ti]) * e
|
|
413
|
+
translations[ti + 1] = state.startVec[ti + 1] + (state.targetVec[ti + 1] - state.startVec[ti + 1]) * e
|
|
414
|
+
translations[ti + 2] = state.startVec[ti + 2] + (state.targetVec[ti + 2] - state.startVec[ti + 2]) * e
|
|
415
|
+
|
|
416
|
+
if (t >= 1) state.active[i] = 0
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
273
420
|
private updateMorphWeightTweens(): boolean {
|
|
274
421
|
const state = this.morphTweenState
|
|
275
422
|
const now = performance.now()
|
|
@@ -427,6 +574,108 @@ export class Model {
|
|
|
427
574
|
}
|
|
428
575
|
}
|
|
429
576
|
|
|
577
|
+
// Move bones using VMD-style relative translations (relative to bind pose world position)
|
|
578
|
+
// This is the default behavior for VMD animations
|
|
579
|
+
moveBones(names: string[], relativeTranslations: Vec3[], durationMs?: number): void {
|
|
580
|
+
const state = this.transTweenState
|
|
581
|
+
const now = performance.now()
|
|
582
|
+
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
583
|
+
const localRot = this.runtimeSkeleton.localRotations
|
|
584
|
+
|
|
585
|
+
// Compute bind pose world positions for all bones
|
|
586
|
+
const skeleton = this.skeleton
|
|
587
|
+
const computeBindPoseWorldPosition = (idx: number): Vec3 => {
|
|
588
|
+
const bone = skeleton.bones[idx]
|
|
589
|
+
const bindPos = new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
|
|
590
|
+
if (bone.parentIndex >= 0 && bone.parentIndex < skeleton.bones.length) {
|
|
591
|
+
const parentWorldPos = computeBindPoseWorldPosition(bone.parentIndex)
|
|
592
|
+
return parentWorldPos.add(bindPos)
|
|
593
|
+
} else {
|
|
594
|
+
return bindPos
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (let i = 0; i < names.length; i++) {
|
|
599
|
+
const name = names[i]
|
|
600
|
+
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
|
|
601
|
+
if (idx < 0 || idx >= this.skeleton.bones.length) continue
|
|
602
|
+
|
|
603
|
+
const bone = this.skeleton.bones[idx]
|
|
604
|
+
const ti = idx * 3
|
|
605
|
+
const qi = idx * 4
|
|
606
|
+
const translations = this.runtimeSkeleton.localTranslations
|
|
607
|
+
const vmdRelativeTranslation = relativeTranslations[i]
|
|
608
|
+
|
|
609
|
+
// VMD translation is relative to bind pose world position
|
|
610
|
+
// targetWorldPos = bindPoseWorldPos + vmdRelativeTranslation
|
|
611
|
+
const bindPoseWorldPos = computeBindPoseWorldPosition(idx)
|
|
612
|
+
const targetWorldPos = bindPoseWorldPos.add(vmdRelativeTranslation)
|
|
613
|
+
|
|
614
|
+
// Convert target world position to local translation
|
|
615
|
+
// We need parent's bind pose world position to transform to parent space
|
|
616
|
+
let parentBindPoseWorldPos: Vec3
|
|
617
|
+
if (bone.parentIndex >= 0) {
|
|
618
|
+
parentBindPoseWorldPos = computeBindPoseWorldPosition(bone.parentIndex)
|
|
619
|
+
} else {
|
|
620
|
+
parentBindPoseWorldPos = new Vec3(0, 0, 0)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Transform target world position to parent's local space
|
|
624
|
+
// In bind pose, parent's world matrix is just a translation
|
|
625
|
+
const parentSpacePos = targetWorldPos.subtract(parentBindPoseWorldPos)
|
|
626
|
+
|
|
627
|
+
// Subtract bindTranslation to get position after bind translation
|
|
628
|
+
const afterBindTranslation = parentSpacePos.subtract(
|
|
629
|
+
new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
// Apply inverse rotation to get local translation
|
|
633
|
+
const localRotation = new Quat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
|
|
634
|
+
const invRotation = localRotation.conjugate().normalize()
|
|
635
|
+
const rotationMat = Mat4.fromQuat(invRotation.x, invRotation.y, invRotation.z, invRotation.w)
|
|
636
|
+
const rm = rotationMat.values
|
|
637
|
+
const localTranslation = new Vec3(
|
|
638
|
+
rm[0] * afterBindTranslation.x + rm[4] * afterBindTranslation.y + rm[8] * afterBindTranslation.z,
|
|
639
|
+
rm[1] * afterBindTranslation.x + rm[5] * afterBindTranslation.y + rm[9] * afterBindTranslation.z,
|
|
640
|
+
rm[2] * afterBindTranslation.x + rm[6] * afterBindTranslation.y + rm[10] * afterBindTranslation.z
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
const [tx, ty, tz] = [localTranslation.x, localTranslation.y, localTranslation.z]
|
|
644
|
+
|
|
645
|
+
if (dur === 0) {
|
|
646
|
+
translations[ti] = tx
|
|
647
|
+
translations[ti + 1] = ty
|
|
648
|
+
translations[ti + 2] = tz
|
|
649
|
+
state.active[idx] = 0
|
|
650
|
+
continue
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let sx = translations[ti]
|
|
654
|
+
let sy = translations[ti + 1]
|
|
655
|
+
let sz = translations[ti + 2]
|
|
656
|
+
|
|
657
|
+
if (state.active[idx] === 1) {
|
|
658
|
+
const startMs = state.startTimeMs[idx]
|
|
659
|
+
const prevDur = Math.max(1, state.durationMs[idx])
|
|
660
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
661
|
+
const e = easeInOut(t)
|
|
662
|
+
sx = state.startVec[ti] + (state.targetVec[ti] - state.startVec[ti]) * e
|
|
663
|
+
sy = state.startVec[ti + 1] + (state.targetVec[ti + 1] - state.startVec[ti + 1]) * e
|
|
664
|
+
sz = state.startVec[ti + 2] + (state.targetVec[ti + 2] - state.startVec[ti + 2]) * e
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
state.startVec[ti] = sx
|
|
668
|
+
state.startVec[ti + 1] = sy
|
|
669
|
+
state.startVec[ti + 2] = sz
|
|
670
|
+
state.targetVec[ti] = tx
|
|
671
|
+
state.targetVec[ti + 1] = ty
|
|
672
|
+
state.targetVec[ti + 2] = tz
|
|
673
|
+
state.startTimeMs[idx] = now
|
|
674
|
+
state.durationMs[idx] = dur
|
|
675
|
+
state.active[idx] = 1
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
430
679
|
getBoneWorldMatrices(): Float32Array {
|
|
431
680
|
return this.runtimeSkeleton.worldMatrices
|
|
432
681
|
}
|
|
@@ -547,14 +796,42 @@ export class Model {
|
|
|
547
796
|
|
|
548
797
|
evaluatePose(): boolean {
|
|
549
798
|
this.updateRotationTweens()
|
|
799
|
+
this.updateTranslationTweens()
|
|
550
800
|
const hasActiveMorphTweens = this.updateMorphWeightTweens()
|
|
551
801
|
if (hasActiveMorphTweens) {
|
|
552
802
|
this.applyMorphs()
|
|
553
803
|
}
|
|
804
|
+
|
|
805
|
+
// Compute initial world matrices (needed for IK solving)
|
|
554
806
|
this.computeWorldMatrices()
|
|
807
|
+
|
|
808
|
+
// Solve IK chains (modifies localRotations)
|
|
809
|
+
this.solveIKChains()
|
|
810
|
+
|
|
811
|
+
// Recompute world matrices with IK rotations applied
|
|
812
|
+
this.computeWorldMatrices()
|
|
813
|
+
|
|
555
814
|
return hasActiveMorphTweens
|
|
556
815
|
}
|
|
557
816
|
|
|
817
|
+
private solveIKChains(): void {
|
|
818
|
+
const ikSolvers = this.runtimeSkeleton.ikSolvers
|
|
819
|
+
if (!ikSolvers || ikSolvers.length === 0) return
|
|
820
|
+
|
|
821
|
+
const ikChainInfo = this.runtimeSkeleton.ikChainInfo
|
|
822
|
+
if (!ikChainInfo) return
|
|
823
|
+
|
|
824
|
+
IKSolverSystem.solve(
|
|
825
|
+
ikSolvers,
|
|
826
|
+
this.skeleton.bones,
|
|
827
|
+
this.runtimeSkeleton.localRotations,
|
|
828
|
+
this.runtimeSkeleton.localTranslations,
|
|
829
|
+
this.runtimeSkeleton.worldMatrices,
|
|
830
|
+
ikChainInfo,
|
|
831
|
+
false // usePhysics - can be enhanced later
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
|
|
558
835
|
private computeWorldMatrices(): void {
|
|
559
836
|
const bones = this.skeleton.bones
|
|
560
837
|
const localRot = this.runtimeSkeleton.localRotations
|
|
@@ -622,11 +899,15 @@ export class Model {
|
|
|
622
899
|
}
|
|
623
900
|
}
|
|
624
901
|
|
|
625
|
-
// Build local matrix: identity + bind translation, then rotation, then append translation
|
|
902
|
+
// Build local matrix: identity + bind translation, then rotation, then local translation, then append translation
|
|
903
|
+
const ti = i * 3
|
|
904
|
+
const localTx = localTrans[ti] + addLocalTx
|
|
905
|
+
const localTy = localTrans[ti + 1] + addLocalTy
|
|
906
|
+
const localTz = localTrans[ti + 2] + addLocalTz
|
|
626
907
|
this.cachedIdentityMat1
|
|
627
908
|
.setIdentity()
|
|
628
909
|
.translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
|
|
629
|
-
this.cachedIdentityMat2.setIdentity().translateInPlace(
|
|
910
|
+
this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz)
|
|
630
911
|
const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
|
|
631
912
|
|
|
632
913
|
const worldOffset = i * 16
|