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/engine.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { bezierInterpolate } from "./bezier-interpolate"
|
|
1
2
|
import { Camera } from "./camera"
|
|
2
|
-
import { Quat, Vec3 } from "./math"
|
|
3
|
+
import { Mat4, Quat, Vec3 } from "./math"
|
|
3
4
|
import { Model } from "./model"
|
|
4
5
|
import { PmxLoader } from "./pmx-loader"
|
|
5
6
|
import { Physics } from "./physics"
|
|
6
|
-
import { VMDKeyFrame, VMDLoader } from "./vmd-loader"
|
|
7
|
+
import { BoneFrame, MorphFrame, VMDKeyFrame, VMDLoader } from "./vmd-loader"
|
|
7
8
|
|
|
8
9
|
export type EngineOptions = {
|
|
9
10
|
ambientColor?: Vec3
|
|
@@ -24,12 +25,6 @@ interface DrawCall {
|
|
|
24
25
|
bindGroup: GPUBindGroup
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
type BoneKeyFrame = {
|
|
28
|
-
boneName: string
|
|
29
|
-
time: number
|
|
30
|
-
rotation: Quat
|
|
31
|
-
}
|
|
32
|
-
|
|
33
28
|
export class Engine {
|
|
34
29
|
private canvas: HTMLCanvasElement
|
|
35
30
|
private device!: GPUDevice
|
|
@@ -79,7 +74,6 @@ export class Engine {
|
|
|
79
74
|
private static readonly DEFAULT_RIM_LIGHT_INTENSITY = 0.45
|
|
80
75
|
private static readonly DEFAULT_CAMERA_DISTANCE = 26.6
|
|
81
76
|
private static readonly DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0)
|
|
82
|
-
private static readonly HAIR_OVER_EYES_ALPHA = 0.5
|
|
83
77
|
private static readonly TRANSPARENCY_EPSILON = 0.001
|
|
84
78
|
private static readonly STATS_FPS_UPDATE_INTERVAL_MS = 1000
|
|
85
79
|
private static readonly STATS_FRAME_TIME_ROUNDING = 100
|
|
@@ -116,6 +110,7 @@ export class Engine {
|
|
|
116
110
|
private physics: Physics | null = null
|
|
117
111
|
private materialSampler!: GPUSampler
|
|
118
112
|
private textureCache = new Map<string, GPUTexture>()
|
|
113
|
+
private vertexBufferNeedsUpdate = false
|
|
119
114
|
// Draw lists
|
|
120
115
|
private opaqueDraws: DrawCall[] = []
|
|
121
116
|
private eyeDraws: DrawCall[] = []
|
|
@@ -143,8 +138,10 @@ export class Engine {
|
|
|
143
138
|
private animationTimeouts: number[] = []
|
|
144
139
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
145
140
|
private playingAnimation = false // Set to true when playAnimation is called
|
|
146
|
-
private
|
|
147
|
-
private
|
|
141
|
+
private animationStartTime: number = 0 // When animation started playing
|
|
142
|
+
private animationDuration: number = 0 // Total animation duration in seconds
|
|
143
|
+
private boneTracks: Map<string, Array<{ boneFrame: BoneFrame; time: number }>> = new Map()
|
|
144
|
+
private morphTracks: Map<string, Array<{ morphFrame: MorphFrame; time: number }>> = new Map()
|
|
148
145
|
|
|
149
146
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
150
147
|
this.canvas = canvas
|
|
@@ -1282,75 +1279,100 @@ export class Engine {
|
|
|
1282
1279
|
this.hasAnimation = true
|
|
1283
1280
|
}
|
|
1284
1281
|
|
|
1285
|
-
public playAnimation(
|
|
1286
|
-
breathBones?: string[] | Record<string, number> // Array of bone names or map of bone name -> rotation range
|
|
1287
|
-
breathDuration?: number // Breathing cycle duration in milliseconds
|
|
1288
|
-
}) {
|
|
1282
|
+
public playAnimation() {
|
|
1289
1283
|
if (this.animationFrames.length === 0) return
|
|
1290
1284
|
|
|
1291
1285
|
this.stopAnimation()
|
|
1292
|
-
|
|
1286
|
+
|
|
1293
1287
|
this.playingAnimation = true
|
|
1294
1288
|
|
|
1295
|
-
//
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1289
|
+
// Process bone frames
|
|
1290
|
+
const allBoneKeyFrames: Array<{ boneFrame: BoneFrame; time: number }> = []
|
|
1291
|
+
for (const keyFrame of this.animationFrames) {
|
|
1292
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
1293
|
+
allBoneKeyFrames.push({
|
|
1294
|
+
boneFrame,
|
|
1295
|
+
time: keyFrame.time,
|
|
1296
|
+
})
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
1299
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
breathBones = Object.keys(options.breathBones)
|
|
1305
|
-
breathRotationRanges = options.breathBones
|
|
1300
|
+
const boneKeyFramesByBone = new Map<string, Array<{ boneFrame: BoneFrame; time: number }>>()
|
|
1301
|
+
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
1302
|
+
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
1303
|
+
boneKeyFramesByBone.set(boneFrame.boneName, [])
|
|
1306
1304
|
}
|
|
1305
|
+
boneKeyFramesByBone.get(boneFrame.boneName)!.push({ boneFrame, time })
|
|
1307
1306
|
}
|
|
1308
1307
|
|
|
1309
|
-
const
|
|
1308
|
+
for (const keyFrames of boneKeyFramesByBone.values()) {
|
|
1309
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
1310
|
+
}
|
|
1310
1311
|
|
|
1311
|
-
|
|
1312
|
+
// Process morph frames
|
|
1313
|
+
const allMorphKeyFrames: Array<{ morphFrame: MorphFrame; time: number }> = []
|
|
1312
1314
|
for (const keyFrame of this.animationFrames) {
|
|
1313
|
-
for (const
|
|
1314
|
-
|
|
1315
|
-
|
|
1315
|
+
for (const morphFrame of keyFrame.morphFrames) {
|
|
1316
|
+
allMorphKeyFrames.push({
|
|
1317
|
+
morphFrame,
|
|
1316
1318
|
time: keyFrame.time,
|
|
1317
|
-
rotation: boneFrame.rotation,
|
|
1318
1319
|
})
|
|
1319
1320
|
}
|
|
1320
1321
|
}
|
|
1321
1322
|
|
|
1322
|
-
const
|
|
1323
|
-
for (const
|
|
1324
|
-
if (!
|
|
1325
|
-
|
|
1323
|
+
const morphKeyFramesByMorph = new Map<string, Array<{ morphFrame: MorphFrame; time: number }>>()
|
|
1324
|
+
for (const { morphFrame, time } of allMorphKeyFrames) {
|
|
1325
|
+
if (!morphKeyFramesByMorph.has(morphFrame.morphName)) {
|
|
1326
|
+
morphKeyFramesByMorph.set(morphFrame.morphName, [])
|
|
1326
1327
|
}
|
|
1327
|
-
|
|
1328
|
+
morphKeyFramesByMorph.get(morphFrame.morphName)!.push({ morphFrame, time })
|
|
1328
1329
|
}
|
|
1329
1330
|
|
|
1330
|
-
for (const keyFrames of
|
|
1331
|
+
for (const keyFrames of morphKeyFramesByMorph.values()) {
|
|
1331
1332
|
keyFrames.sort((a, b) => a.time - b.time)
|
|
1332
1333
|
}
|
|
1333
1334
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1335
|
+
// Store tracks for frame-based animation
|
|
1336
|
+
this.boneTracks = boneKeyFramesByBone
|
|
1337
|
+
this.morphTracks = morphKeyFramesByMorph
|
|
1338
|
+
|
|
1339
|
+
// Calculate animation duration from max frame time (already in seconds)
|
|
1340
|
+
let maxFrameTime = 0
|
|
1341
|
+
for (const keyFrames of this.boneTracks.values()) {
|
|
1342
|
+
if (keyFrames.length > 0) {
|
|
1343
|
+
const lastTime = keyFrames[keyFrames.length - 1].time
|
|
1344
|
+
if (lastTime > maxFrameTime) {
|
|
1345
|
+
maxFrameTime = lastTime
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
for (const keyFrames of this.morphTracks.values()) {
|
|
1350
|
+
if (keyFrames.length > 0) {
|
|
1351
|
+
const lastTime = keyFrames[keyFrames.length - 1].time
|
|
1352
|
+
if (lastTime > maxFrameTime) {
|
|
1353
|
+
maxFrameTime = lastTime
|
|
1354
|
+
}
|
|
1343
1355
|
}
|
|
1344
1356
|
}
|
|
1357
|
+
this.animationDuration = maxFrameTime > 0 ? maxFrameTime : 0
|
|
1358
|
+
this.animationStartTime = performance.now()
|
|
1345
1359
|
|
|
1360
|
+
// Initialize bones and morphs to time 0 pose
|
|
1346
1361
|
if (this.currentModel) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1362
|
+
const skeleton = this.currentModel.getSkeleton()
|
|
1363
|
+
const bonesWithTime0 = new Set<string>()
|
|
1364
|
+
|
|
1365
|
+
// Apply time 0 bone keyframes
|
|
1366
|
+
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
1367
|
+
if (keyFrames.length > 0 && keyFrames[0].time === 0) {
|
|
1368
|
+
const boneFrame = keyFrames[0].boneFrame
|
|
1369
|
+
this.rotateBones([boneName], [boneFrame.rotation], 0)
|
|
1370
|
+
this.moveBones([boneName], [boneFrame.translation], 0)
|
|
1371
|
+
bonesWithTime0.add(boneName)
|
|
1372
|
+
}
|
|
1351
1373
|
}
|
|
1352
1374
|
|
|
1353
|
-
|
|
1375
|
+
// Reset bones without time 0 keyframes
|
|
1354
1376
|
const bonesToReset: string[] = []
|
|
1355
1377
|
for (const bone of skeleton.bones) {
|
|
1356
1378
|
if (!bonesWithTime0.has(bone.name)) {
|
|
@@ -1364,6 +1386,14 @@ export class Engine {
|
|
|
1364
1386
|
this.rotateBones(bonesToReset, identityQuats, 0)
|
|
1365
1387
|
}
|
|
1366
1388
|
|
|
1389
|
+
// Apply time 0 morph keyframes
|
|
1390
|
+
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
1391
|
+
if (keyFrames.length > 0 && keyFrames[0].time === 0) {
|
|
1392
|
+
const morphFrame = keyFrames[0].morphFrame
|
|
1393
|
+
this.setMorphWeight(morphName, morphFrame.weight, 0)
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1367
1397
|
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1368
1398
|
if (this.physics) {
|
|
1369
1399
|
this.currentModel.evaluatePose()
|
|
@@ -1384,70 +1414,6 @@ export class Engine {
|
|
|
1384
1414
|
this.device.queue.submit([encoder.finish()])
|
|
1385
1415
|
}
|
|
1386
1416
|
}
|
|
1387
|
-
for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1388
|
-
for (let i = 0; i < keyFrames.length; i++) {
|
|
1389
|
-
const boneKeyFrame = keyFrames[i]
|
|
1390
|
-
const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null
|
|
1391
|
-
|
|
1392
|
-
if (boneKeyFrame.time === 0) continue
|
|
1393
|
-
|
|
1394
|
-
let durationMs = 0
|
|
1395
|
-
if (i === 0) {
|
|
1396
|
-
durationMs = boneKeyFrame.time * 1000
|
|
1397
|
-
} else if (previousBoneKeyFrame) {
|
|
1398
|
-
durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0
|
|
1402
|
-
const delayMs = scheduleTime * 1000
|
|
1403
|
-
|
|
1404
|
-
if (delayMs <= 0) {
|
|
1405
|
-
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
|
|
1406
|
-
} else {
|
|
1407
|
-
const timeoutId = window.setTimeout(() => {
|
|
1408
|
-
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
|
|
1409
|
-
}, delayMs)
|
|
1410
|
-
this.animationTimeouts.push(timeoutId)
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
// Setup breathing animation if enabled
|
|
1416
|
-
if (enableBreath && this.currentModel) {
|
|
1417
|
-
// Find the last frame time
|
|
1418
|
-
let maxTime = 0
|
|
1419
|
-
for (const keyFrame of this.animationFrames) {
|
|
1420
|
-
if (keyFrame.time > maxTime) {
|
|
1421
|
-
maxTime = keyFrame.time
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Get last frame rotations directly from animation data for breathing bones
|
|
1426
|
-
const lastFrameRotations = new Map<string, Quat>()
|
|
1427
|
-
for (const bone of breathBones) {
|
|
1428
|
-
const keyFrames = boneKeyFramesByBone.get(bone)
|
|
1429
|
-
if (keyFrames && keyFrames.length > 0) {
|
|
1430
|
-
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1431
|
-
let lastRotation: Quat | null = null
|
|
1432
|
-
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1433
|
-
if (keyFrames[i].time <= maxTime) {
|
|
1434
|
-
lastRotation = keyFrames[i].rotation
|
|
1435
|
-
break
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
if (lastRotation) {
|
|
1439
|
-
lastFrameRotations.set(bone, lastRotation)
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Start breathing after animation completes
|
|
1445
|
-
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1446
|
-
const animationEndTime = maxTime * 1000 + 200 // Small buffer for final tweens to complete
|
|
1447
|
-
this.breathingTimeout = window.setTimeout(() => {
|
|
1448
|
-
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration)
|
|
1449
|
-
}, animationEndTime)
|
|
1450
|
-
}
|
|
1451
1417
|
}
|
|
1452
1418
|
|
|
1453
1419
|
public stopAnimation() {
|
|
@@ -1456,69 +1422,195 @@ export class Engine {
|
|
|
1456
1422
|
}
|
|
1457
1423
|
this.animationTimeouts = []
|
|
1458
1424
|
this.playingAnimation = false
|
|
1425
|
+
this.boneTracks.clear()
|
|
1426
|
+
this.morphTracks.clear()
|
|
1459
1427
|
}
|
|
1460
1428
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
}
|
|
1466
|
-
this.breathingBaseRotations.clear()
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
private startBreathing(
|
|
1470
|
-
bones: string[],
|
|
1471
|
-
baseRotations: Map<string, Quat>,
|
|
1472
|
-
rotationRanges?: Record<string, number>,
|
|
1473
|
-
durationMs: number = 4000
|
|
1474
|
-
) {
|
|
1429
|
+
// Frame-based animation update (called every frame)
|
|
1430
|
+
// Similar to reference: MmdRuntimeModelAnimation.animate(frameTime)
|
|
1431
|
+
// frameTime is in seconds (already converted from VMD frame numbers in loader)
|
|
1432
|
+
private animate(frameTime: number): void {
|
|
1475
1433
|
if (!this.currentModel) return
|
|
1476
1434
|
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1435
|
+
// Helper to find upper bound index (binary search)
|
|
1436
|
+
const upperBoundFrameIndex = (time: number, keyFrames: Array<{ boneFrame: BoneFrame; time: number }>): number => {
|
|
1437
|
+
let left = 0
|
|
1438
|
+
let right = keyFrames.length
|
|
1439
|
+
while (left < right) {
|
|
1440
|
+
const mid = Math.floor((left + right) / 2)
|
|
1441
|
+
if (keyFrames[mid].time <= time) {
|
|
1442
|
+
left = mid + 1
|
|
1443
|
+
} else {
|
|
1444
|
+
right = mid
|
|
1445
|
+
}
|
|
1483
1446
|
}
|
|
1447
|
+
return left
|
|
1484
1448
|
}
|
|
1485
1449
|
|
|
1486
|
-
const
|
|
1487
|
-
const
|
|
1450
|
+
const boneNamesToRotate: string[] = []
|
|
1451
|
+
const rotationsToApply: Quat[] = []
|
|
1452
|
+
const boneNamesToMove: string[] = []
|
|
1453
|
+
const translationsToApply: Vec3[] = []
|
|
1454
|
+
const morphNamesToSet: string[] = []
|
|
1455
|
+
const morphWeightsToSet: number[] = []
|
|
1456
|
+
|
|
1457
|
+
// Process each bone track
|
|
1458
|
+
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
1459
|
+
if (keyFrames.length === 0) continue
|
|
1460
|
+
|
|
1461
|
+
// Clamp frame time to track range (all times are in seconds)
|
|
1462
|
+
const startTime = keyFrames[0].time
|
|
1463
|
+
const endTime = keyFrames[keyFrames.length - 1].time
|
|
1464
|
+
const clampedFrameTime = Math.max(startTime, Math.min(endTime, frameTime))
|
|
1488
1465
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const animate = (isInhale: boolean) => {
|
|
1492
|
-
if (!this.currentModel) return
|
|
1466
|
+
const upperBoundIndex = upperBoundFrameIndex(clampedFrameTime, keyFrames)
|
|
1467
|
+
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
1493
1468
|
|
|
1494
|
-
|
|
1495
|
-
const breathingQuats: Quat[] = []
|
|
1469
|
+
if (upperBoundIndexMinusOne < 0) continue
|
|
1496
1470
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
if (!baseRot) continue
|
|
1471
|
+
const timeB = keyFrames[upperBoundIndex]?.time
|
|
1472
|
+
const boneFrameA = keyFrames[upperBoundIndexMinusOne].boneFrame
|
|
1500
1473
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1474
|
+
if (timeB === undefined) {
|
|
1475
|
+
// Last keyframe or beyond - use the last keyframe value
|
|
1476
|
+
boneNamesToRotate.push(boneName)
|
|
1477
|
+
rotationsToApply.push(boneFrameA.rotation)
|
|
1478
|
+
boneNamesToMove.push(boneName)
|
|
1479
|
+
translationsToApply.push(boneFrameA.translation)
|
|
1480
|
+
} else {
|
|
1481
|
+
// Interpolate between two keyframes
|
|
1482
|
+
const timeA = keyFrames[upperBoundIndexMinusOne].time
|
|
1483
|
+
const boneFrameB = keyFrames[upperBoundIndex].boneFrame
|
|
1484
|
+
const gradient = (clampedFrameTime - timeA) / (timeB - timeA)
|
|
1485
|
+
|
|
1486
|
+
// Interpolate rotation using Bezier
|
|
1487
|
+
const interp = boneFrameB.interpolation
|
|
1488
|
+
const rotWeight = bezierInterpolate(
|
|
1489
|
+
interp[0] / 127, // x1
|
|
1490
|
+
interp[1] / 127, // x2
|
|
1491
|
+
interp[2] / 127, // y1
|
|
1492
|
+
interp[3] / 127, // y2
|
|
1493
|
+
gradient
|
|
1494
|
+
)
|
|
1495
|
+
const interpolatedRotation = Quat.slerp(boneFrameA.rotation, boneFrameB.rotation, rotWeight)
|
|
1496
|
+
|
|
1497
|
+
// Interpolate translation using Bezier (separate curves for X, Y, Z)
|
|
1498
|
+
// VMD interpolation layout (from reference, 4x4 grid, row-major):
|
|
1499
|
+
// Row 0: X_x1, Y_x1, phy1, phy2,
|
|
1500
|
+
// Row 1: X_y1, Y_y1, Z_y1, R_y1,
|
|
1501
|
+
// Row 2: X_x2, Y_x2, Z_x2, R_x2,
|
|
1502
|
+
// Row 3: X_y2, Y_y2, Z_y2, R_y2,
|
|
1503
|
+
// Row 4: Y_x1, Z_x1, R_x1, X_y1,
|
|
1504
|
+
// Row 5: Y_y1, Z_y1, R_y1, X_x2,
|
|
1505
|
+
// Row 6: Y_x2, Z_x2, R_x2, X_y2,
|
|
1506
|
+
// Row 7: Y_y2, Z_y2, R_y2, 00,
|
|
1507
|
+
// Row 8: Z_x1, R_x1, X_y1, Y_y1,
|
|
1508
|
+
// Row 9: Z_y1, R_y1, X_x2, Y_x2,
|
|
1509
|
+
// Row 10: Z_x2, R_x2, X_y2, Y_y2,
|
|
1510
|
+
// Row 11: Z_y2, R_y2, 00, 00,
|
|
1511
|
+
// Row 12: R_x1, X_y1, Y_y1, Z_y1,
|
|
1512
|
+
// Row 13: R_y1, X_x2, Y_x2, Z_x2,
|
|
1513
|
+
// Row 14: R_x2, X_y2, Y_y2, Z_y2,
|
|
1514
|
+
// Row 15: R_y2, 00, 00, 00
|
|
1515
|
+
// For rotation: R_x1=16, R_y1=20, R_x2=24, R_y2=28
|
|
1516
|
+
// For position X: X_x1=0, X_y1=4, X_x2=8, X_y2=12
|
|
1517
|
+
// For position Y: Y_x1=16, Y_y1=20, Y_x2=24, Y_y2=28
|
|
1518
|
+
// For position Z: Z_x1=32, Z_y1=36, Z_x2=40, Z_y2=44
|
|
1519
|
+
const xWeight = bezierInterpolate(
|
|
1520
|
+
interp[0] / 127, // X_x1
|
|
1521
|
+
interp[8] / 127, // X_x2
|
|
1522
|
+
interp[4] / 127, // X_y1
|
|
1523
|
+
interp[12] / 127, // X_y2
|
|
1524
|
+
gradient
|
|
1525
|
+
)
|
|
1526
|
+
const yWeight = bezierInterpolate(
|
|
1527
|
+
interp[16] / 127, // Y_x1
|
|
1528
|
+
interp[24] / 127, // Y_x2
|
|
1529
|
+
interp[20] / 127, // Y_y1
|
|
1530
|
+
interp[28] / 127, // Y_y2
|
|
1531
|
+
gradient
|
|
1532
|
+
)
|
|
1533
|
+
const zWeight = bezierInterpolate(
|
|
1534
|
+
interp[32] / 127, // Z_x1
|
|
1535
|
+
interp[40] / 127, // Z_x2
|
|
1536
|
+
interp[36] / 127, // Z_y1
|
|
1537
|
+
interp[44] / 127, // Z_y2
|
|
1538
|
+
gradient
|
|
1539
|
+
)
|
|
1503
1540
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1541
|
+
const interpolatedTranslation = new Vec3(
|
|
1542
|
+
boneFrameA.translation.x + (boneFrameB.translation.x - boneFrameA.translation.x) * xWeight,
|
|
1543
|
+
boneFrameA.translation.y + (boneFrameB.translation.y - boneFrameA.translation.y) * yWeight,
|
|
1544
|
+
boneFrameA.translation.z + (boneFrameB.translation.z - boneFrameA.translation.z) * zWeight
|
|
1545
|
+
)
|
|
1508
1546
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1547
|
+
boneNamesToRotate.push(boneName)
|
|
1548
|
+
rotationsToApply.push(interpolatedRotation)
|
|
1549
|
+
boneNamesToMove.push(boneName)
|
|
1550
|
+
translationsToApply.push(interpolatedTranslation)
|
|
1511
1551
|
}
|
|
1552
|
+
}
|
|
1512
1553
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1554
|
+
// Helper to find upper bound index for morph frames
|
|
1555
|
+
const upperBoundMorphIndex = (time: number, keyFrames: Array<{ morphFrame: MorphFrame; time: number }>): number => {
|
|
1556
|
+
let left = 0
|
|
1557
|
+
let right = keyFrames.length
|
|
1558
|
+
while (left < right) {
|
|
1559
|
+
const mid = Math.floor((left + right) / 2)
|
|
1560
|
+
if (keyFrames[mid].time <= time) {
|
|
1561
|
+
left = mid + 1
|
|
1562
|
+
} else {
|
|
1563
|
+
right = mid
|
|
1564
|
+
}
|
|
1515
1565
|
}
|
|
1566
|
+
return left
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Process each morph track
|
|
1570
|
+
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
1571
|
+
if (keyFrames.length === 0) continue
|
|
1572
|
+
|
|
1573
|
+
// Clamp frame time to track range
|
|
1574
|
+
const startTime = keyFrames[0].time
|
|
1575
|
+
const endTime = keyFrames[keyFrames.length - 1].time
|
|
1576
|
+
const clampedFrameTime = Math.max(startTime, Math.min(endTime, frameTime))
|
|
1516
1577
|
|
|
1517
|
-
|
|
1578
|
+
const upperBoundIndex = upperBoundMorphIndex(clampedFrameTime, keyFrames)
|
|
1579
|
+
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
1580
|
+
|
|
1581
|
+
if (upperBoundIndexMinusOne < 0) continue
|
|
1582
|
+
|
|
1583
|
+
const timeB = keyFrames[upperBoundIndex]?.time
|
|
1584
|
+
const morphFrameA = keyFrames[upperBoundIndexMinusOne].morphFrame
|
|
1585
|
+
|
|
1586
|
+
if (timeB === undefined) {
|
|
1587
|
+
// Last keyframe or beyond - use the last keyframe value
|
|
1588
|
+
morphNamesToSet.push(morphName)
|
|
1589
|
+
morphWeightsToSet.push(morphFrameA.weight)
|
|
1590
|
+
} else {
|
|
1591
|
+
// Linear interpolation between two keyframes
|
|
1592
|
+
const timeA = keyFrames[upperBoundIndexMinusOne].time
|
|
1593
|
+
const morphFrameB = keyFrames[upperBoundIndex].morphFrame
|
|
1594
|
+
const gradient = (clampedFrameTime - timeA) / (timeB - timeA)
|
|
1595
|
+
const interpolatedWeight = morphFrameA.weight + (morphFrameB.weight - morphFrameA.weight) * gradient
|
|
1596
|
+
|
|
1597
|
+
morphNamesToSet.push(morphName)
|
|
1598
|
+
morphWeightsToSet.push(interpolatedWeight)
|
|
1599
|
+
}
|
|
1518
1600
|
}
|
|
1519
1601
|
|
|
1520
|
-
//
|
|
1521
|
-
|
|
1602
|
+
// Apply all rotations, translations, and morphs at once (no tweening - direct application)
|
|
1603
|
+
if (boneNamesToRotate.length > 0) {
|
|
1604
|
+
this.rotateBones(boneNamesToRotate, rotationsToApply, 0)
|
|
1605
|
+
}
|
|
1606
|
+
if (boneNamesToMove.length > 0) {
|
|
1607
|
+
this.moveBones(boneNamesToMove, translationsToApply, 0)
|
|
1608
|
+
}
|
|
1609
|
+
if (morphNamesToSet.length > 0) {
|
|
1610
|
+
for (let i = 0; i < morphNamesToSet.length; i++) {
|
|
1611
|
+
this.setMorphWeight(morphNamesToSet[i], morphWeightsToSet[i], 0)
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1522
1614
|
}
|
|
1523
1615
|
|
|
1524
1616
|
public getStats(): EngineStats {
|
|
@@ -1552,7 +1644,6 @@ export class Engine {
|
|
|
1552
1644
|
public dispose() {
|
|
1553
1645
|
this.stopRenderLoop()
|
|
1554
1646
|
this.stopAnimation()
|
|
1555
|
-
this.stopBreathing()
|
|
1556
1647
|
if (this.camera) this.camera.detachControl()
|
|
1557
1648
|
if (this.resizeObserver) {
|
|
1558
1649
|
this.resizeObserver.disconnect()
|
|
@@ -1577,6 +1668,26 @@ export class Engine {
|
|
|
1577
1668
|
this.currentModel?.rotateBones(bones, rotations, durationMs)
|
|
1578
1669
|
}
|
|
1579
1670
|
|
|
1671
|
+
// moveBones now takes relative translations (VMD-style) by default
|
|
1672
|
+
public moveBones(bones: string[], relativeTranslations: Vec3[], durationMs?: number) {
|
|
1673
|
+
this.currentModel?.moveBones(bones, relativeTranslations, durationMs)
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
public setMorphWeight(name: string, weight: number, durationMs?: number): void {
|
|
1677
|
+
if (!this.currentModel) return
|
|
1678
|
+
this.currentModel.setMorphWeight(name, weight, durationMs)
|
|
1679
|
+
if (!durationMs || durationMs === 0) {
|
|
1680
|
+
this.vertexBufferNeedsUpdate = true
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
private updateVertexBuffer(): void {
|
|
1685
|
+
if (!this.currentModel || !this.vertexBuffer) return
|
|
1686
|
+
const vertices = this.currentModel.getVertices()
|
|
1687
|
+
if (!vertices || vertices.length === 0) return
|
|
1688
|
+
this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices)
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1580
1691
|
// Step 7: Create vertex, index, and joint buffers
|
|
1581
1692
|
private async setupModelBuffers(model: Model) {
|
|
1582
1693
|
this.currentModel = model
|
|
@@ -1992,14 +2103,45 @@ export class Engine {
|
|
|
1992
2103
|
this.updateCameraUniforms()
|
|
1993
2104
|
this.updateRenderTarget()
|
|
1994
2105
|
|
|
2106
|
+
// Animate VMD animation if playing
|
|
2107
|
+
if (this.playingAnimation && this.currentModel && this.animationDuration > 0) {
|
|
2108
|
+
const elapsedSeconds = (currentTime - this.animationStartTime) / 1000
|
|
2109
|
+
if (elapsedSeconds >= this.animationDuration) {
|
|
2110
|
+
// Animation has ended, stop it
|
|
2111
|
+
this.stopAnimation()
|
|
2112
|
+
} else {
|
|
2113
|
+
const frameTime = elapsedSeconds
|
|
2114
|
+
this.animate(frameTime)
|
|
2115
|
+
}
|
|
2116
|
+
} else if (this.playingAnimation && this.animationDuration <= 0) {
|
|
2117
|
+
// Animation has no duration or invalid, stop it immediately
|
|
2118
|
+
this.stopAnimation()
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// Update model pose first (this may update morph weights via tweens)
|
|
2122
|
+
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
2123
|
+
if (this.currentModel) {
|
|
2124
|
+
const hasActiveMorphTweens = this.currentModel.evaluatePose()
|
|
2125
|
+
if (hasActiveMorphTweens) {
|
|
2126
|
+
this.vertexBufferNeedsUpdate = true
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Update vertex buffer if morphs changed
|
|
2131
|
+
if (this.vertexBufferNeedsUpdate) {
|
|
2132
|
+
this.updateVertexBuffer()
|
|
2133
|
+
this.vertexBufferNeedsUpdate = false
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1995
2136
|
// Use single encoder for both compute and render (reduces sync points)
|
|
1996
2137
|
const encoder = this.device.createCommandEncoder()
|
|
1997
2138
|
|
|
1998
2139
|
this.updateModelPose(deltaTime, encoder)
|
|
1999
2140
|
|
|
2000
|
-
// Hide model if animation is loaded but
|
|
2001
|
-
//
|
|
2002
|
-
|
|
2141
|
+
// Hide model if animation is loaded but hasn't started playing yet (prevents A-pose flash)
|
|
2142
|
+
// Once animation has played (even if it stopped), continue rendering normally
|
|
2143
|
+
// Still update physics and poses, just don't render visually before first play
|
|
2144
|
+
if (this.hasAnimation && !this.playingAnimation && this.animationStartTime === 0) {
|
|
2003
2145
|
// Submit encoder to ensure matrices are uploaded and physics initializes
|
|
2004
2146
|
this.device.queue.submit([encoder.finish()])
|
|
2005
2147
|
return
|
|
@@ -2169,7 +2311,8 @@ export class Engine {
|
|
|
2169
2311
|
}
|
|
2170
2312
|
|
|
2171
2313
|
private updateModelPose(deltaTime: number, encoder: GPUCommandEncoder) {
|
|
2172
|
-
|
|
2314
|
+
// Note: evaluatePose is called earlier in render() to update vertex buffer before encoder creation
|
|
2315
|
+
// Here we just get the matrices and update physics/compute
|
|
2173
2316
|
const worldMats = this.currentModel!.getBoneWorldMatrices()
|
|
2174
2317
|
|
|
2175
2318
|
if (this.physics) {
|