8 GLSL Shader Systems in 6 Months: Building Elemental Effects for a Character Animation Engine

Published: (March 11, 2026 at 03:44 PM EDT)
2 min read
Source: Dev.to

Source: Dev.to

What This Is

Emotive Engine is an open‑source project.
For v3.4.1 I wrote 8 elemental shader systems from scratch: fire, water, ice, …
The point was to prove that the architecture repeats – every element follows the same pattern.

Live Demo

[All 161 gestures]

Element Architecture

Every element type in the system consists of exactly 5 components:

ComponentDescription
Factory (fireEffectFactory.js)Takes a config object and returns a gesture with a 3d.evaluate() function that drives transforms per frame.
Instanced Material (InstancedFireMaterial.js)A THREE.ShaderMaterial with custom GLSL. GPU‑instanced so all elements of one type render in a single draw call.
Overlay Shader (FireMaterial.js)Paints directly onto the mascot mesh. Uses the same visual language as the instanced material but is mapped to the character’s geometry.
Gesture Configs (one file per gesture)Declarative objects: duration, spawn mode, model list, cutout pattern, grain, atmospheric preset.
Registration (ElementRegistrations.js)Wires everything into the ElementTypeRegistry: models, materials, update hooks, bloom thresholds.

Example: Fire Registration

ElementTypeRegistry.register('fire', {
    basePath: 'models/Elements/Fire/',
    models: [
        'flame-wisp.glb',
        'flame-tongue.glb',
        'ember-cluster.glb',
        'fire-burst.glb',
        'flame-ring.glb',
    ],
    createMaterial: createInstancedFireMaterial,
    updateMaterial: updateInstancedFireMaterial,
    setShaderAnimation: setInstancedFireArcAnimation,
    resetShaderAnimation: resetFireAnimation,
    setGestureGlow: setInstancedFireGestureGlow,
    setCutout: setInstancedFireCutout,
    resetCutout: resetFireCutout,
    setGrain: setFireGrain,
    resetGrain: resetFireGrain,
    // … more hooks
});

Note: Every element has this exact same shape. The ElementInstancedSpawner never calls registry.createMaterial() or registry.updateMaterial() directly; the element‑specific GLSL handles the rest.

Fire Shader Details

Naïve Additive Approach

// color = noise, alpha = noise

Problem: The fire looks muddy because color and alpha are coupled.

Fixed Approach – Decouple Color from Alpha

// Color ramp always in warm‑to‑hot range (0.5‑1.0)
// Noise shifts hue (orange → yellow‑white) but color is NEVER dark/brown.
float colorIntensity = 0.5 + localIntensity * 0.5;
vec3 color = fireColor(colorIntensity, uTemperature, edgeFactor);

// Alpha drives visibility with a wide smoothstep
float alpha = smoothstep(0.1, 0.85, localIntensity) * uOpacity;

// No floor color needed. Low‑noise areas are invisible via alpha,
// not dark via color.
if (alpha   

License

MIT licensed – PRs welcome.

  • npm: npm install @joshtol/emotive-engine
  • CodePen collection: Emotive Engine — Elemental Shaders
0 views
Back to Blog

Related posts

Read more »