-
-
Notifications
You must be signed in to change notification settings - Fork 663
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.
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 |
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.
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); }
}Every melonJS shader effect declares a fragment-stage function:
vec4 apply(vec4 color, vec2 uv);-
color— the pre-sampled texture color atuv(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().
// 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);
});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 }));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.
-
Only sample within the sprite quad.
uSampleronly contains the sprite's texture region — coordinates outside[0, 1](or the region's UV range) return clamped/wrapped values, not adjacent sprites. -
Use
vColorif you want sprite tint / alpha to flow through. Most effects doreturn 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 aShaderEffecttriggers a WebGL program recompile.