reze-engine 0.1.16 → 0.2.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 +104 -104
- package/dist/engine.d.ts +8 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +112 -13
- package/dist/physics.d.ts +1 -0
- package/dist/physics.d.ts.map +1 -1
- package/dist/physics.js +58 -0
- package/dist/vmd-loader.d.ts +25 -0
- package/dist/vmd-loader.d.ts.map +1 -0
- package/dist/vmd-loader.js +141 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +141 -16
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/physics.ts +752 -680
- package/src/pmx-loader.ts +1054 -1054
- package/src/vmd-loader.ts +179 -0
package/src/engine.ts
CHANGED
|
@@ -3,6 +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 { VMDKeyFrame, VMDLoader } from "./vmd-loader"
|
|
6
7
|
|
|
7
8
|
export interface EngineStats {
|
|
8
9
|
fps: number
|
|
@@ -10,6 +11,13 @@ export interface EngineStats {
|
|
|
10
11
|
gpuMemory: number // MB (estimated total GPU memory)
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
// Internal type for organizing bone keyframes during animation playback
|
|
15
|
+
type BoneKeyFrame = {
|
|
16
|
+
boneName: string
|
|
17
|
+
time: number
|
|
18
|
+
rotation: Quat
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export class Engine {
|
|
14
22
|
private canvas: HTMLCanvasElement
|
|
15
23
|
private device!: GPUDevice
|
|
@@ -68,9 +76,9 @@ export class Engine {
|
|
|
68
76
|
private bloomComposeBindGroup?: GPUBindGroup
|
|
69
77
|
// Bloom settings
|
|
70
78
|
public bloomThreshold: number = 0.3
|
|
71
|
-
public bloomIntensity: number = 0.
|
|
79
|
+
public bloomIntensity: number = 0.12
|
|
72
80
|
// Rim light settings
|
|
73
|
-
private rimLightIntensity: number = 0.
|
|
81
|
+
private rimLightIntensity: number = 0.45
|
|
74
82
|
private rimLightPower: number = 2.0
|
|
75
83
|
private rimLightColor: [number, number, number] = [1.0, 1.0, 1.0]
|
|
76
84
|
private currentModel: Model | null = null
|
|
@@ -94,6 +102,9 @@ export class Engine {
|
|
|
94
102
|
private animationFrameId: number | null = null
|
|
95
103
|
private renderLoopCallback: (() => void) | null = null
|
|
96
104
|
|
|
105
|
+
private animationFrames: VMDKeyFrame[] = []
|
|
106
|
+
private animationTimeouts: number[] = []
|
|
107
|
+
|
|
97
108
|
constructor(canvas: HTMLCanvasElement) {
|
|
98
109
|
this.canvas = canvas
|
|
99
110
|
}
|
|
@@ -1345,14 +1356,14 @@ export class Engine {
|
|
|
1345
1356
|
|
|
1346
1357
|
this.lightCount = 0
|
|
1347
1358
|
|
|
1348
|
-
this.setAmbient(
|
|
1349
|
-
this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.
|
|
1350
|
-
this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.
|
|
1351
|
-
this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.
|
|
1359
|
+
this.setAmbient(1)
|
|
1360
|
+
this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.02)
|
|
1361
|
+
this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.015)
|
|
1362
|
+
this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.01)
|
|
1352
1363
|
this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
|
|
1353
1364
|
}
|
|
1354
1365
|
|
|
1355
|
-
|
|
1366
|
+
private addLight(direction: Vec3, color: Vec3, intensity: number = 1.0): boolean {
|
|
1356
1367
|
if (this.lightCount >= 4) return false
|
|
1357
1368
|
|
|
1358
1369
|
const normalized = direction.normalize()
|
|
@@ -1371,10 +1382,131 @@ export class Engine {
|
|
|
1371
1382
|
return true
|
|
1372
1383
|
}
|
|
1373
1384
|
|
|
1374
|
-
|
|
1385
|
+
private setAmbient(intensity: number) {
|
|
1375
1386
|
this.lightData[0] = intensity
|
|
1376
1387
|
}
|
|
1377
1388
|
|
|
1389
|
+
public async loadAnimation(url: string) {
|
|
1390
|
+
const frames = await VMDLoader.load(url)
|
|
1391
|
+
this.animationFrames = frames
|
|
1392
|
+
console.log(this.animationFrames)
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
public playAnimation() {
|
|
1396
|
+
if (this.animationFrames.length === 0) return
|
|
1397
|
+
|
|
1398
|
+
this.stopAnimation()
|
|
1399
|
+
|
|
1400
|
+
const allBoneKeyFrames: BoneKeyFrame[] = []
|
|
1401
|
+
for (const keyFrame of this.animationFrames) {
|
|
1402
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
1403
|
+
allBoneKeyFrames.push({
|
|
1404
|
+
boneName: boneFrame.boneName,
|
|
1405
|
+
time: keyFrame.time,
|
|
1406
|
+
rotation: boneFrame.rotation,
|
|
1407
|
+
})
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const boneKeyFramesByBone = new Map<string, BoneKeyFrame[]>()
|
|
1412
|
+
for (const boneKeyFrame of allBoneKeyFrames) {
|
|
1413
|
+
if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
|
|
1414
|
+
boneKeyFramesByBone.set(boneKeyFrame.boneName, [])
|
|
1415
|
+
}
|
|
1416
|
+
boneKeyFramesByBone.get(boneKeyFrame.boneName)!.push(boneKeyFrame)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
for (const keyFrames of boneKeyFramesByBone.values()) {
|
|
1420
|
+
keyFrames.sort((a, b) => a.time - b.time)
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const time0Rotations: Array<{ boneName: string; rotation: Quat }> = []
|
|
1424
|
+
const bonesWithTime0 = new Set<string>()
|
|
1425
|
+
for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1426
|
+
if (keyFrames.length > 0 && keyFrames[0].time === 0) {
|
|
1427
|
+
time0Rotations.push({
|
|
1428
|
+
boneName: boneName,
|
|
1429
|
+
rotation: keyFrames[0].rotation,
|
|
1430
|
+
})
|
|
1431
|
+
bonesWithTime0.add(boneName)
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (this.currentModel) {
|
|
1436
|
+
if (time0Rotations.length > 0) {
|
|
1437
|
+
const boneNames = time0Rotations.map((r) => r.boneName)
|
|
1438
|
+
const rotations = time0Rotations.map((r) => r.rotation)
|
|
1439
|
+
this.rotateBones(boneNames, rotations, 0)
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const skeleton = this.currentModel.getSkeleton()
|
|
1443
|
+
const bonesToReset: string[] = []
|
|
1444
|
+
for (const bone of skeleton.bones) {
|
|
1445
|
+
if (!bonesWithTime0.has(bone.name)) {
|
|
1446
|
+
bonesToReset.push(bone.name)
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (bonesToReset.length > 0) {
|
|
1451
|
+
const identityQuat = new Quat(0, 0, 0, 1)
|
|
1452
|
+
const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
|
|
1453
|
+
this.rotateBones(bonesToReset, identityQuats, 0)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
this.currentModel.evaluatePose()
|
|
1457
|
+
|
|
1458
|
+
// Reset physics immediately and upload matrices to prevent A-pose flash
|
|
1459
|
+
if (this.physics) {
|
|
1460
|
+
const worldMats = this.currentModel.getBoneWorldMatrices()
|
|
1461
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
|
|
1462
|
+
|
|
1463
|
+
// Upload matrices immediately so next frame shows correct pose
|
|
1464
|
+
this.device.queue.writeBuffer(
|
|
1465
|
+
this.worldMatrixBuffer!,
|
|
1466
|
+
0,
|
|
1467
|
+
worldMats.buffer,
|
|
1468
|
+
worldMats.byteOffset,
|
|
1469
|
+
worldMats.byteLength
|
|
1470
|
+
)
|
|
1471
|
+
this.computeSkinMatrices()
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
|
|
1475
|
+
for (let i = 0; i < keyFrames.length; i++) {
|
|
1476
|
+
const boneKeyFrame = keyFrames[i]
|
|
1477
|
+
const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null
|
|
1478
|
+
|
|
1479
|
+
if (boneKeyFrame.time === 0) continue
|
|
1480
|
+
|
|
1481
|
+
let durationMs = 0
|
|
1482
|
+
if (i === 0) {
|
|
1483
|
+
durationMs = boneKeyFrame.time * 1000
|
|
1484
|
+
} else if (previousBoneKeyFrame) {
|
|
1485
|
+
durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0
|
|
1489
|
+
const delayMs = scheduleTime * 1000
|
|
1490
|
+
|
|
1491
|
+
if (delayMs <= 0) {
|
|
1492
|
+
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
|
|
1493
|
+
} else {
|
|
1494
|
+
const timeoutId = window.setTimeout(() => {
|
|
1495
|
+
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
|
|
1496
|
+
}, delayMs)
|
|
1497
|
+
this.animationTimeouts.push(timeoutId)
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
public stopAnimation() {
|
|
1504
|
+
for (const timeoutId of this.animationTimeouts) {
|
|
1505
|
+
clearTimeout(timeoutId)
|
|
1506
|
+
}
|
|
1507
|
+
this.animationTimeouts = []
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1378
1510
|
public getStats(): EngineStats {
|
|
1379
1511
|
return { ...this.stats }
|
|
1380
1512
|
}
|
|
@@ -1405,6 +1537,7 @@ export class Engine {
|
|
|
1405
1537
|
|
|
1406
1538
|
public dispose() {
|
|
1407
1539
|
this.stopRenderLoop()
|
|
1540
|
+
this.stopAnimation()
|
|
1408
1541
|
if (this.camera) this.camera.detachControl()
|
|
1409
1542
|
if (this.resizeObserver) {
|
|
1410
1543
|
this.resizeObserver.disconnect()
|
|
@@ -2153,20 +2286,14 @@ export class Engine {
|
|
|
2153
2286
|
}
|
|
2154
2287
|
}
|
|
2155
2288
|
|
|
2156
|
-
// Update model pose and physics
|
|
2157
2289
|
private updateModelPose(deltaTime: number) {
|
|
2158
|
-
// Step 1: Animation evaluation (computes matrices to CPU memory, no upload yet)
|
|
2159
2290
|
this.currentModel!.evaluatePose()
|
|
2160
|
-
|
|
2161
|
-
// Step 2: Get world matrices (still in CPU memory)
|
|
2162
2291
|
const worldMats = this.currentModel!.getBoneWorldMatrices()
|
|
2163
2292
|
|
|
2164
|
-
// Step 3: Physics modifies matrices in-place
|
|
2165
2293
|
if (this.physics) {
|
|
2166
2294
|
this.physics.step(deltaTime, worldMats, this.currentModel!.getBoneInverseBindMatrices())
|
|
2167
2295
|
}
|
|
2168
2296
|
|
|
2169
|
-
// Step 4: Upload ONCE with final result (animation + physics)
|
|
2170
2297
|
this.device.queue.writeBuffer(
|
|
2171
2298
|
this.worldMatrixBuffer!,
|
|
2172
2299
|
0,
|
|
@@ -2174,8 +2301,6 @@ export class Engine {
|
|
|
2174
2301
|
worldMats.byteOffset,
|
|
2175
2302
|
worldMats.byteLength
|
|
2176
2303
|
)
|
|
2177
|
-
|
|
2178
|
-
// Step 5: GPU skinning
|
|
2179
2304
|
this.computeSkinMatrices()
|
|
2180
2305
|
}
|
|
2181
2306
|
|