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/README.md
CHANGED
|
@@ -1,66 +1,67 @@
|
|
|
1
|
-
# Reze Engine
|
|
2
|
-
|
|
3
|
-
A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- Physics
|
|
8
|
-
- Alpha blending
|
|
9
|
-
- Post alpha eye rendering
|
|
10
|
-
- Rim lighting
|
|
11
|
-
- Bloom
|
|
12
|
-
- Outlines
|
|
13
|
-
- MSAA 4x anti-aliasing
|
|
14
|
-
- GPU-accelerated skinning
|
|
15
|
-
- Bone and morph api
|
|
16
|
-
- VMD animation
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await engine.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- **[
|
|
60
|
-
- **[
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
# Reze Engine
|
|
2
|
+
|
|
3
|
+
A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Physics
|
|
8
|
+
- Alpha blending
|
|
9
|
+
- Post alpha eye rendering
|
|
10
|
+
- Rim lighting
|
|
11
|
+
- Bloom
|
|
12
|
+
- Outlines
|
|
13
|
+
- MSAA 4x anti-aliasing
|
|
14
|
+
- GPU-accelerated skinning
|
|
15
|
+
- Bone and morph api
|
|
16
|
+
- VMD animation
|
|
17
|
+
- Ik solver
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
export default function Scene() {
|
|
23
|
+
const canvasRef = useRef < HTMLCanvasElement > null
|
|
24
|
+
const engineRef = useRef < Engine > null
|
|
25
|
+
|
|
26
|
+
const initEngine = useCallback(async () => {
|
|
27
|
+
if (canvasRef.current) {
|
|
28
|
+
try {
|
|
29
|
+
const engine = new Engine(canvasRef.current)
|
|
30
|
+
engineRef.current = engine
|
|
31
|
+
await engine.init()
|
|
32
|
+
await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
|
|
33
|
+
|
|
34
|
+
engine.runRenderLoop(() => {})
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
void (async () => {
|
|
43
|
+
initEngine()
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
if (engineRef.current) {
|
|
48
|
+
engineRef.current.dispose()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, [initEngine])
|
|
52
|
+
|
|
53
|
+
return <canvas ref={canvasRef} className="w-full h-full" />
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Projects Using This Engine
|
|
58
|
+
|
|
59
|
+
- **[MiKaPo](https://mikapo.vercel.app)** - Online real-time motion capture for MMD using webcam and MediaPipe
|
|
60
|
+
- **[Popo](https://popo.love)** - Fine-tuned LLM that generates MMD poses from natural language descriptions
|
|
61
|
+
- **[MPL](https://mmd-mpl.vercel.app)** - Semantic motion programming language for scripting MMD animations with intuitive syntax
|
|
62
|
+
|
|
63
|
+
## Tutorial
|
|
64
|
+
|
|
65
|
+
Learn WebGPU from scratch by building an anime character renderer in incremental steps. The tutorial covers the complete rendering pipeline from a simple triangle to fully textured, skeletal-animated characters.
|
|
66
|
+
|
|
67
|
+
[How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bezier interpolation for VMD animations
|
|
3
|
+
* Based on the reference implementation from babylon-mmd
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Bezier interpolation function
|
|
7
|
+
* @param x1 First control point X (0-127, normalized to 0-1)
|
|
8
|
+
* @param x2 Second control point X (0-127, normalized to 0-1)
|
|
9
|
+
* @param y1 First control point Y (0-127, normalized to 0-1)
|
|
10
|
+
* @param y2 Second control point Y (0-127, normalized to 0-1)
|
|
11
|
+
* @param t Interpolation parameter (0-1)
|
|
12
|
+
* @returns Interpolated value (0-1)
|
|
13
|
+
*/
|
|
14
|
+
export declare function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number;
|
|
15
|
+
//# sourceMappingURL=bezier-interpolate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bezier-interpolate.d.ts","sourceRoot":"","sources":["../src/bezier-interpolate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgCnG"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bezier interpolation for VMD animations
|
|
3
|
+
* Based on the reference implementation from babylon-mmd
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Bezier interpolation function
|
|
7
|
+
* @param x1 First control point X (0-127, normalized to 0-1)
|
|
8
|
+
* @param x2 Second control point X (0-127, normalized to 0-1)
|
|
9
|
+
* @param y1 First control point Y (0-127, normalized to 0-1)
|
|
10
|
+
* @param y2 Second control point Y (0-127, normalized to 0-1)
|
|
11
|
+
* @param t Interpolation parameter (0-1)
|
|
12
|
+
* @returns Interpolated value (0-1)
|
|
13
|
+
*/
|
|
14
|
+
export function bezierInterpolate(x1, x2, y1, y2, t) {
|
|
15
|
+
// Clamp t to [0, 1]
|
|
16
|
+
t = Math.max(0, Math.min(1, t));
|
|
17
|
+
// Binary search for the t value that gives us the desired x
|
|
18
|
+
// We're solving for t in the Bezier curve: x(t) = 3*(1-t)^2*t*x1 + 3*(1-t)*t^2*x2 + t^3
|
|
19
|
+
let start = 0;
|
|
20
|
+
let end = 1;
|
|
21
|
+
let mid = 0.5;
|
|
22
|
+
// Iterate until we find the t value that gives us the desired x
|
|
23
|
+
for (let i = 0; i < 15; i++) {
|
|
24
|
+
// Evaluate Bezier curve at mid point
|
|
25
|
+
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid;
|
|
26
|
+
if (Math.abs(x - t) < 0.0001) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (x < t) {
|
|
30
|
+
start = mid;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
end = mid;
|
|
34
|
+
}
|
|
35
|
+
mid = (start + end) / 2;
|
|
36
|
+
}
|
|
37
|
+
// Now evaluate the y value at this t
|
|
38
|
+
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid;
|
|
39
|
+
return y;
|
|
40
|
+
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -54,7 +54,6 @@ export declare class Engine {
|
|
|
54
54
|
private static readonly DEFAULT_RIM_LIGHT_INTENSITY;
|
|
55
55
|
private static readonly DEFAULT_CAMERA_DISTANCE;
|
|
56
56
|
private static readonly DEFAULT_CAMERA_TARGET;
|
|
57
|
-
private static readonly HAIR_OVER_EYES_ALPHA;
|
|
58
57
|
private static readonly TRANSPARENCY_EPSILON;
|
|
59
58
|
private static readonly STATS_FPS_UPDATE_INTERVAL_MS;
|
|
60
59
|
private static readonly STATS_FRAME_TIME_ROUNDING;
|
|
@@ -101,12 +100,9 @@ export declare class Engine {
|
|
|
101
100
|
private stats;
|
|
102
101
|
private animationFrameId;
|
|
103
102
|
private renderLoopCallback;
|
|
104
|
-
private
|
|
105
|
-
private animationTimeouts;
|
|
103
|
+
private player;
|
|
106
104
|
private hasAnimation;
|
|
107
|
-
private
|
|
108
|
-
private breathingTimeout;
|
|
109
|
-
private breathingBaseRotations;
|
|
105
|
+
private animationStartTime;
|
|
110
106
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
|
|
111
107
|
init(): Promise<void>;
|
|
112
108
|
private createPipelines;
|
|
@@ -118,20 +114,23 @@ export declare class Engine {
|
|
|
118
114
|
private setupCamera;
|
|
119
115
|
private setupLighting;
|
|
120
116
|
private setAmbientColor;
|
|
121
|
-
loadAnimation(url: string): Promise<void>;
|
|
122
|
-
playAnimation(
|
|
123
|
-
breathBones?: string[] | Record<string, number>;
|
|
124
|
-
breathDuration?: number;
|
|
125
|
-
}): void;
|
|
117
|
+
loadAnimation(url: string, audioUrl?: string): Promise<void>;
|
|
118
|
+
playAnimation(): void;
|
|
126
119
|
stopAnimation(): void;
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
pauseAnimation(): void;
|
|
121
|
+
seekAnimation(time: number): void;
|
|
122
|
+
getAnimationProgress(): import("./player").AnimationProgress;
|
|
123
|
+
/**
|
|
124
|
+
* Apply animation pose to model
|
|
125
|
+
*/
|
|
126
|
+
private applyPose;
|
|
129
127
|
getStats(): EngineStats;
|
|
130
128
|
runRenderLoop(callback?: () => void): void;
|
|
131
129
|
stopRenderLoop(): void;
|
|
132
130
|
dispose(): void;
|
|
133
131
|
loadModel(path: string): Promise<void>;
|
|
134
132
|
rotateBones(bones: string[], rotations: Quat[], durationMs?: number): void;
|
|
133
|
+
moveBones(bones: string[], relativeTranslations: Vec3[], durationMs?: number): void;
|
|
135
134
|
setMorphWeight(name: string, weight: number, durationMs?: number): void;
|
|
136
135
|
private updateVertexBuffer;
|
|
137
136
|
private setupModelBuffers;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAMnC,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,IAAI,CAAA;CACpB,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Camera } from "./camera";
|
|
|
2
2
|
import { Quat, Vec3 } from "./math";
|
|
3
3
|
import { PmxLoader } from "./pmx-loader";
|
|
4
4
|
import { Physics } from "./physics";
|
|
5
|
-
import {
|
|
5
|
+
import { Player } from "./player";
|
|
6
6
|
export class Engine {
|
|
7
7
|
constructor(canvas, options) {
|
|
8
8
|
this.cameraMatrixData = new Float32Array(36);
|
|
@@ -48,12 +48,9 @@ export class Engine {
|
|
|
48
48
|
};
|
|
49
49
|
this.animationFrameId = null;
|
|
50
50
|
this.renderLoopCallback = null;
|
|
51
|
-
this.
|
|
52
|
-
this.animationTimeouts = [];
|
|
51
|
+
this.player = new Player();
|
|
53
52
|
this.hasAnimation = false; // Set to true when loadAnimation is called
|
|
54
|
-
this.
|
|
55
|
-
this.breathingTimeout = null;
|
|
56
|
-
this.breathingBaseRotations = new Map();
|
|
53
|
+
this.animationStartTime = 0; // Track when animation first started (for A-pose prevention)
|
|
57
54
|
this.canvas = canvas;
|
|
58
55
|
if (options) {
|
|
59
56
|
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
@@ -1122,72 +1119,56 @@ export class Engine {
|
|
|
1122
1119
|
this.lightData[2] = color.z;
|
|
1123
1120
|
this.lightData[3] = 0.0; // Padding for vec3f alignment
|
|
1124
1121
|
}
|
|
1125
|
-
async loadAnimation(url) {
|
|
1126
|
-
|
|
1127
|
-
this.animationFrames = frames;
|
|
1122
|
+
async loadAnimation(url, audioUrl) {
|
|
1123
|
+
await this.player.loadVmd(url, audioUrl);
|
|
1128
1124
|
this.hasAnimation = true;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (Array.isArray(options.breathBones)) {
|
|
1142
|
-
breathBones = options.breathBones;
|
|
1143
|
-
}
|
|
1144
|
-
else {
|
|
1145
|
-
breathBones = Object.keys(options.breathBones);
|
|
1146
|
-
breathRotationRanges = options.breathBones;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
const breathDuration = options?.breathDuration ?? 4000;
|
|
1150
|
-
const allBoneKeyFrames = [];
|
|
1151
|
-
for (const keyFrame of this.animationFrames) {
|
|
1152
|
-
for (const boneFrame of keyFrame.boneFrames) {
|
|
1153
|
-
allBoneKeyFrames.push({
|
|
1154
|
-
boneName: boneFrame.boneName,
|
|
1155
|
-
time: keyFrame.time,
|
|
1156
|
-
rotation: boneFrame.rotation,
|
|
1157
|
-
});
|
|
1125
|
+
// Show first frame (time 0) immediately
|
|
1126
|
+
if (this.currentModel) {
|
|
1127
|
+
const initialPose = this.player.getPoseAtTime(0);
|
|
1128
|
+
this.applyPose(initialPose);
|
|
1129
|
+
// Reset bones without time 0 keyframes
|
|
1130
|
+
const skeleton = this.currentModel.getSkeleton();
|
|
1131
|
+
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1132
|
+
const bonesToReset = [];
|
|
1133
|
+
for (const bone of skeleton.bones) {
|
|
1134
|
+
if (!bonesWithPose.has(bone.name)) {
|
|
1135
|
+
bonesToReset.push(bone.name);
|
|
1136
|
+
}
|
|
1158
1137
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
boneKeyFramesByBone.set(boneKeyFrame.boneName, []);
|
|
1138
|
+
if (bonesToReset.length > 0) {
|
|
1139
|
+
const identityQuat = new Quat(0, 0, 0, 1);
|
|
1140
|
+
const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
|
|
1141
|
+
this.rotateBones(bonesToReset, identityQuats, 0);
|
|
1164
1142
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
boneName: boneName,
|
|
1176
|
-
rotation: keyFrames[0].rotation,
|
|
1177
|
-
});
|
|
1178
|
-
bonesWithTime0.add(boneName);
|
|
1143
|
+
// Update model pose and physics
|
|
1144
|
+
this.currentModel.evaluatePose();
|
|
1145
|
+
if (this.physics) {
|
|
1146
|
+
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1147
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1148
|
+
// Upload matrices immediately
|
|
1149
|
+
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1150
|
+
const encoder = this.device.createCommandEncoder();
|
|
1151
|
+
this.computeSkinMatrices(encoder);
|
|
1152
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1179
1153
|
}
|
|
1180
1154
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1155
|
+
}
|
|
1156
|
+
playAnimation() {
|
|
1157
|
+
if (!this.hasAnimation || !this.currentModel)
|
|
1158
|
+
return;
|
|
1159
|
+
const wasPaused = this.player.isPausedState();
|
|
1160
|
+
const wasPlaying = this.player.isPlayingState();
|
|
1161
|
+
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1162
|
+
if (!wasPlaying && !wasPaused) {
|
|
1163
|
+
// Get initial pose at time 0
|
|
1164
|
+
const initialPose = this.player.getPoseAtTime(0);
|
|
1165
|
+
this.applyPose(initialPose);
|
|
1166
|
+
// Reset bones without time 0 keyframes
|
|
1187
1167
|
const skeleton = this.currentModel.getSkeleton();
|
|
1168
|
+
const bonesWithPose = new Set(initialPose.boneRotations.keys());
|
|
1188
1169
|
const bonesToReset = [];
|
|
1189
1170
|
for (const bone of skeleton.bones) {
|
|
1190
|
-
if (!
|
|
1171
|
+
if (!bonesWithPose.has(bone.name)) {
|
|
1191
1172
|
bonesToReset.push(bone.name);
|
|
1192
1173
|
}
|
|
1193
1174
|
}
|
|
@@ -1208,121 +1189,62 @@ export class Engine {
|
|
|
1208
1189
|
this.device.queue.submit([encoder.finish()]);
|
|
1209
1190
|
}
|
|
1210
1191
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
if (boneKeyFrame.time === 0)
|
|
1216
|
-
continue;
|
|
1217
|
-
let durationMs = 0;
|
|
1218
|
-
if (i === 0) {
|
|
1219
|
-
durationMs = boneKeyFrame.time * 1000;
|
|
1220
|
-
}
|
|
1221
|
-
else if (previousBoneKeyFrame) {
|
|
1222
|
-
durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000;
|
|
1223
|
-
}
|
|
1224
|
-
const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0;
|
|
1225
|
-
const delayMs = scheduleTime * 1000;
|
|
1226
|
-
if (delayMs <= 0) {
|
|
1227
|
-
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
|
|
1228
|
-
}
|
|
1229
|
-
else {
|
|
1230
|
-
const timeoutId = window.setTimeout(() => {
|
|
1231
|
-
this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
|
|
1232
|
-
}, delayMs);
|
|
1233
|
-
this.animationTimeouts.push(timeoutId);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// Setup breathing animation if enabled
|
|
1238
|
-
if (enableBreath && this.currentModel) {
|
|
1239
|
-
// Find the last frame time
|
|
1240
|
-
let maxTime = 0;
|
|
1241
|
-
for (const keyFrame of this.animationFrames) {
|
|
1242
|
-
if (keyFrame.time > maxTime) {
|
|
1243
|
-
maxTime = keyFrame.time;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
// Get last frame rotations directly from animation data for breathing bones
|
|
1247
|
-
const lastFrameRotations = new Map();
|
|
1248
|
-
for (const bone of breathBones) {
|
|
1249
|
-
const keyFrames = boneKeyFramesByBone.get(bone);
|
|
1250
|
-
if (keyFrames && keyFrames.length > 0) {
|
|
1251
|
-
// Find the rotation at the last frame time (closest keyframe <= maxTime)
|
|
1252
|
-
let lastRotation = null;
|
|
1253
|
-
for (let i = keyFrames.length - 1; i >= 0; i--) {
|
|
1254
|
-
if (keyFrames[i].time <= maxTime) {
|
|
1255
|
-
lastRotation = keyFrames[i].rotation;
|
|
1256
|
-
break;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
if (lastRotation) {
|
|
1260
|
-
lastFrameRotations.set(bone, lastRotation);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
// Start breathing after animation completes
|
|
1265
|
-
// Use the last frame rotations directly from animation data (no need to capture from model)
|
|
1266
|
-
const animationEndTime = maxTime * 1000 + 200; // Small buffer for final tweens to complete
|
|
1267
|
-
this.breathingTimeout = window.setTimeout(() => {
|
|
1268
|
-
this.startBreathing(breathBones, lastFrameRotations, breathRotationRanges, breathDuration);
|
|
1269
|
-
}, animationEndTime);
|
|
1192
|
+
// Start playback (or resume if paused)
|
|
1193
|
+
this.player.play();
|
|
1194
|
+
if (this.animationStartTime === 0) {
|
|
1195
|
+
this.animationStartTime = performance.now();
|
|
1270
1196
|
}
|
|
1271
1197
|
}
|
|
1272
1198
|
stopAnimation() {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.
|
|
1277
|
-
this.playingAnimation = false;
|
|
1199
|
+
this.player.stop();
|
|
1200
|
+
}
|
|
1201
|
+
pauseAnimation() {
|
|
1202
|
+
this.player.pause();
|
|
1278
1203
|
}
|
|
1279
|
-
|
|
1280
|
-
if (this.
|
|
1281
|
-
|
|
1282
|
-
|
|
1204
|
+
seekAnimation(time) {
|
|
1205
|
+
if (!this.currentModel || !this.hasAnimation)
|
|
1206
|
+
return;
|
|
1207
|
+
this.player.seek(time);
|
|
1208
|
+
// Immediately apply pose at seeked time
|
|
1209
|
+
const pose = this.player.getPoseAtTime(time);
|
|
1210
|
+
this.applyPose(pose);
|
|
1211
|
+
// Update model pose and physics
|
|
1212
|
+
this.currentModel.evaluatePose();
|
|
1213
|
+
if (this.physics) {
|
|
1214
|
+
const worldMats = this.currentModel.getBoneWorldMatrices();
|
|
1215
|
+
this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
|
|
1216
|
+
// Upload matrices immediately
|
|
1217
|
+
this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
|
|
1218
|
+
const encoder = this.device.createCommandEncoder();
|
|
1219
|
+
this.computeSkinMatrices(encoder);
|
|
1220
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1283
1221
|
}
|
|
1284
|
-
this.breathingBaseRotations.clear();
|
|
1285
1222
|
}
|
|
1286
|
-
|
|
1223
|
+
getAnimationProgress() {
|
|
1224
|
+
return this.player.getProgress();
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Apply animation pose to model
|
|
1228
|
+
*/
|
|
1229
|
+
applyPose(pose) {
|
|
1287
1230
|
if (!this.currentModel)
|
|
1288
1231
|
return;
|
|
1289
|
-
//
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1232
|
+
// Apply bone rotations
|
|
1233
|
+
if (pose.boneRotations.size > 0) {
|
|
1234
|
+
const boneNames = Array.from(pose.boneRotations.keys());
|
|
1235
|
+
const rotations = Array.from(pose.boneRotations.values());
|
|
1236
|
+
this.rotateBones(boneNames, rotations, 0);
|
|
1237
|
+
}
|
|
1238
|
+
// Apply bone translations
|
|
1239
|
+
if (pose.boneTranslations.size > 0) {
|
|
1240
|
+
const boneNames = Array.from(pose.boneTranslations.keys());
|
|
1241
|
+
const translations = Array.from(pose.boneTranslations.values());
|
|
1242
|
+
this.moveBones(boneNames, translations, 0);
|
|
1243
|
+
}
|
|
1244
|
+
// Apply morph weights
|
|
1245
|
+
for (const [morphName, weight] of pose.morphWeights.entries()) {
|
|
1246
|
+
this.setMorphWeight(morphName, weight, 0);
|
|
1296
1247
|
}
|
|
1297
|
-
const halfCycleMs = durationMs / 2;
|
|
1298
|
-
const defaultRotation = 0.02; // Default rotation range if not specified per bone
|
|
1299
|
-
// Start breathing cycle - oscillate around exact base rotation (final pose)
|
|
1300
|
-
// Each bone can have its own rotation range, or use default
|
|
1301
|
-
const animate = (isInhale) => {
|
|
1302
|
-
if (!this.currentModel)
|
|
1303
|
-
return;
|
|
1304
|
-
const breathingBoneNames = [];
|
|
1305
|
-
const breathingQuats = [];
|
|
1306
|
-
for (const bone of bones) {
|
|
1307
|
-
const baseRot = this.breathingBaseRotations.get(bone);
|
|
1308
|
-
if (!baseRot)
|
|
1309
|
-
continue;
|
|
1310
|
-
// Get rotation range for this bone (per-bone or default)
|
|
1311
|
-
const rotation = rotationRanges?.[bone] ?? defaultRotation;
|
|
1312
|
-
// Oscillate around base rotation with the bone's rotation range
|
|
1313
|
-
// isInhale: base * rotation, exhale: base * (-rotation)
|
|
1314
|
-
const oscillationRot = Quat.fromEuler(isInhale ? rotation : -rotation, 0, 0);
|
|
1315
|
-
const finalRot = baseRot.multiply(oscillationRot);
|
|
1316
|
-
breathingBoneNames.push(bone);
|
|
1317
|
-
breathingQuats.push(finalRot);
|
|
1318
|
-
}
|
|
1319
|
-
if (breathingBoneNames.length > 0) {
|
|
1320
|
-
this.rotateBones(breathingBoneNames, breathingQuats, halfCycleMs);
|
|
1321
|
-
}
|
|
1322
|
-
this.breathingTimeout = window.setTimeout(() => animate(!isInhale), halfCycleMs);
|
|
1323
|
-
};
|
|
1324
|
-
// Start breathing from exhale position (closer to base) to minimize initial movement
|
|
1325
|
-
animate(false);
|
|
1326
1248
|
}
|
|
1327
1249
|
getStats() {
|
|
1328
1250
|
return { ...this.stats };
|
|
@@ -1348,7 +1270,6 @@ export class Engine {
|
|
|
1348
1270
|
dispose() {
|
|
1349
1271
|
this.stopRenderLoop();
|
|
1350
1272
|
this.stopAnimation();
|
|
1351
|
-
this.stopBreathing();
|
|
1352
1273
|
if (this.camera)
|
|
1353
1274
|
this.camera.detachControl();
|
|
1354
1275
|
if (this.resizeObserver) {
|
|
@@ -1369,6 +1290,10 @@ export class Engine {
|
|
|
1369
1290
|
rotateBones(bones, rotations, durationMs) {
|
|
1370
1291
|
this.currentModel?.rotateBones(bones, rotations, durationMs);
|
|
1371
1292
|
}
|
|
1293
|
+
// moveBones now takes relative translations (VMD-style) by default
|
|
1294
|
+
moveBones(bones, relativeTranslations, durationMs) {
|
|
1295
|
+
this.currentModel?.moveBones(bones, relativeTranslations, durationMs);
|
|
1296
|
+
}
|
|
1372
1297
|
setMorphWeight(name, weight, durationMs) {
|
|
1373
1298
|
if (!this.currentModel)
|
|
1374
1299
|
return;
|
|
@@ -1745,6 +1670,13 @@ export class Engine {
|
|
|
1745
1670
|
this.lastFrameTime = currentTime;
|
|
1746
1671
|
this.updateCameraUniforms();
|
|
1747
1672
|
this.updateRenderTarget();
|
|
1673
|
+
// Animate VMD animation if playing
|
|
1674
|
+
if (this.hasAnimation && this.currentModel) {
|
|
1675
|
+
const pose = this.player.update(currentTime);
|
|
1676
|
+
if (pose) {
|
|
1677
|
+
this.applyPose(pose);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1748
1680
|
// Update model pose first (this may update morph weights via tweens)
|
|
1749
1681
|
// We need to do this before creating the encoder to ensure vertex buffer is ready
|
|
1750
1682
|
if (this.currentModel) {
|
|
@@ -1761,9 +1693,10 @@ export class Engine {
|
|
|
1761
1693
|
// Use single encoder for both compute and render (reduces sync points)
|
|
1762
1694
|
const encoder = this.device.createCommandEncoder();
|
|
1763
1695
|
this.updateModelPose(deltaTime, encoder);
|
|
1764
|
-
// Hide model if animation is loaded but
|
|
1765
|
-
//
|
|
1766
|
-
|
|
1696
|
+
// Hide model if animation is loaded but hasn't started playing yet (prevents A-pose flash)
|
|
1697
|
+
// Once animation has played (even if it stopped), continue rendering normally
|
|
1698
|
+
// Still update physics and poses, just don't render visually before first play
|
|
1699
|
+
if (this.hasAnimation && !this.player.isPlayingState() && this.animationStartTime === 0) {
|
|
1767
1700
|
// Submit encoder to ensure matrices are uploaded and physics initializes
|
|
1768
1701
|
this.device.queue.submit([encoder.finish()]);
|
|
1769
1702
|
return;
|
|
@@ -1964,7 +1897,6 @@ Engine.DEFAULT_BLOOM_INTENSITY = 0.12;
|
|
|
1964
1897
|
Engine.DEFAULT_RIM_LIGHT_INTENSITY = 0.45;
|
|
1965
1898
|
Engine.DEFAULT_CAMERA_DISTANCE = 26.6;
|
|
1966
1899
|
Engine.DEFAULT_CAMERA_TARGET = new Vec3(0, 12.5, 0);
|
|
1967
|
-
Engine.HAIR_OVER_EYES_ALPHA = 0.5;
|
|
1968
1900
|
Engine.TRANSPARENCY_EPSILON = 0.001;
|
|
1969
1901
|
Engine.STATS_FPS_UPDATE_INTERVAL_MS = 1000;
|
|
1970
1902
|
Engine.STATS_FRAME_TIME_ROUNDING = 100;
|