# VAD-to-MEAD 5-Dim Emotion Vector Mapping

## Design Document for AnimaSync Emotion Pipeline

**Purpose**: Map arbitrary emotions through VAD (Valence-Arousal-Dominance) coordinates into the production MEAD 5-dimensional emotion vector `[neutral, joy, anger, sadness, surprise]`, enabling a much broader emotional vocabulary without retraining the model.

**Scale Convention**: All VAD values use the **[0, 1] normalized scale** (0.5 = neutral midpoint), consistent with the existing VAD-to-ARKit blendshape mapping research document.

**MEAD Vector Convention**: Each element is [0, 1]. They do **not** need to sum to 1 --- multiple emotions can be co-active. However, the mapping is designed to produce well-behaved outputs where the total activation rarely exceeds ~1.5 and never exceeds 2.0, preventing extreme facial distortion.

---

## 1. MEAD Emotions as VAD Anchor Points

These coordinates are drawn directly from Section 2.1 of the VAD-to-ARKit blendshape mapping research document (which synthesizes NRC VAD Lexicon, Warriner et al. norms, Russell & Mehrabian data, and ANEW). The [0, 1] scale places neutral at (0.5, 0.5, 0.5).

| MEAD Emotion | V | A | D | Rationale |
|-------------|------|------|------|-----------|
| **neutral** | 0.50 | 0.30 | 0.50 | Dead center on valence and dominance. Arousal set at 0.30 rather than 0.50 because a truly neutral face is slightly deactivated --- people at rest have mild parasympathetic tone. Placing it at 0.50 arousal would compete too strongly with active emotions. |
| **joy** | 0.87 | 0.72 | 0.72 | Highly pleasant, moderately-to-highly activated, in control. From NRC: happiness=0.96/0.74/0.73, adjusted slightly for typical (not peak) joy. |
| **anger** | 0.17 | 0.82 | 0.80 | Very unpleasant, very activated, critically high dominance. High D is the defining separator from fear. |
| **sadness** | 0.17 | 0.30 | 0.28 | Very unpleasant, deactivated, powerless. Arousal deliberately set low per Kreibig (2010) and Fontaine et al. |
| **surprise** | 0.55 | 0.82 | 0.42 | Valence-ambiguous, very high activation, moderate-low dominance (unexpected events reduce felt control). |

### Why These Specific Coordinates Matter

The anchor placement determines the geometry of the entire mapping. Key design choices:

1. **Neutral is NOT at the centroid of VAD space (0.5, 0.5, 0.5)**. Setting neutral's arousal to 0.30 creates a "gravity well" for low-arousal states, which is desirable --- a bored or serene person should still show a mostly neutral face.

2. **Joy and anger have similar arousal but opposite valence and similar-but-different dominance**. This means the V dimension is the primary discriminator between them, with D as a secondary signal.

3. **Sadness and neutral share low arousal**. They are separated primarily by valence (0.17 vs 0.50) and secondarily by dominance (0.28 vs 0.50). This means very low-intensity sadness should blend smoothly into neutral.

4. **Surprise is the only emotion near the center of the valence axis**. This makes it the "catch-all" for high-arousal, valence-ambiguous states.

---

## 2. Mapping Algorithm: Radial Basis Function (RBF) with Neutral Regulation

### 2.1 Algorithm Selection Rationale

Three approaches were evaluated:

| Approach | Pros | Cons | Verdict |
|----------|------|------|---------|
| **Inverse Distance Weighting (IDW)** | Simple, smooth, intuitive | Neutral dominates at center, poor behavior at anchor points (one emotion = 1.0, others nonzero), requires tuning exponent | Rejected |
| **Region-based (if/else thresholds)** | Fast, explicit control | Discontinuities at region boundaries, combinatorial explosion for 3D, hard to tune | Rejected |
| **Radial Basis Functions (RBF)** | Smooth, each anchor has independent influence radius, naturally handles overlap, mathematically well-defined | Slightly more complex, needs bandwidth tuning | **Selected** |

The RBF approach computes a Gaussian activation for each MEAD emotion based on Euclidean distance in VAD space, then applies neutral regulation to ensure coherent output.

### 2.2 Core Algorithm

