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/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 { Vec3, Quat, Mat4 } from "./math"
3
- export type { AnimationData, BoneKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint } from "./animation"
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 { Rigidbody, Joint, Physics } from "./physics"
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 { AnimationData, BoneKeyframe, MorphKeyframe, BoneInterpolation, interpolateControlPoints, rawInterpolationToBoneInterpolation } from "./animation"
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
- // Convert VMDKeyFrame[] to AnimationData
801
- const boneTracks: Record<string, BoneKeyframe[]> = {}
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 (!boneTracks[bf.boneName]) boneTracks[bf.boneName] = []
807
- boneTracks[bf.boneName].push({
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 data.boneTracks) {
837
- const keyframes = data.boneTracks[name]
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 data.morphTracks) {
854
- const keyframes = data.morphTracks[name]
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
- private computeWorldMatrices(): void {
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)