reze-engine 0.7.0 → 0.8.1
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 +24 -45
- package/dist/animation.d.ts +0 -27
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +3 -17
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +2 -2
- package/dist/engine.d.ts +49 -52
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +613 -614
- package/dist/ik-solver.d.ts +0 -11
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +1 -11
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/math.d.ts +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +24 -0
- package/dist/model.d.ts +10 -50
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +60 -93
- package/dist/pmx-loader.js +1 -1
- package/package.json +1 -1
- package/src/animation.ts +3 -37
- package/src/camera.ts +2 -2
- package/src/engine.ts +646 -750
- package/src/ik-solver.ts +1 -11
- package/src/index.ts +3 -4
- package/src/math.ts +27 -0
- package/src/model.ts +68 -99
- package/src/pmx-loader.ts +1 -1
package/src/ik-solver.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* IK Solver implementation
|
|
3
|
-
* Based on reference from babylon-mmd and Saba MMD library
|
|
4
|
-
* https://github.com/benikabocha/saba/blob/master/src/Saba/Model/MMD/MMDIkSolver.cpp
|
|
5
|
-
*/
|
|
1
|
+
// IK solver (MMD-style; see Saba MMDIkSolver.cpp)
|
|
6
2
|
|
|
7
3
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
8
4
|
import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
|
|
@@ -76,16 +72,10 @@ class IKChain {
|
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
/**
|
|
80
|
-
* Solve IK chains for a model
|
|
81
|
-
*/
|
|
82
75
|
export class IKSolverSystem {
|
|
83
76
|
private static readonly EPSILON = 1.0e-8
|
|
84
77
|
private static readonly THRESHOLD = (88 * Math.PI) / 180
|
|
85
78
|
|
|
86
|
-
/**
|
|
87
|
-
* Solve all IK chains
|
|
88
|
-
*/
|
|
89
79
|
public static solve(
|
|
90
80
|
ikSolvers: IKSolver[],
|
|
91
81
|
bones: Bone[],
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { Engine, type EngineStats } from "./engine"
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
4
|
-
export { bezierInterpolate, interpolateControlPoints, rawInterpolationToBoneInterpolation, LINEAR_INTERPOLATION } from "./animation"
|
|
1
|
+
export { Engine, type EngineStats } from "./engine"
|
|
2
|
+
export { Model } from "./model"
|
|
3
|
+
export { Vec3, Quat, Mat4 } from "./math"
|
package/src/math.ts
CHANGED
|
@@ -295,6 +295,33 @@ export class Mat4 {
|
|
|
295
295
|
)
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// LH ortho, NDC depth 0=near 1=far
|
|
299
|
+
static orthographicLh(left: number, right: number, bottom: number, top: number, near: number, far: number): Mat4 {
|
|
300
|
+
const rl = 1 / (right - left)
|
|
301
|
+
const tb = 1 / (top - bottom)
|
|
302
|
+
const fn = 1 / (far - near)
|
|
303
|
+
return new Mat4(
|
|
304
|
+
new Float32Array([
|
|
305
|
+
2 * rl,
|
|
306
|
+
0,
|
|
307
|
+
0,
|
|
308
|
+
0,
|
|
309
|
+
0,
|
|
310
|
+
2 * tb,
|
|
311
|
+
0,
|
|
312
|
+
0,
|
|
313
|
+
0,
|
|
314
|
+
0,
|
|
315
|
+
fn,
|
|
316
|
+
0,
|
|
317
|
+
-(right + left) * rl,
|
|
318
|
+
-(top + bottom) * tb,
|
|
319
|
+
-near * fn,
|
|
320
|
+
1,
|
|
321
|
+
])
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
298
325
|
multiply(other: Mat4): Mat4 {
|
|
299
326
|
// Column-major multiplication (matches WGSL/GLSL convention):
|
|
300
327
|
// result = a * b
|
package/src/model.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
2
|
-
import {
|
|
2
|
+
import { Engine } from "./engine"
|
|
3
|
+
import { PmxLoader } from "./pmx-loader"
|
|
4
|
+
import { Rigidbody, Joint } from "./physics"
|
|
3
5
|
import { IKSolverSystem } from "./ik-solver"
|
|
4
6
|
import { VMDLoader } from "./vmd-loader"
|
|
5
|
-
import {
|
|
7
|
+
import { BoneInterpolation, interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation"
|
|
6
8
|
|
|
7
9
|
const VMD_FPS = 30
|
|
8
10
|
const VERTEX_STRIDE = 8
|
|
@@ -148,6 +150,28 @@ interface TweenState {
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
export class Model {
|
|
153
|
+
private static _nextId = 0
|
|
154
|
+
private static nextDefaultName(): string {
|
|
155
|
+
return "model_" + Model._nextId++
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static async loadPmx(path: string, name?: string): Promise<Model> {
|
|
159
|
+
const model = await PmxLoader.load(path)
|
|
160
|
+
model.setName(name ?? Model.nextDefaultName())
|
|
161
|
+
await Engine.getInstance().registerModel(model, path)
|
|
162
|
+
return model
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private _name: string = ""
|
|
166
|
+
|
|
167
|
+
get name(): string {
|
|
168
|
+
return this._name
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setName(value: string): void {
|
|
172
|
+
this._name = value
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
private vertexData: Float32Array<ArrayBuffer>
|
|
152
176
|
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
153
177
|
private vertexCount: number
|
|
@@ -184,7 +208,6 @@ export class Model {
|
|
|
184
208
|
|
|
185
209
|
// Animation runtime
|
|
186
210
|
private _hasAnimation: boolean = false
|
|
187
|
-
private _animationData: AnimationData | null = null
|
|
188
211
|
private boneTracks: Map<string, Array<{ boneName: string; frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation; time: number }>> = new Map()
|
|
189
212
|
private morphTracks: Map<string, Array<{ morphName: string; frame: number; weight: number; time: number }>> = new Map()
|
|
190
213
|
private animationDuration: number = 0
|
|
@@ -196,9 +219,6 @@ export class Model {
|
|
|
196
219
|
private boneTrackIndices: Map<string, number> = new Map()
|
|
197
220
|
private morphTrackIndices: Map<string, number> = new Map()
|
|
198
221
|
|
|
199
|
-
// Physics runtime
|
|
200
|
-
private physics: Physics | null = null
|
|
201
|
-
|
|
202
222
|
// IK and Physics enable flags
|
|
203
223
|
private ikEnabled = true
|
|
204
224
|
private physicsEnabled = true
|
|
@@ -235,11 +255,6 @@ export class Model {
|
|
|
235
255
|
this.initializeRuntimeMorph()
|
|
236
256
|
this.initializeTweenBuffers()
|
|
237
257
|
this.applyMorphs()
|
|
238
|
-
|
|
239
|
-
// Initialize physics if rigidbodies exist
|
|
240
|
-
if (rigidbodies.length > 0) {
|
|
241
|
-
this.physics = new Physics(rigidbodies, joints)
|
|
242
|
-
}
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
private initializeRuntimeSkeleton(): void {
|
|
@@ -456,6 +471,13 @@ export class Model {
|
|
|
456
471
|
return this.skeleton
|
|
457
472
|
}
|
|
458
473
|
|
|
474
|
+
// World bone origin (world matrix col3); unknown name → null
|
|
475
|
+
getBoneWorldPosition(boneName: string): Vec3 | null {
|
|
476
|
+
const idx = this.runtimeSkeleton.nameIndex[boneName]
|
|
477
|
+
if (idx === undefined || idx < 0) return null
|
|
478
|
+
return this.runtimeSkeleton.worldMatrices[idx].getPosition()
|
|
479
|
+
}
|
|
480
|
+
|
|
459
481
|
getSkinning(): Skinning {
|
|
460
482
|
return this.skinning
|
|
461
483
|
}
|
|
@@ -581,14 +603,7 @@ export class Model {
|
|
|
581
603
|
}
|
|
582
604
|
}
|
|
583
605
|
|
|
584
|
-
|
|
585
|
-
* Convert VMD-style relative translation (relative to bind pose world position) to local translation
|
|
586
|
-
* This helper is used by both moveBones and getPoseAtTime to ensure consistent translation handling
|
|
587
|
-
* @param boneIdx - Bone index
|
|
588
|
-
* @param vmdRelativeTranslation - VMD relative translation
|
|
589
|
-
* @param rotation - Optional rotation to use for conversion. If not provided, uses current localRotation.
|
|
590
|
-
* Use animation rotation (from frame) to avoid conflicts when IK modifies rotation.
|
|
591
|
-
*/
|
|
606
|
+
// VMD translation (world delta from bind pose) → bone local space; optional rotation for animation vs IK
|
|
592
607
|
private convertVMDTranslationToLocal(boneIdx: number, vmdRelativeTranslation: Vec3, rotation?: Quat): Vec3 {
|
|
593
608
|
const skeleton = this.skeleton
|
|
594
609
|
const bones = skeleton.bones
|
|
@@ -648,6 +663,10 @@ export class Model {
|
|
|
648
663
|
return localTranslation
|
|
649
664
|
}
|
|
650
665
|
|
|
666
|
+
getWorldMatrices(): Mat4[] {
|
|
667
|
+
return this.runtimeSkeleton.worldMatrices
|
|
668
|
+
}
|
|
669
|
+
|
|
651
670
|
getBoneWorldMatrices(): Float32Array {
|
|
652
671
|
// Convert Mat4[] to Float32Array for WebGPU compatibility
|
|
653
672
|
const boneCount = this.skeleton.bones.length
|
|
@@ -698,6 +717,11 @@ export class Model {
|
|
|
698
717
|
this.runtimeMorph.weights[idx] = clampedWeight
|
|
699
718
|
this.tweenState.morphActive[idx] = 0
|
|
700
719
|
this.applyMorphs()
|
|
720
|
+
try {
|
|
721
|
+
Engine.getInstance().markVertexBufferDirty(this)
|
|
722
|
+
} catch {
|
|
723
|
+
/* not registered yet */
|
|
724
|
+
}
|
|
701
725
|
return
|
|
702
726
|
}
|
|
703
727
|
|
|
@@ -791,50 +815,29 @@ export class Model {
|
|
|
791
815
|
}
|
|
792
816
|
}
|
|
793
817
|
|
|
794
|
-
/**
|
|
795
|
-
* Load VMD animation file
|
|
796
|
-
*/
|
|
797
818
|
async loadVmd(vmdUrl: string): Promise<void> {
|
|
798
819
|
const vmdKeyFrames = await VMDLoader.load(vmdUrl)
|
|
799
820
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const morphTracks: Record<string, MorphKeyframe[]> = {}
|
|
821
|
+
this.resetAllBones()
|
|
822
|
+
this.resetAllMorphs()
|
|
803
823
|
|
|
824
|
+
// Build bone tracks: Map<boneName, Array<{boneName, frame, rotation, translation, interpolation, time}>>
|
|
825
|
+
const boneTracksByBone: Record<string, Array<{ frame: number; rotation: Quat; translation: Vec3; interpolation: BoneInterpolation }>> = {}
|
|
804
826
|
for (const keyFrame of vmdKeyFrames) {
|
|
805
827
|
for (const bf of keyFrame.boneFrames) {
|
|
806
|
-
if (!
|
|
807
|
-
|
|
828
|
+
if (!boneTracksByBone[bf.boneName]) boneTracksByBone[bf.boneName] = []
|
|
829
|
+
boneTracksByBone[bf.boneName].push({
|
|
808
830
|
frame: bf.frame,
|
|
809
831
|
rotation: bf.rotation,
|
|
810
832
|
translation: bf.translation,
|
|
811
833
|
interpolation: rawInterpolationToBoneInterpolation(bf.interpolation),
|
|
812
834
|
})
|
|
813
835
|
}
|
|
814
|
-
for (const mf of keyFrame.morphFrames) {
|
|
815
|
-
if (!morphTracks[mf.morphName]) morphTracks[mf.morphName] = []
|
|
816
|
-
morphTracks[mf.morphName].push({
|
|
817
|
-
frame: mf.frame,
|
|
818
|
-
weight: mf.weight,
|
|
819
|
-
})
|
|
820
|
-
}
|
|
821
836
|
}
|
|
822
837
|
|
|
823
|
-
this.loadAnimationData({ boneTracks, morphTracks })
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Load animation from structured keyframe data.
|
|
828
|
-
* This is the primary way to set animation data — loadVmd delegates to this.
|
|
829
|
-
*/
|
|
830
|
-
loadAnimationData(data: AnimationData): void {
|
|
831
|
-
this._animationData = data
|
|
832
|
-
this.resetAllBones()
|
|
833
|
-
this.resetAllMorphs()
|
|
834
|
-
|
|
835
838
|
this.boneTracks = new Map()
|
|
836
|
-
for (const name in
|
|
837
|
-
const keyframes =
|
|
839
|
+
for (const name in boneTracksByBone) {
|
|
840
|
+
const keyframes = boneTracksByBone[name]
|
|
838
841
|
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
|
|
839
842
|
this.boneTracks.set(
|
|
840
843
|
name,
|
|
@@ -849,9 +852,18 @@ export class Model {
|
|
|
849
852
|
)
|
|
850
853
|
}
|
|
851
854
|
|
|
855
|
+
// Build morph tracks: Map<morphName, Array<{morphName, frame, weight, time}>>
|
|
856
|
+
const morphTracksByMorph: Record<string, Array<{ frame: number; weight: number }>> = {}
|
|
857
|
+
for (const keyFrame of vmdKeyFrames) {
|
|
858
|
+
for (const mf of keyFrame.morphFrames) {
|
|
859
|
+
if (!morphTracksByMorph[mf.morphName]) morphTracksByMorph[mf.morphName] = []
|
|
860
|
+
morphTracksByMorph[mf.morphName].push({ frame: mf.frame, weight: mf.weight })
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
852
864
|
this.morphTracks = new Map()
|
|
853
|
-
for (const name in
|
|
854
|
-
const keyframes =
|
|
865
|
+
for (const name in morphTracksByMorph) {
|
|
866
|
+
const keyframes = morphTracksByMorph[name]
|
|
855
867
|
const sorted = [...keyframes].sort((a, b) => a.frame - b.frame)
|
|
856
868
|
this.morphTracks.set(
|
|
857
869
|
name,
|
|
@@ -881,15 +893,8 @@ export class Model {
|
|
|
881
893
|
this.animationTime = 0
|
|
882
894
|
this.getPoseAtTime(0)
|
|
883
895
|
|
|
884
|
-
if (this.physics) {
|
|
885
|
-
this.computeWorldMatrices()
|
|
886
|
-
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
887
|
-
}
|
|
888
896
|
}
|
|
889
897
|
|
|
890
|
-
/**
|
|
891
|
-
* Reset all bones to their default pose
|
|
892
|
-
*/
|
|
893
898
|
public resetAllBones(): void {
|
|
894
899
|
for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
|
|
895
900
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx]
|
|
@@ -900,9 +905,6 @@ export class Model {
|
|
|
900
905
|
localTrans.set(Vec3.zeros())
|
|
901
906
|
}
|
|
902
907
|
this.computeWorldMatrices()
|
|
903
|
-
if (this.physics) {
|
|
904
|
-
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
905
|
-
}
|
|
906
908
|
}
|
|
907
909
|
|
|
908
910
|
public resetAllMorphs(): void {
|
|
@@ -914,30 +916,24 @@ export class Model {
|
|
|
914
916
|
this.applyMorphs()
|
|
915
917
|
}
|
|
916
918
|
|
|
917
|
-
/**
|
|
918
|
-
* Enable or disable IK solving
|
|
919
|
-
*/
|
|
920
919
|
public setIKEnabled(enabled: boolean): void {
|
|
921
920
|
this.ikEnabled = enabled
|
|
922
921
|
}
|
|
923
922
|
|
|
924
|
-
/**
|
|
925
|
-
* Enable or disable physics simulation
|
|
926
|
-
*/
|
|
927
923
|
public setPhysicsEnabled(enabled: boolean): void {
|
|
928
924
|
this.physicsEnabled = enabled
|
|
929
925
|
}
|
|
930
926
|
|
|
927
|
+
public getPhysicsEnabled(): boolean {
|
|
928
|
+
return this.physicsEnabled
|
|
929
|
+
}
|
|
930
|
+
|
|
931
931
|
playAnimation(): void {
|
|
932
932
|
if (!this._hasAnimation) return
|
|
933
933
|
|
|
934
934
|
this.isPaused = false
|
|
935
935
|
this.isPlaying = true
|
|
936
936
|
|
|
937
|
-
if (this.physics && this.animationTime === 0) {
|
|
938
|
-
this.computeWorldMatrices()
|
|
939
|
-
this.physics.reset(this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
940
|
-
}
|
|
941
937
|
}
|
|
942
938
|
|
|
943
939
|
pauseAnimation(): void {
|
|
@@ -957,13 +953,6 @@ export class Model {
|
|
|
957
953
|
this.animationTime = clampedTime
|
|
958
954
|
}
|
|
959
955
|
|
|
960
|
-
/**
|
|
961
|
-
* Get current animation progress
|
|
962
|
-
*/
|
|
963
|
-
getAnimationData(): AnimationData | null {
|
|
964
|
-
return this._animationData
|
|
965
|
-
}
|
|
966
|
-
|
|
967
956
|
getAnimationProgress(): { current: number; duration: number; percentage: number } {
|
|
968
957
|
const duration = this.animationDuration
|
|
969
958
|
const percentage = duration > 0 ? (this.animationTime / duration) * 100 : 0
|
|
@@ -974,9 +963,6 @@ export class Model {
|
|
|
974
963
|
}
|
|
975
964
|
}
|
|
976
965
|
|
|
977
|
-
/**
|
|
978
|
-
* Binary search upper bound helper (static to avoid recreation)
|
|
979
|
-
*/
|
|
980
966
|
private static upperBound<T extends { time: number }>(time: number, keyFrames: T[], startIdx: number = 0): number {
|
|
981
967
|
let left = startIdx,
|
|
982
968
|
right = keyFrames.length
|
|
@@ -988,10 +974,6 @@ export class Model {
|
|
|
988
974
|
return left
|
|
989
975
|
}
|
|
990
976
|
|
|
991
|
-
/**
|
|
992
|
-
* Find keyframe index with caching optimization
|
|
993
|
-
* Uses cached index as starting point for faster lookup when time is close
|
|
994
|
-
*/
|
|
995
977
|
private findKeyframeIndex<T extends { time: number }>(time: number, keyFrames: T[], cachedIdx: number): number {
|
|
996
978
|
if (keyFrames.length === 0) return -1
|
|
997
979
|
|
|
@@ -1011,10 +993,6 @@ export class Model {
|
|
|
1011
993
|
return idx
|
|
1012
994
|
}
|
|
1013
995
|
|
|
1014
|
-
/**
|
|
1015
|
-
* Get pose at specific time (internal helper)
|
|
1016
|
-
* Optimized for per-frame performance
|
|
1017
|
-
*/
|
|
1018
996
|
private getPoseAtTime(time: number): void {
|
|
1019
997
|
if (!this._hasAnimation) return
|
|
1020
998
|
|
|
@@ -1098,12 +1076,7 @@ export class Model {
|
|
|
1098
1076
|
}
|
|
1099
1077
|
}
|
|
1100
1078
|
|
|
1101
|
-
|
|
1102
|
-
* Updates the model pose by recomputing all matrices.
|
|
1103
|
-
* If animation is playing, applies animation pose first.
|
|
1104
|
-
* deltaTime: Time elapsed since last update in seconds
|
|
1105
|
-
* Returns true if vertices were modified (morphs changed)
|
|
1106
|
-
*/
|
|
1079
|
+
// Returns true when morphs changed (vertex buffer may need upload)
|
|
1107
1080
|
update(deltaTime: number): boolean {
|
|
1108
1081
|
// Update tween time (in milliseconds)
|
|
1109
1082
|
this.tweenTimeMs += deltaTime * 1000
|
|
@@ -1145,10 +1118,6 @@ export class Model {
|
|
|
1145
1118
|
this.computeWorldMatrices()
|
|
1146
1119
|
}
|
|
1147
1120
|
|
|
1148
|
-
if (this.physicsEnabled && this.physics) {
|
|
1149
|
-
this.physics.step(deltaTime, this.runtimeSkeleton.worldMatrices, this.skeleton.inverseBindMatrices)
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1121
|
return verticesChanged
|
|
1153
1122
|
}
|
|
1154
1123
|
|
|
@@ -1291,7 +1260,7 @@ export class Model {
|
|
|
1291
1260
|
}
|
|
1292
1261
|
}
|
|
1293
1262
|
|
|
1294
|
-
|
|
1263
|
+
public computeWorldMatrices(): void {
|
|
1295
1264
|
const bones = this.skeleton.bones
|
|
1296
1265
|
const localRot = this.runtimeSkeleton.localRotations
|
|
1297
1266
|
const localTrans = this.runtimeSkeleton.localTranslations
|
package/src/pmx-loader.ts
CHANGED
|
@@ -262,7 +262,7 @@ export class PmxLoader {
|
|
|
262
262
|
this.getFloat32(),
|
|
263
263
|
]
|
|
264
264
|
// edgeSize float
|
|
265
|
-
const edgeSize = this.getFloat32()
|
|
265
|
+
const edgeSize = this.getFloat32()*2 // double the size for better visibility
|
|
266
266
|
|
|
267
267
|
const textureIndex = this.getNonVertexIndex(this.textureIndexSize)
|
|
268
268
|
const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize)
|