import { useConvaiClient } from '@convai/web-sdk';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { useGLTF } from '@react-three/drei';
function CharacterController() {
const convaiClient = useConvaiClient({<ConvaiConfig>});
const characterRef = useRef<THREE.SkinnedMesh | null>(null);
const startTimeRef = useRef<number>(0);
const { scene } = useGLTF('/character.glb');
useEffect(() => {
// 在加载的模型中查找面部 SkinnedMesh
scene.traverse((child) => {
if (child instanceof THREE.SkinnedMesh && child.morphTargetInfluences) {
if (child.name.toLowerCase().includes('head') ||
child.name.toLowerCase().includes('face') ||
child.morphTargetInfluences.length > 50) {
characterRef.current = child;
}
}
});
}, [scene]);
// 跟踪机器人何时开始说话
useEffect(() => {
const unsubscribe = convaiClient.on('speakingChange', (isSpeaking: boolean) => {
if (isSpeaking) {
startTimeRef.current = performance.now();
}
});
return unsubscribe;
}, [convaiClient]);
useEffect(() => {
if (!convaiClient.state.isConnected) return;
let animationId: number;
const animate = () => {
const queue = convaiClient.blendshapeQueue;
if (queue.hasFrames() && queue.isConversationActive()) {
// 计算机器人开始说话以来经过的时间
const elapsedTime = (performance.now() - startTimeRef.current) / 1000;
// 根据经过时间获取帧(与音频同步)
const result = queue.getFrameAtTime(elapsedTime);
if (result && characterRef.current?.morphTargetInfluences) {
applyBlendshapesToCharacter(result.frame, characterRef.current.morphTargetInfluences);
}
}
animationId = requestAnimationFrame(animate);
};
animate();
return () => cancelAnimationFrame(animationId);
}, [convaiClient.state.isConnected]);
return <primitive object={scene} />;
}
// 辅助函数:将 blendshape 应用到角色
function applyBlendshapesToCharacter(frame: Float32Array, influences: number[]) {
const maxIndex = Math.min(frame.length, influences.length);
for (let i = 0; i < maxIndex; i++) {
influences[i] = frame[i];
}
}