reze-engine 0.9.5 → 0.10.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 +8 -4
- package/dist/animation.d.ts +26 -13
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +95 -35
- package/dist/engine.d.ts +0 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -10
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +8 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +55 -38
- package/package.json +1 -1
- package/src/animation.ts +124 -40
- package/src/engine.ts +34 -44
- package/src/ik-solver.ts +7 -7
- package/src/index.ts +9 -5
- package/src/model.ts +64 -42
package/src/engine.ts
CHANGED
|
@@ -81,7 +81,7 @@ interface ModelInstance {
|
|
|
81
81
|
export class Engine {
|
|
82
82
|
private static instance: Engine | null = null
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
static getInstance(): Engine {
|
|
85
85
|
if (!Engine.instance) {
|
|
86
86
|
throw new Error("Engine not ready: create Engine, await init(), then load models via engine.loadModel().")
|
|
87
87
|
}
|
|
@@ -199,7 +199,7 @@ export class Engine {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// Step 1: Get WebGPU device and context
|
|
202
|
-
|
|
202
|
+
async init() {
|
|
203
203
|
const adapter = await navigator.gpu?.requestAdapter()
|
|
204
204
|
const device = await adapter?.requestDevice()
|
|
205
205
|
if (!device) {
|
|
@@ -747,7 +747,7 @@ export class Engine {
|
|
|
747
747
|
// Screen-stable edgeline: extrusion ∝ camera distance (same idea as MMD viewers / babylon-mmd-style scaling)
|
|
748
748
|
let camDist = max(length(camera.viewPos - worldPos), 0.25);
|
|
749
749
|
let refDist = 30.0;
|
|
750
|
-
let edgeScale = 0.
|
|
750
|
+
let edgeScale = 0.025;
|
|
751
751
|
let expandedPos = worldPos + worldNormal * material.edgeSize * edgeScale * (camDist / refDist);
|
|
752
752
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
753
753
|
return output;
|
|
@@ -972,10 +972,10 @@ export class Engine {
|
|
|
972
972
|
}
|
|
973
973
|
|
|
974
974
|
/** Set static camera look-at / orbit center. Clears any model follow binding. */
|
|
975
|
-
|
|
975
|
+
setCameraTarget(v: Vec3): void
|
|
976
976
|
/** Bind camera orbit center to a model's bone (Souls-style follow cam). Pass null to unbind. */
|
|
977
|
-
|
|
978
|
-
|
|
977
|
+
setCameraTarget(model: Model | null, boneName: string, offset?: Vec3): void
|
|
978
|
+
setCameraTarget(modelOrVec: Model | Vec3 | null, boneName?: string, offset?: Vec3): void {
|
|
979
979
|
if (modelOrVec === null) {
|
|
980
980
|
this.cameraTargetModel = null
|
|
981
981
|
return
|
|
@@ -995,7 +995,7 @@ export class Engine {
|
|
|
995
995
|
}
|
|
996
996
|
|
|
997
997
|
/** Souls-style follow cam: orbit center tracks a model bone each frame. Shorthand for setCameraTarget(model, boneName, offset). */
|
|
998
|
-
|
|
998
|
+
setCameraFollow(model: Model | null, boneName?: string, offset?: Vec3): void {
|
|
999
999
|
if (model === null) {
|
|
1000
1000
|
this.cameraTargetModel = null
|
|
1001
1001
|
return
|
|
@@ -1007,12 +1007,12 @@ export class Engine {
|
|
|
1007
1007
|
this.cameraTargetOffset.z = offset?.z ?? 0
|
|
1008
1008
|
}
|
|
1009
1009
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1010
|
+
getCameraDistance(): number { return this.camera.radius }
|
|
1011
|
+
setCameraDistance(d: number): void { this.camera.radius = d }
|
|
1012
|
+
getCameraAlpha(): number { return this.camera.alpha }
|
|
1013
|
+
setCameraAlpha(a: number): void { this.camera.alpha = a }
|
|
1014
|
+
getCameraBeta(): number { return this.camera.beta }
|
|
1015
|
+
setCameraBeta(b: number): void { this.camera.beta = b }
|
|
1016
1016
|
|
|
1017
1017
|
// Step 5: Create lighting buffers
|
|
1018
1018
|
private setupLighting() {
|
|
@@ -1039,16 +1039,6 @@ export class Engine {
|
|
|
1039
1039
|
this.updateLightBuffer()
|
|
1040
1040
|
}
|
|
1041
1041
|
|
|
1042
|
-
public clearLights() {
|
|
1043
|
-
this.lightCount = 0
|
|
1044
|
-
// Clear all light data by setting intensity to 0
|
|
1045
|
-
for (let i = 0; i < 4; i++) {
|
|
1046
|
-
const baseIndex = 4 + i * 8
|
|
1047
|
-
this.lightData[baseIndex + 7] = 0 // color.w / intensity
|
|
1048
|
-
}
|
|
1049
|
-
this.updateLightBuffer()
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
1042
|
private addLight(direction: Vec3, color: Vec3, intensity: number = 1.0): boolean {
|
|
1053
1043
|
if (this.lightCount >= 4) return false
|
|
1054
1044
|
|
|
@@ -1068,7 +1058,7 @@ export class Engine {
|
|
|
1068
1058
|
return true
|
|
1069
1059
|
}
|
|
1070
1060
|
|
|
1071
|
-
|
|
1061
|
+
addGround(options?: {
|
|
1072
1062
|
width?: number
|
|
1073
1063
|
height?: number
|
|
1074
1064
|
diffuseColor?: Vec3
|
|
@@ -1113,11 +1103,11 @@ export class Engine {
|
|
|
1113
1103
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
|
|
1114
1104
|
}
|
|
1115
1105
|
|
|
1116
|
-
|
|
1106
|
+
getStats(): EngineStats {
|
|
1117
1107
|
return { ...this.stats }
|
|
1118
1108
|
}
|
|
1119
1109
|
|
|
1120
|
-
|
|
1110
|
+
runRenderLoop(callback?: () => void) {
|
|
1121
1111
|
this.renderLoopCallback = callback || null
|
|
1122
1112
|
|
|
1123
1113
|
const loop = () => {
|
|
@@ -1133,7 +1123,7 @@ export class Engine {
|
|
|
1133
1123
|
this.animationFrameId = requestAnimationFrame(loop)
|
|
1134
1124
|
}
|
|
1135
1125
|
|
|
1136
|
-
|
|
1126
|
+
stopRenderLoop() {
|
|
1137
1127
|
if (this.animationFrameId !== null) {
|
|
1138
1128
|
cancelAnimationFrame(this.animationFrameId)
|
|
1139
1129
|
this.animationFrameId = null
|
|
@@ -1141,7 +1131,7 @@ export class Engine {
|
|
|
1141
1131
|
this.renderLoopCallback = null
|
|
1142
1132
|
}
|
|
1143
1133
|
|
|
1144
|
-
|
|
1134
|
+
dispose() {
|
|
1145
1135
|
this.stopRenderLoop()
|
|
1146
1136
|
this.forEachInstance((inst) => inst.model.stopAnimation())
|
|
1147
1137
|
if (Engine.instance === this) Engine.instance = null
|
|
@@ -1159,9 +1149,9 @@ export class Engine {
|
|
|
1159
1149
|
}
|
|
1160
1150
|
}
|
|
1161
1151
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1152
|
+
async loadModel(path: string): Promise<Model>
|
|
1153
|
+
async loadModel(name: string, path: string): Promise<Model>
|
|
1154
|
+
async loadModel(nameOrPath: string, path?: string): Promise<Model> {
|
|
1165
1155
|
const pmxPath = path === undefined ? nameOrPath : path
|
|
1166
1156
|
const name = path === undefined ? "model_" + (this._nextDefaultModelId++) : nameOrPath
|
|
1167
1157
|
const model = await PmxLoader.load(pmxPath)
|
|
@@ -1170,7 +1160,7 @@ export class Engine {
|
|
|
1170
1160
|
return model
|
|
1171
1161
|
}
|
|
1172
1162
|
|
|
1173
|
-
|
|
1163
|
+
async addModel(model: Model, pmxPath: string, name?: string): Promise<string> {
|
|
1174
1164
|
const requested = name ?? model.name
|
|
1175
1165
|
let key = requested
|
|
1176
1166
|
let n = 1
|
|
@@ -1184,19 +1174,19 @@ export class Engine {
|
|
|
1184
1174
|
return key
|
|
1185
1175
|
}
|
|
1186
1176
|
|
|
1187
|
-
|
|
1177
|
+
removeModel(name: string): void {
|
|
1188
1178
|
this.modelInstances.delete(name)
|
|
1189
1179
|
}
|
|
1190
1180
|
|
|
1191
|
-
|
|
1181
|
+
getModelNames(): string[] {
|
|
1192
1182
|
return Array.from(this.modelInstances.keys())
|
|
1193
1183
|
}
|
|
1194
1184
|
|
|
1195
|
-
|
|
1185
|
+
getModel(name: string): Model | null {
|
|
1196
1186
|
return this.modelInstances.get(name)?.model ?? null
|
|
1197
1187
|
}
|
|
1198
1188
|
|
|
1199
|
-
|
|
1189
|
+
markVertexBufferDirty(modelNameOrModel?: string | Model): void {
|
|
1200
1190
|
if (modelNameOrModel === undefined) return
|
|
1201
1191
|
if (typeof modelNameOrModel === "string") {
|
|
1202
1192
|
const inst = this.modelInstances.get(modelNameOrModel)
|
|
@@ -1211,38 +1201,38 @@ export class Engine {
|
|
|
1211
1201
|
}
|
|
1212
1202
|
}
|
|
1213
1203
|
|
|
1214
|
-
|
|
1204
|
+
setMaterialVisible(modelName: string, materialName: string, visible: boolean): void {
|
|
1215
1205
|
const inst = this.modelInstances.get(modelName)
|
|
1216
1206
|
if (!inst) return
|
|
1217
1207
|
if (visible) inst.hiddenMaterials.delete(materialName)
|
|
1218
1208
|
else inst.hiddenMaterials.add(materialName)
|
|
1219
1209
|
}
|
|
1220
1210
|
|
|
1221
|
-
|
|
1211
|
+
toggleMaterialVisible(modelName: string, materialName: string): void {
|
|
1222
1212
|
const inst = this.modelInstances.get(modelName)
|
|
1223
1213
|
if (!inst) return
|
|
1224
1214
|
if (inst.hiddenMaterials.has(materialName)) inst.hiddenMaterials.delete(materialName)
|
|
1225
1215
|
else inst.hiddenMaterials.add(materialName)
|
|
1226
1216
|
}
|
|
1227
1217
|
|
|
1228
|
-
|
|
1218
|
+
isMaterialVisible(modelName: string, materialName: string): boolean {
|
|
1229
1219
|
const inst = this.modelInstances.get(modelName)
|
|
1230
1220
|
return inst ? !inst.hiddenMaterials.has(materialName) : false
|
|
1231
1221
|
}
|
|
1232
1222
|
|
|
1233
|
-
|
|
1223
|
+
setIKEnabled(enabled: boolean): void {
|
|
1234
1224
|
this.ikEnabled = enabled
|
|
1235
1225
|
}
|
|
1236
1226
|
|
|
1237
|
-
|
|
1227
|
+
getIKEnabled(): boolean {
|
|
1238
1228
|
return this.ikEnabled
|
|
1239
1229
|
}
|
|
1240
1230
|
|
|
1241
|
-
|
|
1231
|
+
setPhysicsEnabled(enabled: boolean): void {
|
|
1242
1232
|
this.physicsEnabled = enabled
|
|
1243
1233
|
}
|
|
1244
1234
|
|
|
1245
|
-
|
|
1235
|
+
getPhysicsEnabled(): boolean {
|
|
1246
1236
|
return this.physicsEnabled
|
|
1247
1237
|
}
|
|
1248
1238
|
|
|
@@ -1803,7 +1793,7 @@ export class Engine {
|
|
|
1803
1793
|
this.onRaycast(hitModel, hitMaterial, screenX, screenY)
|
|
1804
1794
|
}
|
|
1805
1795
|
|
|
1806
|
-
|
|
1796
|
+
render() {
|
|
1807
1797
|
if (!this.multisampleTexture || !this.camera || !this.device) return
|
|
1808
1798
|
|
|
1809
1799
|
const currentTime = performance.now()
|
package/src/ik-solver.ts
CHANGED
|
@@ -21,13 +21,13 @@ const enum InternalSolveAxis {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
class IKChain {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
readonly boneIndex: number
|
|
25
|
+
readonly minimumAngle: Vec3 | null
|
|
26
|
+
readonly maximumAngle: Vec3 | null
|
|
27
|
+
readonly rotationOrder: InternalEulerRotationOrder
|
|
28
|
+
readonly solveAxis: InternalSolveAxis
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
constructor(boneIndex: number, link: IKLink) {
|
|
31
31
|
this.boneIndex = boneIndex
|
|
32
32
|
|
|
33
33
|
if (link.hasLimit && link.minAngle && link.maxAngle) {
|
|
@@ -76,7 +76,7 @@ export class IKSolverSystem {
|
|
|
76
76
|
private static readonly EPSILON = 1.0e-8
|
|
77
77
|
private static readonly THRESHOLD = (88 * Math.PI) / 180
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
static solve(
|
|
80
80
|
ikSolvers: IKSolver[],
|
|
81
81
|
bones: Bone[],
|
|
82
82
|
localRotations: Quat[],
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export { Engine, type EngineStats } from "./engine"
|
|
2
2
|
export { Model } from "./model"
|
|
3
3
|
export { Vec3, Quat, Mat4 } from "./math"
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export type {
|
|
5
|
+
AnimationClip,
|
|
6
|
+
AnimationPlayOptions,
|
|
7
|
+
AnimationProgress,
|
|
8
|
+
BoneKeyframe,
|
|
9
|
+
MorphKeyframe,
|
|
10
|
+
BoneInterpolation,
|
|
11
|
+
ControlPoint,
|
|
9
12
|
} from "./animation"
|
|
13
|
+
export { FPS } from "./animation"
|
|
10
14
|
export { Physics, type PhysicsOptions } from "./physics"
|
package/src/model.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { IKSolverSystem } from "./ik-solver"
|
|
|
5
5
|
import { VMDLoader, type VMDKeyFrame } from "./vmd-loader"
|
|
6
6
|
import {
|
|
7
7
|
AnimationClip,
|
|
8
|
+
AnimationPlayOptions,
|
|
9
|
+
AnimationProgress,
|
|
8
10
|
AnimationState,
|
|
9
11
|
BoneInterpolation,
|
|
10
12
|
BoneKeyframe,
|
|
@@ -13,7 +15,6 @@ import {
|
|
|
13
15
|
rawInterpolationToBoneInterpolation,
|
|
14
16
|
} from "./animation"
|
|
15
17
|
|
|
16
|
-
const VMD_FPS = 30
|
|
17
18
|
const VERTEX_STRIDE = 8
|
|
18
19
|
|
|
19
20
|
export interface Texture {
|
|
@@ -480,7 +481,7 @@ export class Model {
|
|
|
480
481
|
return this.runtimeMorph.weights
|
|
481
482
|
}
|
|
482
483
|
|
|
483
|
-
// ------- Bone helpers (
|
|
484
|
+
// ------- Bone helpers (API) -------
|
|
484
485
|
|
|
485
486
|
rotateBones(boneRotations: Record<string, Quat>, durationMs?: number): void {
|
|
486
487
|
const state = this.tweenState
|
|
@@ -822,7 +823,6 @@ export class Model {
|
|
|
822
823
|
rotation: kf.rotation,
|
|
823
824
|
translation: kf.translation,
|
|
824
825
|
interpolation: kf.interpolation,
|
|
825
|
-
time: kf.frame / VMD_FPS,
|
|
826
826
|
}))
|
|
827
827
|
)
|
|
828
828
|
}
|
|
@@ -843,27 +843,33 @@ export class Model {
|
|
|
843
843
|
morphName: name,
|
|
844
844
|
frame: kf.frame,
|
|
845
845
|
weight: kf.weight,
|
|
846
|
-
time: kf.frame / VMD_FPS,
|
|
847
846
|
}))
|
|
848
847
|
)
|
|
849
848
|
}
|
|
850
|
-
let
|
|
849
|
+
let maxFrame = 0
|
|
851
850
|
for (const frames of boneTracks.values()) {
|
|
852
|
-
if (frames.length > 0)
|
|
851
|
+
if (frames.length > 0) maxFrame = Math.max(maxFrame, frames[frames.length - 1].frame)
|
|
853
852
|
}
|
|
854
853
|
for (const frames of morphTracks.values()) {
|
|
855
|
-
if (frames.length > 0)
|
|
854
|
+
if (frames.length > 0) maxFrame = Math.max(maxFrame, frames[frames.length - 1].frame)
|
|
856
855
|
}
|
|
857
|
-
return { boneTracks, morphTracks,
|
|
856
|
+
return { boneTracks, morphTracks, frameCount: maxFrame }
|
|
858
857
|
}
|
|
859
858
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
859
|
+
loadAnimation(animationName: string, source: string): Promise<void>
|
|
860
|
+
loadAnimation(animationName: string, source: AnimationClip): void
|
|
861
|
+
loadAnimation(animationName: string, source: string | AnimationClip): Promise<void> | void {
|
|
862
|
+
if (typeof source !== "string") {
|
|
863
|
+
this.animationState.loadAnimation(animationName, source)
|
|
864
|
+
return
|
|
865
|
+
}
|
|
866
|
+
return VMDLoader.load(source).then((vmdKeyFrames) => {
|
|
867
|
+
const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames)
|
|
868
|
+
this.animationState.loadAnimation(animationName, clip)
|
|
869
|
+
})
|
|
864
870
|
}
|
|
865
871
|
|
|
866
|
-
|
|
872
|
+
resetAllBones(): void {
|
|
867
873
|
for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
|
|
868
874
|
const localRot = this.runtimeSkeleton.localRotations[boneIdx]
|
|
869
875
|
const localTrans = this.runtimeSkeleton.localTranslations[boneIdx]
|
|
@@ -874,7 +880,7 @@ export class Model {
|
|
|
874
880
|
this.computeWorldMatrices()
|
|
875
881
|
}
|
|
876
882
|
|
|
877
|
-
|
|
883
|
+
resetAllMorphs(): void {
|
|
878
884
|
for (let morphIdx = 0; morphIdx < this.morphing.morphs.length; morphIdx++) {
|
|
879
885
|
const morphName = this.morphing.morphs[morphIdx].name
|
|
880
886
|
this.setMorphWeight(morphName, 0)
|
|
@@ -883,21 +889,26 @@ export class Model {
|
|
|
883
889
|
this.applyMorphs()
|
|
884
890
|
}
|
|
885
891
|
|
|
886
|
-
|
|
887
|
-
return this.animationState
|
|
892
|
+
getAnimationClip(name: string): AnimationClip | null {
|
|
893
|
+
return this.animationState.getAnimationClip(name)
|
|
888
894
|
}
|
|
889
895
|
|
|
890
896
|
play(): void
|
|
891
897
|
play(name: string): boolean
|
|
892
|
-
play(name?:
|
|
898
|
+
play(name: string, options?: AnimationPlayOptions): boolean
|
|
899
|
+
play(name?: string, options?: AnimationPlayOptions): void | boolean {
|
|
893
900
|
if (name === undefined) {
|
|
894
901
|
this.animationState.play()
|
|
895
902
|
return
|
|
896
903
|
}
|
|
897
|
-
|
|
904
|
+
this.resetAllBones()
|
|
905
|
+
this.resetAllMorphs()
|
|
906
|
+
return this.animationState.play(name, options)
|
|
898
907
|
}
|
|
899
908
|
|
|
900
909
|
show(name: string): void {
|
|
910
|
+
this.resetAllBones()
|
|
911
|
+
this.resetAllMorphs()
|
|
901
912
|
this.animationState.show(name)
|
|
902
913
|
}
|
|
903
914
|
|
|
@@ -924,46 +935,55 @@ export class Model {
|
|
|
924
935
|
this.animationState.stop()
|
|
925
936
|
}
|
|
926
937
|
|
|
927
|
-
|
|
928
|
-
|
|
938
|
+
// Seek by absolute timeline seconds, not frame index.
|
|
939
|
+
seek(seconds: number): void {
|
|
940
|
+
this.animationState.seek(seconds)
|
|
929
941
|
}
|
|
930
942
|
|
|
931
943
|
// @deprecated Use model.seek()
|
|
932
|
-
seekAnimation(
|
|
933
|
-
this.animationState.seek(
|
|
944
|
+
seekAnimation(seconds: number): void {
|
|
945
|
+
this.animationState.seek(seconds)
|
|
934
946
|
}
|
|
935
947
|
|
|
936
|
-
getAnimationProgress():
|
|
948
|
+
getAnimationProgress(): AnimationProgress {
|
|
937
949
|
const p = this.animationState.getProgress()
|
|
938
|
-
return {
|
|
950
|
+
return {
|
|
951
|
+
current: p.current,
|
|
952
|
+
duration: p.duration,
|
|
953
|
+
percentage: p.percentage,
|
|
954
|
+
animationName: p.animationName,
|
|
955
|
+
looping: p.looping,
|
|
956
|
+
playing: p.playing,
|
|
957
|
+
paused: p.paused,
|
|
958
|
+
}
|
|
939
959
|
}
|
|
940
960
|
|
|
941
|
-
private static upperBound<T extends {
|
|
961
|
+
private static upperBound<T extends { frame: number }>(frame: number, keyFrames: T[], startIdx: number = 0): number {
|
|
942
962
|
let left = startIdx,
|
|
943
963
|
right = keyFrames.length
|
|
944
964
|
while (left < right) {
|
|
945
965
|
const mid = Math.floor((left + right) / 2)
|
|
946
|
-
if (keyFrames[mid].
|
|
966
|
+
if (keyFrames[mid].frame <= frame) left = mid + 1
|
|
947
967
|
else right = mid
|
|
948
968
|
}
|
|
949
969
|
return left
|
|
950
970
|
}
|
|
951
971
|
|
|
952
|
-
private findKeyframeIndex<T extends {
|
|
972
|
+
private findKeyframeIndex<T extends { frame: number }>(frame: number, keyFrames: T[], cachedIdx: number): number {
|
|
953
973
|
if (keyFrames.length === 0) return -1
|
|
954
974
|
|
|
955
975
|
if (cachedIdx >= 0 && cachedIdx < keyFrames.length) {
|
|
956
|
-
const
|
|
957
|
-
const
|
|
958
|
-
if (
|
|
976
|
+
const currentFrame = keyFrames[cachedIdx].frame
|
|
977
|
+
const nextFrame = cachedIdx + 1 < keyFrames.length ? keyFrames[cachedIdx + 1].frame : Infinity
|
|
978
|
+
if (frame >= currentFrame && frame < nextFrame) {
|
|
959
979
|
return cachedIdx
|
|
960
980
|
}
|
|
961
981
|
}
|
|
962
|
-
const idx = Model.upperBound(
|
|
982
|
+
const idx = Model.upperBound(frame, keyFrames, 0) - 1
|
|
963
983
|
return idx
|
|
964
984
|
}
|
|
965
985
|
|
|
966
|
-
private applyPoseFromClip(clip: AnimationClip | null,
|
|
986
|
+
private applyPoseFromClip(clip: AnimationClip | null, frame: number): void {
|
|
967
987
|
if (!clip) return
|
|
968
988
|
if (clip !== this.lastAppliedClip) {
|
|
969
989
|
this.boneTrackIndices.clear()
|
|
@@ -975,8 +995,8 @@ export class Model {
|
|
|
975
995
|
if (keyFrames.length === 0) continue
|
|
976
996
|
|
|
977
997
|
const cachedIdx = this.boneTrackIndices.get(boneName) ?? -1
|
|
978
|
-
const
|
|
979
|
-
const idx = this.findKeyframeIndex(
|
|
998
|
+
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame))
|
|
999
|
+
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx)
|
|
980
1000
|
|
|
981
1001
|
if (idx < 0) continue
|
|
982
1002
|
|
|
@@ -997,8 +1017,8 @@ export class Model {
|
|
|
997
1017
|
const localTranslation = this.convertVMDTranslationToLocal(boneIdx, frameA.translation, frameRotation)
|
|
998
1018
|
localTrans.set(localTranslation)
|
|
999
1019
|
} else {
|
|
1000
|
-
const
|
|
1001
|
-
const gradient = (
|
|
1020
|
+
const frameDelta = frameB.frame - frameA.frame
|
|
1021
|
+
const gradient = frameDelta > 0 ? (clampedFrame - frameA.frame) / frameDelta : 0
|
|
1002
1022
|
const interp = frameB.interpolation
|
|
1003
1023
|
|
|
1004
1024
|
const rotT = interpolateControlPoints(interp.rotation, gradient)
|
|
@@ -1025,8 +1045,8 @@ export class Model {
|
|
|
1025
1045
|
if (keyFrames.length === 0) continue
|
|
1026
1046
|
|
|
1027
1047
|
const cachedIdx = this.morphTrackIndices.get(morphName) ?? -1
|
|
1028
|
-
const
|
|
1029
|
-
const idx = this.findKeyframeIndex(
|
|
1048
|
+
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame))
|
|
1049
|
+
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx)
|
|
1030
1050
|
|
|
1031
1051
|
if (idx < 0) continue
|
|
1032
1052
|
|
|
@@ -1041,7 +1061,9 @@ export class Model {
|
|
|
1041
1061
|
const weight = frameB
|
|
1042
1062
|
? frameA.weight +
|
|
1043
1063
|
(frameB.weight - frameA.weight) *
|
|
1044
|
-
(
|
|
1064
|
+
(keyFrames[idx + 1].frame > keyFrames[idx].frame
|
|
1065
|
+
? (clampedFrame - keyFrames[idx].frame) / (keyFrames[idx + 1].frame - keyFrames[idx].frame)
|
|
1066
|
+
: 0)
|
|
1045
1067
|
: frameA.weight
|
|
1046
1068
|
|
|
1047
1069
|
this.runtimeMorph.weights[morphIdx] = weight
|
|
@@ -1059,9 +1081,9 @@ export class Model {
|
|
|
1059
1081
|
|
|
1060
1082
|
this.animationState.update(deltaTime)
|
|
1061
1083
|
const clip = this.animationState.getCurrentClip()
|
|
1062
|
-
const
|
|
1084
|
+
const frame = this.animationState.getCurrentFrame()
|
|
1063
1085
|
if (clip !== null) {
|
|
1064
|
-
this.applyPoseFromClip(clip,
|
|
1086
|
+
this.applyPoseFromClip(clip, frame)
|
|
1065
1087
|
}
|
|
1066
1088
|
|
|
1067
1089
|
// Apply morphs if tweens changed morphs or animation changed morphs
|
|
@@ -1223,7 +1245,7 @@ export class Model {
|
|
|
1223
1245
|
}
|
|
1224
1246
|
}
|
|
1225
1247
|
|
|
1226
|
-
|
|
1248
|
+
computeWorldMatrices(): void {
|
|
1227
1249
|
const bones = this.skeleton.bones
|
|
1228
1250
|
const localRot = this.runtimeSkeleton.localRotations
|
|
1229
1251
|
const localTrans = this.runtimeSkeleton.localTranslations
|