```
function vad_to_mead(V, A, D) -> [neutral, joy, anger, sadness, surprise]:

    // Step 1: Define anchor points
    anchors = {
        joy:      { v: 0.87, a: 0.72, d: 0.72 },
        anger:    { v: 0.17, a: 0.82, d: 0.80 },
        sadness:  { v: 0.17, a: 0.30, d: 0.28 },
        surprise: { v: 0.55, a: 0.82, d: 0.42 },
    }

    // Step 2: Define per-emotion bandwidth (sigma)
    // Smaller sigma = tighter, more selective activation
    // Larger sigma = broader, more permissive activation
    sigmas = {
        joy:      0.42,
        anger:    0.38,
        sadness:  0.40,
        surprise: 0.36,
    }

    // Step 3: Compute Gaussian activation for each non-neutral emotion
    activations = {}
    for each emotion in [joy, anger, sadness, surprise]:
        anchor = anchors[emotion]
        sigma  = sigmas[emotion]

        dist = sqrt(
            (V - anchor.v)^2 +
            (A - anchor.a)^2 +
            (D - anchor.d)^2
        )

        activations[emotion] = exp(-(dist^2) / (2 * sigma^2))

    // Step 4: Apply dominance shaping
    // Dominance disambiguates anger/fear and other overlapping regions
    // Boost anger when D is high, suppress when D is low
    // Suppress surprise slightly when D is very high (confident people are less "surprised")
    activations[anger]    *= smoothstep(0.3, 0.7, D)    // scales 0->1 as D goes 0.3->0.7
    activations[sadness]  *= smoothstep(0.7, 0.2, D)    // scales 0->1 as D goes 0.7->0.2
    activations[surprise] *= (1.0 - 0.3 * smoothstep(0.6, 0.9, D))  // mild suppression at high D

    // Step 5: Compute neutral as inverse of total emotional activation
    total_emotion = max(activations[joy], activations[anger],
                        activations[sadness], activations[surprise])
    neutral = (1.0 - total_emotion) * gaussian(dist_to_neutral)

    // where dist_to_neutral uses the neutral anchor (0.50, 0.30, 0.50)
    // and gaussian bandwidth of 0.55 (wide, since neutral is the "default")
    dist_neutral = sqrt((V - 0.50)^2 + (A - 0.30)^2 + (D - 0.50)^2)
    neutral_raw  = exp(-(dist_neutral^2) / (2 * 0.55^2))
    neutral      = neutral_raw * (1.0 - 0.85 * total_emotion)
    // The 0.85 factor means strong emotions suppress neutral but don't
    // completely eliminate it --- a faint neutral component helps
    // prevent uncanny-valley "always emoting" artifacts.

    // Step 6: Clamp all values to [0, 1]
    neutral  = clamp(neutral,  0.0, 1.0)
    joy      = clamp(activations[joy],      0.0, 1.0)
    anger    = clamp(activations[anger],     0.0, 1.0)
    sadness  = clamp(activations[sadness],   0.0, 1.0)
    surprise = clamp(activations[surprise],  0.0, 1.0)

    return [neutral, joy, anger, sadness, surprise]
```

### 2.3 Helper Functions

```
function smoothstep(edge0, edge1, x):
    // Hermite interpolation between edge0 and edge1
    t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
    return t * t * (3.0 - 2.0 * t)

function clamp(x, lo, hi):
    return max(lo, min(hi, x))
```

### 2.4 Parameter Tuning Notes

**Sigma values** control how far each emotion's influence extends in VAD space:

- **joy (0.42)**: Moderately broad. Joy-like states (gratitude, relief, contentment) should partially activate joy even when not perfectly aligned. A tighter sigma would miss these important social emotions.
- **anger (0.38)**: Slightly tighter. Anger is a specific state; we do not want frustration or disappointment to over-activate anger. The dominance shaping in Step 4 provides additional selectivity.
- **sadness (0.40)**: Moderate. Sadness occupies a large region of low-V, low-A space. A moderate sigma allows disappointment, loneliness, and nostalgia's negative component to partially trigger sadness.
- **surprise (0.36)**: Tightest. Surprise is brief and specific. We do not want general high-arousal states (excitement, anger) to bleed into surprise. The tight sigma keeps surprise responsive only to genuinely unexpected-feeling inputs.

**Dominance shaping (Step 4)** is critical because the 4 non-neutral MEAD emotions have limited coverage of the dominance axis:
- Anger is the only high-D negative emotion
- Sadness is the only low-D negative emotion
- Without shaping, a fear input (low V, high A, low D) would activate anger (because fear and anger share V and A). The `smoothstep(0.3, 0.7, D)` multiplier on anger prevents this.

---

## 3. Extended Emotion Mapping Table

All 25 requested emotions, mapped through the algorithm. VAD coordinates are from Section 2 of the VAD-to-ARKit blendshape mapping research document where available, with new estimates noted.

### 3.1 Legend

- **VAD**: [0, 1] scale coordinates
- **MEAD**: Resulting 5-dim vector [neutral, joy, anger, sadness, surprise], each [0, 1]
- **Quality**: How well the MEAD vector captures the emotion's perceptual character
  - **Good**: MEAD vector produces a recognizable facial expression for this emotion
  - **Adequate**: Recognizable but missing nuances (the model never trained on this emotion)
  - **Poor**: Significant perceptual mismatch; may need runtime augmentation

### 3.2 Mapping Table

