首次播放不掉帧

This commit is contained in:
yinsx
2026-01-04 14:06:14 +08:00
parent 2bd183463d
commit f98ff21394
7 changed files with 4856 additions and 33 deletions

View File

@ -12,6 +12,7 @@ class BlendShapeAnimator {
this.idleAnimations = {};
this.blendShapeScale = config.blendShapeScale || 1.0;
this.dataFps = config.dataFps || 30;
this.playbackSpeed = config.playbackSpeed || 1.0; // 播放倍速0.5=慢速, 1.0=正常, 2.0=快速
this.isStreaming = false;
this.streamingComplete = true;
this.streamingWaitStart = null;
@ -333,7 +334,7 @@ class BlendShapeAnimator {
}
const frameDuration = 1000 / this.dataFps;
const elapsed = now - this.animationStartTime - this.streamingStallMs;
const elapsed = (now - this.animationStartTime - this.streamingStallMs) * this.playbackSpeed;
const exactFrame = elapsed / frameDuration;
const targetFrameIndex = Math.floor(exactFrame);
const frameProgress = exactFrame - targetFrameIndex;
@ -710,6 +711,8 @@ class BlendShapeAnimator {
updateConfig(key, value) {
if (key === 'blendShapeScale') {
this.blendShapeScale = value;
} else if (key === 'playbackSpeed') {
this.playbackSpeed = value;
} else if (this[key]) {
this[key] = value;
}

View File

@ -51,6 +51,13 @@
style="width: 100%; cursor: pointer;">
</div>
<div class="input-group">
<label>播放倍速: <span id="speedValue">1.0x</span></label>
<input type="range" id="speedSlider" min="0.25" max="3" step="0.25" value="1.0"
oninput="updatePlaybackSpeed(this.value)"
style="width: 100%; cursor: pointer;">
</div>
<button id="generateBtn" onclick="generateAnimation()">生成动画</button>
<button onclick="playAnimation()">播放动画</button>
<button onclick="stopAnimation()">停止动画</button>

View File

@ -54,36 +54,13 @@ function init() {
} else {
console.log(`✓ 构建缓存完成,共 ${totalTargets} 个形态键`);
// 预热:直接调用生成动画流程(和用户点击按钮完全一样)
// 预热:使用写死的预热数据,不请求接口
showStatus("预热中...", "info");
const apiUrl = document.getElementById('apiUrl').value;
const streamEnabled = document.getElementById('streamEnabled')?.checked;
console.log('预热 apiUrl:', apiUrl, 'stream:', streamEnabled);
if (apiUrl) {
try {
// 和 generateAnimation 走完全一样的路径
if (streamEnabled) {
await generateAnimationStream('你好', apiUrl);
} else {
await generateAnimationBatch('你好', apiUrl);
playAnimation();
}
// 等播放完
await new Promise(resolve => {
const check = () => animator.isPlaying ? requestAnimationFrame(check) : resolve();
check();
});
// 完全重置状态
animator.stopAnimation();
animator.loadAnimationFrames([]);
animator.endStreaming();
morphAdapter.resetAll();
console.log('✓ 预热完成');
} catch (e) {
console.warn('预热失败:', e);
}
} else {
console.warn('预热跳过: apiUrl 为空');
try {
await warmupWithLocalData();
console.log('✓ 预热完成');
} catch (e) {
console.warn('预热失败:', e);
}
showStatus("就绪", "success");
@ -95,6 +72,47 @@ function init() {
);
}
// 使用本地预热数据进行预热(不请求接口)
async function warmupWithLocalData() {
console.log('=== warmupWithLocalData 开始 ===');
// 加载预热数据
const response = await fetch('预热数据.json');
const frames = await response.json();
console.log(`加载预热数据: ${frames.length}`);
// 保存原始播放速度设置预热倍速4倍速快速预热
const originalSpeed = animator.playbackSpeed;
animator.playbackSpeed = 4.0;
console.log(`预热倍速: ${animator.playbackSpeed}x`);
// 加载并播放
animator.loadAnimationFrames(frames);
animator.playAnimation();
// 等待播放完成
await new Promise(resolve => {
const checkDone = () => {
if (!animator.isPlaying) {
resolve();
} else {
requestAnimationFrame(checkDone);
}
};
requestAnimationFrame(checkDone);
});
// 重置状态
animator.stopAnimation();
animator.loadAnimationFrames([]);
morphAdapter.resetAll();
// 恢复原始播放速度
animator.playbackSpeed = originalSpeed;
console.log('✓ 本地数据预热完成');
}
// 偷偷调接口预热 - 完全走一遍生成动画流程
async function warmupWithApi() {
console.log('=== warmupWithApi 开始 ===');
@ -133,7 +151,7 @@ async function warmupWithApi() {
}
}
async function generateAnimation() {
async function generateAnimation() {
const text = document.getElementById('textInput').value.trim();
const apiUrl = document.getElementById('apiUrl').value;
const btn = document.getElementById('generateBtn');
@ -176,6 +194,19 @@ async function generateAnimationBatch(text, apiUrl) {
animator.loadAnimationFrames(data.frames);
console.log("动画数据:", data.frames);
// 打印可复制的 JSON 格式过滤掉值为0的字段
const filteredFrames = data.frames.map(frame => {
const filtered = { timeCode: frame.timeCode, blendShapes: {} };
for (const [key, value] of Object.entries(frame.blendShapes)) {
if (value !== 0) {
filtered.blendShapes[key] = value;
}
}
return filtered;
});
console.log("========== 复制以下内容作为预热数据 ==========");
console.log(JSON.stringify(filteredFrames));
console.log("========== 复制结束 ==========");
showStatus(`动画生成成功!共 ${data.frames.length}`, "success");
}
@ -355,6 +386,11 @@ function updateFps(value) {
document.getElementById('fpsValue').textContent = value;
}
function updatePlaybackSpeed(value) {
animator.updateConfig('playbackSpeed', parseFloat(value));
document.getElementById('speedValue').textContent = value + 'x';
}
function testBlendShape() {
if (morphAdapter.getCacheSize() === 0) {
showStatus("模型未加载", "error");

File diff suppressed because it is too large Load Diff