Decal shader (similar to +Forward Decal… but for Compatibility-mobile)

Godot port of this shader and this one as well..

Yes, there are some decal shaders out there, but no one is like the default Godot Forward+ Decal.. I think is very important to be able to fade the decal across Y-Axis, specially if there is no direct connection with cull-masks layers…

Shader code
shader_type spatial;
render_mode world_vertex_coords, blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;

uniform vec2 Scale = vec2(1.000000, 1.000000);
uniform sampler2D Albedo : source_color, repeat_disable;
uniform sampler2D Normal : source_color, hint_normal, repeat_disable;
uniform sampler2D Roughtness : source_color, hint_default_white, repeat_disable;
uniform sampler2D Emission : source_color, hint_default_black, repeat_disable;
uniform float Albedo_Mix : hint_range(0.0, 1.1, 0.1) = 1.1;

uniform float Emission_Energy : hint_range(0.0, 30, 0.1) = 1.0;
uniform vec4 Modulate : source_color = vec4(1.0,1.0,1.0,1.0);
uniform vec4 Modulate_Emission : source_color = vec4(1.0,1.0,1.0,1.0);

uniform sampler2D DEPTH_TEXTURE: hint_depth_texture;
uniform float cube_half_size = 0.5;

// Y-axis fade controls
uniform bool enable_y_fade = false; // Toggle for fade effect
uniform float fade_start = 0.3; // Where fading begins (0-1)
uniform float fade_end = 0.7;   // Where fading ends (0-1)
uniform float fade_power = 2.0; // Curve sharpness

varying mat4 INV_MODEL_MATRIX;

void vertex(){
    INV_MODEL_MATRIX = inverse(MODEL_MATRIX);
}

vec3 world_pos_from_depth(float depth, vec2 screen_uv, mat4 inverse_proj, mat4 inverse_view) {
    float z = depth;
    vec4 clipSpacePosition = vec4(screen_uv * 2.0 - 1.0, z, 1.0);
    vec4 viewSpacePosition = inverse_proj * clipSpacePosition;
    viewSpacePosition /= viewSpacePosition.w;
    vec4 worldSpacePosition = inverse_view * viewSpacePosition;
    return worldSpacePosition.xyz;
}

float smooth_fade(float y_pos) {
    // Normalize Y position (-0.5 to 0.5) -> (0 to 1)
    float normalized_y = clamp((y_pos + cube_half_size) / (2.0 * cube_half_size), 0.0, 1.0);
    
    // Calculate fade factor
    float fade_factor = smoothstep(fade_start, fade_end, normalized_y);
    fade_factor = pow(fade_factor, fade_power);
    
    return 1.0 - fade_factor;
}

void fragment() {
    float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;
    vec3 world_pos = world_pos_from_depth(depth, SCREEN_UV, INV_PROJECTION_MATRIX, (INV_VIEW_MATRIX));
    vec4 test_pos = (INV_MODEL_MATRIX * vec4(world_pos, 1.0));
    
    if (abs(test_pos.x) > cube_half_size || abs(test_pos.y) > cube_half_size || abs(test_pos.z) > cube_half_size) {
        discard;
    }

    vec2 NEW_UV = (test_pos.xz) + 0.5;
    
    vec4 n_out3p0 = texture(Albedo, NEW_UV * Scale);
    vec3 emission_map = texture(Emission, NEW_UV * Scale).rgb;
    vec3 normal_map = texture(Normal, NEW_UV * Scale).rgb;
    float rought_map = texture(Roughtness, NEW_UV * Scale).r;

    // Default fade factor (1.0 means no fade)
    float y_fade = 1.0;
    
    // Only calculate fade if enabled
    if (enable_y_fade) {
        y_fade = smooth_fade(test_pos.y);
        if (y_fade <= 0.0) {
            discard;
        }
    }

    // Apply fade to all channels (or full strength if fade disabled)
    ALBEDO = n_out3p0.rgb * Modulate.rgb * y_fade;
    ALPHA = n_out3p0.a * Albedo_Mix * Modulate.a * y_fade;
    NORMAL = normal_map;
    EMISSION = emission_map * Emission_Energy * Modulate_Emission.rgb * y_fade;
    ROUGHNESS = rought_map;
}