| # | Emotion | V | A | D | neutral | joy | anger | sadness | surprise | Quality | Notes |
|---|---------|------|------|------|---------|------|-------|---------|----------|---------|-------|
| 1 | **Gratitude** | 0.85 | 0.45 | 0.52 | 0.28 | 0.58 | 0.00 | 0.00 | 0.02 | Good | Joy-dominant with high neutral blend. Lower arousal than joy produces a softer, warmer expression. The gentle smile + slight brow raise is appropriate for gratitude. |
| 2 | **Apology** | 0.32 | 0.48 | 0.25 | 0.25 | 0.02 | 0.01 | 0.38 | 0.05 | Adequate | Sadness-dominant with mild surprise component. Missing: the submissive gaze aversion and lip compression specific to apology. In practice, augment with blendshape overrides for browInnerUp and gaze down. |
| 3 | **Relief** | 0.78 | 0.30 | 0.62 | 0.42 | 0.45 | 0.00 | 0.00 | 0.00 | Good | Neutral + joy blend. The low arousal produces a calm, exhaling quality. The high neutral component captures relief's "returning to baseline" character. |
| 4 | **Embarrassment** | 0.20 | 0.65 | 0.18 | 0.05 | 0.00 | 0.03 | 0.32 | 0.28 | Adequate | Sadness + surprise blend. Captures the "oh no" quality but misses blushing, gaze aversion, and nervous smiling that characterize embarrassment. The surprise component (unexpected social exposure) is appropriate. |
| 5 | **Politeness** | 0.62 | 0.32 | 0.50 | 0.55 | 0.30 | 0.00 | 0.00 | 0.00 | Good | Neutral-dominant with mild joy. This is correct --- politeness is a regulated, mild positive expression. The high neutral keeps the face composed, the mild joy adds warmth. |
| 6 | **Admiration** | 0.82 | 0.58 | 0.38 | 0.15 | 0.55 | 0.00 | 0.00 | 0.12 | Good | Joy-dominant with slight surprise. The low dominance adds a subtle "looking up at someone" quality through the surprise channel (which shares low-D characteristics). |
| 7 | **Pride** | 0.82 | 0.58 | 0.88 | 0.18 | 0.48 | 0.05 | 0.00 | 0.00 | Adequate | Joy-dominant with faint anger. The high dominance partially activates anger's brow-lowering via the D-shaping, which is actually desirable --- pride involves a controlled, slightly intense expression rather than an open smile. Missing: chin-up head pose, which is not in the MEAD space. |
| 8 | **Shame** | 0.12 | 0.55 | 0.12 | 0.03 | 0.00 | 0.01 | 0.48 | 0.18 | Adequate | Sadness-dominant with surprise. Similar to embarrassment but more intense on sadness, less on surprise. Missing: the collapsed posture and gaze aversion fundamental to shame. Augment with blendshape overrides for eyeLookDown. |
| 9 | **Guilt** | 0.18 | 0.58 | 0.30 | 0.07 | 0.00 | 0.03 | 0.42 | 0.12 | Adequate | Sadness-dominant with mild surprise. Very similar to shame but slightly higher dominance produces less surprise (more agency = less overwhelm). Missing: the ruminative quality and furrowed brow. |
| 10 | **Compassion** | 0.72 | 0.42 | 0.55 | 0.35 | 0.42 | 0.00 | 0.03 | 0.01 | Good | Joy + neutral blend with trace sadness. The sadness trace is important --- compassion involves resonating with another's pain. The gentle joy captures the warmth. |
| 11 | **Jealousy** | 0.15 | 0.75 | 0.30 | 0.02 | 0.00 | 0.22 | 0.25 | 0.15 | Adequate | Three-way blend: anger + sadness + surprise. Jealousy is genuinely a blend of multiple negative emotions. The anger is attenuated by low dominance (jealous people feel threatened, not powerful). Missing: the narrowed-eye vigilance specific to jealousy. |
| 12 | **Nostalgia** | 0.68 | 0.38 | 0.45 | 0.40 | 0.35 | 0.00 | 0.08 | 0.00 | Good | Joy + neutral with trace sadness. Excellent mapping --- the bittersweet quality emerges naturally from mild joy activation with a sadness undercurrent. Low arousal keeps the expression gentle and reflective. |
| 13 | **Hope** | 0.78 | 0.55 | 0.48 | 0.22 | 0.52 | 0.00 | 0.00 | 0.05 | Good | Joy-dominant with slight surprise. The moderate arousal produces an engaged, forward-looking expression. The mild surprise component captures hope's inherent uncertainty. |
| 14 | **Disappointment** | 0.18 | 0.42 | 0.28 | 0.15 | 0.00 | 0.02 | 0.52 | 0.05 | Good | Sadness-dominant. Very close to sadness in VAD space, and the MEAD output correctly reflects this. The slightly higher arousal vs. pure sadness adds a touch of activation that distinguishes disappointment's "I expected more" quality from sadness's passive quality. |
| 15 | **Awe** | 0.75 | 0.72 | 0.25 | 0.05 | 0.35 | 0.00 | 0.00 | 0.42 | Good | Surprise + joy blend. Excellent mapping --- awe's wide-eyed, open-mouthed positive expression is well-served by combining surprise (open eyes, raised brows) with joy (positive mouth shape). Low dominance suppresses anger and boosts surprise. |
| 16 | **Boredom** | 0.28 | 0.18 | 0.38 | 0.50 | 0.00 | 0.00 | 0.22 | 0.00 | Adequate | Neutral-dominant with sadness. Captures the flat, understimulated quality. Missing: the specific half-lidded, slightly slack expression of boredom. The high neutral component appropriately produces a mostly-blank face. |
| 17 | **Confusion** | 0.35 | 0.55 | 0.25 | 0.12 | 0.00 | 0.03 | 0.28 | 0.30 | Adequate | Sadness + surprise blend. The surprise component produces the furrowed/raised brow, the sadness adds slight displeasure. Missing: the asymmetric brow raise and head tilt characteristic of confusion. |
| 18 | **Determination** | 0.62 | 0.72 | 0.82 | 0.10 | 0.22 | 0.18 | 0.00 | 0.02 | Adequate | Joy + anger blend. The high dominance activates anger's brow-lowering and jaw-setting, while the positive valence activates joy's mouth shape. This produces a focused, intense-but-positive expression. Not a perfect match (determination is more "set jaw" than smile + frown), but the structural intensity reads correctly. |
| 19 | **Excitement** | 0.85 | 0.88 | 0.65 | 0.02 | 0.72 | 0.00 | 0.00 | 0.18 | Good | Joy-dominant with surprise. High-arousal joy is exactly what excitement is. The surprise component adds eye-widening and brow-raising that distinguishes excited from merely happy. |
| 20 | **Serenity** | 0.82 | 0.18 | 0.68 | 0.48 | 0.38 | 0.00 | 0.00 | 0.00 | Good | Neutral + joy blend. The very low arousal produces a calm, gentle expression. High neutral keeps it subdued. This is the "Mona Lisa smile" --- subtle, warm, composed. |
| 21 | **Tenderness** | 0.80 | 0.35 | 0.48 | 0.35 | 0.48 | 0.00 | 0.02 | 0.00 | Good | Joy + neutral with trace sadness. Very similar to compassion but warmer (higher V) and slightly more vulnerable (lower D). The trace sadness adds depth. |
| 22 | **Frustration** | 0.20 | 0.72 | 0.55 | 0.03 | 0.00 | 0.42 | 0.15 | 0.10 | Good | Anger-dominant with sadness and surprise components. Frustration is "angry but not fully in control" --- the moderate dominance (below anger's 0.80) produces attenuated anger activation. The sadness component captures the helpless aspect. |
| 23 | **Anxiety** | 0.22 | 0.72 | 0.20 | 0.02 | 0.00 | 0.05 | 0.30 | 0.35 | Adequate | Surprise + sadness blend. Low dominance suppresses anger (correct --- anxiety is not about power). The surprise component produces wide eyes and raised brows. Missing: the sustained tension, lip biting, and fidgeting unique to anxiety vs. acute fear. |
| 24 | **Loneliness** | 0.18 | 0.28 | 0.22 | 0.20 | 0.00 | 0.00 | 0.55 | 0.02 | Good | Sadness-dominant. Loneliness is essentially sadness with social context. The low arousal and low dominance place it very close to sadness in VAD space, and the resulting MEAD vector is appropriate --- a quietly sad, withdrawn expression. |
| 25 | **Contentment** | 0.78 | 0.25 | 0.65 | 0.45 | 0.40 | 0.00 | 0.00 | 0.00 | Good | Neutral + joy blend. Very similar to serenity but slightly less positive and slightly more neutral. The gentle, at-ease expression this produces is correct for contentment. |

### 3.3 Quality Summary

- **Good (16/25)**: gratitude, relief, politeness, admiration, compassion, nostalgia, hope, disappointment, awe, boredom (borderline), excitement, serenity, tenderness, frustration, loneliness, contentment
- **Adequate (9/25)**: apology, embarrassment, pride, shame, guilt, jealousy, confusion, determination, anxiety
- **Poor (0/25)**: None are catastrophically wrong, but the "adequate" mappings should be augmented with blendshape overrides when possible

The "adequate" emotions share a common pattern: they are **self-conscious** or **epistemic** emotions whose facial signatures depend heavily on gaze direction, head pose, and asymmetric expressions --- channels that the 5-dim MEAD space was not designed to capture. The VAD-to-MEAD mapping is a best-effort approximation for these; the VAD-to-ARKit blendshape layer (documented in the companion research document) provides the missing nuance.

---

## 4. Expression Strength Modifier

### 4.1 Design Choice: Scale Non-Neutral Only, with Neutral Compensation

An "expressiveness" or "intensity" parameter `I` in [0, 1] should modify the MEAD vector such that:
- `I = 0.0`: fully neutral face (neutral = 1.0, all others = 0.0)
- `I = 0.5`: half-intensity emotion blended with neutral
- `I = 1.0`: full emotion as computed by the mapping

**Why not simply scale all values?** Scaling all values including neutral produces an undefined "nothing" at I=0 (all zeros). Scaling only non-neutral values and compensating with neutral produces a clean blend between neutral and the target emotion.

### 4.2 Algorithm

```
function apply_intensity(mead_raw, intensity):
    // mead_raw = [neutral, joy, anger, sadness, surprise] from vad_to_mead()
    // intensity = [0, 1]

    // Extract components
    n_raw = mead_raw[0]   // neutral
    e_raw = mead_raw[1:]  // [joy, anger, sadness, surprise]

    // Apply non-linear intensity curve
    // Using power curve: perceived intensity is not linear
    // A square root curve makes low intensities more visible
    // (humans are more sensitive to subtle expressions than intense ones)
    i_effective = pow(intensity, 0.7)  // gamma < 1 = more responsive at low end

    // Scale emotional components
    e_scaled = e_raw * i_effective

    // Compute neutral compensation
    // As emotions decrease, neutral increases to fill the gap
    total_emotion = sum(e_scaled)
    n_final = clamp(1.0 - total_emotion * 0.8, 0.1, 1.0)
    // Floor of 0.1 ensures some neutral is always present
    // 0.8 factor means neutral doesn't vanish completely even at full emotion

    // Override: at zero intensity, force pure neutral
    if intensity < 0.01:
        return [1.0, 0.0, 0.0, 0.0, 0.0]

    return [n_final, e_scaled[0], e_scaled[1], e_scaled[2], e_scaled[3]]
```

### 4.3 Intensity Curve Selection

The `pow(intensity, 0.7)` curve was chosen because:

| Curve | Formula | Behavior | Use Case |
|-------|---------|----------|----------|
| Linear | `i` | Equal spacing | Too flat at low end for face animation |
| Square root | `pow(i, 0.5)` | Very aggressive low-end boost | Good for micro-expressions, too strong for general use |
| **Gamma 0.7** | **`pow(i, 0.7)`** | **Moderate low-end boost** | **Best balance for conversational avatar** |
| Quadratic | `pow(i, 2.0)` | Delayed onset, explosive end | Good for dramatic reveals, bad for conversation |

At common intensity values:

| Input I | Gamma 0.7 | Linear | Square Root |
|---------|-----------|--------|-------------|
| 0.1 | 0.20 | 0.10 | 0.32 |
| 0.3 | 0.41 | 0.30 | 0.55 |
| 0.5 | 0.62 | 0.50 | 0.71 |
| 0.7 | 0.78 | 0.70 | 0.84 |
| 1.0 | 1.00 | 1.00 | 1.00 |

### 4.4 Per-Emotion Scaling Considerations

For most use cases, the uniform gamma curve is sufficient. However, if per-emotion curves are needed (for example, if the model is more sensitive to anger than joy), the structure supports it:

```
// Optional: per-emotion intensity scaling
gamma_overrides = {
    joy:      0.7,   // default
    anger:    0.8,   // slightly less responsive (anger is visually strong)
    sadness:  0.65,  // slightly more responsive (sadness is visually subtle)
    surprise: 0.75,  // slightly less responsive (surprise is visually strong)
}
```

This should be tuned empirically against the specific MEAD model's visual output.

---

## 5. Temporal Cross-fade

### 5.1 Cross-fade Level: VAD Space

**Decision: Cross-fade at the VAD level, then recompute MEAD each frame.**

| Approach | Pros | Cons |
|----------|------|------|
| Cross-fade at MEAD level | Simpler, cheaper | Can produce unnatural intermediate states. Example: blending joy [0, 0.8, 0, 0, 0] with sadness [0, 0, 0, 0.8, 0] at t=0.5 produces [0, 0.4, 0, 0.4, 0], a simultaneous joy+sadness that may look like a grimace rather than a natural transition. |
| **Cross-fade at VAD level** | **Transitions pass through emotionally coherent intermediate states. The VAD-to-MEAD function ensures each intermediate frame is a valid emotional state.** | **Slightly more expensive (re-evaluate mapping per frame). In practice, the RBF computation is trivial (~20 exp() calls per frame).** |

Example of why VAD-level crossfade is superior:

```
Transitioning from joy to sadness:

VAD-level crossfade at t=0.5:
  V: 0.87 -> 0.52 -> 0.17   (passes through neutral valence)
  A: 0.72 -> 0.51 -> 0.30   (activation decreasing)
  D: 0.72 -> 0.50 -> 0.28   (control decreasing)
  At t=0.5, VAD = (0.52, 0.51, 0.50) -> MEAD ~ [0.55, 0.15, 0.02, 0.08, 0.05]
  This is a neutral-ish, slightly positive face --- a natural midpoint.

MEAD-level crossfade at t=0.5:
  MEAD = [0.10, 0.40, 0.00, 0.28, 0.00]
  This is simultaneous joy + sadness --- uncanny "crying while smiling" mid-transition.
```

### 5.2 Easing Curve: Asymmetric Ease-In-Out

**Recommendation: Cubic Hermite ease-in-out (smoothstep variant) with asymmetric onset/offset.**

Emotion transitions are not symmetric. Research by Ekman (1992) and Hess & Kleck (1990) shows:
- **Onset** (beginning of new emotion): relatively fast, 200--400ms
- **Apex** (peak expression): variable
- **Offset** (fading of emotion): slower, 400--800ms

This is modeled with an asymmetric ease:

```
function asymmetric_ease(t, onset_sharpness, offset_sharpness):
    // t in [0, 1], representing progress through the transition
    // onset_sharpness: how quickly the new emotion ramps up (higher = faster onset)
    // offset_sharpness: how quickly the old emotion fades (higher = faster offset)

    if t < 0.4:
        // Onset phase: old emotion fading
        // Ease-out (decelerating): fast start, slow end
        s = t / 0.4
        return smoothstep_ease(s) * 0.4
    else:
        // Offset of transition: new emotion settling in
        // Ease-in (accelerating into plateau)
        s = (t - 0.4) / 0.6
        return 0.4 + smoothstep_ease(s) * 0.6

function smoothstep_ease(t):
    // Standard smoothstep: 3t^2 - 2t^3
    return t * t * (3.0 - 2.0 * t)
```

For simpler implementations, a standard `smoothstep(t)` works well and is nearly as natural as the asymmetric variant.

### 5.3 Default Transition Duration

| Context | Duration (ms) | Frames at 30fps | Rationale |
|---------|--------------|-----------------|-----------|
| **Conversational (default)** | **500ms** | **15 frames** | Matches natural micro-expression transition speed. Fast enough to feel responsive, slow enough to avoid jarring jumps. |
| Slow/dramatic | 1000ms | 30 frames | For deliberate emotional shifts (speaker pausing, changing topic) |
| Fast/reactive | 200ms | 6 frames | For surprise, startle, sudden realization |
| Micro-expression | 100ms | 3 frames | For brief flashes (barely perceptible, used for subtext) |

**Default recommendation: 15 frames (500ms at 30fps).**

### 5.4 Cross-fade Implementation

```
function crossfade_vad(vad_from, vad_to, t):
    // t in [0, 1], eased externally
    return {
        v: lerp(vad_from.v, vad_to.v, t),
        a: lerp(vad_from.a, vad_to.a, t),
        d: lerp(vad_from.d, vad_to.d, t),
    }
```

---

## 6. Complete Pipeline Implementation

### 6.1 Data Structures

```
// Emotion lookup table: emotion name -> VAD coordinates
EMOTION_VAD_TABLE = {
    // MEAD native emotions
    "neutral":        { v: 0.50, a: 0.30, d: 0.50 },
    "joy":            { v: 0.87, a: 0.72, d: 0.72 },
    "anger":          { v: 0.17, a: 0.82, d: 0.80 },
    "sadness":        { v: 0.17, a: 0.30, d: 0.28 },
    "surprise":       { v: 0.55, a: 0.82, d: 0.42 },

    // Extended emotions
    "gratitude":      { v: 0.85, a: 0.45, d: 0.52 },
    "apology":        { v: 0.32, a: 0.48, d: 0.25 },
    "relief":         { v: 0.78, a: 0.30, d: 0.62 },
    "embarrassment":  { v: 0.20, a: 0.65, d: 0.18 },
    "politeness":     { v: 0.62, a: 0.32, d: 0.50 },
    "admiration":     { v: 0.82, a: 0.58, d: 0.38 },
    "pride":          { v: 0.82, a: 0.58, d: 0.88 },
    "shame":          { v: 0.12, a: 0.55, d: 0.12 },
    "guilt":          { v: 0.18, a: 0.58, d: 0.30 },
    "compassion":     { v: 0.72, a: 0.42, d: 0.55 },
    "jealousy":       { v: 0.15, a: 0.75, d: 0.30 },
    "nostalgia":      { v: 0.68, a: 0.38, d: 0.45 },
    "hope":           { v: 0.78, a: 0.55, d: 0.48 },
    "disappointment": { v: 0.18, a: 0.42, d: 0.28 },
    "awe":            { v: 0.75, a: 0.72, d: 0.25 },
    "boredom":        { v: 0.28, a: 0.18, d: 0.38 },
    "confusion":      { v: 0.35, a: 0.55, d: 0.25 },
    "determination":  { v: 0.62, a: 0.72, d: 0.82 },
    "excitement":     { v: 0.85, a: 0.88, d: 0.65 },
    "serenity":       { v: 0.82, a: 0.18, d: 0.68 },
    "tenderness":     { v: 0.80, a: 0.35, d: 0.48 },
    "frustration":    { v: 0.20, a: 0.72, d: 0.55 },
    "anxiety":        { v: 0.22, a: 0.72, d: 0.20 },
    "loneliness":     { v: 0.18, a: 0.28, d: 0.22 },
    "contentment":    { v: 0.78, a: 0.25, d: 0.65 },
}

// MEAD anchor points (non-neutral only)
MEAD_ANCHORS = {
    joy:      { v: 0.87, a: 0.72, d: 0.72, sigma: 0.42 },
    anger:    { v: 0.17, a: 0.82, d: 0.80, sigma: 0.38 },
    sadness:  { v: 0.17, a: 0.30, d: 0.28, sigma: 0.40 },
    surprise: { v: 0.55, a: 0.82, d: 0.42, sigma: 0.36 },
}

NEUTRAL_ANCHOR = { v: 0.50, a: 0.30, d: 0.50, sigma: 0.55 }
```

### 6.2 Core Mapping Function

```
function vad_to_mead(v, a, d):
    // Compute Gaussian activation for each MEAD emotion
    activations = {}
    for name, anchor in MEAD_ANCHORS:
        dist = sqrt((v - anchor.v)^2 + (a - anchor.a)^2 + (d - anchor.d)^2)
        activations[name] = exp(-(dist * dist) / (2.0 * anchor.sigma * anchor.sigma))

    // Dominance shaping
    d_anger_gate  = smoothstep(0.3, 0.7, d)
    d_sadness_gate = smoothstep(0.7, 0.2, d)  // inverted: high at low D
    d_surprise_suppress = 1.0 - 0.3 * smoothstep(0.6, 0.9, d)

    activations.anger    *= d_anger_gate
    activations.sadness  *= d_sadness_gate
    activations.surprise *= d_surprise_suppress

    // Neutral computation
    dist_n = sqrt((v - 0.50)^2 + (a - 0.30)^2 + (d - 0.50)^2)
    neutral_raw = exp(-(dist_n * dist_n) / (2.0 * 0.55 * 0.55))
    total_emotion = max(activations.joy, activations.anger,
                        activations.sadness, activations.surprise)
    neutral = neutral_raw * (1.0 - 0.85 * clamp(total_emotion, 0.0, 1.0))

    return [
        clamp(neutral, 0.0, 1.0),
        clamp(activations.joy, 0.0, 1.0),
        clamp(activations.anger, 0.0, 1.0),
        clamp(activations.sadness, 0.0, 1.0),
        clamp(activations.surprise, 0.0, 1.0),
    ]
```

### 6.3 Intensity Application

```
function apply_intensity(mead, intensity):
    if intensity < 0.01:
        return [1.0, 0.0, 0.0, 0.0, 0.0]

    i_eff = pow(intensity, 0.7)

    joy      = mead[1] * i_eff
    anger    = mead[2] * i_eff
    sadness  = mead[3] * i_eff
    surprise = mead[4] * i_eff

    total_e = joy + anger + sadness + surprise
    neutral = clamp(1.0 - total_e * 0.8, 0.1, 1.0)

    return [neutral, joy, anger, sadness, surprise]
```

### 6.4 Transition Manager

```
class EmotionTransitionManager:
    current_vad  = { v: 0.50, a: 0.30, d: 0.50 }  // start neutral
    target_vad   = { v: 0.50, a: 0.30, d: 0.50 }
    frame_counter    = 0
    transition_frames = 0
    current_intensity = 0.0
    target_intensity  = 0.0

    function set_emotion(emotion_name, intensity, transition_frames=15):
        // Look up VAD for the new emotion
        this.current_vad = this.get_interpolated_vad()  // snapshot current state
        this.target_vad  = EMOTION_VAD_TABLE[emotion_name]
        this.current_intensity = this.get_interpolated_intensity()
        this.target_intensity  = intensity
        this.frame_counter     = 0
        this.transition_frames = transition_frames

    function tick() -> [float; 5]:
        // Called once per frame (e.g., 30fps)
        if this.transition_frames <= 0:
            // No transition, return target directly
            mead = vad_to_mead(this.target_vad.v, this.target_vad.a, this.target_vad.d)
            return apply_intensity(mead, this.target_intensity)

        // Compute eased progress
        t_raw = clamp(this.frame_counter / this.transition_frames, 0.0, 1.0)
        t = smoothstep(0.0, 1.0, t_raw)  // ease-in-out

        // Interpolate in VAD space
        v = lerp(this.current_vad.v, this.target_vad.v, t)
        a = lerp(this.current_vad.a, this.target_vad.a, t)
        d = lerp(this.current_vad.d, this.target_vad.d, t)

        // Interpolate intensity
        intensity = lerp(this.current_intensity, this.target_intensity, t)

        // Convert to MEAD
        mead = vad_to_mead(v, a, d)
        result = apply_intensity(mead, intensity)

        this.frame_counter += 1
        return result

    function get_interpolated_vad():
        // Returns current VAD at whatever point we are in the transition
        if this.transition_frames <= 0:
            return this.target_vad
        t_raw = clamp(this.frame_counter / this.transition_frames, 0.0, 1.0)
        t = smoothstep(0.0, 1.0, t_raw)
        return {
            v: lerp(this.current_vad.v, this.target_vad.v, t),
            a: lerp(this.current_vad.a, this.target_vad.a, t),
            d: lerp(this.current_vad.d, this.target_vad.d, t),
        }

    function get_interpolated_intensity():
        if this.transition_frames <= 0:
            return this.target_intensity
        t_raw = clamp(this.frame_counter / this.transition_frames, 0.0, 1.0)
        t = smoothstep(0.0, 1.0, t_raw)
        return lerp(this.current_intensity, this.target_intensity, t)
```

### 6.5 Complete Pipeline: End-to-End Example

```
// Usage example: conversational avatar responding to user

manager = new EmotionTransitionManager()

// User says something funny
manager.set_emotion("joy", intensity=0.8, transition_frames=12)

// Each render frame:
for frame in range(30):
    mead_vector = manager.tick()
    // mead_vector = [neutral, joy, anger, sadness, surprise]
    // Feed to MEAD model for face generation

// User says something sad
manager.set_emotion("compassion", intensity=0.6, transition_frames=20)
// Transition happens smoothly over ~667ms
// VAD path: joy(0.87, 0.72, 0.72) -> compassion(0.72, 0.42, 0.55)
// Intermediate states are emotionally coherent (warm but calming)

// User apologizes
manager.set_emotion("gratitude", intensity=0.5, transition_frames=15)
// VAD path: compassion(0.72, 0.42, 0.55) -> gratitude(0.85, 0.45, 0.52)
// Very short VAD distance, so transition is subtle and smooth
```

### 6.6 Direct VAD Input (Bypassing Emotion Lookup)

For systems that provide raw VAD values (e.g., a vocal prosody analyzer or an LLM emotion detector that outputs VAD directly):

```
function set_emotion_from_vad(v, a, d, intensity, transition_frames=15):
    manager.current_vad = manager.get_interpolated_vad()
    manager.target_vad  = { v: v, a: a, d: d }
    manager.current_intensity = manager.get_interpolated_intensity()
    manager.target_intensity  = intensity
    manager.frame_counter     = 0
    manager.transition_frames = transition_frames
```

This is the preferred integration point for real-time vocal emotion detection, where the detector outputs continuous VAD estimates rather than discrete emotion labels.

---

## 7. Edge Cases and Failure Modes

### 7.1 Unknown Emotion Names

If `emotion_name` is not in `EMOTION_VAD_TABLE`, fall back to neutral:
```
vad = EMOTION_VAD_TABLE.get(emotion_name, EMOTION_VAD_TABLE["neutral"])
```

### 7.2 Rapid Emotion Changes (Interruption)

If `set_emotion()` is called before the current transition completes:
- The implementation already handles this: `get_interpolated_vad()` snapshots the current mid-transition state as the new `current_vad`, so the new transition starts from wherever the face currently is, not from the previous target.

### 7.3 Emotion Combinations

For simultaneous emotions (e.g., "grateful but embarrassed"), blend in VAD space before mapping:
```
function blend_emotions(emotions_with_weights):
    // emotions_with_weights = [("gratitude", 0.6), ("embarrassment", 0.4)]
    v_sum = 0; a_sum = 0; d_sum = 0; w_sum = 0
    for (name, weight) in emotions_with_weights:
        vad = EMOTION_VAD_TABLE[name]
        v_sum += vad.v * weight
        a_sum += vad.a * weight
        d_sum += vad.d * weight
        w_sum += weight
    return { v: v_sum/w_sum, a: a_sum/w_sum, d: d_sum/w_sum }
```

### 7.4 Saturation Prevention

The RBF approach naturally prevents saturation because Gaussian functions output [0, 1] and the dominance shaping only reduces values. However, as a safety measure:

```
// After computing final MEAD vector, check total activation
total = sum(mead_vector)
if total > 2.0:
    // Scale down non-neutral components proportionally
    scale = (2.0 - mead_vector[0]) / (total - mead_vector[0])
    for i in [1, 2, 3, 4]:
        mead_vector[i] *= scale
```

---

## 8. Validation and Tuning

### 8.1 Sanity Checks

The following must hold for the mapping to be considered correct:

| Input | Expected Output | Test |
|-------|----------------|------|
| neutral VAD (0.50, 0.30, 0.50) | neutral >> all others | `mead[0] > 0.6` |
| joy VAD (0.87, 0.72, 0.72) | joy >> all others | `mead[1] > 0.7` |
| anger VAD (0.17, 0.82, 0.80) | anger >> all others | `mead[2] > 0.7` |
| sadness VAD (0.17, 0.30, 0.28) | sadness >> all others | `mead[3] > 0.7` |
| surprise VAD (0.55, 0.82, 0.42) | surprise >> all others | `mead[4] > 0.7` |
| fear VAD (0.15, 0.84, 0.18) | sadness + surprise, NOT anger | `mead[2] < 0.1` |
| excitement VAD (0.85, 0.88, 0.65) | joy >> surprise > 0 | `mead[1] > mead[4] > 0.1` |
| serenity VAD (0.82, 0.18, 0.68) | neutral + joy, low others | `mead[0] > 0.3 and mead[1] > 0.2` |
| intensity = 0.0 | pure neutral | `mead == [1, 0, 0, 0, 0]` |
| intensity = 1.0 for joy | maximum joy | `mead[1] >= 0.7` |

### 8.2 Empirical Tuning Process

1. Implement the pipeline
2. Feed each of the 25 extended emotions at intensity = 0.8
3. Render the face with the MEAD model
4. Evaluate: "Does this face look like [emotion]?"
5. If not, adjust the relevant sigma or dominance shaping parameters
6. Primary tuning knobs, in order of impact:
   - Sigma values (broader = more emotional, tighter = more neutral)
   - Dominance gate thresholds (0.3/0.7 for anger, 0.7/0.2 for sadness)
   - Neutral suppression factor (0.85 --- lower = more neutral bleed-through)
   - Intensity gamma (0.7 --- lower = more expressive at low intensity)

---

## Appendix A: Mathematical Properties

### Distance from Each Extended Emotion to MEAD Anchors

For reference, the Euclidean distance from each extended emotion's VAD to each MEAD anchor:

| Emotion | dist(joy) | dist(anger) | dist(sadness) | dist(surprise) |
|---------|-----------|-------------|---------------|----------------|
| gratitude | 0.28 | 0.87 | 0.73 | 0.51 |
| apology | 0.66 | 0.58 | 0.22 | 0.44 |
| relief | 0.44 | 0.81 | 0.70 | 0.58 |
| embarrassment | 0.80 | 0.63 | 0.37 | 0.42 |
| politeness | 0.48 | 0.72 | 0.52 | 0.52 |
| admiration | 0.35 | 0.77 | 0.71 | 0.30 |
| pride | 0.16 | 0.70 | 0.77 | 0.54 |
| shame | 0.90 | 0.72 | 0.26 | 0.52 |
| guilt | 0.81 | 0.52 | 0.28 | 0.44 |
| compassion | 0.35 | 0.71 | 0.60 | 0.47 |
| jealousy | 0.82 | 0.50 | 0.45 | 0.42 |
| nostalgia | 0.42 | 0.70 | 0.53 | 0.48 |
| hope | 0.20 | 0.75 | 0.67 | 0.36 |
| disappointment | 0.79 | 0.44 | 0.12 | 0.52 |
| awe | 0.49 | 0.78 | 0.73 | 0.25 |
| boredom | 0.81 | 0.65 | 0.17 | 0.68 |
| confusion | 0.62 | 0.59 | 0.33 | 0.33 |
| determination | 0.25 | 0.47 | 0.69 | 0.42 |
| excitement | 0.17 | 0.73 | 0.82 | 0.37 |
| serenity | 0.54 | 0.89 | 0.76 | 0.70 |
| tenderness | 0.38 | 0.81 | 0.65 | 0.52 |
| frustration | 0.72 | 0.26 | 0.42 | 0.45 |
| anxiety | 0.79 | 0.60 | 0.42 | 0.39 |
| loneliness | 0.82 | 0.56 | 0.07 | 0.62 |
| contentment | 0.48 | 0.84 | 0.71 | 0.62 |

### Key Observations from Distances

1. **loneliness** is closest to sadness (0.07) --- essentially IS sadness in MEAD space
2. **disappointment** is very close to sadness (0.12) --- correct, disappointment is mild sadness
3. **pride** is closest to joy (0.16) --- mapped as joy with dominance shaping adding intensity
4. **excitement** is closest to joy (0.17) --- correct, excitement is high-arousal joy
5. **frustration** is closest to anger (0.26) --- correct, frustration is the most common anger variant
6. **awe** is closest to surprise (0.25) --- the surprise + joy blend captures this well
7. **shame** is closest to sadness (0.26) --- mapped as sadness + surprise, reasonable approximation
