Skip to content

Shader Effects

Olivier Biot edited this page May 14, 2026 · 3 revisions

Shader Effects

melonJS ships a library of WebGL post-processing effects that can be applied to any renderable (sprite, image layer, container, camera) without writing GLSL. You can also write your own effect by extending ShaderEffect.

All shader effects require the WebGL renderer. Under Canvas they are silently no-ops.

Built-in Effects

Each built-in effect is a class exported from melonjs. Construct it with a reference to the renderer and an options object, then attach it via addPostEffect() on a renderable, or viewport.addPostEffect() for a camera-wide effect.

Class What it does Common uses
BlurEffect Gaussian blur Depth-of-field, frosted UI
ChromaticAberrationEffect Per-channel offset CRT/glitch look
ColorMatrixEffect 4×5 matrix transform Custom color grading
DesaturateEffect Reduce saturation Death/pause overlays
DissolveEffect Animated noise dissolve Scene transitions
DropShadowEffect Offset shadow UI panels, sprites
FlashEffect Full-screen color flash Damage feedback
GlowEffect Soft halo outside the sprite Magic items, selection
HologramEffect Scanlines + edge glow Sci-fi projections
InvertEffect Invert RGB Stylistic shock
OutlineEffect Hard 1-pixel outline Highlight on hover
PixelateEffect Snap to coarse pixels Retro look, transitions
RadialGradientEffect Radial color gradient Lighting falloff
ScanlineEffect Horizontal scanlines CRT/arcade overlay
SepiaEffect Brown-tone conversion Flashback / "old film"
ShineEffect Sweeping highlight band Coins, gems, polished surfaces
TintPulseEffect Pulsing color overlay Status effects (poison, freeze)
VignetteEffect Darkened corners Cinematic framing, focus
WaveEffect Sinusoidal distortion Heat haze, water
Screenshot 2026-05-14 at 10 38 18

Applying an effect

import { ShineEffect, timer, event } from "melonjs";

// once the application is running, the renderer is ready
const shine = new ShineEffect(renderer, {
    color: [1.0, 0.95, 0.7],
    speed: 0.8,
    width: 0.18,
    intensity: 0.7,
});

// attach to a single sprite
coinSprite.addPostEffect(shine);

// or to the whole camera (affects every rendered pixel)
app.viewport.addPostEffect(shine);

// time-driven effects need a per-frame update
event.on(event.GAME_UPDATE, () => {
    shine.setTime(timer.getTime() / 1000.0);
});

Most effects accept their tunables both at construction (new XEffect(renderer, options)) and at runtime via setColor(...), setIntensity(...), setTime(...), etc. Check each class's docs for the exact setter list.

Writing a Custom Effect

Extend ShaderEffect and pass the GLSL body of an apply(color, uv) function — melonJS handles the rest of the vertex shader, the texture sampler, and the WebGL pipeline.

import { ShaderEffect } from "melonjs";

class StripesEffect extends ShaderEffect {
    constructor(renderer, options = {}) {
        super(
            renderer,
            `
            uniform vec3  uColor;
            uniform float uFrequency;
            uniform float uTime;
            vec4 apply(vec4 color, vec2 uv) {
                if (color.a == 0.0) return color;
                float stripe = step(0.5, fract(uv.y * uFrequency + uTime));
                return vec4(mix(color.rgb, uColor, stripe * 0.4), color.a);
            }
            `
        );
        this.setUniform("uColor", new Float32Array(options.color ?? [1.0, 0.0, 0.0]));
        this.setUniform("uFrequency", options.frequency ?? 8.0);
        this.setUniform("uTime", 0.0);
    }

    setTime(t) { this.setUniform("uTime", t); }
}

The apply contract

Every melonJS shader effect declares a fragment-stage function:

vec4 apply(vec4 color, vec2 uv);
  • color — the pre-sampled texture color at uv (RGBA, alpha-premultiplied on output)
  • uv — texture coordinates, [0, 1] over the sprite/region
  • The return value is the final pixel color (RGBA)

Useful builtins exposed by the engine's vertex stage:

Identifier Type Description
uSampler sampler2D The texture being drawn
vColor vec4 Vertex tint (sprite tint × per-vertex alpha)

Anything else you need (time, mouse position, hover state, …) you declare yourself with uniform and update via setUniform().

Setting uniforms

// scalar
shader.setUniform("uIntensity", 0.5);

// vector — wrap in Float32Array
shader.setUniform("uColor", new Float32Array([1.0, 0.5, 0.2]));

// the per-frame time pattern most effects use
event.on(event.GAME_UPDATE, () => {
    shader.setUniform("uTime", timer.getTime() / 1000.0);
});

Chaining effects

addPostEffect() can be called multiple times — effects run in the order they're added. For example, blur first, then a vignette over the blurred result:

mySprite.addPostEffect(new BlurEffect(renderer, { radius: 6.0 }));
mySprite.addPostEffect(new VignetteEffect(renderer, { intensity: 0.6 }));

Sharing one effect across many renderables

Effects are stateless per-renderable — one ShineEffect instance attached to N sprites runs the same shader for all of them, with one set of uniforms. That's usually what you want for grouped objects (every coin pulses in sync). If you need per-sprite phase offsets, give each sprite its own instance.

Best practices

  • Only sample within the sprite quad. uSampler only contains the sprite's texture region — coordinates outside [0, 1] (or the region's UV range) return clamped/wrapped values, not adjacent sprites.
  • Use vColor if you want sprite tint / alpha to flow through. Most effects do return result * vColor; somewhere.
  • Guard with if (color.a == 0.0) return color; for effects that should only act on visible pixels (avoids tinting transparent padding).
  • Animate via setUniform(), not by rebuilding the shader. Recreating a ShaderEffect triggers a WebGL program recompile.

Clone this wiki locally