Layer Blend Modes
The ETextAnimationBlendMode enum defines how an FPerLayerCurves evaluation result merges into FGlyphAnimationState. Blending executes at channel granularity inside FPerLayerCurves::Evaluate; per-layer combination happens in FBakedGlyph::Evaluate.
The Four Blend Modes
Additive
Accumulates the current layer’s channel values onto the existing state:
OutState.Channel += LayerResult.Channel
Use cases:
- Shake stacking (position shake + rotation shake)
- Combining multiple animations (base translation animation + extra wave animation)
// Base layer sets the position, overlay layer adds random shake
BaseLayer: PositionOffset.X = 100 → 200 // left to right
ShakeLayer: PositionOffset.X = -5 → +5 // Additive → actual position = 95 → 205
Override
Directly replaces the existing state with the current layer’s channel values (commonly used for the base layer):
OutState.Channel = LayerResult.Channel
Use cases:
- The first layer sets the baseline transform (
CreateLayer(..., Override, ...)is the default implementation’s base layer mode) - A layer needs absolute control over specific channels
Multiply
Multiplies the current layer’s channel values with the existing state:
OutState.Channel *= LayerResult.Channel
Use cases:
- Intensity modulation (global opacity envelope)
- Fade in/out control (channel value 1.0 has no effect, 0.0 fully eliminates)
CrossFade
OutState.Channel = Lerp(OutState.Channel, LayerResult.Channel, 0.5f)
Per-Channel Evaluation Semantics
Internal flow of FPerLayerCurves::Evaluate(float LocalTime, FGlyphAnimationState& InOutState):
- Check all 21 channels one by one: empty channels (
Keys.Num() == 0) are skipped directly, preserving the existingInOutStatevalues - Non-empty channels temporarily build an
FRichCurveand interpolate atLocalTime - Combine the value into
InOutStateaccording to this layer’sBlendMode:
for Channel in 21 non-empty channels:
Value = CurveEval(Channel, LocalTime)
switch BlendMode:
Additive: InOutState[Channel] += Value
Override: InOutState[Channel] = Value
Multiply: InOutState[Channel] *= Value
CrossFade: InOutState[Channel] = Lerp(Out, Value, 0.5f)
NOTE
No cross-layer contamination: Layer[0] has ScaleX but no Opacity, Layer[1] has Opacity but no ScaleX → final ScaleX comes from Layer[0], Opacity from Layer[1].
Special Rules for Tint Channels
The identity value of the Color / ShadowColor channels is Transparent (0,0,0,0). Applying the multiply rule (0 × anything = 0) would permanently tint black, so tint channels use additive stacking (Base.Color += Layer.Color, consistent with the bake pipeline). Multi-layer tinting controls intensity through the A channel (tint share).
NOTE
Tint rendering uses Pigment-Based Mixing (PBM) physical pigment mixing (
ApplyTint) — a real-time RGB pigment mixing algorithm built on Kubelka–Munk (K–M) two-flux theory with constants reference-calibrated against Mixbox 2.0: yellow + blue makes green, complementary pairs mix into muddy colors (olive/brown/dark purple) instead of gray, and deep colors lightened with white stay saturated. No look-up tables, O(1) constant time.
Multi-Layer Blend Example
Layer 0: Override — set baseline position, scale, color
Layer 1: Additive — add small position and rotation shake
Layer 2: Multiply — global opacity envelope
Layer 3: Additive — shadow offset animation
FBakedGlyph::Evaluate starts from FGlyphAnimationState::Identity() and combines layers in index order; FactoryOverride is applied once after all layers are blended.
Design Recommendations
TIP
The following recommendations help organize layer blending effectively:
- Use Override for the first layer to establish baseline values, avoiding accidental accumulation
- Use Additive for middle layers to stack auxiliary animations for independent adjustment
- Use Multiply for the last layer for global modulation such as uniform fades
- Watch out for multiply/divide traps on tint layers: the tint identity is (0,0,0,0), so Multiply/Divide on tint produces wrong results — use Additive stacking or control via the A channel (share)