Damage Hurt — Pulsing Blood-Red Screen Edge

A full-screen canvas_item​ shader that overlays a pulsing, blood-red vignette around the edges of the screen — the classic “you just got hurt” effect. The inner boundary uses value noise instead of a smooth curve, so it reads as organic/vein-like rather than a flat UI gradient.


What it does

  • Draws a colored vignette that fades in from all four screen edges toward the center.
  • The inner edge of that vignette is perturbed with smooth noise, so the boundary looks ragged and organic instead of a perfect rounded rectangle.
  • The whole effect pulses in intensity on a sine wave, and the color is subtly mottled using the same noise field so it doesn’t read as a flat color fill.
  • Every visual property — color, size, softness, raggedness, pulse speed, pulse strength, and overall intensity — is exposed as a shader parameter, so it can be tuned live in the Inspector or driven from script (e.g. scaling intensity with the player’s current health).

Setup

  1. Add a ColorRect​ to your HUD/UI layer, sized to cover the full screen (Anchor Preset → ​Full Rect​).
  2. Set its Mouse Filter to Ignore so it doesn’t block input to anything underneath.
  3. Give it a new ShaderMaterial​, and assign the shader code below to a new Shader​ resource on that material.
  4. Tune the parameters in the Inspector, or drive them from script (see ​Triggering it from code​, below).

Triggering it from code

The shader is fully functional on its own as a looping ambient pulse, but it’s also designed to be driven from gameplay — the cleanest hook is max_alpha​, since animating it alone takes the effect from invisible to full intensity and back.

gdscript

@export var hurt_material: ShaderMaterial

## Call this when the player takes damage. strength ~0.6–1.0 for a sharp hit,
## lower for a graze. duration controls how long it takes to fade back out.
func flash_damage(strength: float, duration: float = 0.6) -> void:
    var tw := create_tween()
    tw.tween_method(
        func(v): hurt_material.set_shader_parameter("max_alpha", v),
        strength, 0.0, duration
    ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)

For a persistent “low health” state rather than a one-off flash, drive max_alpha​ and/or pulse_amount​ continuously from current health percentage instead of tweening a single flash.

Notes

  • Works in both 2D and 3D projects — it’s a canvas_item​ shader on a UI ColorRect​, independent of your scene’s render pipeline.
  • ​size​, roughness​, and softness​ interact: larger roughness​ values relative to size​ can push the noisy edge outside the visible fade range and look clipped — if that happens, raise size​ or lower roughness​.
  • Performance is cheap — a handful of rand()​/mix()​ calls per pixel, no loops, suitable for mobile.
  • Made with AI
Shader code
shader_type canvas_item;

// Full-screen damage/hurt vignette — pulsing blood-red edges.
// Put this on a ColorRect sized to cover the whole screen (Full Rect anchor,
// Mouse Filter = Ignore so it doesn't block clicks on anything underneath).

uniform vec4 blood_color : source_color = vec4(0.55, 0.0, 0.0, 1.0);
uniform float size : hint_range(0.0, 0.5) = 0.22;        // how far inward from the edges the effect reaches
uniform float softness : hint_range(0.0, 1.0) = 0.6;      // 0 = hard edge, 1 = very gradual fade
uniform float roughness : hint_range(0.0, 1.0) = 0.35;    // how ragged/vein-like the inner edge looks
uniform float pulse_speed : hint_range(0.0, 10.0) = 1.8;  // pulses per second-ish
uniform float pulse_amount : hint_range(0.0, 1.0) = 0.5;  // how strongly it pulses (0 = steady, 1 = full fade in/out)
uniform float max_alpha : hint_range(0.0, 1.0) = 0.85;    // overall intensity cap

float rand(vec2 co) {
	return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}

float value_noise(vec2 p) {
	vec2 i = floor(p);
	vec2 f = fract(p);
	float a = rand(i);
	float b = rand(i + vec2(1.0, 0.0));
	float c = rand(i + vec2(0.0, 1.0));
	float d = rand(i + vec2(1.0, 1.0));
	vec2 u = smoothstep(0.0, 1.0, f);
	return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

void fragment() {
	// Distance from this pixel to the nearest screen edge: 0 at the border,
	// up to 0.5 at dead center.
	vec2 edge_dist = min(UV, vec2(1.0) - UV);
	float edge = min(edge_dist.x, edge_dist.y);

	// Slowly shifting organic noise so the inner boundary looks ragged and
	// alive instead of a perfect rounded rectangle.
	float n = value_noise(UV * 6.0 + vec2(0.0, TIME * 0.05));
	edge += (n - 0.5) * roughness * size;

	float soft_start = size * (1.0 - softness);
	float vignette = clamp(1.0 - smoothstep(soft_start, size, edge), 0.0, 1.0);

	// Smooth pulse — strength controlled by pulse_amount, rate by pulse_speed.
	float pulse = 1.0 - pulse_amount + pulse_amount * (sin(TIME * pulse_speed * TAU) * 0.5 + 0.5);

	// Subtle mottling using the same noise field, so the red isn't perfectly flat.
	vec3 mottled_color = blood_color.rgb * (0.85 + 0.3 * n);

	float alpha = vignette * pulse * max_alpha * blood_color.a;
	COLOR = vec4(mottled_color, alpha);
}
Live Preview
Tags
damage, hurt, Post processing, pulsing, ui
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

0 Comments
Oldest
Newest Most Voted