reze-engine 0.2.18 → 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 +10 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +284 -144
- 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 +82 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +357 -4
- package/dist/pmx-loader.d.ts +3 -1
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +218 -130
- 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 +308 -165
- package/src/ik-solver.ts +488 -0
- package/src/math.ts +555 -546
- package/src/model.ts +930 -421
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1173 -1054
- package/src/vmd-loader.ts +276 -179
package/src/model.ts
CHANGED
|
@@ -1,421 +1,930 @@
|
|
|
1
|
-
import { Mat4, Quat, easeInOut } from "./math"
|
|
2
|
-
import { Rigidbody, Joint } from "./physics"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Cached
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i <
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut } from "./math"
|
|
2
|
+
import { Rigidbody, Joint } from "./physics"
|
|
3
|
+
import { IKSolverSystem } from "./ik-solver"
|
|
4
|
+
|
|
5
|
+
const VERTEX_STRIDE = 8
|
|
6
|
+
|
|
7
|
+
export interface Texture {
|
|
8
|
+
path: string
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Material {
|
|
13
|
+
name: string
|
|
14
|
+
diffuse: [number, number, number, number]
|
|
15
|
+
specular: [number, number, number]
|
|
16
|
+
ambient: [number, number, number]
|
|
17
|
+
shininess: number
|
|
18
|
+
diffuseTextureIndex: number
|
|
19
|
+
normalTextureIndex: number
|
|
20
|
+
sphereTextureIndex: number
|
|
21
|
+
sphereMode: number
|
|
22
|
+
toonTextureIndex: number
|
|
23
|
+
edgeFlag: number
|
|
24
|
+
edgeColor: [number, number, number, number]
|
|
25
|
+
edgeSize: number
|
|
26
|
+
vertexCount: number
|
|
27
|
+
isEye?: boolean // New: marks eye materials
|
|
28
|
+
isFace?: boolean // New: marks face/skin materials
|
|
29
|
+
isHair?: boolean // New: marks hair materials
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Bone {
|
|
33
|
+
name: string
|
|
34
|
+
parentIndex: number // -1 if no parent
|
|
35
|
+
bindTranslation: [number, number, number]
|
|
36
|
+
children: number[] // child bone indices (built on skeleton creation)
|
|
37
|
+
appendParentIndex?: number // index of the bone to inherit from
|
|
38
|
+
appendRatio?: number // 0..1
|
|
39
|
+
appendRotate?: boolean
|
|
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
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface Skeleton {
|
|
91
|
+
bones: Bone[]
|
|
92
|
+
inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Skinning {
|
|
96
|
+
joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
|
|
97
|
+
weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Vertex morph offset data
|
|
101
|
+
export interface VertexMorphOffset {
|
|
102
|
+
vertexIndex: number
|
|
103
|
+
positionOffset: [number, number, number]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Group morph reference (for type 0)
|
|
107
|
+
export interface GroupMorphReference {
|
|
108
|
+
morphIndex: number
|
|
109
|
+
ratio: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Morph definition
|
|
113
|
+
export interface Morph {
|
|
114
|
+
name: string
|
|
115
|
+
type: number // 0=group, 1=vertex, 2=bone, 3=UV, 8=material
|
|
116
|
+
vertexOffsets: VertexMorphOffset[] // Only for type 1 (vertex morph)
|
|
117
|
+
groupReferences?: GroupMorphReference[] // Only for type 0 (group morph)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Morphing {
|
|
121
|
+
morphs: Morph[]
|
|
122
|
+
offsetsBuffer: Float32Array // Dense buffer: morphCount * vertexCount * 3 floats
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Runtime skeleton pose state (updated each frame)
|
|
126
|
+
export interface SkeletonRuntime {
|
|
127
|
+
nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
|
|
128
|
+
localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
|
|
129
|
+
localTranslations: Float32Array // vec3 per bone length = boneCount*3
|
|
130
|
+
worldMatrices: Float32Array // mat4 per bone length = boneCount*16
|
|
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
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Runtime morph state
|
|
137
|
+
export interface MorphRuntime {
|
|
138
|
+
nameIndex: Record<string, number> // Cached lookup: morph name -> morph index
|
|
139
|
+
weights: Float32Array // One weight per morph (0.0 to 1.0)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Rotation tween state per bone
|
|
143
|
+
interface RotationTweenState {
|
|
144
|
+
active: Uint8Array // 0/1 per bone
|
|
145
|
+
startQuat: Float32Array // quat per bone (x,y,z,w)
|
|
146
|
+
targetQuat: Float32Array // quat per bone (x,y,z,w)
|
|
147
|
+
startTimeMs: Float32Array // one float per bone (ms)
|
|
148
|
+
durationMs: Float32Array // one float per bone (ms)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Morph weight tween state per morph
|
|
152
|
+
interface MorphWeightTweenState {
|
|
153
|
+
active: Uint8Array // 0/1 per morph
|
|
154
|
+
startWeight: Float32Array // one float per morph
|
|
155
|
+
targetWeight: Float32Array // one float per morph
|
|
156
|
+
startTimeMs: Float32Array // one float per morph (ms)
|
|
157
|
+
durationMs: Float32Array // one float per morph (ms)
|
|
158
|
+
}
|
|
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
|
+
|
|
169
|
+
export class Model {
|
|
170
|
+
private vertexData: Float32Array<ArrayBuffer>
|
|
171
|
+
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
172
|
+
private vertexCount: number
|
|
173
|
+
private indexData: Uint32Array<ArrayBuffer>
|
|
174
|
+
private textures: Texture[] = []
|
|
175
|
+
private materials: Material[] = []
|
|
176
|
+
// Static skeleton/skinning (not necessarily serialized yet)
|
|
177
|
+
private skeleton: Skeleton
|
|
178
|
+
private skinning: Skinning
|
|
179
|
+
|
|
180
|
+
// Static morph data (from PMX)
|
|
181
|
+
private morphing: Morphing
|
|
182
|
+
|
|
183
|
+
// Physics data from PMX
|
|
184
|
+
private rigidbodies: Rigidbody[] = []
|
|
185
|
+
private joints: Joint[] = []
|
|
186
|
+
|
|
187
|
+
// Runtime skeleton pose state (updated each frame)
|
|
188
|
+
private runtimeSkeleton!: SkeletonRuntime
|
|
189
|
+
|
|
190
|
+
// Runtime morph state
|
|
191
|
+
private runtimeMorph!: MorphRuntime
|
|
192
|
+
|
|
193
|
+
// Cached identity matrices to avoid allocations in computeWorldMatrices
|
|
194
|
+
private cachedIdentityMat1 = Mat4.identity()
|
|
195
|
+
private cachedIdentityMat2 = Mat4.identity()
|
|
196
|
+
|
|
197
|
+
private rotTweenState!: RotationTweenState
|
|
198
|
+
private transTweenState!: TranslationTweenState
|
|
199
|
+
private morphTweenState!: MorphWeightTweenState
|
|
200
|
+
|
|
201
|
+
constructor(
|
|
202
|
+
vertexData: Float32Array<ArrayBuffer>,
|
|
203
|
+
indexData: Uint32Array<ArrayBuffer>,
|
|
204
|
+
textures: Texture[],
|
|
205
|
+
materials: Material[],
|
|
206
|
+
skeleton: Skeleton,
|
|
207
|
+
skinning: Skinning,
|
|
208
|
+
morphing: Morphing,
|
|
209
|
+
rigidbodies: Rigidbody[] = [],
|
|
210
|
+
joints: Joint[] = []
|
|
211
|
+
) {
|
|
212
|
+
// Store base vertex data (original positions before morphing)
|
|
213
|
+
this.baseVertexData = new Float32Array(vertexData)
|
|
214
|
+
this.vertexData = vertexData
|
|
215
|
+
this.vertexCount = vertexData.length / VERTEX_STRIDE
|
|
216
|
+
this.indexData = indexData
|
|
217
|
+
this.textures = textures
|
|
218
|
+
this.materials = materials
|
|
219
|
+
this.skeleton = skeleton
|
|
220
|
+
this.skinning = skinning
|
|
221
|
+
this.morphing = morphing
|
|
222
|
+
this.rigidbodies = rigidbodies
|
|
223
|
+
this.joints = joints
|
|
224
|
+
|
|
225
|
+
if (this.skeleton.bones.length == 0) {
|
|
226
|
+
throw new Error("Model has no bones")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.initializeRuntimeSkeleton()
|
|
230
|
+
this.initializeRotTweenBuffers()
|
|
231
|
+
this.initializeTransTweenBuffers()
|
|
232
|
+
this.initializeRuntimeMorph()
|
|
233
|
+
this.initializeMorphTweenBuffers()
|
|
234
|
+
this.applyMorphs() // Apply initial morphs (all weights are 0, so no change)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private initializeRuntimeSkeleton(): void {
|
|
238
|
+
const boneCount = this.skeleton.bones.length
|
|
239
|
+
|
|
240
|
+
this.runtimeSkeleton = {
|
|
241
|
+
localRotations: new Float32Array(boneCount * 4),
|
|
242
|
+
localTranslations: new Float32Array(boneCount * 3),
|
|
243
|
+
worldMatrices: new Float32Array(boneCount * 16),
|
|
244
|
+
nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
|
|
245
|
+
acc[bone.name] = index
|
|
246
|
+
return acc
|
|
247
|
+
}, {} as Record<string, number>),
|
|
248
|
+
computedBones: new Array(boneCount).fill(false),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const rotations = this.runtimeSkeleton.localRotations
|
|
252
|
+
for (let i = 0; i < this.skeleton.bones.length; i++) {
|
|
253
|
+
const qi = i * 4
|
|
254
|
+
if (rotations[qi + 3] === 0) {
|
|
255
|
+
rotations[qi] = 0
|
|
256
|
+
rotations[qi + 1] = 0
|
|
257
|
+
rotations[qi + 2] = 0
|
|
258
|
+
rotations[qi + 3] = 1
|
|
259
|
+
}
|
|
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
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private initializeRotTweenBuffers(): void {
|
|
316
|
+
const n = this.skeleton.bones.length
|
|
317
|
+
this.rotTweenState = {
|
|
318
|
+
active: new Uint8Array(n),
|
|
319
|
+
startQuat: new Float32Array(n * 4),
|
|
320
|
+
targetQuat: new Float32Array(n * 4),
|
|
321
|
+
startTimeMs: new Float32Array(n),
|
|
322
|
+
durationMs: new Float32Array(n),
|
|
323
|
+
}
|
|
324
|
+
}
|
|
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
|
+
|
|
337
|
+
private initializeMorphTweenBuffers(): void {
|
|
338
|
+
const n = this.morphing.morphs.length
|
|
339
|
+
this.morphTweenState = {
|
|
340
|
+
active: new Uint8Array(n),
|
|
341
|
+
startWeight: new Float32Array(n),
|
|
342
|
+
targetWeight: new Float32Array(n),
|
|
343
|
+
startTimeMs: new Float32Array(n),
|
|
344
|
+
durationMs: new Float32Array(n),
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private initializeRuntimeMorph(): void {
|
|
349
|
+
const morphCount = this.morphing.morphs.length
|
|
350
|
+
this.runtimeMorph = {
|
|
351
|
+
nameIndex: this.morphing.morphs.reduce((acc, morph, index) => {
|
|
352
|
+
acc[morph.name] = index
|
|
353
|
+
return acc
|
|
354
|
+
}, {} as Record<string, number>),
|
|
355
|
+
weights: new Float32Array(morphCount),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private updateRotationTweens(): void {
|
|
360
|
+
const state = this.rotTweenState
|
|
361
|
+
const now = performance.now()
|
|
362
|
+
const rotations = this.runtimeSkeleton.localRotations
|
|
363
|
+
const boneCount = this.skeleton.bones.length
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < boneCount; i++) {
|
|
366
|
+
if (state.active[i] !== 1) continue
|
|
367
|
+
|
|
368
|
+
const startMs = state.startTimeMs[i]
|
|
369
|
+
const durMs = Math.max(1, state.durationMs[i])
|
|
370
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
371
|
+
const e = easeInOut(t)
|
|
372
|
+
|
|
373
|
+
const qi = i * 4
|
|
374
|
+
const startQuat = new Quat(
|
|
375
|
+
state.startQuat[qi],
|
|
376
|
+
state.startQuat[qi + 1],
|
|
377
|
+
state.startQuat[qi + 2],
|
|
378
|
+
state.startQuat[qi + 3]
|
|
379
|
+
)
|
|
380
|
+
const targetQuat = new Quat(
|
|
381
|
+
state.targetQuat[qi],
|
|
382
|
+
state.targetQuat[qi + 1],
|
|
383
|
+
state.targetQuat[qi + 2],
|
|
384
|
+
state.targetQuat[qi + 3]
|
|
385
|
+
)
|
|
386
|
+
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
387
|
+
|
|
388
|
+
rotations[qi] = result.x
|
|
389
|
+
rotations[qi + 1] = result.y
|
|
390
|
+
rotations[qi + 2] = result.z
|
|
391
|
+
rotations[qi + 3] = result.w
|
|
392
|
+
|
|
393
|
+
if (t >= 1) state.active[i] = 0
|
|
394
|
+
}
|
|
395
|
+
}
|
|
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
|
+
|
|
420
|
+
private updateMorphWeightTweens(): boolean {
|
|
421
|
+
const state = this.morphTweenState
|
|
422
|
+
const now = performance.now()
|
|
423
|
+
const weights = this.runtimeMorph.weights
|
|
424
|
+
const morphCount = this.morphing.morphs.length
|
|
425
|
+
let hasActiveTweens = false
|
|
426
|
+
|
|
427
|
+
for (let i = 0; i < morphCount; i++) {
|
|
428
|
+
if (state.active[i] !== 1) continue
|
|
429
|
+
|
|
430
|
+
hasActiveTweens = true
|
|
431
|
+
const startMs = state.startTimeMs[i]
|
|
432
|
+
const durMs = Math.max(1, state.durationMs[i])
|
|
433
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
434
|
+
const e = easeInOut(t)
|
|
435
|
+
|
|
436
|
+
weights[i] = state.startWeight[i] + (state.targetWeight[i] - state.startWeight[i]) * e
|
|
437
|
+
|
|
438
|
+
if (t >= 1) {
|
|
439
|
+
weights[i] = state.targetWeight[i]
|
|
440
|
+
state.active[i] = 0
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return hasActiveTweens
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Get interleaved vertex data for GPU upload
|
|
448
|
+
// Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
|
|
449
|
+
getVertices(): Float32Array<ArrayBuffer> {
|
|
450
|
+
return this.vertexData
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Get texture information
|
|
454
|
+
getTextures(): Texture[] {
|
|
455
|
+
return this.textures
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Get material information
|
|
459
|
+
getMaterials(): Material[] {
|
|
460
|
+
return this.materials
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Get vertex count
|
|
464
|
+
getVertexCount(): number {
|
|
465
|
+
return this.vertexCount
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Get index data for GPU upload
|
|
469
|
+
getIndices(): Uint32Array<ArrayBuffer> {
|
|
470
|
+
return this.indexData
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Accessors for skeleton/skinning
|
|
474
|
+
getSkeleton(): Skeleton {
|
|
475
|
+
return this.skeleton
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
getSkinning(): Skinning {
|
|
479
|
+
return this.skinning
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Accessors for physics data
|
|
483
|
+
getRigidbodies(): Rigidbody[] {
|
|
484
|
+
return this.rigidbodies
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
getJoints(): Joint[] {
|
|
488
|
+
return this.joints
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Accessors for morphing
|
|
492
|
+
getMorphing(): Morphing {
|
|
493
|
+
return this.morphing
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
getMorphWeights(): Float32Array {
|
|
497
|
+
return this.runtimeMorph.weights
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ------- Bone helpers (public API) -------
|
|
501
|
+
|
|
502
|
+
getBoneNames(): string[] {
|
|
503
|
+
return this.skeleton.bones.map((b) => b.name)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
|
|
507
|
+
const state = this.rotTweenState
|
|
508
|
+
const normalized = quats.map((q) => q.normalize())
|
|
509
|
+
const now = performance.now()
|
|
510
|
+
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
511
|
+
|
|
512
|
+
for (let i = 0; i < names.length; i++) {
|
|
513
|
+
const name = names[i]
|
|
514
|
+
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
|
|
515
|
+
if (idx < 0 || idx >= this.skeleton.bones.length) continue
|
|
516
|
+
|
|
517
|
+
const qi = idx * 4
|
|
518
|
+
const rotations = this.runtimeSkeleton.localRotations
|
|
519
|
+
const [tx, ty, tz, tw] = normalized[i].toArray()
|
|
520
|
+
|
|
521
|
+
if (dur === 0) {
|
|
522
|
+
rotations[qi] = tx
|
|
523
|
+
rotations[qi + 1] = ty
|
|
524
|
+
rotations[qi + 2] = tz
|
|
525
|
+
rotations[qi + 3] = tw
|
|
526
|
+
state.active[idx] = 0
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let sx = rotations[qi]
|
|
531
|
+
let sy = rotations[qi + 1]
|
|
532
|
+
let sz = rotations[qi + 2]
|
|
533
|
+
let sw = rotations[qi + 3]
|
|
534
|
+
|
|
535
|
+
if (state.active[idx] === 1) {
|
|
536
|
+
const startMs = state.startTimeMs[idx]
|
|
537
|
+
const prevDur = Math.max(1, state.durationMs[idx])
|
|
538
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
539
|
+
const e = easeInOut(t)
|
|
540
|
+
const startQuat = new Quat(
|
|
541
|
+
state.startQuat[qi],
|
|
542
|
+
state.startQuat[qi + 1],
|
|
543
|
+
state.startQuat[qi + 2],
|
|
544
|
+
state.startQuat[qi + 3]
|
|
545
|
+
)
|
|
546
|
+
const targetQuat = new Quat(
|
|
547
|
+
state.targetQuat[qi],
|
|
548
|
+
state.targetQuat[qi + 1],
|
|
549
|
+
state.targetQuat[qi + 2],
|
|
550
|
+
state.targetQuat[qi + 3]
|
|
551
|
+
)
|
|
552
|
+
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
553
|
+
const cx = result.x
|
|
554
|
+
const cy = result.y
|
|
555
|
+
const cz = result.z
|
|
556
|
+
const cw = result.w
|
|
557
|
+
sx = cx
|
|
558
|
+
sy = cy
|
|
559
|
+
sz = cz
|
|
560
|
+
sw = cw
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
state.startQuat[qi] = sx
|
|
564
|
+
state.startQuat[qi + 1] = sy
|
|
565
|
+
state.startQuat[qi + 2] = sz
|
|
566
|
+
state.startQuat[qi + 3] = sw
|
|
567
|
+
state.targetQuat[qi] = tx
|
|
568
|
+
state.targetQuat[qi + 1] = ty
|
|
569
|
+
state.targetQuat[qi + 2] = tz
|
|
570
|
+
state.targetQuat[qi + 3] = tw
|
|
571
|
+
state.startTimeMs[idx] = now
|
|
572
|
+
state.durationMs[idx] = dur
|
|
573
|
+
state.active[idx] = 1
|
|
574
|
+
}
|
|
575
|
+
}
|
|
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
|
+
|
|
679
|
+
getBoneWorldMatrices(): Float32Array {
|
|
680
|
+
return this.runtimeSkeleton.worldMatrices
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
getBoneInverseBindMatrices(): Float32Array {
|
|
684
|
+
return this.skeleton.inverseBindMatrices
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
getMorphNames(): string[] {
|
|
688
|
+
return this.morphing.morphs.map((m) => m.name)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
setMorphWeight(name: string, weight: number, durationMs?: number): void {
|
|
692
|
+
const idx = this.runtimeMorph.nameIndex[name] ?? -1
|
|
693
|
+
if (idx < 0 || idx >= this.runtimeMorph.weights.length) return
|
|
694
|
+
|
|
695
|
+
const clampedWeight = Math.max(0, Math.min(1, weight))
|
|
696
|
+
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
697
|
+
|
|
698
|
+
if (dur === 0) {
|
|
699
|
+
// Instant change
|
|
700
|
+
this.runtimeMorph.weights[idx] = clampedWeight
|
|
701
|
+
this.morphTweenState.active[idx] = 0
|
|
702
|
+
this.applyMorphs()
|
|
703
|
+
return
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Animated change
|
|
707
|
+
const state = this.morphTweenState
|
|
708
|
+
const now = performance.now()
|
|
709
|
+
const currentWeight = this.runtimeMorph.weights[idx]
|
|
710
|
+
|
|
711
|
+
// If already tweening, start from current interpolated value
|
|
712
|
+
let startWeight = currentWeight
|
|
713
|
+
if (state.active[idx] === 1) {
|
|
714
|
+
const startMs = state.startTimeMs[idx]
|
|
715
|
+
const prevDur = Math.max(1, state.durationMs[idx])
|
|
716
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
717
|
+
const e = easeInOut(t)
|
|
718
|
+
startWeight = state.startWeight[idx] + (state.targetWeight[idx] - state.startWeight[idx]) * e
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
state.startWeight[idx] = startWeight
|
|
722
|
+
state.targetWeight[idx] = clampedWeight
|
|
723
|
+
state.startTimeMs[idx] = now
|
|
724
|
+
state.durationMs[idx] = dur
|
|
725
|
+
state.active[idx] = 1
|
|
726
|
+
|
|
727
|
+
// Immediately apply morphs with current weight
|
|
728
|
+
this.runtimeMorph.weights[idx] = startWeight
|
|
729
|
+
this.applyMorphs()
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private applyMorphs(): void {
|
|
733
|
+
// Reset vertex data to base positions
|
|
734
|
+
this.vertexData.set(this.baseVertexData)
|
|
735
|
+
|
|
736
|
+
const vertexCount = this.vertexCount
|
|
737
|
+
const morphCount = this.morphing.morphs.length
|
|
738
|
+
const weights = this.runtimeMorph.weights
|
|
739
|
+
|
|
740
|
+
// First pass: Compute effective weights for all morphs (handling group morphs)
|
|
741
|
+
const effectiveWeights = new Float32Array(morphCount)
|
|
742
|
+
effectiveWeights.set(weights) // Start with direct weights
|
|
743
|
+
|
|
744
|
+
// Apply group morphs: group morph weight * ratio affects referenced morphs
|
|
745
|
+
for (let morphIdx = 0; morphIdx < morphCount; morphIdx++) {
|
|
746
|
+
const morph = this.morphing.morphs[morphIdx]
|
|
747
|
+
if (morph.type === 0 && morph.groupReferences) {
|
|
748
|
+
const groupWeight = weights[morphIdx]
|
|
749
|
+
if (groupWeight > 0.0001) {
|
|
750
|
+
for (const ref of morph.groupReferences) {
|
|
751
|
+
if (ref.morphIndex >= 0 && ref.morphIndex < morphCount) {
|
|
752
|
+
// Add group morph's contribution to the referenced morph
|
|
753
|
+
effectiveWeights[ref.morphIndex] += groupWeight * ref.ratio
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Clamp effective weights to [0, 1]
|
|
761
|
+
for (let i = 0; i < morphCount; i++) {
|
|
762
|
+
effectiveWeights[i] = Math.max(0, Math.min(1, effectiveWeights[i]))
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Second pass: Apply vertex morphs with their effective weights
|
|
766
|
+
for (let morphIdx = 0; morphIdx < morphCount; morphIdx++) {
|
|
767
|
+
const effectiveWeight = effectiveWeights[morphIdx]
|
|
768
|
+
if (effectiveWeight === 0 || effectiveWeight < 0.0001) continue
|
|
769
|
+
|
|
770
|
+
const morph = this.morphing.morphs[morphIdx]
|
|
771
|
+
if (morph.type !== 1) continue // Only process vertex morphs
|
|
772
|
+
|
|
773
|
+
// For vertex morphs, iterate through vertices that have offsets
|
|
774
|
+
for (const vertexOffset of morph.vertexOffsets) {
|
|
775
|
+
const vIdx = vertexOffset.vertexIndex
|
|
776
|
+
if (vIdx < 0 || vIdx >= vertexCount) continue
|
|
777
|
+
|
|
778
|
+
// Get morph offset for this vertex
|
|
779
|
+
const offsetX = vertexOffset.positionOffset[0]
|
|
780
|
+
const offsetY = vertexOffset.positionOffset[1]
|
|
781
|
+
const offsetZ = vertexOffset.positionOffset[2]
|
|
782
|
+
|
|
783
|
+
// Skip if offset is zero
|
|
784
|
+
if (Math.abs(offsetX) < 0.0001 && Math.abs(offsetY) < 0.0001 && Math.abs(offsetZ) < 0.0001) {
|
|
785
|
+
continue
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Apply weighted offset to vertex position (positions are at stride 0, 8, 16, ...)
|
|
789
|
+
const vertexIdx = vIdx * VERTEX_STRIDE
|
|
790
|
+
this.vertexData[vertexIdx] += offsetX * effectiveWeight
|
|
791
|
+
this.vertexData[vertexIdx + 1] += offsetY * effectiveWeight
|
|
792
|
+
this.vertexData[vertexIdx + 2] += offsetZ * effectiveWeight
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
evaluatePose(): boolean {
|
|
798
|
+
this.updateRotationTweens()
|
|
799
|
+
this.updateTranslationTweens()
|
|
800
|
+
const hasActiveMorphTweens = this.updateMorphWeightTweens()
|
|
801
|
+
if (hasActiveMorphTweens) {
|
|
802
|
+
this.applyMorphs()
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Compute initial world matrices (needed for IK solving)
|
|
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
|
+
|
|
814
|
+
return hasActiveMorphTweens
|
|
815
|
+
}
|
|
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
|
+
|
|
835
|
+
private computeWorldMatrices(): void {
|
|
836
|
+
const bones = this.skeleton.bones
|
|
837
|
+
const localRot = this.runtimeSkeleton.localRotations
|
|
838
|
+
const localTrans = this.runtimeSkeleton.localTranslations
|
|
839
|
+
const worldBuf = this.runtimeSkeleton.worldMatrices
|
|
840
|
+
const computed = this.runtimeSkeleton.computedBones.fill(false)
|
|
841
|
+
const boneCount = bones.length
|
|
842
|
+
|
|
843
|
+
if (boneCount === 0) return
|
|
844
|
+
|
|
845
|
+
const computeWorld = (i: number): void => {
|
|
846
|
+
if (computed[i]) return
|
|
847
|
+
|
|
848
|
+
const b = bones[i]
|
|
849
|
+
if (b.parentIndex >= boneCount) {
|
|
850
|
+
console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const qi = i * 4
|
|
854
|
+
let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
|
|
855
|
+
let addLocalTx = 0,
|
|
856
|
+
addLocalTy = 0,
|
|
857
|
+
addLocalTz = 0
|
|
858
|
+
|
|
859
|
+
// Optimized append rotation check - only check necessary conditions
|
|
860
|
+
const appendParentIdx = b.appendParentIndex
|
|
861
|
+
const hasAppend =
|
|
862
|
+
b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
|
|
863
|
+
|
|
864
|
+
if (hasAppend) {
|
|
865
|
+
const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
|
|
866
|
+
const hasRatio = Math.abs(ratio) > 1e-6
|
|
867
|
+
|
|
868
|
+
if (hasRatio) {
|
|
869
|
+
const apQi = appendParentIdx * 4
|
|
870
|
+
const apTi = appendParentIdx * 3
|
|
871
|
+
|
|
872
|
+
if (b.appendRotate) {
|
|
873
|
+
let ax = localRot[apQi]
|
|
874
|
+
let ay = localRot[apQi + 1]
|
|
875
|
+
let az = localRot[apQi + 2]
|
|
876
|
+
const aw = localRot[apQi + 3]
|
|
877
|
+
const absRatio = ratio < 0 ? -ratio : ratio
|
|
878
|
+
if (ratio < 0) {
|
|
879
|
+
ax = -ax
|
|
880
|
+
ay = -ay
|
|
881
|
+
az = -az
|
|
882
|
+
}
|
|
883
|
+
const identityQuat = new Quat(0, 0, 0, 1)
|
|
884
|
+
const appendQuat = new Quat(ax, ay, az, aw)
|
|
885
|
+
const result = Quat.slerp(identityQuat, appendQuat, absRatio)
|
|
886
|
+
const rx = result.x
|
|
887
|
+
const ry = result.y
|
|
888
|
+
const rz = result.z
|
|
889
|
+
const rw = result.w
|
|
890
|
+
rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (b.appendMove) {
|
|
894
|
+
const appendRatio = b.appendRatio ?? 1
|
|
895
|
+
addLocalTx = localTrans[apTi] * appendRatio
|
|
896
|
+
addLocalTy = localTrans[apTi + 1] * appendRatio
|
|
897
|
+
addLocalTz = localTrans[apTi + 2] * appendRatio
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
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
|
|
907
|
+
this.cachedIdentityMat1
|
|
908
|
+
.setIdentity()
|
|
909
|
+
.translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
|
|
910
|
+
this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz)
|
|
911
|
+
const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
|
|
912
|
+
|
|
913
|
+
const worldOffset = i * 16
|
|
914
|
+
if (b.parentIndex >= 0) {
|
|
915
|
+
const p = b.parentIndex
|
|
916
|
+
if (!computed[p]) computeWorld(p)
|
|
917
|
+
const parentOffset = p * 16
|
|
918
|
+
// Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
|
|
919
|
+
Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
|
|
920
|
+
worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
|
|
921
|
+
} else {
|
|
922
|
+
worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
|
|
923
|
+
}
|
|
924
|
+
computed[i] = true
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Process all bones (recursion handles dependencies automatically)
|
|
928
|
+
for (let i = 0; i < boneCount; i++) computeWorld(i)
|
|
929
|
+
}
|
|
930
|
+
}
|