Avatar Blendshapes (Morph Targets)¶
Overview¶
The AICO avatar uses Ready Player Me with ARKit-compatible blendshapes (morph targets) for facial expressions and eye gaze control. These work independently from skeletal animations, allowing real-time facial control during idle, talking, and other body animations.
Avatar Configuration¶
Current Avatar URL:
File Location:
File Size: ~3x larger than base avatar (ARKit adds 52 morph targets)
Available Blendshapes (52 Total)¶
Eye Gaze Control (8 morph targets)¶
These control where the avatar looks. Values range from 0.0 (no influence) to 1.0 (full influence).
| Morph Target | Description | Usage Example |
|---|---|---|
eyeLookUpLeft |
Left eye looks up | influences[dict['eyeLookUpLeft']] = 0.5 |
eyeLookUpRight |
Right eye looks up | influences[dict['eyeLookUpRight']] = 0.5 |
eyeLookDownLeft |
Left eye looks down | influences[dict['eyeLookDownLeft']] = 0.4 |
eyeLookDownRight |
Right eye looks down | influences[dict['eyeLookDownRight']] = 0.4 |
eyeLookInLeft |
Left eye looks inward (toward nose) | influences[dict['eyeLookInLeft']] = 0.3 |
eyeLookInRight |
Right eye looks inward (toward nose) | influences[dict['eyeLookInRight']] = 0.3 |
eyeLookOutLeft |
Left eye looks outward (away from nose) | influences[dict['eyeLookOutLeft']] = 0.3 |
eyeLookOutRight |
Right eye looks outward (away from nose) | influences[dict['eyeLookOutRight']] = 0.3 |
Current Implementation:
- Camera at Y=0.85m (chest level), eyes at Y=1.6m (eye level)
- Avatar uses eyeLookDownLeft/Right at 0.4 influence for natural eye contact with camera
- Applied every frame in applyEyeGaze() function
Eye Expressions (6 morph targets)¶
| Morph Target | Description | Typical Range |
|---|---|---|
eyeBlinkLeft |
Left eye blink | 0.0 - 1.0 |
eyeBlinkRight |
Right eye blink | 0.0 - 1.0 |
eyeSquintLeft |
Left eye squint | 0.0 - 0.7 |
eyeSquintRight |
Right eye squint | 0.0 - 0.7 |
eyeWideLeft |
Left eye wide open (surprise) | 0.0 - 0.8 |
eyeWideRight |
Right eye wide open (surprise) | 0.0 - 0.8 |
Eyebrow Control (5 morph targets)¶
| Morph Target | Description | Typical Range |
|---|---|---|
browDownLeft |
Left eyebrow down (frown/concern) | 0.0 - 0.6 |
browDownRight |
Right eyebrow down (frown/concern) | 0.0 - 0.6 |
browInnerUp |
Inner eyebrows up (sadness/concern) | 0.0 - 0.7 |
browOuterUpLeft |
Left outer eyebrow up (surprise) | 0.0 - 0.8 |
browOuterUpRight |
Right outer eyebrow up (surprise) | 0.0 - 0.8 |
Mouth Expressions (24 morph targets)¶
Basic Mouth Shapes¶
| Morph Target | Description | Typical Range |
|---|---|---|
jawOpen |
Open jaw (talking, surprise) | 0.0 - 0.7 |
mouthClose |
Close mouth tightly | 0.0 - 0.5 |
mouthFunnel |
Mouth funnel shape (O shape) | 0.0 - 0.8 |
mouthPucker |
Pucker lips (kiss shape) | 0.0 - 0.7 |
Smiles and Frowns¶
| Morph Target | Description | Typical Range |
|---|---|---|
mouthSmileLeft |
Left side smile | 0.0 - 1.0 |
mouthSmileRight |
Right side smile | 0.0 - 1.0 |
mouthFrownLeft |
Left side frown | 0.0 - 0.7 |
mouthFrownRight |
Right side frown | 0.0 - 0.7 |
Mouth Movement¶
| Morph Target | Description | Typical Range |
|---|---|---|
mouthLeft |
Mouth moves left | 0.0 - 0.5 |
mouthRight |
Mouth moves right | 0.0 - 0.5 |
mouthUpperUpLeft |
Left upper lip up | 0.0 - 0.6 |
mouthUpperUpRight |
Right upper lip up | 0.0 - 0.6 |
mouthLowerDownLeft |
Left lower lip down | 0.0 - 0.6 |
mouthLowerDownRight |
Right lower lip down | 0.0 - 0.6 |
mouthStretchLeft |
Stretch mouth left | 0.0 - 0.5 |
mouthStretchRight |
Stretch mouth right | 0.0 - 0.5 |
Lip Control¶
| Morph Target | Description | Typical Range |
|---|---|---|
mouthRollLower |
Roll lower lip inward | 0.0 - 0.6 |
mouthRollUpper |
Roll upper lip inward | 0.0 - 0.6 |
mouthShrugLower |
Lower lip shrug | 0.0 - 0.5 |
mouthShrugUpper |
Upper lip shrug | 0.0 - 0.5 |
mouthPressLeft |
Press left side of mouth | 0.0 - 0.5 |
mouthPressRight |
Press right side of mouth | 0.0 - 0.5 |
mouthDimpleLeft |
Left dimple | 0.0 - 0.7 |
mouthDimpleRight |
Right dimple | 0.0 - 0.7 |
Jaw Control (3 morph targets)¶
| Morph Target | Description | Typical Range |
|---|---|---|
jawForward |
Jaw moves forward | 0.0 - 0.5 |
jawLeft |
Jaw moves left | 0.0 - 0.4 |
jawRight |
Jaw moves right | 0.0 - 0.4 |
Cheek Control (3 morph targets)¶
| Morph Target | Description | Typical Range |
|---|---|---|
cheekPuff |
Puff both cheeks | 0.0 - 0.8 |
cheekSquintLeft |
Left cheek squint (smile) | 0.0 - 0.6 |
cheekSquintRight |
Right cheek squint (smile) | 0.0 - 0.6 |
Nose Control (2 morph targets)¶
| Morph Target | Description | Typical Range |
|---|---|---|
noseSneerLeft |
Left nostril flare (disgust) | 0.0 - 0.5 |
noseSneerRight |
Right nostril flare (disgust) | 0.0 - 0.5 |
Tongue (1 morph target)¶
| Morph Target | Description | Typical Range |
|---|---|---|
tongueOut |
Stick tongue out | 0.0 - 1.0 |
Code Usage¶
Accessing Morph Targets¶
// Find meshes with morph targets
let eyeMeshes = [];
avatar.traverse((node) => {
if (node.isMesh && node.morphTargetInfluences) {
eyeMeshes.push(node);
}
});
// Apply morph target by name
eyeMeshes.forEach(mesh => {
const dict = mesh.morphTargetDictionary;
const influences = mesh.morphTargetInfluences;
// Set specific morph target
if (dict['mouthSmileLeft'] !== undefined) {
influences[dict['mouthSmileLeft']] = 0.8; // 80% smile
}
});
Affected Meshes¶
The avatar has 4 meshes with morph targets: 1. EyeLeft - All 52 morph targets 2. EyeRight - All 52 morph targets 3. Wolf3D_Head - All 52 morph targets 4. Wolf3D_Teeth - All 52 morph targets
Important: Apply morph targets to ALL meshes for consistent facial expressions.
Integration with Animations¶
Morph targets work independently from skeletal animations: - Skeletal animations (idle, talking) control body and head bones - Morph targets control facial mesh deformations - Both can run simultaneously without conflict
Best Practice: Apply morph targets after animation mixer updates:
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// Update skeletal animations first
if (mixer) {
mixer.update(delta);
}
// Apply morph targets after (overrides animation facial expressions)
applyFacialExpressions();
renderer.render(scene, camera);
}
Common Expression Combinations¶
Happiness¶
influences[dict['mouthSmileLeft']] = 0.8;
influences[dict['mouthSmileRight']] = 0.8;
influences[dict['cheekSquintLeft']] = 0.5;
influences[dict['cheekSquintRight']] = 0.5;
influences[dict['browOuterUpLeft']] = 0.3;
influences[dict['browOuterUpRight']] = 0.3;
Sadness¶
influences[dict['mouthFrownLeft']] = 0.6;
influences[dict['mouthFrownRight']] = 0.6;
influences[dict['browInnerUp']] = 0.7;
influences[dict['browDownLeft']] = 0.4;
influences[dict['browDownRight']] = 0.4;
Surprise¶
influences[dict['eyeWideLeft']] = 0.8;
influences[dict['eyeWideRight']] = 0.8;
influences[dict['browOuterUpLeft']] = 0.8;
influences[dict['browOuterUpRight']] = 0.8;
influences[dict['jawOpen']] = 0.5;
influences[dict['mouthFunnel']] = 0.4;
Anger¶
influences[dict['browDownLeft']] = 0.6;
influences[dict['browDownRight']] = 0.6;
influences[dict['eyeSquintLeft']] = 0.5;
influences[dict['eyeSquintRight']] = 0.5;
influences[dict['mouthFrownLeft']] = 0.4;
influences[dict['mouthFrownRight']] = 0.4;
influences[dict['jawForward']] = 0.3;
Thinking/Concentration¶
influences[dict['eyeSquintLeft']] = 0.3;
influences[dict['eyeSquintRight']] = 0.3;
influences[dict['browDownLeft']] = 0.3;
influences[dict['browDownRight']] = 0.3;
influences[dict['mouthPucker']] = 0.2;
Current Implementation¶
Natural Blinking ✅ Implemented¶
Realistic blink behavior using eyeBlinkLeft and eyeBlinkRight:
- Duration: 180ms total (scientifically accurate)
- Timing: Random intervals 2-6 seconds
- Curve: Asymmetric (fast close 54ms, pause 18ms, slow open 108ms)
- Implementation: startBlinking() in viewer.js
Emotion Expression System ✅ Implemented¶
12 canonical emotions mapped to ARKit blend shapes:
- Emotions: neutral, calm, curious, playful, warm_concern, protective, focused, encouraging, reassuring, apologetic, tired, reflective
- Transition: Smooth 5% interpolation per frame (~1-2 seconds)
- Integration: Auto-syncs with EmotionProvider (2s polling)
- Implementation: emotionPresets and applyEmotionExpression() in viewer.js
Eye Gaze Control ✅ Implemented¶
Natural eye contact using eyeLookDownLeft/Right:
- Direction: 30% downward gaze for warm eye contact
- Application: Applied every frame after animation updates
- Implementation: applyEyeGaze() in viewer.js
Animation Variation System ✅ Implemented¶
Dynamic idle animations with natural variations:
- Base: idle.glb plays most of the time
- Variations: 7 variations (idle_var1.glb - idle_var7.glb)
- Timing: Random intervals 3-10 seconds
- Logic: No same variation twice in a row
- Transitions: 0.5s crossfade between animations
- Implementation: loadAnimationGroup() and variation system in viewer.js
Future Use Cases¶
Lip Sync for Speech 🚧 Planned¶
Use mouth morph targets synchronized with audio phonemes:
- A/E sounds: jawOpen, mouthFunnel
- O sounds: mouthPucker, mouthFunnel
- M/B/P sounds: mouthClose, mouthPressLeft/Right
- F/V sounds: mouthRollLower, mouthUpperUpLeft/Right
User Interaction Feedback 🚧 Planned¶
Visual feedback for user actions: - User speaks → avatar shows listening expression - Processing → thinking expression - Response ready → slight smile, eye contact
Performance Considerations¶
- Morph Target Count: 52 per mesh × 4 meshes = 208 total influences
- Update Frequency: Every frame (60 FPS)
- Performance Impact: Minimal on modern hardware
- File Size Impact: ~3x larger GLB file (worth it for expressiveness)
References¶
Version History¶
- 2025-11-23: Added emotion expression system (12 canonical emotions), natural blinking (180ms asymmetric), animation variation system
- 2025-11-17: Initial documentation with full ARKit blendshape set
- Avatar ID:
6918f89f132e61458ce01a74 - Implementation: Eye gaze control, blinking, emotion expressions, idle variations