Files
a2f-service/examples/3d/blendshapeAnimator.js
yinsx c10cfa7c33 1
2026-02-03 14:16:42 +08:00

659 lines
22 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.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.lastFrameBlendShapes = {};
// 空闲动画参数
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 = {};
}
appendAnimationFrames(frames) {
if (!Array.isArray(frames) || frames.length === 0) {
return;
}
this.animationFrames.push(...frames);
const newNames = this._collectAnimationShapeNames(frames);
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 = {};
}
_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;
}
}
_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;
}