// BlendShape Animation SDK - Engine Agnostic class BlendShapeAnimator { constructor(config = {}) { this.morphTargetAdapter = null; this.scene = null; this.animationFrames = []; this.animationShapeNames = []; this.isPlaying = false; this.currentFrameIndex = 0; this.currentSentenceIndex = -1; this.animationStartTime = 0; 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; 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 || { intervalMin: 2000, intervalMax: 5000, duration: 150, speed: 100 }; this.eyeLookParams = config.eyeLookParams || { intervalMin: 2000, intervalMax: 6000, durationMin: 1000, durationMax: 2500, speed: 250 }; this.expressionParams = config.expressionParams || { intervalMin: 3000, intervalMax: 8000, speed: 400, strength: 1.0 }; this.enabledExpressions = new Set(); this.expressionDurations = {}; // 播放动画时禁用的 blendshape 列表(由空闲动画控制) this.disabledShapesInAnimation = config.disabledShapesInAnimation || [ 'eyeblinkleft', 'eyeblinkright', // 眨眼 'browdownleft', 'browdownright', // 眉毛下 'browinnerup', // 眉毛内上 'browouterupleft', 'browouterupright' // 眉毛外上 ]; // 状态标志 this.isBlinkEnabled = false; this.isEyeLookEnabled = false; this.isExpressionEnabled = false; // 定时器 this.blinkInterval = null; this.eyeLookInterval = null; // 回调 this.onStatusChange = config.onStatusChange || (() => {}); // 不再启动独立的 RAF 循环,由外部渲染循环调用 tick() } // 每帧调用,由外部渲染循环驱动 tick() { if (this.isPlaying) { this._animateFrameOnce(); } this._updateIdleAnimationsOnce(); } // 设置形态键适配器 setMorphTargetAdapter(adapter) { this.morphTargetAdapter = adapter; } // 加载动画帧数据 loadAnimationFrames(frames) { this.animationFrames = frames || []; this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames); this.lastFrameBlendShapes = {}; // 不重置 _hasPrewarmed,因为全局预热只需要做一次 } appendAnimationFrames(frames) { if (!Array.isArray(frames) || frames.length === 0) { return; } this.animationFrames.push(...frames); const newNames = this._collectAnimationShapeNames(frames); // 不重置 _hasPrewarmed if (newNames.length === 0) { return; } const existingNames = new Set(this.animationShapeNames); newNames.forEach(name => { if (!existingNames.has(name)) { existingNames.add(name); this.animationShapeNames.push(name); } }); } startStreaming() { console.log('Starting streaming mode'); this.isStreaming = true; this.streamingComplete = false; this.streamingWaitStart = null; this.streamingStallMs = 0; } endStreaming() { console.log('Ending streaming mode'); this.streamingComplete = true; this.isStreaming = false; this.streamingWaitStart = null; this.streamingStallMs = 0; } // 播放动画 async playAnimation() { console.log('=== playAnimation 开始 ==='); if (this.animationFrames.length === 0) { this.onStatusChange('error', '请先加载动画数据'); return; } if (!this.morphTargetAdapter) { this.onStatusChange('error', '未设置形态键适配器'); return; } // 停止并立即重置眼球移动 if (this.isEyeLookEnabled) { this._stopRandomEyeLook(); this._immediateResetEyeLook(); } // 停止并立即重置随机表情 if (this.isExpressionEnabled && window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.stop(); this._immediateResetExpressions(); } // 注意:不停止眨眼,让眨眼继续运行 this.stopAnimation(false, false); this.isPlaying = true; this.currentFrameIndex = 0; this.streamingWaitStart = null; this.streamingStallMs = 0; this._primeFirstFrame(); this._scheduleAnimationStart(); this.onStatusChange('info', '播放中...'); } // 停止动画 stopAnimation(resumeExpressions = true, resetBlendShapes = true) { this.isPlaying = false; if (resetBlendShapes) { this._resetAnimationInfluences(); } // 恢复眼球移动 if (resumeExpressions && this.isEyeLookEnabled) { this._startRandomEyeLook(); } // 恢复随机表情 if (resumeExpressions && this.isExpressionEnabled && window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.start(); } this.onStatusChange('info', '已停止'); } _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.entries(blendShapes).forEach(([name, value]) => { if (Math.abs(value || 0) >= threshold) { names.add(name); } }); }); return Array.from(names); } _resetAnimationInfluences() { if (!this.morphTargetAdapter || this.animationShapeNames.length === 0) { return; } this.animationShapeNames.forEach(name => { this.morphTargetAdapter.setInfluence(name, 0); }); this.lastFrameBlendShapes = {}; } _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(); const now = frameStartTime; if (this.streamingWaitStart !== null && this.animationFrames.length > this.currentFrameIndex + 1) { this.streamingStallMs += now - this.streamingWaitStart; this.streamingWaitStart = null; } const frameDuration = 1000 / this.dataFps; const elapsed = (now - this.animationStartTime - this.streamingStallMs) * this.playbackSpeed; const exactFrame = elapsed / frameDuration; const targetFrameIndex = Math.floor(exactFrame); const frameProgress = exactFrame - targetFrameIndex; if (targetFrameIndex >= this.animationFrames.length) { if (this.isStreaming && !this.streamingComplete) { if (this.streamingWaitStart === null) { this.streamingWaitStart = now; console.log(`Waiting for more frames... (current: ${this.animationFrames.length}, target: ${targetFrameIndex})`); } const waitTime = now - this.streamingWaitStart; if (waitTime > 30000) { console.warn('Streaming timeout after 30s, stopping animation'); this.stopAnimation(); } return; } console.log(`Animation complete. Total frames: ${this.animationFrames.length}`); this.stopAnimation(); return; } const currentFrame = this.animationFrames[targetFrameIndex]; const nextFrame = this.animationFrames[Math.min(targetFrameIndex + 1, this.animationFrames.length - 1)]; const currentBlendShapes = currentFrame?.blendShapes || {}; const nextBlendShapes = nextFrame?.blendShapes || {}; const smoothProgress = this._easeOutQuad(frameProgress); const shapeNames = this.animationShapeNames.length > 0 ? this.animationShapeNames : Object.keys(currentBlendShapes); let updateCount = 0; for (const key of shapeNames) { // 跳过禁用列表中的 blendshape,让空闲动画继续控制它们 if (this.disabledShapesInAnimation.includes(key.toLowerCase())) { continue; } const currentValue = currentBlendShapes[key] || 0; const nextValue = nextBlendShapes[key] || 0; 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++; } this._lastFrameTime = now; this.currentFrameIndex = targetFrameIndex; // 更新当前句子显示 const sentenceIndex = currentFrame?.sentenceIndex ?? -1; if (sentenceIndex !== this.currentSentenceIndex) { this.currentSentenceIndex = sentenceIndex; this._updateCurrentSentenceDisplay(); } // 不再递归调用 RAF,由统一循环驱动 } // 设置空闲动画 setIdleAnimation(name, target, duration = 200, easing = 'easeOutQuad') { const currentValue = this.morphTargetAdapter ? this.morphTargetAdapter.getInfluence(name) || 0 : (this.idleAnimations[name]?.target || 0); const anim = this.idleAnimations[name] || {}; const unchanged = Math.abs(currentValue - target) < 1e-4 && Math.abs((anim.target ?? 0) - target) < 1e-4; this.idleAnimations[name] = { target: target, duration: duration, easing: easing, // 如果目标基本不变,保持当前起点,避免反复重新插值导致抖动 startTime: unchanged ? performance.now() : null, startValue: currentValue }; } // 更新空闲动画(单次调用,不递归) _updateIdleAnimationsOnce() { const now = performance.now(); for (const name in this.idleAnimations) { const anim = this.idleAnimations[name]; if (!this.morphTargetAdapter) continue; if (!anim.startTime) { anim.startTime = now; anim.startValue = this.morphTargetAdapter.getInfluence(name) || 0; } const elapsed = now - anim.startTime; const duration = anim.duration || 200; let progress = Math.min(elapsed / duration, 1.0); progress = this._applyEasing(progress, anim.easing); const currentValue = this._lerp(anim.startValue, anim.target, progress); this.morphTargetAdapter.setInfluence(name, currentValue); if (progress >= 1.0) { if (anim.target === 0) { delete this.idleAnimations[name]; } else { // 目标保持不变,锁定在当前值,避免反复重新插值造成抖动 anim.startValue = anim.target; anim.startTime = now; } } } } // 眨眼控制 toggleBlink(enabled) { this.isBlinkEnabled = enabled; if (enabled) { this._startRandomBlink(); this.onStatusChange('success', '随机眨眼已开启'); } else { this._stopRandomBlink(); this.onStatusChange('info', '随机眨眼已关闭'); } } _startRandomBlink() { this._stopRandomBlink(); const scheduleNext = () => { const delay = this.blinkParams.intervalMin + Math.random() * (this.blinkParams.intervalMax - this.blinkParams.intervalMin); this.blinkInterval = setTimeout(() => { if (this.isBlinkEnabled) { this._doBlink(); const holdDuration = Math.max(0, this.blinkParams.duration || 0); this.blinkInterval = setTimeout(() => { if (this.isBlinkEnabled) { scheduleNext(); } }, holdDuration); } }, delay); }; scheduleNext(); } _stopRandomBlink() { if (this.blinkInterval) { clearTimeout(this.blinkInterval); this.blinkInterval = null; } } _doBlink() { const blinkShapes = ['eyeblinkleft', 'eyeblinkright']; blinkShapes.forEach(name => { this.setIdleAnimation(name, 1.0, this.blinkParams.speed, 'easeOutQuad'); }); setTimeout(() => { blinkShapes.forEach(name => { this.setIdleAnimation(name, 0, this.blinkParams.speed + 20, 'easeInOutQuad'); }); }, this.blinkParams.duration); } // 眼球移动控制 toggleEyeLook(enabled) { this.isEyeLookEnabled = enabled; if (enabled) { this._startRandomEyeLook(); this.onStatusChange('success', '眼球移动已开启'); } else { this._stopRandomEyeLook(); this._resetEyeLook(); this.onStatusChange('info', '眼球移动已关闭'); } } _startRandomEyeLook() { this._stopRandomEyeLook(); const scheduleNext = () => { const delay = this.eyeLookParams.intervalMin + Math.random() * (this.eyeLookParams.intervalMax - this.eyeLookParams.intervalMin); this.eyeLookInterval = setTimeout(() => { if (this.isEyeLookEnabled) { const holdDuration = this._doRandomEyeLook(); const waitDuration = Math.max(0, holdDuration || 0); this.eyeLookInterval = setTimeout(() => { if (this.isEyeLookEnabled) { scheduleNext(); } }, waitDuration); } }, delay); }; scheduleNext(); } _stopRandomEyeLook() { if (this.eyeLookInterval) { clearTimeout(this.eyeLookInterval); this.eyeLookInterval = null; } } _doRandomEyeLook() { const directions = [ { shapes: {} }, { shapes: { 'eyelookupleft': 0.3, 'eyelookupright': 0.3 } }, { shapes: { 'eyelookdownleft': 0.3, 'eyelookdownright': 0.3 } }, { shapes: { 'eyelookinleft': 0.4, 'eyelookoutright': 0.4 } }, { shapes: { 'eyelookoutleft': 0.4, 'eyelookinright': 0.4 } } ]; const direction = directions[Math.floor(Math.random() * directions.length)]; const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed * 0.8, 'easeInOutQuad'); }); for (const [name, value] of Object.entries(direction.shapes)) { this.setIdleAnimation(name, value, this.eyeLookParams.speed, 'easeInOutQuad'); } const holdDuration = this.eyeLookParams.durationMin + Math.random() * (this.eyeLookParams.durationMax - this.eyeLookParams.durationMin); setTimeout(() => { if (this.isEyeLookEnabled) { eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed * 1.2, 'easeInOutQuad'); }); } }, holdDuration); return holdDuration; } _resetEyeLook() { const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed, 'easeInOutQuad'); }); } _immediateResetEyeLook() { const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; if (!this.morphTargetAdapter) return; eyeLookShapes.forEach(name => { this.morphTargetAdapter.setInfluence(name, 0); delete this.idleAnimations[name]; }); } _immediateResetExpressions() { if (!this.morphTargetAdapter || !window.ExpressionLibrary) return; // 获取所有表情的 blendshape 名称并立即重置 const expressions = window.ExpressionLibrary.expressions; for (const exprKey in expressions) { const expr = expressions[exprKey]; if (expr.blendShapes) { for (const shapeName in expr.blendShapes) { this.morphTargetAdapter.setInfluence(shapeName, 0); delete this.idleAnimations[shapeName]; } } } } // 随机表情控制 toggleRandomExpression(enabled) { if (!window.ExpressionLibrary) { this.onStatusChange('error', '表情库未加载'); return; } this.isExpressionEnabled = enabled; if (enabled) { window.ExpressionLibrary.randomPlayer.intervalMin = this.expressionParams.intervalMin; window.ExpressionLibrary.randomPlayer.intervalMax = this.expressionParams.intervalMax; if (!this.isPlaying) { window.ExpressionLibrary.randomPlayer.start(); } this.onStatusChange('success', '随机表情已开启'); } else { window.ExpressionLibrary.randomPlayer.stop(); this.onStatusChange('info', '随机表情已关闭'); } } // 缓动函数 _easeOutQuad(t) { return t * (2 - t); } _lerp(start, end, t) { return start + (end - start) * t; } _updateCurrentSentenceDisplay() { const sentenceDiv = document.getElementById('currentSentence'); const sentenceText = document.getElementById('sentenceText'); if (!sentenceDiv || !sentenceText) return; if (this.currentSentenceIndex >= 0 && this.currentSentenceIndex < this.sentenceTexts.length) { sentenceDiv.style.display = 'block'; sentenceText.textContent = this.sentenceTexts[this.currentSentenceIndex]; console.log(`[前端调试] 显示句子 ${this.currentSentenceIndex}: ${this.sentenceTexts[this.currentSentenceIndex]}`); } else { sentenceDiv.style.display = 'none'; } } _applyEasing(t, type) { switch(type) { case 'easeOutQuad': return t * (2 - t); case 'easeInOutQuad': return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; case 'easeInOutCubic': return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; case 'easeOutCubic': return 1 - Math.pow(1 - t, 3); case 'linear': default: return t; } } // 更新配置 updateConfig(key, value) { if (key === 'blendShapeScale') { this.blendShapeScale = value; } else if (key === 'playbackSpeed') { this.playbackSpeed = value; } else if (this[key]) { this[key] = value; } } } // 导出到全局 if (typeof window !== 'undefined') { window.BlendShapeAnimator = BlendShapeAnimator; }