Bloody Pool! – Smooth blood trail

Aye! My first shader!!

 

WHAT IS THIS?

This is when someone messes with you and you stab them in the chest, then, the subject will try to run all over the place bleeding, leaving a blood trail behind.

 

HOW TO SET THIS UP?!

See this repository in GitHub here or download the demo project here, to see for yourself. Or follow the next instructions:

  1. Add a MeshInstance3D node, add PlaneMesh > ShaderMaterial > New shader, and place this code inside.
  2. Ideally, you’d want to set up the inspector values like in screenshot 2.* For Blood texture use a radial-centered GradientTexture2D or any shape as long as it is blurred and has a white margin (also disable mipmaps from import settings if it looks off)
  3. Over the same MeshInstance3D node add the following script:
# SHADER LOGIC
# This script animates the individual blood drops in the shader using uniforms 'positions' and 'scales'
# Make sure you don't call 'drop_at()' too often or it will flood the pool until 'drying_time' finishes

extends MeshInstance3D
class_name BloodPool

@export var pool_size:int = 64 ## This value MUST match the constant 'TOTAL_BLOOD_DROPS' in shader or it will throw index out-of-reach errors
@export var growing_time:float = 0.2 ## Time the blood drop will grow in size, usually fast
@export var drying_time:float = 4 ## Time the blod drop will start drying, usually slow
@export var delay_until_drying_starts:float = 0.2 ## Time the blod drop will remain idle when is fully grown

@onready var _mat:ShaderMaterial = get_active_material(0)
var _bloody_pool:Array[BloodDrop]


func _ready():
	# Fill up shader variables. They will not be immediately visible because all 'scales' are initialized as 0.0
	var positions:PackedVector2Array = []
	positions.resize(pool_size)
	_mat.set_shader_parameter("positions", positions)
	
	var scales:PackedFloat32Array = []
	scales.resize(pool_size)
	_mat.set_shader_parameter("scales", scales)
	
	# Setup '_bloody_pool' to manage and animate all active blood drops
	BloodDrop.mat = _mat
	for i in pool_size:
		_bloody_pool.append( BloodDrop.new(i) )


func drop_at(pos:Vector2):
	# Find any 'BloodDrop' inactive to use it
	# The tween will make it inactive again when animations finishes, so it can be reused
	for blod_drop in _bloody_pool:
		if not blod_drop.active:
			blod_drop.start(pos)
			var tween:Tween = get_tree().create_tween()
			tween.tween_method(blod_drop.animate, 0.0, growing_time, growing_time)
			tween.tween_method(blod_drop.animate, growing_time, 0.0, drying_time).set_delay(delay_until_drying_starts)
			tween.finished.connect(blod_drop.end)
			break


class BloodDrop:
	var active:bool
	var _index:int
	static var mat:ShaderMaterial
	
	func _init(index:int):
		_index = index
	
	func start(pos:Vector2):
		active = true
		mat["shader_parameter/positions"][_index] = pos
		
	func animate(value:float):
		mat["shader_parameter/scales"][_index] = value

	func end():
		active = false

 

.. And then just call blood_pool.drop_at(global_position) inside your character script to bleed it down

 

THAT’S IT!

Comment doubts, improvements, and stuff. See ya

 

Shader code
shader_type spatial;


const int TOTAL_BLOOD_DROPS = 64;
const float BLOOD_ROUGHNESS = 0.05;
const float HEIGHTMAP_STRENGHT = 0.2;
const float BLOOD_DENSITY_STRENGHT = 5.0; // honestly, this is just a magic number depending on the heightmap..
const float BLOOD_BLENDING = 0.15; // Blend between floor texture and blood

group_uniforms SetupTheseVariables;
uniform sampler2D floor_texture:hint_default_white;
uniform sampler2D floor_heightmap:hint_default_black;
uniform sampler2D floor_roughnessmap:hint_default_black;
uniform sampler2D blood_texture:repeat_disable; // Radial-centered GradientTexture2D or any shape as long as it is blurred and has a white margin (also disable mipmaps from import settings)
uniform vec3 blood_color:source_color;
uniform float blood_merge_factor:hint_range(0.0, 1.0) = 0.2; // Example 0.6: 60% of 'blood_texture' is the actual blood. 40% is the merging area (coalescence effect). Set it depending on your grayscaled 'blood_texture'
uniform bool blood_density_on_heightmap = true; // Lightens peaks, darkens valleys
group_uniforms;

group_uniforms ExternallySetted;
uniform vec2 positions[TOTAL_BLOOD_DROPS];
uniform float scales[TOTAL_BLOOD_DROPS];
group_uniforms;


vec2 scale_from_center(vec2 uv, float s){
	return ((uv - 0.5) * 1.0/s) + 0.5;
}

void fragment(){
	// Draw base PBR properties
	ALBEDO = texture( floor_texture, UV ).rgb;
	ROUGHNESS = texture(floor_roughnessmap, UV).x;

	float blended_pixel = 1.0;

	// This will blend every drop of blod first so it looks more realistic
	for(int i=0; i<TOTAL_BLOOD_DROPS; i++){
		if(scales[i] <= 0.01){
			continue;
		}
		vec2 uv_translated = UV - positions[i] + 0.5;
		vec2 uv_scaled = scale_from_center( uv_translated, scales[i] );
		blended_pixel *= texture( blood_texture, uv_scaled ).r;
	}

	// Now apply all properties to the drops of blod
	for(int i=0; i<TOTAL_BLOOD_DROPS; i++){
		if(scales[i] <= 0.01){
			continue;
		}
		if(blended_pixel < blood_merge_factor){
			ROUGHNESS = BLOOD_ROUGHNESS;
			ALBEDO = mix(blood_color, ALBEDO, BLOOD_BLENDING);

			if(blood_density_on_heightmap){
				ALBEDO *= texture(floor_heightmap, UV).r * BLOOD_DENSITY_STRENGHT;
			}
		}
	}
}


void vertex(){
	VERTEX.y = texture(floor_heightmap, UV).x * HEIGHTMAP_STRENGHT;
}
Tags
3d, animation, blood, fragment, track, trail
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from dip

Car Tracks On Snow Or Sand – Using viewport textures and particles

Stylized Cartoon Grass

Related shaders

Smooth 3D pixel filtering

Trail dewiggle

simple trail effect

Subscribe
Notify of
guest

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Julian Todd
Julian Todd
1 year ago

Looks nice, but those 2 loops up to 64 on each pixel might be problematic.

Maybe it could be done with vertex shading instead, given that this animation would normally happens close to the player where the triangles are many times larger than the pixels, so those loops won’t create such a load on the GPU. (It would be important to make the texture sleep when it is at a distance.)

TheYellowArchitect
1 year ago

Kinda off-topic, but I am looking to have permanent blood stains. Is a shader like the above more performant than multiple decals? There will be many (a bloody mess)

daliborgk
daliborgk
11 months ago

A simple and efficient approach would be to use a viewport that dosent clear as described in this talk: https://youtu.be/cwZGq1qJYoQ?si=YS_GfHPohn3CmclP

Instructions
4 months ago

Very cool!

PiedadYan
10 days ago

Hey, is it possible to use another type of mesh. Every time i try to use one of my own(made in blender), puddles stop forming. (Outside of that, great shader!)