// Credits: Shader based on OlisUnfinishedProjects 4.0 Decal Shader 
// https://godotshaders.com/shader/decal-shader-4-0-port/
// https://stackoverflow.com/questions/32227283/getting-world-position-from-depth-buffer-value
Tags
3d, compatibility, decal, Forward+, godot 4, Godot 4.0, LeLu, pbr
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.

More from LeLu

Decal shader (+Y_Fading +Particle Alpha SCISSOR Treshold)

Related shaders

Glitch worldborder but it has a very specific implementation idk

Decal shader 4.0 port

Decal shader (+Y_Fading +Particle Alpha SCISSOR Treshold)

guest

10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
aguy
aguy
5 months ago

can someone explain how to make it work? i can’t figure it out. i tried doing the same thing as other decals but it just does not show anything

Purpbatboi2i
3 months ago
Reply to  LeLu

so, here to say that, this shader doesn’t work, even with those instructions

[0]
[0]
5 months ago

Also having trouble, I have this shader copied to res://decal.gdshader. I have two MeshInstance3d one is Decal and a 1x1x1 boxmesh the other is the Floor_box 10x1x10.
the Decal is part way in the floor (have tried multiple amounts from 0.01 to 0.9). the decal image chosen is the icon.svg in this case (have tried other image types and sizes with no change) the project is a new one set to compatibility renderer on creation with no additional changes. my godot version is 4.4.1

Below is my scene example of my setup.
Let me know if you have any ideas thanks.

[gd_scene load_steps=6 format=3 uid="uid://bgynloenlrrwn"]


[ext_resource type="Shader" uid="uid://c3vd7skmp61a4" path="res://decal.gdshader" id="1_a202f"]
[ext_resource type="Texture2D" uid="uid://drm0ba47unwop" path="res://icon.svg" id="2_noarx"]


[sub_resource type="BoxMesh" id="BoxMesh_noarx"]
size = Vector3(10, 1, 10)


[sub_resource type="ShaderMaterial" id="ShaderMaterial_a0tk4"]
render_priority = 0
shader = ExtResource("1_a202f")
shader_parameter/Scale = Vector2(1, 1)
shader_parameter/Albedo = ExtResource("2_noarx")
shader_parameter/Albedo_Mix = 1.1
shader_parameter/Emission_Energy = 1.0
shader_parameter/Modulate = Color(1, 1, 1, 1)
shader_parameter/Modulate_Emission = Color(1, 1, 1, 1)
shader_parameter/cube_half_size = 0.5
shader_parameter/enable_y_fade = false
shader_parameter/fade_start = 0.3
shader_parameter/fade_end = 0.7
shader_parameter/fade_power = 2.0


[sub_resource type="BoxMesh" id="BoxMesh_r3fl7"]


[node name="Node3D" type="Node3D"]


[node name="Floor_box(10x1x10)" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
mesh = SubResource("BoxMesh_noarx")


[node name="Decal" type="MeshInstance3D" parent="."]
material_override = SubResource("ShaderMaterial_a0tk4")
mesh = SubResource("BoxMesh_r3fl7")


[0]
[0]
5 months ago
Reply to  [0]
Last edited 5 months ago by [0]
aguy
aguy
3 months ago

I didn’t look into this code for quite a while, because I don’t need in my current project but now I noticed that there is something missing in the code. in the line 31: float z = depth; should be instead float z = depth * 2.0 – 1.0; . i only tested in godot 4.2.2. also this shader works with shadows which is a plus

beopenbefree
3 months ago
Reply to  aguy

thanks, now it works

Edge Lord
10 days ago
Reply to  aguy

could not get the og shader to run … but this fix got the job done Thanks man 👍

toxiccrack
toxiccrack
1 month ago

I also noticed that line 59 should be
vec2 NEW_UV = (test_pos.xz) + 0.5 / Scale;
to make the Scale property work correctly

Edge Lord
9 days ago

I ran into a problem using this decal as a fake shadow (I think other use cases will face this as well) the decal is responding in funny ways to the camera and the environment … not sure if i have any chance if fixing it … so in its current state i would not recommend it unless your camera is more or less static