-
Notifications
You must be signed in to change notification settings - Fork 1
Avatar, Animation, Lipsync ‐ General
This page details the animation system used in our application for avatar animations, including full-body animations and lip-sync. It covers the technical aspects of the implementation, how animations are loaded and applied, and how to add custom animations.
- Overview
- Key Components
- Animation Loading and Application
- Blending and Transitioning
- Lip-Sync Animation
- Performance Considerations
- Adding Custom Animations
- Known Issues and Solutions
Our animation system primarily uses Three.js for handling 3D animations. It supports both full-body animations and lip-sync, with the ability to blend between different animations smoothly. The system is designed to be performant, allowing for multiple animation previews simultaneously.
- Avatar.tsx: Main component for avatar rendering and animation.
- AnimationPreview.tsx: Component for previewing individual animations.
- LipSyncControlPanel.tsx: Handles lip-sync animations based on audio input.
Animations are loaded using the GLTFLoader from Three.js. Here's a basic example of how animations are loaded and applied:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
const loader = new GLTFLoader();
loader.load(animationUrl, (gltf) => {
const animationClip = gltf.animations[0];
const action = mixer.clipAction(animationClip);
action.play();
});
The system supports smooth blending between animations using Three.js' built-in crossFadeTo method:
if (currentActionRef.current && currentActionRef.current !== newAction) {
newAction.reset();
newAction.play();
currentActionRef.current.crossFadeTo(newAction, 0.5, true);
currentActionRef.current = newAction;
}
This allows for seamless transitions between different animations, such as from an action back to an idle state.
Lip-sync is implemented using blend shapes (morph targets). The system uses a mapping from visemes to blend shapes:
const visemeToBlendShape: { [key: string]: string[] } = {
'X': ['viseme_sil', 'viseme_sil'],
'A': ['viseme_PP', 'viseme_PP'],
// ... other mappings ...
};
Lip-sync animation is performed by interpolating between different visemes based on audio cues:
const animateLipsync = (delta: number) => {
// ... find current viseme based on audio time ...
const shapes = Object.keys(targetValuesRef.current);
const values = shapes.map(shape => {
const current = currentValuesRef.current[shape] || 0;
const target = targetValuesRef.current[shape] || 0;
return THREE.MathUtils.lerp(current, target, lerpFactorRef.current);
});
setBlendShapes(shapes, values);
};
The animation system is designed to be performant, allowing for multiple animation previews to run simultaneously. This is achieved by:
- Using efficient Three.js methods for animation mixing and updating.
- Updating animations in the
useFrame
hook to sync with the render loop. - Optimizing blend shape calculations and applications.
Custom animations can be added to the system in two formats:
- .glb: Preferred format for Ready Player Me avatars. Higher compatibility and fewer issues.
- .fbx: Can be directly added from Mixamo, but may cause compatibility issues.
To add a new animation:
- Place the animation file in the appropriate directory (e.g.,
/public/animations/
). - Load the animation in your component:
loader.load('/animations/your-new-animation.glb', (gltf) => {
const newAnimation = gltf.animations[0];
const action = mixer.clipAction(newAnimation);
action.play();
});
-
Rotation Issue with Mixed Formats:
- Issue: Mixing .fbx and .glb animations can cause brief undesired rotations.
- Solution: Fully reset the avatar to an idle or neutral state before transitioning to the next animation.
Example implementation:
const resetAndPlayAnimation = (newAnimationUrl: string) => { // First, crossfade to idle animation idleActionRef.current!.play(); currentActionRef.current!.crossFadeTo(idleActionRef.current!, 0.5, true); // After reset, load and play new animation setTimeout(() => { loader.load(newAnimationUrl, (gltf) => { const newAnimation = gltf.animations[0]; const newAction = mixer.clipAction(newAnimation); newAction.play(); idleActionRef.current!.crossFadeTo(newAction, 0.5, true); }); }, 500); // Adjust timeout as needed };
-
Teeth Animation:
- The
Wolf3D_Teeth
mesh uses a separate 'mouthOpen' morph target instead of individual visemes. - Ensure the 'mouthOpen' morph target is properly set in the
setBlendShapes
function. - As the teeth visemes are somewhat missing some of the animations, it is recommended to target them separately and applying desired amplitudes to 'mouthOpen'
- The