Files
a2f-service/examples/3d/blendshapeAnimator.js
2026-01-04 09:58:26 +08:00

723 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.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;
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 (this[key]) {
this[key] = value;
}
}
}
// 导出到全局
if (typeof window !== 'undefined') {
window.BlendShapeAnimator = BlendShapeAnimator;
}