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 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
- ## Usage
19
-
20
- ```javascript
21
- export default function Scene() {
22
- const canvasRef = useRef < HTMLCanvasElement > null
23
- const engineRef = useRef < Engine > null
24
-
25
- const initEngine = useCallback(async () => {
26
- if (canvasRef.current) {
27
- try {
28
- const engine = new Engine(canvasRef.current)
29
- engineRef.current = engine
30
- await engine.init()
31
- await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
32
-
33
- engine.runRenderLoop(() => {})
34
- } catch (error) {
35
- console.error(error)
36
- }
37
- }
38
- }, [])
39
-
40
- useEffect(() => {
41
- void (async () => {
42
- initEngine()
43
- })()
44
-
45
- return () => {
46
- if (engineRef.current) {
47
- engineRef.current.dispose()
48
- }
49
- }
50
- }, [initEngine])
51
-
52
- return <canvas ref={canvasRef} className="w-full h-full" />
53
- }
54
- ```
55
-
56
- ## Projects Using This Engine
57
-
58
- - **[MiKaPo](https://mikapo.vercel.app)** - Online real-time motion capture for MMD using webcam and MediaPipe
59
- - **[Popo](https://popo.love)** - Fine-tuned LLM that generates MMD poses from natural language descriptions
60
- - **[MPL](https://mmd-mpl.vercel.app)** - Semantic motion programming language for scripting MMD animations with intuitive syntax
61
-
62
- ## Tutorial
63
-
64
- 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.
65
-
66
- [How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
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 animationFrames;
105
- private animationTimeouts;
103
+ private player;
106
104
  private hasAnimation;
107
- private playingAnimation;
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(options?: {
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
- private stopBreathing;
128
- private startBreathing;
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;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
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 { VMDLoader } from "./vmd-loader";
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.animationFrames = [];
52
- this.animationTimeouts = [];
51
+ this.player = new Player();
53
52
  this.hasAnimation = false; // Set to true when loadAnimation is called
54
- this.playingAnimation = false; // Set to true when playAnimation is called
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
- const frames = await VMDLoader.load(url);
1127
- this.animationFrames = frames;
1122
+ async loadAnimation(url, audioUrl) {
1123
+ await this.player.loadVmd(url, audioUrl);
1128
1124
  this.hasAnimation = true;
1129
- }
1130
- playAnimation(options) {
1131
- if (this.animationFrames.length === 0)
1132
- return;
1133
- this.stopAnimation();
1134
- this.stopBreathing();
1135
- this.playingAnimation = true;
1136
- // Enable breathing if breathBones is provided
1137
- const enableBreath = options?.breathBones !== undefined && options.breathBones !== null;
1138
- let breathBones = [];
1139
- let breathRotationRanges = undefined;
1140
- if (enableBreath && options.breathBones) {
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
- const boneKeyFramesByBone = new Map();
1161
- for (const boneKeyFrame of allBoneKeyFrames) {
1162
- if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
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
- boneKeyFramesByBone.get(boneKeyFrame.boneName).push(boneKeyFrame);
1166
- }
1167
- for (const keyFrames of boneKeyFramesByBone.values()) {
1168
- keyFrames.sort((a, b) => a.time - b.time);
1169
- }
1170
- const time0Rotations = [];
1171
- const bonesWithTime0 = new Set();
1172
- for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
1173
- if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1174
- time0Rotations.push({
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
- if (this.currentModel) {
1182
- if (time0Rotations.length > 0) {
1183
- const boneNames = time0Rotations.map((r) => r.boneName);
1184
- const rotations = time0Rotations.map((r) => r.rotation);
1185
- this.rotateBones(boneNames, rotations, 0);
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 (!bonesWithTime0.has(bone.name)) {
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
- for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
1212
- for (let i = 0; i < keyFrames.length; i++) {
1213
- const boneKeyFrame = keyFrames[i];
1214
- const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null;
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
- for (const timeoutId of this.animationTimeouts) {
1274
- clearTimeout(timeoutId);
1275
- }
1276
- this.animationTimeouts = [];
1277
- this.playingAnimation = false;
1199
+ this.player.stop();
1200
+ }
1201
+ pauseAnimation() {
1202
+ this.player.pause();
1278
1203
  }
1279
- stopBreathing() {
1280
- if (this.breathingTimeout !== null) {
1281
- clearTimeout(this.breathingTimeout);
1282
- this.breathingTimeout = null;
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
- startBreathing(bones, baseRotations, rotationRanges, durationMs = 4000) {
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
- // Store base rotations directly from last frame of animation data
1290
- // These are the exact rotations from the animation - use them as-is
1291
- for (const bone of bones) {
1292
- const baseRot = baseRotations.get(bone);
1293
- if (baseRot) {
1294
- this.breathingBaseRotations.set(bone, baseRot);
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 not playing yet (prevents A-pose flash)
1765
- // Still update physics and poses, just don't render visually
1766
- if (this.hasAnimation && !this.playingAnimation) {
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;