(Updated) Decal with Parallax Mapping – Godot 4.7
(Update 8/29/2026) Fixed weird normals when looking at side of the decal box mesh.
(Update 8/25/2026) Works in 4.7 and fixed normals being messed up 🙂
Shader code
// Based of carpenterblue's shader: https://godotshaders.com/shader/decal-shader-for-4-3-compatibility-renderer-transparency-support-no-repeat/
shader_type spatial;
render_mode cull_back, depth_test_disabled;
group_uniforms albedo;
uniform vec4 albedo_color : source_color = vec4(1.0);
uniform sampler2D albedo_texture : source_color, repeat_disable;
group_uniforms roughness;
uniform float roughness_scale = 1.0;
uniform sampler2D roughness_texture : hint_default_white, repeat_disable;
group_uniforms metallic;
uniform float metallic_scale = 0.0;
uniform sampler2D metallic_texture : hint_default_white, repeat_disable;
group_uniforms normal_mapping;
uniform sampler2D normal_map_texture : hint_normal, repeat_disable;
group_uniforms height_mapping ;
uniform sampler2D texture_heightmap : hint_default_white, repeat_disable;
uniform float heightmap_scale : hint_range(0.0, 64.0, 0.01);
uniform bool use_heightmap = false;
const int HEIGHTMAP_MAX_LAYERS = 16;
const int HEIGHTMAP_MIN_LAYERS = 4;
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture;
uniform sampler2D NORMAL_TEXTURE : hint_normal_roughness_texture;
const float CUBE_HALF_SIZE = 0.5;
const float DISTANCE = 0.2;
varying mat4 INV_MODEL_MATRIX;
varying float final_roughness;
void vertex(){
INV_MODEL_MATRIX = inverse(MODEL_MATRIX);
NORMAL = vec3(0.0, 0.0, 1.0);
BINORMAL = vec3(0.0, 1.0, 0.0);
TANGENT = vec3(1.0, 0.0, 0.0);
}
// Credit: https://stackoverflow.com/questions/32227283/getting-world-position-from-depth-buffer-value
vec3 world_pos_from_depth(float depth, vec2 screen_uv, mat4 inverse_proj, mat4 inverse_view) {
float z = depth;
vec4 clip_space_position = vec4(screen_uv * 2.0 - 1.0, z, 1.0);
vec4 view_space_position = inverse_proj * clip_space_position;
view_space_position /= view_space_position.w;
vec4 world_space_position = inverse_view * view_space_position;
return world_space_position.xyz;
}
vec2 apply_heightmap(vec2 uvs, vec3 view_dir, float camera_distance){
if ((use_heightmap == true) && (camera_distance < 10.0)) {
float num_layers = mix(float(HEIGHTMAP_MAX_LAYERS), float(HEIGHTMAP_MIN_LAYERS), abs(dot(vec3(0.0, 0.0, 1.0), view_dir)));
float new_heightmap_scale = heightmap_scale;
new_heightmap_scale *= 1.0 - pow(min(camera_distance / 10.0, 1.0), 2.0);
float layer_depth = 1.0 / num_layers;
float current_layer_depth = 0.0;
vec2 p = view_dir.xy * new_heightmap_scale * 0.01;
vec2 delta = p / num_layers;
vec2 ofs = uvs;
float depth = 1.0 - texture(texture_heightmap, ofs).r;
float current_depth = 0.0;
while (current_depth < depth) {
ofs -= delta;
depth = 1.0 - texture(texture_heightmap, ofs).r;
current_depth += layer_depth;
}
vec2 prev_ofs = ofs + delta;
float after_depth = depth - current_depth;
float before_depth = (1.0 - texture(texture_heightmap, prev_ofs).r) - current_depth + layer_depth;
float weight = after_depth / (after_depth - before_depth);
ofs = mix(ofs, prev_ofs, weight);
return ofs;
}
return uvs;
}
void fragment() {
float camera_distance = distance(CAMERA_POSITION_WORLD, NODE_POSITION_WORLD);
vec3 view_dir = normalize(normalize(-VERTEX + EYE_OFFSET) * mat3(TANGENT, -BINORMAL, NORMAL));
float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;
mat3 rotation = mat3(MODEL_MATRIX);
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) > DISTANCE)) {
discard;
}
vec2 decal_uvs = (test_pos.xy * vec2(1.0, -1.0)) + 0.5;
decal_uvs = apply_heightmap(decal_uvs, view_dir, camera_distance);
vec4 color = texture(albedo_texture, decal_uvs) * albedo_color;
ALBEDO = color.rgb;
if (color.a < 0.25) { discard; }
ALPHA *= color.a;
vec4 normals_view_remapped = (texture(NORMAL_TEXTURE, SCREEN_UV));
NORMAL_MAP = texture(normal_map_texture, decal_uvs).xyz;
ROUGHNESS = roughness_scale * texture(roughness_texture, decal_uvs).r;
METALLIC = metallic_scale * texture(metallic_texture, decal_uvs).r;
NORMAL_MAP_DEPTH = 1.0;
}

