723 lines
24 KiB
JavaScript
723 lines
24 KiB
JavaScript
// 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;
|
||
}
|