reze-engine 0.2.11 → 0.2.13
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 +71 -71
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +436 -434
- package/dist/pool.d.ts +38 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +422 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +2404 -2402
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1054 -1054
- package/src/vmd-loader.ts +179 -179
package/src/model.ts
CHANGED
|
@@ -1,421 +1,421 @@
|
|
|
1
|
-
import { Mat4, Quat, easeInOut } from "./math"
|
|
2
|
-
import { Rigidbody, Joint } from "./physics"
|
|
3
|
-
|
|
4
|
-
const VERTEX_STRIDE = 8
|
|
5
|
-
|
|
6
|
-
export interface Texture {
|
|
7
|
-
path: string
|
|
8
|
-
name: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface Material {
|
|
12
|
-
name: string
|
|
13
|
-
diffuse: [number, number, number, number]
|
|
14
|
-
specular: [number, number, number]
|
|
15
|
-
ambient: [number, number, number]
|
|
16
|
-
shininess: number
|
|
17
|
-
diffuseTextureIndex: number
|
|
18
|
-
normalTextureIndex: number
|
|
19
|
-
sphereTextureIndex: number
|
|
20
|
-
sphereMode: number
|
|
21
|
-
toonTextureIndex: number
|
|
22
|
-
edgeFlag: number
|
|
23
|
-
edgeColor: [number, number, number, number]
|
|
24
|
-
edgeSize: number
|
|
25
|
-
vertexCount: number
|
|
26
|
-
isEye?: boolean // New: marks eye materials
|
|
27
|
-
isFace?: boolean // New: marks face/skin materials
|
|
28
|
-
isHair?: boolean // New: marks hair materials
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface Bone {
|
|
32
|
-
name: string
|
|
33
|
-
parentIndex: number // -1 if no parent
|
|
34
|
-
bindTranslation: [number, number, number]
|
|
35
|
-
children: number[] // child bone indices (built on skeleton creation)
|
|
36
|
-
appendParentIndex?: number // index of the bone to inherit from
|
|
37
|
-
appendRatio?: number // 0..1
|
|
38
|
-
appendRotate?: boolean
|
|
39
|
-
appendMove?: boolean
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface Skeleton {
|
|
43
|
-
bones: Bone[]
|
|
44
|
-
inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface Skinning {
|
|
48
|
-
joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
|
|
49
|
-
weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Runtime skeleton pose state (updated each frame)
|
|
53
|
-
export interface SkeletonRuntime {
|
|
54
|
-
nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
|
|
55
|
-
localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
|
|
56
|
-
localTranslations: Float32Array // vec3 per bone length = boneCount*3
|
|
57
|
-
worldMatrices: Float32Array // mat4 per bone length = boneCount*16
|
|
58
|
-
computedBones: boolean[] // length = boneCount
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Rotation tween state per bone
|
|
62
|
-
interface RotationTweenState {
|
|
63
|
-
active: Uint8Array // 0/1 per bone
|
|
64
|
-
startQuat: Float32Array // quat per bone (x,y,z,w)
|
|
65
|
-
targetQuat: Float32Array // quat per bone (x,y,z,w)
|
|
66
|
-
startTimeMs: Float32Array // one float per bone (ms)
|
|
67
|
-
durationMs: Float32Array // one float per bone (ms)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export class Model {
|
|
71
|
-
private vertexData: Float32Array<ArrayBuffer>
|
|
72
|
-
private vertexCount: number
|
|
73
|
-
private indexData: Uint32Array<ArrayBuffer>
|
|
74
|
-
private textures: Texture[] = []
|
|
75
|
-
private materials: Material[] = []
|
|
76
|
-
// Static skeleton/skinning (not necessarily serialized yet)
|
|
77
|
-
private skeleton: Skeleton
|
|
78
|
-
private skinning: Skinning
|
|
79
|
-
|
|
80
|
-
// Physics data from PMX
|
|
81
|
-
private rigidbodies: Rigidbody[] = []
|
|
82
|
-
private joints: Joint[] = []
|
|
83
|
-
|
|
84
|
-
// Runtime skeleton pose state (updated each frame)
|
|
85
|
-
private runtimeSkeleton!: SkeletonRuntime
|
|
86
|
-
|
|
87
|
-
// Cached identity matrices to avoid allocations in computeWorldMatrices
|
|
88
|
-
private cachedIdentityMat1 = Mat4.identity()
|
|
89
|
-
private cachedIdentityMat2 = Mat4.identity()
|
|
90
|
-
|
|
91
|
-
private rotTweenState!: RotationTweenState
|
|
92
|
-
|
|
93
|
-
constructor(
|
|
94
|
-
vertexData: Float32Array<ArrayBuffer>,
|
|
95
|
-
indexData: Uint32Array<ArrayBuffer>,
|
|
96
|
-
textures: Texture[],
|
|
97
|
-
materials: Material[],
|
|
98
|
-
skeleton: Skeleton,
|
|
99
|
-
skinning: Skinning,
|
|
100
|
-
rigidbodies: Rigidbody[] = [],
|
|
101
|
-
joints: Joint[] = []
|
|
102
|
-
) {
|
|
103
|
-
this.vertexData = vertexData
|
|
104
|
-
this.vertexCount = vertexData.length / VERTEX_STRIDE
|
|
105
|
-
this.indexData = indexData
|
|
106
|
-
this.textures = textures
|
|
107
|
-
this.materials = materials
|
|
108
|
-
this.skeleton = skeleton
|
|
109
|
-
this.skinning = skinning
|
|
110
|
-
this.rigidbodies = rigidbodies
|
|
111
|
-
this.joints = joints
|
|
112
|
-
|
|
113
|
-
if (this.skeleton.bones.length == 0) {
|
|
114
|
-
throw new Error("Model has no bones")
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
this.initializeRuntimeSkeleton()
|
|
118
|
-
this.initializeRotTweenBuffers()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private initializeRuntimeSkeleton(): void {
|
|
122
|
-
const boneCount = this.skeleton.bones.length
|
|
123
|
-
|
|
124
|
-
this.runtimeSkeleton = {
|
|
125
|
-
localRotations: new Float32Array(boneCount * 4),
|
|
126
|
-
localTranslations: new Float32Array(boneCount * 3),
|
|
127
|
-
worldMatrices: new Float32Array(boneCount * 16),
|
|
128
|
-
nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
|
|
129
|
-
acc[bone.name] = index
|
|
130
|
-
return acc
|
|
131
|
-
}, {} as Record<string, number>),
|
|
132
|
-
computedBones: new Array(boneCount).fill(false),
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const rotations = this.runtimeSkeleton.localRotations
|
|
136
|
-
for (let i = 0; i < this.skeleton.bones.length; i++) {
|
|
137
|
-
const qi = i * 4
|
|
138
|
-
if (rotations[qi + 3] === 0) {
|
|
139
|
-
rotations[qi] = 0
|
|
140
|
-
rotations[qi + 1] = 0
|
|
141
|
-
rotations[qi + 2] = 0
|
|
142
|
-
rotations[qi + 3] = 1
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private initializeRotTweenBuffers(): void {
|
|
148
|
-
const n = this.skeleton.bones.length
|
|
149
|
-
this.rotTweenState = {
|
|
150
|
-
active: new Uint8Array(n),
|
|
151
|
-
startQuat: new Float32Array(n * 4),
|
|
152
|
-
targetQuat: new Float32Array(n * 4),
|
|
153
|
-
startTimeMs: new Float32Array(n),
|
|
154
|
-
durationMs: new Float32Array(n),
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private updateRotationTweens(): void {
|
|
159
|
-
const state = this.rotTweenState
|
|
160
|
-
const now = performance.now()
|
|
161
|
-
const rotations = this.runtimeSkeleton.localRotations
|
|
162
|
-
const boneCount = this.skeleton.bones.length
|
|
163
|
-
|
|
164
|
-
for (let i = 0; i < boneCount; i++) {
|
|
165
|
-
if (state.active[i] !== 1) continue
|
|
166
|
-
|
|
167
|
-
const startMs = state.startTimeMs[i]
|
|
168
|
-
const durMs = Math.max(1, state.durationMs[i])
|
|
169
|
-
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
170
|
-
const e = easeInOut(t)
|
|
171
|
-
|
|
172
|
-
const qi = i * 4
|
|
173
|
-
const startQuat = new Quat(
|
|
174
|
-
state.startQuat[qi],
|
|
175
|
-
state.startQuat[qi + 1],
|
|
176
|
-
state.startQuat[qi + 2],
|
|
177
|
-
state.startQuat[qi + 3]
|
|
178
|
-
)
|
|
179
|
-
const targetQuat = new Quat(
|
|
180
|
-
state.targetQuat[qi],
|
|
181
|
-
state.targetQuat[qi + 1],
|
|
182
|
-
state.targetQuat[qi + 2],
|
|
183
|
-
state.targetQuat[qi + 3]
|
|
184
|
-
)
|
|
185
|
-
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
186
|
-
|
|
187
|
-
rotations[qi] = result.x
|
|
188
|
-
rotations[qi + 1] = result.y
|
|
189
|
-
rotations[qi + 2] = result.z
|
|
190
|
-
rotations[qi + 3] = result.w
|
|
191
|
-
|
|
192
|
-
if (t >= 1) state.active[i] = 0
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Get interleaved vertex data for GPU upload
|
|
197
|
-
// Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
|
|
198
|
-
getVertices(): Float32Array<ArrayBuffer> {
|
|
199
|
-
return this.vertexData
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Get texture information
|
|
203
|
-
getTextures(): Texture[] {
|
|
204
|
-
return this.textures
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Get material information
|
|
208
|
-
getMaterials(): Material[] {
|
|
209
|
-
return this.materials
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Get vertex count
|
|
213
|
-
getVertexCount(): number {
|
|
214
|
-
return this.vertexCount
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Get index data for GPU upload
|
|
218
|
-
getIndices(): Uint32Array<ArrayBuffer> {
|
|
219
|
-
return this.indexData
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Accessors for skeleton/skinning
|
|
223
|
-
getSkeleton(): Skeleton {
|
|
224
|
-
return this.skeleton
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
getSkinning(): Skinning {
|
|
228
|
-
return this.skinning
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Accessors for physics data
|
|
232
|
-
getRigidbodies(): Rigidbody[] {
|
|
233
|
-
return this.rigidbodies
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
getJoints(): Joint[] {
|
|
237
|
-
return this.joints
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ------- Bone helpers (public API) -------
|
|
241
|
-
|
|
242
|
-
getBoneNames(): string[] {
|
|
243
|
-
return this.skeleton.bones.map((b) => b.name)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
|
|
247
|
-
const state = this.rotTweenState
|
|
248
|
-
const normalized = quats.map((q) => q.normalize())
|
|
249
|
-
const now = performance.now()
|
|
250
|
-
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i < names.length; i++) {
|
|
253
|
-
const name = names[i]
|
|
254
|
-
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
|
|
255
|
-
if (idx < 0 || idx >= this.skeleton.bones.length) continue
|
|
256
|
-
|
|
257
|
-
const qi = idx * 4
|
|
258
|
-
const rotations = this.runtimeSkeleton.localRotations
|
|
259
|
-
const [tx, ty, tz, tw] = normalized[i].toArray()
|
|
260
|
-
|
|
261
|
-
if (dur === 0) {
|
|
262
|
-
rotations[qi] = tx
|
|
263
|
-
rotations[qi + 1] = ty
|
|
264
|
-
rotations[qi + 2] = tz
|
|
265
|
-
rotations[qi + 3] = tw
|
|
266
|
-
state.active[idx] = 0
|
|
267
|
-
continue
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
let sx = rotations[qi]
|
|
271
|
-
let sy = rotations[qi + 1]
|
|
272
|
-
let sz = rotations[qi + 2]
|
|
273
|
-
let sw = rotations[qi + 3]
|
|
274
|
-
|
|
275
|
-
if (state.active[idx] === 1) {
|
|
276
|
-
const startMs = state.startTimeMs[idx]
|
|
277
|
-
const prevDur = Math.max(1, state.durationMs[idx])
|
|
278
|
-
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
279
|
-
const e = easeInOut(t)
|
|
280
|
-
const startQuat = new Quat(
|
|
281
|
-
state.startQuat[qi],
|
|
282
|
-
state.startQuat[qi + 1],
|
|
283
|
-
state.startQuat[qi + 2],
|
|
284
|
-
state.startQuat[qi + 3]
|
|
285
|
-
)
|
|
286
|
-
const targetQuat = new Quat(
|
|
287
|
-
state.targetQuat[qi],
|
|
288
|
-
state.targetQuat[qi + 1],
|
|
289
|
-
state.targetQuat[qi + 2],
|
|
290
|
-
state.targetQuat[qi + 3]
|
|
291
|
-
)
|
|
292
|
-
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
293
|
-
const cx = result.x
|
|
294
|
-
const cy = result.y
|
|
295
|
-
const cz = result.z
|
|
296
|
-
const cw = result.w
|
|
297
|
-
sx = cx
|
|
298
|
-
sy = cy
|
|
299
|
-
sz = cz
|
|
300
|
-
sw = cw
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
state.startQuat[qi] = sx
|
|
304
|
-
state.startQuat[qi + 1] = sy
|
|
305
|
-
state.startQuat[qi + 2] = sz
|
|
306
|
-
state.startQuat[qi + 3] = sw
|
|
307
|
-
state.targetQuat[qi] = tx
|
|
308
|
-
state.targetQuat[qi + 1] = ty
|
|
309
|
-
state.targetQuat[qi + 2] = tz
|
|
310
|
-
state.targetQuat[qi + 3] = tw
|
|
311
|
-
state.startTimeMs[idx] = now
|
|
312
|
-
state.durationMs[idx] = dur
|
|
313
|
-
state.active[idx] = 1
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
getBoneWorldMatrices(): Float32Array {
|
|
318
|
-
return this.runtimeSkeleton.worldMatrices
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
getBoneInverseBindMatrices(): Float32Array {
|
|
322
|
-
return this.skeleton.inverseBindMatrices
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
evaluatePose(): void {
|
|
326
|
-
this.updateRotationTweens()
|
|
327
|
-
this.computeWorldMatrices()
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private computeWorldMatrices(): void {
|
|
331
|
-
const bones = this.skeleton.bones
|
|
332
|
-
const localRot = this.runtimeSkeleton.localRotations
|
|
333
|
-
const localTrans = this.runtimeSkeleton.localTranslations
|
|
334
|
-
const worldBuf = this.runtimeSkeleton.worldMatrices
|
|
335
|
-
const computed = this.runtimeSkeleton.computedBones.fill(false)
|
|
336
|
-
const boneCount = bones.length
|
|
337
|
-
|
|
338
|
-
if (boneCount === 0) return
|
|
339
|
-
|
|
340
|
-
const computeWorld = (i: number): void => {
|
|
341
|
-
if (computed[i]) return
|
|
342
|
-
|
|
343
|
-
const b = bones[i]
|
|
344
|
-
if (b.parentIndex >= boneCount) {
|
|
345
|
-
console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const qi = i * 4
|
|
349
|
-
let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
|
|
350
|
-
let addLocalTx = 0,
|
|
351
|
-
addLocalTy = 0,
|
|
352
|
-
addLocalTz = 0
|
|
353
|
-
|
|
354
|
-
// Optimized append rotation check - only check necessary conditions
|
|
355
|
-
const appendParentIdx = b.appendParentIndex
|
|
356
|
-
const hasAppend =
|
|
357
|
-
b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
|
|
358
|
-
|
|
359
|
-
if (hasAppend) {
|
|
360
|
-
const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
|
|
361
|
-
const hasRatio = Math.abs(ratio) > 1e-6
|
|
362
|
-
|
|
363
|
-
if (hasRatio) {
|
|
364
|
-
const apQi = appendParentIdx * 4
|
|
365
|
-
const apTi = appendParentIdx * 3
|
|
366
|
-
|
|
367
|
-
if (b.appendRotate) {
|
|
368
|
-
let ax = localRot[apQi]
|
|
369
|
-
let ay = localRot[apQi + 1]
|
|
370
|
-
let az = localRot[apQi + 2]
|
|
371
|
-
const aw = localRot[apQi + 3]
|
|
372
|
-
const absRatio = ratio < 0 ? -ratio : ratio
|
|
373
|
-
if (ratio < 0) {
|
|
374
|
-
ax = -ax
|
|
375
|
-
ay = -ay
|
|
376
|
-
az = -az
|
|
377
|
-
}
|
|
378
|
-
const identityQuat = new Quat(0, 0, 0, 1)
|
|
379
|
-
const appendQuat = new Quat(ax, ay, az, aw)
|
|
380
|
-
const result = Quat.slerp(identityQuat, appendQuat, absRatio)
|
|
381
|
-
const rx = result.x
|
|
382
|
-
const ry = result.y
|
|
383
|
-
const rz = result.z
|
|
384
|
-
const rw = result.w
|
|
385
|
-
rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (b.appendMove) {
|
|
389
|
-
const appendRatio = b.appendRatio ?? 1
|
|
390
|
-
addLocalTx = localTrans[apTi] * appendRatio
|
|
391
|
-
addLocalTy = localTrans[apTi + 1] * appendRatio
|
|
392
|
-
addLocalTz = localTrans[apTi + 2] * appendRatio
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Build local matrix: identity + bind translation, then rotation, then append translation
|
|
398
|
-
this.cachedIdentityMat1
|
|
399
|
-
.setIdentity()
|
|
400
|
-
.translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
|
|
401
|
-
this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
|
|
402
|
-
const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
|
|
403
|
-
|
|
404
|
-
const worldOffset = i * 16
|
|
405
|
-
if (b.parentIndex >= 0) {
|
|
406
|
-
const p = b.parentIndex
|
|
407
|
-
if (!computed[p]) computeWorld(p)
|
|
408
|
-
const parentOffset = p * 16
|
|
409
|
-
// Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
|
|
410
|
-
Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
|
|
411
|
-
worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
|
|
412
|
-
} else {
|
|
413
|
-
worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
|
|
414
|
-
}
|
|
415
|
-
computed[i] = true
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Process all bones (recursion handles dependencies automatically)
|
|
419
|
-
for (let i = 0; i < boneCount; i++) computeWorld(i)
|
|
420
|
-
}
|
|
421
|
-
}
|
|
1
|
+
import { Mat4, Quat, easeInOut } from "./math"
|
|
2
|
+
import { Rigidbody, Joint } from "./physics"
|
|
3
|
+
|
|
4
|
+
const VERTEX_STRIDE = 8
|
|
5
|
+
|
|
6
|
+
export interface Texture {
|
|
7
|
+
path: string
|
|
8
|
+
name: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Material {
|
|
12
|
+
name: string
|
|
13
|
+
diffuse: [number, number, number, number]
|
|
14
|
+
specular: [number, number, number]
|
|
15
|
+
ambient: [number, number, number]
|
|
16
|
+
shininess: number
|
|
17
|
+
diffuseTextureIndex: number
|
|
18
|
+
normalTextureIndex: number
|
|
19
|
+
sphereTextureIndex: number
|
|
20
|
+
sphereMode: number
|
|
21
|
+
toonTextureIndex: number
|
|
22
|
+
edgeFlag: number
|
|
23
|
+
edgeColor: [number, number, number, number]
|
|
24
|
+
edgeSize: number
|
|
25
|
+
vertexCount: number
|
|
26
|
+
isEye?: boolean // New: marks eye materials
|
|
27
|
+
isFace?: boolean // New: marks face/skin materials
|
|
28
|
+
isHair?: boolean // New: marks hair materials
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Bone {
|
|
32
|
+
name: string
|
|
33
|
+
parentIndex: number // -1 if no parent
|
|
34
|
+
bindTranslation: [number, number, number]
|
|
35
|
+
children: number[] // child bone indices (built on skeleton creation)
|
|
36
|
+
appendParentIndex?: number // index of the bone to inherit from
|
|
37
|
+
appendRatio?: number // 0..1
|
|
38
|
+
appendRotate?: boolean
|
|
39
|
+
appendMove?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Skeleton {
|
|
43
|
+
bones: Bone[]
|
|
44
|
+
inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Skinning {
|
|
48
|
+
joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
|
|
49
|
+
weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Runtime skeleton pose state (updated each frame)
|
|
53
|
+
export interface SkeletonRuntime {
|
|
54
|
+
nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
|
|
55
|
+
localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
|
|
56
|
+
localTranslations: Float32Array // vec3 per bone length = boneCount*3
|
|
57
|
+
worldMatrices: Float32Array // mat4 per bone length = boneCount*16
|
|
58
|
+
computedBones: boolean[] // length = boneCount
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Rotation tween state per bone
|
|
62
|
+
interface RotationTweenState {
|
|
63
|
+
active: Uint8Array // 0/1 per bone
|
|
64
|
+
startQuat: Float32Array // quat per bone (x,y,z,w)
|
|
65
|
+
targetQuat: Float32Array // quat per bone (x,y,z,w)
|
|
66
|
+
startTimeMs: Float32Array // one float per bone (ms)
|
|
67
|
+
durationMs: Float32Array // one float per bone (ms)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class Model {
|
|
71
|
+
private vertexData: Float32Array<ArrayBuffer>
|
|
72
|
+
private vertexCount: number
|
|
73
|
+
private indexData: Uint32Array<ArrayBuffer>
|
|
74
|
+
private textures: Texture[] = []
|
|
75
|
+
private materials: Material[] = []
|
|
76
|
+
// Static skeleton/skinning (not necessarily serialized yet)
|
|
77
|
+
private skeleton: Skeleton
|
|
78
|
+
private skinning: Skinning
|
|
79
|
+
|
|
80
|
+
// Physics data from PMX
|
|
81
|
+
private rigidbodies: Rigidbody[] = []
|
|
82
|
+
private joints: Joint[] = []
|
|
83
|
+
|
|
84
|
+
// Runtime skeleton pose state (updated each frame)
|
|
85
|
+
private runtimeSkeleton!: SkeletonRuntime
|
|
86
|
+
|
|
87
|
+
// Cached identity matrices to avoid allocations in computeWorldMatrices
|
|
88
|
+
private cachedIdentityMat1 = Mat4.identity()
|
|
89
|
+
private cachedIdentityMat2 = Mat4.identity()
|
|
90
|
+
|
|
91
|
+
private rotTweenState!: RotationTweenState
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
vertexData: Float32Array<ArrayBuffer>,
|
|
95
|
+
indexData: Uint32Array<ArrayBuffer>,
|
|
96
|
+
textures: Texture[],
|
|
97
|
+
materials: Material[],
|
|
98
|
+
skeleton: Skeleton,
|
|
99
|
+
skinning: Skinning,
|
|
100
|
+
rigidbodies: Rigidbody[] = [],
|
|
101
|
+
joints: Joint[] = []
|
|
102
|
+
) {
|
|
103
|
+
this.vertexData = vertexData
|
|
104
|
+
this.vertexCount = vertexData.length / VERTEX_STRIDE
|
|
105
|
+
this.indexData = indexData
|
|
106
|
+
this.textures = textures
|
|
107
|
+
this.materials = materials
|
|
108
|
+
this.skeleton = skeleton
|
|
109
|
+
this.skinning = skinning
|
|
110
|
+
this.rigidbodies = rigidbodies
|
|
111
|
+
this.joints = joints
|
|
112
|
+
|
|
113
|
+
if (this.skeleton.bones.length == 0) {
|
|
114
|
+
throw new Error("Model has no bones")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.initializeRuntimeSkeleton()
|
|
118
|
+
this.initializeRotTweenBuffers()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private initializeRuntimeSkeleton(): void {
|
|
122
|
+
const boneCount = this.skeleton.bones.length
|
|
123
|
+
|
|
124
|
+
this.runtimeSkeleton = {
|
|
125
|
+
localRotations: new Float32Array(boneCount * 4),
|
|
126
|
+
localTranslations: new Float32Array(boneCount * 3),
|
|
127
|
+
worldMatrices: new Float32Array(boneCount * 16),
|
|
128
|
+
nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
|
|
129
|
+
acc[bone.name] = index
|
|
130
|
+
return acc
|
|
131
|
+
}, {} as Record<string, number>),
|
|
132
|
+
computedBones: new Array(boneCount).fill(false),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rotations = this.runtimeSkeleton.localRotations
|
|
136
|
+
for (let i = 0; i < this.skeleton.bones.length; i++) {
|
|
137
|
+
const qi = i * 4
|
|
138
|
+
if (rotations[qi + 3] === 0) {
|
|
139
|
+
rotations[qi] = 0
|
|
140
|
+
rotations[qi + 1] = 0
|
|
141
|
+
rotations[qi + 2] = 0
|
|
142
|
+
rotations[qi + 3] = 1
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private initializeRotTweenBuffers(): void {
|
|
148
|
+
const n = this.skeleton.bones.length
|
|
149
|
+
this.rotTweenState = {
|
|
150
|
+
active: new Uint8Array(n),
|
|
151
|
+
startQuat: new Float32Array(n * 4),
|
|
152
|
+
targetQuat: new Float32Array(n * 4),
|
|
153
|
+
startTimeMs: new Float32Array(n),
|
|
154
|
+
durationMs: new Float32Array(n),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private updateRotationTweens(): void {
|
|
159
|
+
const state = this.rotTweenState
|
|
160
|
+
const now = performance.now()
|
|
161
|
+
const rotations = this.runtimeSkeleton.localRotations
|
|
162
|
+
const boneCount = this.skeleton.bones.length
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < boneCount; i++) {
|
|
165
|
+
if (state.active[i] !== 1) continue
|
|
166
|
+
|
|
167
|
+
const startMs = state.startTimeMs[i]
|
|
168
|
+
const durMs = Math.max(1, state.durationMs[i])
|
|
169
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
|
|
170
|
+
const e = easeInOut(t)
|
|
171
|
+
|
|
172
|
+
const qi = i * 4
|
|
173
|
+
const startQuat = new Quat(
|
|
174
|
+
state.startQuat[qi],
|
|
175
|
+
state.startQuat[qi + 1],
|
|
176
|
+
state.startQuat[qi + 2],
|
|
177
|
+
state.startQuat[qi + 3]
|
|
178
|
+
)
|
|
179
|
+
const targetQuat = new Quat(
|
|
180
|
+
state.targetQuat[qi],
|
|
181
|
+
state.targetQuat[qi + 1],
|
|
182
|
+
state.targetQuat[qi + 2],
|
|
183
|
+
state.targetQuat[qi + 3]
|
|
184
|
+
)
|
|
185
|
+
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
186
|
+
|
|
187
|
+
rotations[qi] = result.x
|
|
188
|
+
rotations[qi + 1] = result.y
|
|
189
|
+
rotations[qi + 2] = result.z
|
|
190
|
+
rotations[qi + 3] = result.w
|
|
191
|
+
|
|
192
|
+
if (t >= 1) state.active[i] = 0
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get interleaved vertex data for GPU upload
|
|
197
|
+
// Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
|
|
198
|
+
getVertices(): Float32Array<ArrayBuffer> {
|
|
199
|
+
return this.vertexData
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get texture information
|
|
203
|
+
getTextures(): Texture[] {
|
|
204
|
+
return this.textures
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get material information
|
|
208
|
+
getMaterials(): Material[] {
|
|
209
|
+
return this.materials
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Get vertex count
|
|
213
|
+
getVertexCount(): number {
|
|
214
|
+
return this.vertexCount
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get index data for GPU upload
|
|
218
|
+
getIndices(): Uint32Array<ArrayBuffer> {
|
|
219
|
+
return this.indexData
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Accessors for skeleton/skinning
|
|
223
|
+
getSkeleton(): Skeleton {
|
|
224
|
+
return this.skeleton
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getSkinning(): Skinning {
|
|
228
|
+
return this.skinning
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Accessors for physics data
|
|
232
|
+
getRigidbodies(): Rigidbody[] {
|
|
233
|
+
return this.rigidbodies
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getJoints(): Joint[] {
|
|
237
|
+
return this.joints
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ------- Bone helpers (public API) -------
|
|
241
|
+
|
|
242
|
+
getBoneNames(): string[] {
|
|
243
|
+
return this.skeleton.bones.map((b) => b.name)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
|
|
247
|
+
const state = this.rotTweenState
|
|
248
|
+
const normalized = quats.map((q) => q.normalize())
|
|
249
|
+
const now = performance.now()
|
|
250
|
+
const dur = durationMs && durationMs > 0 ? durationMs : 0
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < names.length; i++) {
|
|
253
|
+
const name = names[i]
|
|
254
|
+
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
|
|
255
|
+
if (idx < 0 || idx >= this.skeleton.bones.length) continue
|
|
256
|
+
|
|
257
|
+
const qi = idx * 4
|
|
258
|
+
const rotations = this.runtimeSkeleton.localRotations
|
|
259
|
+
const [tx, ty, tz, tw] = normalized[i].toArray()
|
|
260
|
+
|
|
261
|
+
if (dur === 0) {
|
|
262
|
+
rotations[qi] = tx
|
|
263
|
+
rotations[qi + 1] = ty
|
|
264
|
+
rotations[qi + 2] = tz
|
|
265
|
+
rotations[qi + 3] = tw
|
|
266
|
+
state.active[idx] = 0
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let sx = rotations[qi]
|
|
271
|
+
let sy = rotations[qi + 1]
|
|
272
|
+
let sz = rotations[qi + 2]
|
|
273
|
+
let sw = rotations[qi + 3]
|
|
274
|
+
|
|
275
|
+
if (state.active[idx] === 1) {
|
|
276
|
+
const startMs = state.startTimeMs[idx]
|
|
277
|
+
const prevDur = Math.max(1, state.durationMs[idx])
|
|
278
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
|
|
279
|
+
const e = easeInOut(t)
|
|
280
|
+
const startQuat = new Quat(
|
|
281
|
+
state.startQuat[qi],
|
|
282
|
+
state.startQuat[qi + 1],
|
|
283
|
+
state.startQuat[qi + 2],
|
|
284
|
+
state.startQuat[qi + 3]
|
|
285
|
+
)
|
|
286
|
+
const targetQuat = new Quat(
|
|
287
|
+
state.targetQuat[qi],
|
|
288
|
+
state.targetQuat[qi + 1],
|
|
289
|
+
state.targetQuat[qi + 2],
|
|
290
|
+
state.targetQuat[qi + 3]
|
|
291
|
+
)
|
|
292
|
+
const result = Quat.slerp(startQuat, targetQuat, e)
|
|
293
|
+
const cx = result.x
|
|
294
|
+
const cy = result.y
|
|
295
|
+
const cz = result.z
|
|
296
|
+
const cw = result.w
|
|
297
|
+
sx = cx
|
|
298
|
+
sy = cy
|
|
299
|
+
sz = cz
|
|
300
|
+
sw = cw
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
state.startQuat[qi] = sx
|
|
304
|
+
state.startQuat[qi + 1] = sy
|
|
305
|
+
state.startQuat[qi + 2] = sz
|
|
306
|
+
state.startQuat[qi + 3] = sw
|
|
307
|
+
state.targetQuat[qi] = tx
|
|
308
|
+
state.targetQuat[qi + 1] = ty
|
|
309
|
+
state.targetQuat[qi + 2] = tz
|
|
310
|
+
state.targetQuat[qi + 3] = tw
|
|
311
|
+
state.startTimeMs[idx] = now
|
|
312
|
+
state.durationMs[idx] = dur
|
|
313
|
+
state.active[idx] = 1
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
getBoneWorldMatrices(): Float32Array {
|
|
318
|
+
return this.runtimeSkeleton.worldMatrices
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
getBoneInverseBindMatrices(): Float32Array {
|
|
322
|
+
return this.skeleton.inverseBindMatrices
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
evaluatePose(): void {
|
|
326
|
+
this.updateRotationTweens()
|
|
327
|
+
this.computeWorldMatrices()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private computeWorldMatrices(): void {
|
|
331
|
+
const bones = this.skeleton.bones
|
|
332
|
+
const localRot = this.runtimeSkeleton.localRotations
|
|
333
|
+
const localTrans = this.runtimeSkeleton.localTranslations
|
|
334
|
+
const worldBuf = this.runtimeSkeleton.worldMatrices
|
|
335
|
+
const computed = this.runtimeSkeleton.computedBones.fill(false)
|
|
336
|
+
const boneCount = bones.length
|
|
337
|
+
|
|
338
|
+
if (boneCount === 0) return
|
|
339
|
+
|
|
340
|
+
const computeWorld = (i: number): void => {
|
|
341
|
+
if (computed[i]) return
|
|
342
|
+
|
|
343
|
+
const b = bones[i]
|
|
344
|
+
if (b.parentIndex >= boneCount) {
|
|
345
|
+
console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const qi = i * 4
|
|
349
|
+
let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
|
|
350
|
+
let addLocalTx = 0,
|
|
351
|
+
addLocalTy = 0,
|
|
352
|
+
addLocalTz = 0
|
|
353
|
+
|
|
354
|
+
// Optimized append rotation check - only check necessary conditions
|
|
355
|
+
const appendParentIdx = b.appendParentIndex
|
|
356
|
+
const hasAppend =
|
|
357
|
+
b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
|
|
358
|
+
|
|
359
|
+
if (hasAppend) {
|
|
360
|
+
const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
|
|
361
|
+
const hasRatio = Math.abs(ratio) > 1e-6
|
|
362
|
+
|
|
363
|
+
if (hasRatio) {
|
|
364
|
+
const apQi = appendParentIdx * 4
|
|
365
|
+
const apTi = appendParentIdx * 3
|
|
366
|
+
|
|
367
|
+
if (b.appendRotate) {
|
|
368
|
+
let ax = localRot[apQi]
|
|
369
|
+
let ay = localRot[apQi + 1]
|
|
370
|
+
let az = localRot[apQi + 2]
|
|
371
|
+
const aw = localRot[apQi + 3]
|
|
372
|
+
const absRatio = ratio < 0 ? -ratio : ratio
|
|
373
|
+
if (ratio < 0) {
|
|
374
|
+
ax = -ax
|
|
375
|
+
ay = -ay
|
|
376
|
+
az = -az
|
|
377
|
+
}
|
|
378
|
+
const identityQuat = new Quat(0, 0, 0, 1)
|
|
379
|
+
const appendQuat = new Quat(ax, ay, az, aw)
|
|
380
|
+
const result = Quat.slerp(identityQuat, appendQuat, absRatio)
|
|
381
|
+
const rx = result.x
|
|
382
|
+
const ry = result.y
|
|
383
|
+
const rz = result.z
|
|
384
|
+
const rw = result.w
|
|
385
|
+
rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (b.appendMove) {
|
|
389
|
+
const appendRatio = b.appendRatio ?? 1
|
|
390
|
+
addLocalTx = localTrans[apTi] * appendRatio
|
|
391
|
+
addLocalTy = localTrans[apTi + 1] * appendRatio
|
|
392
|
+
addLocalTz = localTrans[apTi + 2] * appendRatio
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Build local matrix: identity + bind translation, then rotation, then append translation
|
|
398
|
+
this.cachedIdentityMat1
|
|
399
|
+
.setIdentity()
|
|
400
|
+
.translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
|
|
401
|
+
this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
|
|
402
|
+
const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
|
|
403
|
+
|
|
404
|
+
const worldOffset = i * 16
|
|
405
|
+
if (b.parentIndex >= 0) {
|
|
406
|
+
const p = b.parentIndex
|
|
407
|
+
if (!computed[p]) computeWorld(p)
|
|
408
|
+
const parentOffset = p * 16
|
|
409
|
+
// Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
|
|
410
|
+
Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
|
|
411
|
+
worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
|
|
412
|
+
} else {
|
|
413
|
+
worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
|
|
414
|
+
}
|
|
415
|
+
computed[i] = true
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Process all bones (recursion handles dependencies automatically)
|
|
419
|
+
for (let i = 0; i < boneCount; i++) computeWorld(i)
|
|
420
|
+
}
|
|
421
|
+
}
|