reze-engine 0.2.19 → 0.3.1
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 +12 -13
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +107 -175
- 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/player.d.ts +100 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +409 -0
- 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 +123 -194
- 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/player.ts +490 -0
- package/src/pmx-loader.ts +1173 -1145
- package/src/vmd-loader.ts +276 -179
package/src/engine.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Quat, Vec3 } from "./math"
|
|
|
3
3
|
import { Model } from "./model"
|
|
4
4
|
import { PmxLoader } from "./pmx-loader"
|
|
5
5
|
import { Physics } from "./physics"
|
|
6
|
-
import {
|
|
6
|
+
import { AnimationPose, Player } from "./player"
|
|
7
7
|
|
|
8
8
|
export type EngineOptions = {
|
|
9
9
|
ambientColor?: Vec3
|
|
@@ -24,12 +24,6 @@ interface DrawCall {
|
|
|
24
24
|
bindGroup: GPUBindGroup
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
type BoneKeyFrame = {
|
|
28
|
-
boneName: string
|
|
29
|
-
time: number
|
|
30
|
-
rotation: Quat
|
|
31
|
-
}
|
|
32
|
-
|
|
33
27
|
export class Engine {
|
|
34
28
|
private canvas: HTMLCanvasElement
|
|
35
29
|
private device!: GPUDevice
|
|
@@ -79,7 +73,6 @@ export class Engine {
|
|
|
79
73
|
private static readonly DEFAULT_RIM_LIGHT_INTENSITY = 0.45
|
|
80
74
|
private static readonly DEFAULT_CAMERA_DISTANCE = 26.6
|
|
81
75
|
private static readonly DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0)
|
|
82
|
-
private static readonly HAIR_OVER_EYES_ALPHA = 0.5
|
|
83
76
|
private static readonly TRANSPARENCY_EPSILON = 0.001
|
|
84
77
|
private static readonly STATS_FPS_UPDATE_INTERVAL_MS = 1000
|
|
85
78
|
private static readonly STATS_FRAME_TIME_ROUNDING = 100
|
|
@@ -140,12 +133,9 @@ export class Engine {
|
|
|
140
133
|
private animationFrameId: number | null = null
|
|
141
134
|
private renderLoopCallback: (() => void) | null = null
|
|
142
135
|
|
|
143
|
-
private
|
|
144
|
-
private animationTimeouts: number[] = []
|
|
136
|
+
private player: Player = new Player()
|
|
145
137
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
146
|
-
private
|
|
147
|
-
private breathingTimeout: number | null = null
|
|
148
|
-
private breathingBaseRotations: Map<string, Quat> = new Map()
|
|
138
|
+
private animationStartTime: number = 0 // Track when animation first started (for A-pose prevention)
|
|
149
139
|
|
|
150
140
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
151
141
|
this.canvas = canvas
|
|
@@ -1277,84 +1267,71 @@ export class Engine {
|
|
|
1277
1267
|
this.lightData[3] = 0.0 // Padding for vec3f alignment
|
|
1278
1268
|
}
|
|
1279
1269
|
|
|
1280
|
-
public async loadAnimation(url: string) {
|
|
1281
|
-
|
|
1282
|
-
this.animationFrames = frames
|
|
1270
|
+
public async loadAnimation(url: string, audioUrl?: string) {
|
|
1271
|
+
await this.player.loadVmd(url, audioUrl)
|
|
1283
1272
|
this.hasAnimation = true
|
|
1284
|
-
}
|
|
1285
1273
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (this.animationFrames.length === 0) return
|
|
1291
|
-
|
|
1292
|
-
this.stopAnimation()
|
|
1293
|
-
this.stopBreathing()
|
|
1294
|
-
this.playingAnimation = true
|
|
1274
|
+
// Show first frame (time 0) immediately
|
|
1275
|
+
if (this.currentModel) {
|
|
1276
|
+
const initialPose = this.player.getPoseAtTime(0)
|
|
1277
|
+
this.applyPose(initialPose)
|
|
1295
1278
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1279
|
+
// Reset bones without time 0 keyframes
|
|
1280
|
+
const skeleton = this.currentModel.getSkeleton()
|
|
1281
|
+
const bonesWithPose = new Set(initialPose.boneRotations.keys())
|
|
1282
|
+
const bonesToReset: string[] = []
|
|
1283
|
+
for (const bone of skeleton.bones) {
|
|
1284
|
+
if (!bonesWithPose.has(bone.name)) {
|
|
1285
|
+
bonesToReset.push(bone.name)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1300
1288
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
breathBones = Object.keys(options.breathBones)
|
|
1306
|
-
breathRotationRanges = options.breathBones
|
|
1289
|
+
if (bonesToReset.length > 0) {
|
|
1290
|
+
const identityQuat = new Quat(0, 0, 0, 1)
|
|
1291
|
+
const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
|
|
1292
|
+
this.rotateBones(bonesToReset, identityQuats, 0)
|
|
1307
1293
|
}
|
|
1308
|
-
}
|
|
1309
1294
|
|
|
1310
|
-
|
|
1295
|
+
// Update model pose and physics
|
|
1296
|
+
this.currentModel.evaluatePose()
|
|
1311
1297
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
allBoneKeyFrames.push({
|
|
1316
|
-
boneName: boneFrame.boneName,
|
|
1317
|
-
time: keyFrame.time,
|
|
1318
|
-
rotation: boneFrame.rotation,
|
|
1319
|
-
})
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1298
|
+
if (this.physics) {
|
|
1299
|
+
const worldMats = this.currentModel.getBoneWorldMatrices()
|
|
1300
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
|
|
1322
1301
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1302
|
+
// Upload matrices immediately
|
|
1303
|
+
this.device.queue.writeBuffer(
|
|
1304
|
+
this.worldMatrixBuffer!,
|
|
1305
|
+
0,
|
|
1306
|
+
worldMats.buffer,
|
|
1307
|
+
worldMats.byteOffset,
|
|
1308
|
+
worldMats.byteLength
|
|
1309
|
+
)
|
|
1310
|
+
const encoder = this.device.createCommandEncoder()
|
|
1311
|
+
this.computeSkinMatrices(encoder)
|
|
1312
|
+
this.device.queue.submit([encoder.finish()])
|
|
1327
1313
|
}
|
|
1328
|
-
boneKeyFramesByBone.get(boneKeyFrame.boneName)!.push(boneKeyFrame)
|
|
1329
1314
|
}
|
|
1315
|
+
}
|
|
1330
1316
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1317
|
+
public playAnimation() {
|
|
1318
|
+
if (!this.hasAnimation || !this.currentModel) return
|
|
1334
1319
|
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1338
|
-
if (keyFrames.length > 0 && keyFrames[0].time === 0) {
|
|
1339
|
-
time0Rotations.push({
|
|
1340
|
-
boneName: boneName,
|
|
1341
|
-
rotation: keyFrames[0].rotation,
|
|
1342
|
-
})
|
|
1343
|
-
bonesWithTime0.add(boneName)
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1320
|
+
const wasPaused = this.player.isPausedState()
|
|
1321
|
+
const wasPlaying = this.player.isPlayingState()
|
|
1346
1322
|
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1323
|
+
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1324
|
+
if (!wasPlaying && !wasPaused) {
|
|
1325
|
+
// Get initial pose at time 0
|
|
1326
|
+
const initialPose = this.player.getPoseAtTime(0)
|
|
1327
|
+
this.applyPose(initialPose)
|
|
1353
1328
|
|
|
1329
|
+
// Reset bones without time 0 keyframes
|
|
1354
1330
|
const skeleton = this.currentModel.getSkeleton()
|
|
1331
|
+
const bonesWithPose = new Set(initialPose.boneRotations.keys())
|
|
1355
1332
|
const bonesToReset: string[] = []
|
|
1356
1333
|
for (const bone of skeleton.bones) {
|
|
1357
|
-
if (!
|
|
1334
|
+
if (!bonesWithPose.has(bone.name)) {
|
|
1358
1335
|
bonesToReset.push(bone.name)
|
|
1359
1336
|
}
|
|
1360
1337
|
}
|
|
@@ -1385,141 +1362,80 @@ export class Engine {
|
|
|
1385
1362
|
this.device.queue.submit([encoder.finish()])
|
|
1386
1363
|
}
|
|
1387
1364
|
}
|
|
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
1365
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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)
|
|
1366
|
+
// Start playback (or resume if paused)
|
|
1367
|
+
this.player.play()
|
|
1368
|
+
if (this.animationStartTime === 0) {
|
|
1369
|
+
this.animationStartTime = performance.now()
|
|
1451
1370
|
}
|
|
1452
1371
|
}
|
|
1453
1372
|
|
|
1454
1373
|
public stopAnimation() {
|
|
1455
|
-
|
|
1456
|
-
clearTimeout(timeoutId)
|
|
1457
|
-
}
|
|
1458
|
-
this.animationTimeouts = []
|
|
1459
|
-
this.playingAnimation = false
|
|
1374
|
+
this.player.stop()
|
|
1460
1375
|
}
|
|
1461
1376
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
clearTimeout(this.breathingTimeout)
|
|
1465
|
-
this.breathingTimeout = null
|
|
1466
|
-
}
|
|
1467
|
-
this.breathingBaseRotations.clear()
|
|
1377
|
+
public pauseAnimation() {
|
|
1378
|
+
this.player.pause()
|
|
1468
1379
|
}
|
|
1469
1380
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
baseRotations: Map<string, Quat>,
|
|
1473
|
-
rotationRanges?: Record<string, number>,
|
|
1474
|
-
durationMs: number = 4000
|
|
1475
|
-
) {
|
|
1476
|
-
if (!this.currentModel) return
|
|
1381
|
+
public seekAnimation(time: number) {
|
|
1382
|
+
if (!this.currentModel || !this.hasAnimation) return
|
|
1477
1383
|
|
|
1478
|
-
|
|
1479
|
-
// These are the exact rotations from the animation - use them as-is
|
|
1480
|
-
for (const bone of bones) {
|
|
1481
|
-
const baseRot = baseRotations.get(bone)
|
|
1482
|
-
if (baseRot) {
|
|
1483
|
-
this.breathingBaseRotations.set(bone, baseRot)
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
const halfCycleMs = durationMs / 2
|
|
1488
|
-
const defaultRotation = 0.02 // Default rotation range if not specified per bone
|
|
1489
|
-
|
|
1490
|
-
// Start breathing cycle - oscillate around exact base rotation (final pose)
|
|
1491
|
-
// Each bone can have its own rotation range, or use default
|
|
1492
|
-
const animate = (isInhale: boolean) => {
|
|
1493
|
-
if (!this.currentModel) return
|
|
1384
|
+
this.player.seek(time)
|
|
1494
1385
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1386
|
+
// Immediately apply pose at seeked time
|
|
1387
|
+
const pose = this.player.getPoseAtTime(time)
|
|
1388
|
+
this.applyPose(pose)
|
|
1497
1389
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
if (!baseRot) continue
|
|
1390
|
+
// Update model pose and physics
|
|
1391
|
+
this.currentModel.evaluatePose()
|
|
1501
1392
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1393
|
+
if (this.physics) {
|
|
1394
|
+
const worldMats = this.currentModel.getBoneWorldMatrices()
|
|
1395
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
|
|
1396
|
+
|
|
1397
|
+
// Upload matrices immediately
|
|
1398
|
+
this.device.queue.writeBuffer(
|
|
1399
|
+
this.worldMatrixBuffer!,
|
|
1400
|
+
0,
|
|
1401
|
+
worldMats.buffer,
|
|
1402
|
+
worldMats.byteOffset,
|
|
1403
|
+
worldMats.byteLength
|
|
1404
|
+
)
|
|
1405
|
+
const encoder = this.device.createCommandEncoder()
|
|
1406
|
+
this.computeSkinMatrices(encoder)
|
|
1407
|
+
this.device.queue.submit([encoder.finish()])
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1504
1410
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const finalRot = baseRot.multiply(oscillationRot)
|
|
1411
|
+
public getAnimationProgress() {
|
|
1412
|
+
return this.player.getProgress()
|
|
1413
|
+
}
|
|
1509
1414
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1415
|
+
/**
|
|
1416
|
+
* Apply animation pose to model
|
|
1417
|
+
*/
|
|
1418
|
+
private applyPose(pose: AnimationPose): void {
|
|
1419
|
+
if (!this.currentModel) return
|
|
1513
1420
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1421
|
+
// Apply bone rotations
|
|
1422
|
+
if (pose.boneRotations.size > 0) {
|
|
1423
|
+
const boneNames = Array.from(pose.boneRotations.keys())
|
|
1424
|
+
const rotations = Array.from(pose.boneRotations.values())
|
|
1425
|
+
this.rotateBones(boneNames, rotations, 0)
|
|
1426
|
+
}
|
|
1517
1427
|
|
|
1518
|
-
|
|
1428
|
+
// Apply bone translations
|
|
1429
|
+
if (pose.boneTranslations.size > 0) {
|
|
1430
|
+
const boneNames = Array.from(pose.boneTranslations.keys())
|
|
1431
|
+
const translations = Array.from(pose.boneTranslations.values())
|
|
1432
|
+
this.moveBones(boneNames, translations, 0)
|
|
1519
1433
|
}
|
|
1520
1434
|
|
|
1521
|
-
//
|
|
1522
|
-
|
|
1435
|
+
// Apply morph weights
|
|
1436
|
+
for (const [morphName, weight] of pose.morphWeights.entries()) {
|
|
1437
|
+
this.setMorphWeight(morphName, weight, 0)
|
|
1438
|
+
}
|
|
1523
1439
|
}
|
|
1524
1440
|
|
|
1525
1441
|
public getStats(): EngineStats {
|
|
@@ -1553,7 +1469,6 @@ export class Engine {
|
|
|
1553
1469
|
public dispose() {
|
|
1554
1470
|
this.stopRenderLoop()
|
|
1555
1471
|
this.stopAnimation()
|
|
1556
|
-
this.stopBreathing()
|
|
1557
1472
|
if (this.camera) this.camera.detachControl()
|
|
1558
1473
|
if (this.resizeObserver) {
|
|
1559
1474
|
this.resizeObserver.disconnect()
|
|
@@ -1578,6 +1493,11 @@ export class Engine {
|
|
|
1578
1493
|
this.currentModel?.rotateBones(bones, rotations, durationMs)
|
|
1579
1494
|
}
|
|
1580
1495
|
|
|
1496
|
+
// moveBones now takes relative translations (VMD-style) by default
|
|
1497
|
+
public moveBones(bones: string[], relativeTranslations: Vec3[], durationMs?: number) {
|
|
1498
|
+
this.currentModel?.moveBones(bones, relativeTranslations, durationMs)
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1581
1501
|
public setMorphWeight(name: string, weight: number, durationMs?: number): void {
|
|
1582
1502
|
if (!this.currentModel) return
|
|
1583
1503
|
this.currentModel.setMorphWeight(name, weight, durationMs)
|
|
@@ -2008,6 +1928,14 @@ export class Engine {
|
|
|
2008
1928
|
this.updateCameraUniforms()
|
|
2009
1929
|
this.updateRenderTarget()
|
|
2010
1930
|
|
|
1931
|
+
// Animate VMD animation if playing
|
|
1932
|
+
if (this.hasAnimation && this.currentModel) {
|
|
1933
|
+
const pose = this.player.update(currentTime)
|
|
1934
|
+
if (pose) {
|
|
1935
|
+
this.applyPose(pose)
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
2011
1939
|
// Update model pose first (this may update morph weights via tweens)
|
|
2012
1940
|
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
2013
1941
|
if (this.currentModel) {
|
|
@@ -2028,9 +1956,10 @@ export class Engine {
|
|
|
2028
1956
|
|
|
2029
1957
|
this.updateModelPose(deltaTime, encoder)
|
|
2030
1958
|
|
|
2031
|
-
// Hide model if animation is loaded but
|
|
2032
|
-
//
|
|
2033
|
-
|
|
1959
|
+
// Hide model if animation is loaded but hasn't started playing yet (prevents A-pose flash)
|
|
1960
|
+
// Once animation has played (even if it stopped), continue rendering normally
|
|
1961
|
+
// Still update physics and poses, just don't render visually before first play
|
|
1962
|
+
if (this.hasAnimation && !this.player.isPlayingState() && this.animationStartTime === 0) {
|
|
2034
1963
|
// Submit encoder to ensure matrices are uploaded and physics initializes
|
|
2035
1964
|
this.device.queue.submit([encoder.finish()])
|
|
2036
1965
|
return
|