真实接口预热
This commit is contained in:
@ -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());
|
||||
}
|
||||
|
||||
// 眨眼控制
|
||||
|
||||
Reference in New Issue
Block a user