真实接口预热

This commit is contained in:
yinsx
2026-01-04 09:58:26 +08:00
parent b56492f80f
commit 2bd183463d
10 changed files with 386 additions and 207 deletions

View File

@ -2,6 +2,7 @@
class BlendShapeAnimator {
constructor(config = {}) {
this.morphTargetAdapter = null;
this.scene = null;
this.animationFrames = [];
this.animationShapeNames = [];
this.isPlaying = false;
@ -16,6 +17,17 @@ class BlendShapeAnimator {
this.streamingWaitStart = null;
this.streamingStallMs = 0;
this.sentenceTexts = []; // 句子文本列表
this.minBlendShapeValue = typeof config.minBlendShapeValue === 'number'
? config.minBlendShapeValue
: 0.1; // Skip tiny blendshape inputs to avoid stalls
this.deltaThreshold = typeof config.deltaThreshold === 'number'
? config.deltaThreshold
: 0.002; // Skip re-applying nearly identical values
this.prewarmFrameCount = typeof config.prewarmFrameCount === 'number'
? config.prewarmFrameCount
: 30;
this.lastFrameBlendShapes = {};
this._hasPrewarmed = false;
// 空闲动画参数
this.blinkParams = config.blinkParams || {
@ -63,8 +75,15 @@ class BlendShapeAnimator {
// 回调
this.onStatusChange = config.onStatusChange || (() => {});
// 启动空闲动画循环
this._updateIdleAnimations();
// 不再启动独立的 RAF 循环,由外部渲染循环调用 tick()
}
// 每帧调用,由外部渲染循环驱动
tick() {
if (this.isPlaying) {
this._animateFrameOnce();
}
this._updateIdleAnimationsOnce();
}
// 设置形态键适配器
@ -76,6 +95,8 @@ class BlendShapeAnimator {
loadAnimationFrames(frames) {
this.animationFrames = frames || [];
this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames);
this.lastFrameBlendShapes = {};
// 不重置 _hasPrewarmed因为全局预热只需要做一次
}
appendAnimationFrames(frames) {
@ -85,6 +106,7 @@ class BlendShapeAnimator {
this.animationFrames.push(...frames);
const newNames = this._collectAnimationShapeNames(frames);
// 不重置 _hasPrewarmed
if (newNames.length === 0) {
return;
@ -116,7 +138,8 @@ class BlendShapeAnimator {
}
// 播放动画
playAnimation() {
async playAnimation() {
console.log('=== playAnimation 开始 ===');
if (this.animationFrames.length === 0) {
this.onStatusChange('error', '请先加载动画数据');
return;
@ -141,45 +164,24 @@ class BlendShapeAnimator {
// 注意:不停止眨眼,让眨眼继续运行
// 预演前10帧以避免首次播放卡顿
console.log('预演前10帧...');
const framesToPreview = Math.min(10, this.animationFrames.length);
for (let i = 0; i < framesToPreview; i++) {
const frame = this.animationFrames[i];
const blendShapes = frame?.blendShapes || {};
for (const key in blendShapes) {
if (this.disabledShapesInAnimation.includes(key.toLowerCase())) {
continue;
}
this.morphTargetAdapter.setInfluence(key, blendShapes[key] * this.blendShapeScale);
}
}
// 重置为0
for (let i = 0; i < framesToPreview; i++) {
const frame = this.animationFrames[i];
const blendShapes = frame?.blendShapes || {};
for (const key in blendShapes) {
this.morphTargetAdapter.setInfluence(key, 0);
}
}
console.log('预演完成');
this.stopAnimation(false);
this.stopAnimation(false, false);
this.isPlaying = true;
this.currentFrameIndex = 0;
this.animationStartTime = performance.now();
this.streamingWaitStart = null;
this.streamingStallMs = 0;
this._animateFrame();
this._primeFirstFrame();
this._scheduleAnimationStart();
this.onStatusChange('info', '播放中...');
}
// 停止动画
stopAnimation(resumeExpressions = true) {
stopAnimation(resumeExpressions = true, resetBlendShapes = true) {
this.isPlaying = false;
this._resetAnimationInfluences();
if (resetBlendShapes) {
this._resetAnimationInfluences();
}
// 恢复眼球移动
if (resumeExpressions && this.isEyeLookEnabled) {
@ -196,12 +198,17 @@ class BlendShapeAnimator {
_collectAnimationShapeNames(frames) {
const names = new Set();
const threshold = Math.min(this.minBlendShapeValue, 0.02);
frames.forEach(frame => {
const blendShapes = frame?.blendShapes;
if (!blendShapes) return;
Object.keys(blendShapes).forEach(name => names.add(name));
Object.entries(blendShapes).forEach(([name, value]) => {
if (Math.abs(value || 0) >= threshold) {
names.add(name);
}
});
});
return Array.from(names);
@ -215,10 +222,107 @@ class BlendShapeAnimator {
this.animationShapeNames.forEach(name => {
this.morphTargetAdapter.setInfluence(name, 0);
});
this.lastFrameBlendShapes = {};
}
// 内部动画帧处理
_animateFrame() {
_prewarmAnimation() {
// 同步预热已移除,改用异步预热
this._hasPrewarmed = true;
}
// 异步分帧预热 - 预热所有 morph targets确保 GPU 真正渲染
async prewarmAsync(scene) {
if (!this.morphTargetAdapter) {
return;
}
// 获取所有可用的 morph targets而不只是当前动画用到的
const allShapes = this.morphTargetAdapter.morphTargetCache
? Object.keys(this.morphTargetAdapter.morphTargetCache)
: this.animationShapeNames;
const shapes = allShapes.filter(
name => !this.disabledShapesInAnimation.includes(name.toLowerCase())
);
if (shapes.length === 0) return;
console.log(`异步预热 ${shapes.length} 个 morph targets...`);
// 用不同的值组合预热,触发所有可能的 shader 变体
const testValues = [0.2, 0.5, 0.8, 1.0];
for (const val of testValues) {
shapes.forEach(name => this.morphTargetAdapter.setInfluence(name, val));
if (scene) scene.render();
await new Promise(r => requestAnimationFrame(r));
}
// 重置并等待几帧确保稳定
shapes.forEach(name => this.morphTargetAdapter.setInfluence(name, 0));
for (let i = 0; i < 5; i++) {
if (scene) scene.render();
await new Promise(r => requestAnimationFrame(r));
}
this._hasPrewarmed = true;
console.log('异步预热完成');
}
_primeFirstFrame() {
if (!this.morphTargetAdapter || this.animationFrames.length === 0) {
return;
}
const firstFrame = this.animationFrames[0];
const blendShapes = firstFrame?.blendShapes || {};
this.lastFrameBlendShapes = {};
for (const key in blendShapes) {
const lower = key.toLowerCase();
if (this.disabledShapesInAnimation.includes(lower)) {
continue;
}
const scaledValue = (blendShapes[key] || 0) * this.blendShapeScale;
if (Math.abs(scaledValue) < this.minBlendShapeValue) {
this.morphTargetAdapter.setInfluence(key, 0);
this.lastFrameBlendShapes[key] = 0;
continue;
}
this.morphTargetAdapter.setInfluence(key, scaledValue);
this.lastFrameBlendShapes[key] = scaledValue;
}
}
_touchAnimationShapes() {
if (!this.morphTargetAdapter || !Array.isArray(this.animationShapeNames) || this.animationShapeNames.length === 0) {
return;
}
const touchValue = Math.max(this.minBlendShapeValue * 1.2, 0.05);
const touched = [];
this.animationShapeNames.forEach(name => {
this.morphTargetAdapter.setInfluence(name, touchValue);
touched.push(name);
});
touched.forEach(name => {
this.morphTargetAdapter.setInfluence(name, 0);
});
}
_scheduleAnimationStart() {
// 只设置开始时间,动画由统一的 RAF 循环驱动
this.animationStartTime = performance.now();
this.streamingWaitStart = null;
this.streamingStallMs = 0;
}
// 内部动画帧处理(单次调用,不递归)
_animateFrameOnce() {
if (!this.isPlaying) return;
const frameStartTime = performance.now();
@ -232,6 +336,7 @@ class BlendShapeAnimator {
const elapsed = now - this.animationStartTime - this.streamingStallMs;
const exactFrame = elapsed / frameDuration;
const targetFrameIndex = Math.floor(exactFrame);
const frameProgress = exactFrame - targetFrameIndex;
if (targetFrameIndex >= this.animationFrames.length) {
if (this.isStreaming && !this.streamingComplete) {
@ -243,12 +348,7 @@ class BlendShapeAnimator {
if (waitTime > 30000) {
console.warn('Streaming timeout after 30s, stopping animation');
this.stopAnimation();
return;
}
if (waitTime > 1000 && Math.floor(waitTime / 1000) !== Math.floor((waitTime - 16) / 1000)) {
console.log(`Still waiting... ${Math.floor(waitTime / 1000)}s`);
}
requestAnimationFrame(() => this._animateFrame());
return;
}
console.log(`Animation complete. Total frames: ${this.animationFrames.length}`);
@ -260,7 +360,6 @@ class BlendShapeAnimator {
const nextFrame = this.animationFrames[Math.min(targetFrameIndex + 1, this.animationFrames.length - 1)];
const currentBlendShapes = currentFrame?.blendShapes || {};
const nextBlendShapes = nextFrame?.blendShapes || {};
const frameProgress = exactFrame - targetFrameIndex;
const smoothProgress = this._easeOutQuad(frameProgress);
const shapeNames = this.animationShapeNames.length > 0
@ -268,8 +367,6 @@ class BlendShapeAnimator {
: Object.keys(currentBlendShapes);
let updateCount = 0;
const setInfluenceStart = performance.now();
for (const key of shapeNames) {
// 跳过禁用列表中的 blendshape,让空闲动画继续控制它们
if (this.disabledShapesInAnimation.includes(key.toLowerCase())) {
@ -281,30 +378,36 @@ class BlendShapeAnimator {
const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress);
const scaledValue = interpolatedValue * this.blendShapeScale;
const previousValue = this.lastFrameBlendShapes[key] ?? 0;
if (Math.abs(scaledValue) < this.minBlendShapeValue) {
if (Math.abs(previousValue) >= this.minBlendShapeValue) {
this.morphTargetAdapter.setInfluence(key, 0);
this.lastFrameBlendShapes[key] = 0;
updateCount++;
}
continue;
}
if (Math.abs(scaledValue - previousValue) < this.deltaThreshold) {
continue;
}
this.morphTargetAdapter.setInfluence(key, scaledValue);
this.lastFrameBlendShapes[key] = scaledValue;
updateCount++;
}
const setInfluenceTime = performance.now() - setInfluenceStart;
this._lastFrameTime = now;
this.currentFrameIndex = targetFrameIndex;
// 性能监控前100帧
if (targetFrameIndex < 100) {
const totalFrameTime = performance.now() - frameStartTime;
if (totalFrameTime > 16.67) {
console.warn(`${targetFrameIndex}耗时${totalFrameTime.toFixed(2)}ms (setInfluence: ${setInfluenceTime.toFixed(2)}ms, 调用${updateCount}次)`);
}
}
// 更新当前句子显示
const sentenceIndex = currentFrame?.sentenceIndex ?? -1;
if (sentenceIndex !== this.currentSentenceIndex) {
this.currentSentenceIndex = sentenceIndex;
this._updateCurrentSentenceDisplay();
}
requestAnimationFrame(() => this._animateFrame());
// 不再递归调用 RAF由统一循环驱动
}
// 设置空闲动画
@ -326,8 +429,8 @@ class BlendShapeAnimator {
};
}
// 更新空闲动画
_updateIdleAnimations() {
// 更新空闲动画(单次调用,不递归)
_updateIdleAnimationsOnce() {
const now = performance.now();
for (const name in this.idleAnimations) {
@ -359,8 +462,6 @@ class BlendShapeAnimator {
}
}
}
requestAnimationFrame(() => this._updateIdleAnimations());
}
// 眨眼控制