Files
a2f-service/examples/3d/blendshapeAnimator.js

410 lines
12 KiB
JavaScript

// BlendShape Animation SDK - Engine Agnostic
class BlendShapeAnimator {
constructor(config = {}) {
this.morphTargetAdapter = null;
this.animationFrames = [];
this.animationShapeNames = [];
this.isPlaying = false;
this.currentFrameIndex = 0;
this.animationStartTime = 0;
this.idleAnimations = {};
this.blendShapeScale = config.blendShapeScale || 1.0;
this.dataFps = config.dataFps || 30;
// 空闲动画参数
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
};
this.enabledExpressions = new Set();
this.expressionDurations = {};
// 状态标志
this.isBlinkEnabled = false;
this.isEyeLookEnabled = false;
this.isExpressionEnabled = false;
// 定时器
this.blinkInterval = null;
this.eyeLookInterval = null;
// 回调
this.onStatusChange = config.onStatusChange || (() => {});
// 启动空闲动画循环
this._updateIdleAnimations();
}
// 设置形态键适配器
setMorphTargetAdapter(adapter) {
this.morphTargetAdapter = adapter;
}
// 加载动画帧数据
loadAnimationFrames(frames) {
this.animationFrames = frames || [];
this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames);
}
// 播放动画
playAnimation() {
if (this.animationFrames.length === 0) {
this.onStatusChange('error', '请先加载动画数据');
return;
}
if (!this.morphTargetAdapter) {
this.onStatusChange('error', '未设置形态键适配器');
return;
}
// 停止随机表情
if (this.isExpressionEnabled && window.ExpressionLibrary) {
window.ExpressionLibrary.randomPlayer.stop();
}
this.stopAnimation(false);
this.isPlaying = true;
this.currentFrameIndex = 0;
this.animationStartTime = performance.now();
this._animateFrame();
this.onStatusChange('info', '播放中...');
}
// 停止动画
stopAnimation(resumeExpressions = true) {
this.isPlaying = false;
this._resetAnimationInfluences();
// 恢复随机表情
if (resumeExpressions && this.isExpressionEnabled && window.ExpressionLibrary) {
window.ExpressionLibrary.randomPlayer.start();
}
this.onStatusChange('info', '已停止');
}
_collectAnimationShapeNames(frames) {
const names = new Set();
frames.forEach(frame => {
const blendShapes = frame?.blendShapes;
if (!blendShapes) return;
Object.keys(blendShapes).forEach(name => 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);
});
}
// 内部动画帧处理
_animateFrame() {
if (!this.isPlaying) return;
const now = performance.now();
const frameDuration = 1000 / this.dataFps;
const elapsed = now - this.animationStartTime;
const exactFrame = elapsed / frameDuration;
const targetFrameIndex = Math.floor(exactFrame);
if (targetFrameIndex >= 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 frameProgress = exactFrame - targetFrameIndex;
const smoothProgress = this._easeOutQuad(frameProgress);
const shapeNames = this.animationShapeNames.length > 0
? this.animationShapeNames
: Object.keys(currentBlendShapes);
for (const key of shapeNames) {
const currentValue = currentBlendShapes[key] || 0;
const nextValue = nextBlendShapes[key] || 0;
const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress);
const scaledValue = interpolatedValue * this.blendShapeScale;
this.morphTargetAdapter.setInfluence(key, scaledValue);
}
this.currentFrameIndex = targetFrameIndex;
requestAnimationFrame(() => this._animateFrame());
}
// 设置空闲动画
setIdleAnimation(name, target, duration = 200, easing = 'easeOutQuad') {
this.idleAnimations[name] = {
target: target,
duration: duration,
easing: easing,
startTime: null,
startValue: this.idleAnimations[name]?.target || 0
};
}
// 更新空闲动画
_updateIdleAnimations() {
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.startTime = null;
}
}
}
requestAnimationFrame(() => this._updateIdleAnimations());
}
// 眨眼控制
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();
scheduleNext();
}
}, 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) {
this._doRandomEyeLook();
scheduleNext();
}
}, 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);
}
_resetEyeLook() {
const eyeLookShapes = [
'eyelookupleft', 'eyelookupright',
'eyelookdownleft', 'eyelookdownright',
'eyelookinleft', 'eyelookinright',
'eyelookoutleft', 'eyelookoutright'
];
eyeLookShapes.forEach(name => {
this.setIdleAnimation(name, 0, this.eyeLookParams.speed, 'easeInOutQuad');
});
}
// 随机表情控制
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;
}
_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;
}