Skip to main content
GameDev.net gamedev.net

Hit Flash and Damage Tint

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Damage feedback has to be legible in a single frame, at small size, while the screen is busy. That makes it a design problem more than a maths one, and two things carry it: a hard impulse rather than a fade, and an outline that reads against a background the same colour as the character.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image5
1 uniform float uAmount; // @param 0.0..1.0 = 0.85 "Flash amount"
2 uniform vec3 uTint; // @param 0.0..1.0 = 1.0, 0.30, 0.26 color "Damage tint"
3 uniform float uOutline; // @param 0.0..0.05 = 0.018 "Outline width"
4 uniform bool uAnimate; // @param = 0 "Replay the hit"
5
6 float spriteAlpha(vec2 uv) {
7 return texture(iChannel0, uv).a;
8 }
9
10 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
11 vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
12 vec2 uv = p + 0.5;
13
14 // A hit is an impulse, not a state: full brightness immediately, then a
15 // fast decay. Sampling that curve is what makes the effect read as impact
16 // rather than as a tinted sprite.
17 float envelope = uAnimate ? exp(-fract(iTime * 0.7) * 6.5) : 1.0;
18 float flash = uAmount * envelope;
19
20 vec4 sprite = texture(iChannel0, uv);
21
22 // Outline from the sprite's own alpha: the largest neighbouring alpha in a
23 // ring around a transparent fragment is how far it is from the silhouette.
24 float ring = 0.0;
25 for (int i = 0; i < 8; i++) {
26 float angle = float(i) * 0.7853982;
27 ring = max(ring, spriteAlpha(uv + vec2(cos(angle), sin(angle)) * uOutline));
28 }
29 float outline = clamp(ring - sprite.a, 0.0, 1.0) * flash;
30
31 vec3 backdrop = mix(vec3(0.04, 0.05, 0.08), vec3(0.09, 0.10, 0.15), uv.y);
32 vec3 body = mix(sprite.rgb, uTint, flash * 0.85);
33 body += uTint * flash * 0.5;
34
35 vec3 color = mix(backdrop, body, sprite.a);
36 color = mix(color, uTint * 1.6, outline);
37 fragColor = vec4(color, 1.0);
38 }
39

Inputs for this pass

  • iChannel0 textures/sprite-demo-v1.png
  • iChannel1 Empty
  • iChannel2 Empty
  • iChannel3 Empty

Learn from this shader

How it works

The flash is an exponential decay: full strength immediately, then gone. A sine or a ramp would rise into the hit, and rising is exactly wrong — an impact has no anticipation. The tint is applied two ways on purpose: mixing toward it keeps the sprite's shading readable, adding it on top makes the flash survive whatever is behind it, and neither alone is enough. The outline comes from the sprite's own alpha — sampling a ring around each fragment and keeping the largest value finds fragments that are transparent but next to something opaque. Because it reads alpha rather than colour, it follows the holes in the artwork too.

Try changing

It starts held so the effect is visible standing still. Turn Replay the hit on and it becomes the impulse it would be in play — most of the cycle is unflashed, which is the point. Push Flash amount to one to see the sprite become a silhouette, which many games ship for a single frame. Widen Outline width until it detaches, and set the tint cold for a shield hit instead of damage.

Using it in a game

Drive the amount from a damage event with a timer, and keep it short: two or three frames is enough. On a sprite this is the shader as written; in 3D the outline comes from a dilated mask or an inverted hull rather than eight alpha samples per pixel.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...