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:
- Add a MeshInstance3D node, add PlaneMesh > ShaderMaterial > New shader, and place this code inside.
- 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)
- 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;
}
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.)
Using fewer vertices instead of many pixels like this makes a lot of sense. I’ll have to try this in a different shader because I assume it will be very different.
And I totally see an improvement in the level of detail at a distance.
Thanks for the feedback!
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)
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
Hi, I actually have something like this HERE. But because it is kinda not much of a shader I didn’t post it on this page…
It is based on the Godot function
Image.blend_rect_mask( src, mask, rect, pos)
Very cool!
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!)