reze-engine 0.2.19 → 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 +7 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +252 -143
- 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 +46 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +201 -3
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +57 -36
- 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 +275 -164
- package/src/ik-solver.ts +488 -0
- package/src/math.ts +555 -546
- package/src/model.ts +284 -3
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1173 -1145
- 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
|
|
@@ -144,8 +138,10 @@ export class Engine {
|
|
|
144
138
|
private animationTimeouts: number[] = []
|
|
145
139
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
146
140
|
private playingAnimation = false // Set to true when playAnimation is called
|
|
147
|
-
private
|
|
148
|
-
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()
|
|
149
145
|
|
|
150
146
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
151
147
|
this.canvas = canvas
|
|
@@ -1283,75 +1279,100 @@ export class Engine {
|
|
|
1283
1279
|
this.hasAnimation = true
|
|
1284
1280
|
}
|
|
1285
1281
|
|
|
1286
|
-
public playAnimation(
|
|
1287
|
-
breathBones?: string[] | Record<string, number> // Array of bone names or map of bone name -> rotation range
|
|
1288
|
-
breathDuration?: number // Breathing cycle duration in milliseconds
|
|
1289
|
-
}) {
|
|
1282
|
+
public playAnimation() {
|
|
1290
1283
|
if (this.animationFrames.length === 0) return
|
|
1291
1284
|
|
|
1292
1285
|
this.stopAnimation()
|
|
1293
|
-
|
|
1286
|
+
|
|
1294
1287
|
this.playingAnimation = true
|
|
1295
1288
|
|
|
1296
|
-
//
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
+
}
|
|
1300
1299
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
breathBones = Object.keys(options.breathBones)
|
|
1306
|
-
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, [])
|
|
1307
1304
|
}
|
|
1305
|
+
boneKeyFramesByBone.get(boneFrame.boneName)!.push({ boneFrame, time })
|
|
1308
1306
|
}
|
|
1309
1307
|
|
|
1310
|
-
const
|
|
1308
|
+
for (const keyFrames of boneKeyFramesByBone.values()) {
|
|
1309
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
1310
|
+
}
|
|
1311
1311
|
|
|
1312
|
-
|
|
1312
|
+
// Process morph frames
|
|
1313
|
+
const allMorphKeyFrames: Array<{ morphFrame: MorphFrame; time: number }> = []
|
|
1313
1314
|
for (const keyFrame of this.animationFrames) {
|
|
1314
|
-
for (const
|
|
1315
|
-
|
|
1316
|
-
|
|
1315
|
+
for (const morphFrame of keyFrame.morphFrames) {
|
|
1316
|
+
allMorphKeyFrames.push({
|
|
1317
|
+
morphFrame,
|
|
1317
1318
|
time: keyFrame.time,
|
|
1318
|
-
rotation: boneFrame.rotation,
|
|
1319
1319
|
})
|
|
1320
1320
|
}
|
|
1321
1321
|
}
|
|
1322
1322
|
|
|
1323
|
-
const
|
|
1324
|
-
for (const
|
|
1325
|
-
if (!
|
|
1326
|
-
|
|
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, [])
|
|
1327
1327
|
}
|
|
1328
|
-
|
|
1328
|
+
morphKeyFramesByMorph.get(morphFrame.morphName)!.push({ morphFrame, time })
|
|
1329
1329
|
}
|
|
1330
1330
|
|
|
1331
|
-
for (const keyFrames of
|
|
1331
|
+
for (const keyFrames of morphKeyFramesByMorph.values()) {
|
|
1332
1332
|
keyFrames.sort((a, b) => a.time - b.time)
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
+
}
|
|
1344
1355
|
}
|
|
1345
1356
|
}
|
|
1357
|
+
this.animationDuration = maxFrameTime > 0 ? maxFrameTime : 0
|
|
1358
|
+
this.animationStartTime = performance.now()
|
|
1346
1359
|
|
|
1360
|
+
// Initialize bones and morphs to time 0 pose
|
|
1347
1361
|
if (this.currentModel) {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
+
}
|
|
1352
1373
|
}
|
|
1353
1374
|
|
|
1354
|
-
|
|
1375
|
+
// Reset bones without time 0 keyframes
|
|
1355
1376
|
const bonesToReset: string[] = []
|
|
1356
1377
|
for (const bone of skeleton.bones) {
|
|
1357
1378
|
if (!bonesWithTime0.has(bone.name)) {
|
|
@@ -1365,6 +1386,14 @@ export class Engine {
|
|
|
1365
1386
|
this.rotateBones(bonesToReset, identityQuats, 0)
|
|
1366
1387
|
}
|
|
1367
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
|
+
|
|
1368
1397
|
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1369
1398
|
if (this.physics) {
|
|
1370
1399
|
this.currentModel.evaluatePose()
|
|
@@ -1385,70 +1414,6 @@ export class Engine {
|
|
|
1385
1414
|
this.device.queue.submit([encoder.finish()])
|
|
1386
1415
|
}
|
|
1387
1416
|
}
|
|
1388
|
-
for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1389
|
-
for (let i = 0; i < keyFrames.length; i++) {
|
|
1390
|
-
const boneKeyFrame = keyFrames[i]
|
|
1391
|
-
const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null
|
|
1392
|
-
|
|
1393
|
-
if (boneKeyFrame.time === 0) continue
|
|
1394
|
-
|
|
1395
|
-
let durationMs = 0
|
|
1396
|
-
if (i === 0) {
|
|
1397
|
-
durationMs = boneKeyFrame.time * 1000
|
|
1398
|
-
} else if (previousBoneKeyFrame) {
|
|
1399
|
-
durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0
|
|
1403
|
-
const delayMs = scheduleTime * 1000
|
|
1404
|
-
|
|
1405
|
-
if (delayMs <= 0) {
|
|
1406
|
-
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
|
|
1407
|
-
} else {
|
|
1408
|
-
const timeoutId = window.setTimeout(() => {
|
|
1409
|
-
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
|
|
1410
|
-
}, delayMs)
|
|
1411
|
-
this.animationTimeouts.push(timeoutId)
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
// Setup breathing animation if enabled
|
|
1417
|
-
if (enableBreath && this.currentModel) {
|
|
1418
|
-
// Find the last frame time
|
|
1419
|
-
let maxTime = 0
|
|
1420
|
-
for (const keyFrame of this.animationFrames) {
|
|
1421
|
-
if (keyFrame.time > maxTime) {
|
|
1422
|
-
maxTime = keyFrame.time
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Get last frame rotations directly from animation data for breathing bones
|
|
1427
|
-
const lastFrameRotations = new Map<string, Quat>()
|
|
1428
|
-
for (const bone of breathBones) {
|
|
1429
|
-
const keyFrames = boneKeyFramesByBone.get(bone)
|
|
1430
|
-
if (keyFrames && keyFrames.length > 0) {
|
|
1431
|
-
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1432
|
-
let lastRotation: Quat | null = null
|
|
1433
|
-
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1434
|
-
if (keyFrames[i].time <= maxTime) {
|
|
1435
|
-
lastRotation = keyFrames[i].rotation
|
|
1436
|
-
break
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
if (lastRotation) {
|
|
1440
|
-
lastFrameRotations.set(bone, lastRotation)
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// Start breathing after animation completes
|
|
1446
|
-
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1447
|
-
const animationEndTime = maxTime * 1000 + 200 // Small buffer for final tweens to complete
|
|
1448
|
-
this.breathingTimeout = window.setTimeout(() => {
|
|
1449
|
-
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration)
|
|
1450
|
-
}, animationEndTime)
|
|
1451
|
-
}
|
|
1452
1417
|
}
|
|
1453
1418
|
|
|
1454
1419
|
public stopAnimation() {
|
|
@@ -1457,69 +1422,195 @@ export class Engine {
|
|
|
1457
1422
|
}
|
|
1458
1423
|
this.animationTimeouts = []
|
|
1459
1424
|
this.playingAnimation = false
|
|
1425
|
+
this.boneTracks.clear()
|
|
1426
|
+
this.morphTracks.clear()
|
|
1460
1427
|
}
|
|
1461
1428
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
this.breathingBaseRotations.clear()
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
private startBreathing(
|
|
1471
|
-
bones: string[],
|
|
1472
|
-
baseRotations: Map<string, Quat>,
|
|
1473
|
-
rotationRanges?: Record<string, number>,
|
|
1474
|
-
durationMs: number = 4000
|
|
1475
|
-
) {
|
|
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 {
|
|
1476
1433
|
if (!this.currentModel) return
|
|
1477
1434
|
|
|
1478
|
-
//
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
+
}
|
|
1484
1446
|
}
|
|
1447
|
+
return left
|
|
1485
1448
|
}
|
|
1486
1449
|
|
|
1487
|
-
const
|
|
1488
|
-
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))
|
|
1489
1465
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const animate = (isInhale: boolean) => {
|
|
1493
|
-
if (!this.currentModel) return
|
|
1466
|
+
const upperBoundIndex = upperBoundFrameIndex(clampedFrameTime, keyFrames)
|
|
1467
|
+
const upperBoundIndexMinusOne = upperBoundIndex - 1
|
|
1494
1468
|
|
|
1495
|
-
|
|
1496
|
-
const breathingQuats: Quat[] = []
|
|
1469
|
+
if (upperBoundIndexMinusOne < 0) continue
|
|
1497
1470
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
if (!baseRot) continue
|
|
1471
|
+
const timeB = keyFrames[upperBoundIndex]?.time
|
|
1472
|
+
const boneFrameA = keyFrames[upperBoundIndexMinusOne].boneFrame
|
|
1501
1473
|
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
+
)
|
|
1504
1540
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
+
)
|
|
1509
1546
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1547
|
+
boneNamesToRotate.push(boneName)
|
|
1548
|
+
rotationsToApply.push(interpolatedRotation)
|
|
1549
|
+
boneNamesToMove.push(boneName)
|
|
1550
|
+
translationsToApply.push(interpolatedTranslation)
|
|
1512
1551
|
}
|
|
1552
|
+
}
|
|
1513
1553
|
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
+
}
|
|
1516
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))
|
|
1517
1577
|
|
|
1518
|
-
|
|
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
|
+
}
|
|
1519
1600
|
}
|
|
1520
1601
|
|
|
1521
|
-
//
|
|
1522
|
-
|
|
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
|
+
}
|
|
1523
1614
|
}
|
|
1524
1615
|
|
|
1525
1616
|
public getStats(): EngineStats {
|
|
@@ -1553,7 +1644,6 @@ export class Engine {
|
|
|
1553
1644
|
public dispose() {
|
|
1554
1645
|
this.stopRenderLoop()
|
|
1555
1646
|
this.stopAnimation()
|
|
1556
|
-
this.stopBreathing()
|
|
1557
1647
|
if (this.camera) this.camera.detachControl()
|
|
1558
1648
|
if (this.resizeObserver) {
|
|
1559
1649
|
this.resizeObserver.disconnect()
|
|
@@ -1578,6 +1668,11 @@ export class Engine {
|
|
|
1578
1668
|
this.currentModel?.rotateBones(bones, rotations, durationMs)
|
|
1579
1669
|
}
|
|
1580
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
|
+
|
|
1581
1676
|
public setMorphWeight(name: string, weight: number, durationMs?: number): void {
|
|
1582
1677
|
if (!this.currentModel) return
|
|
1583
1678
|
this.currentModel.setMorphWeight(name, weight, durationMs)
|
|
@@ -2008,6 +2103,21 @@ export class Engine {
|
|
|
2008
2103
|
this.updateCameraUniforms()
|
|
2009
2104
|
this.updateRenderTarget()
|
|
2010
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
|
+
|
|
2011
2121
|
// Update model pose first (this may update morph weights via tweens)
|
|
2012
2122
|
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
2013
2123
|
if (this.currentModel) {
|
|
@@ -2028,9 +2138,10 @@ export class Engine {
|
|
|
2028
2138
|
|
|
2029
2139
|
this.updateModelPose(deltaTime, encoder)
|
|
2030
2140
|
|
|
2031
|
-
// Hide model if animation is loaded but
|
|
2032
|
-
//
|
|
2033
|
-
|
|
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) {
|
|
2034
2145
|
// Submit encoder to ensure matrices are uploaded and physics initializes
|
|
2035
2146
|
this.device.queue.submit([encoder.finish()])
|
|
2036
2147
|
return
|