(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;
}
Live Preview
Tags
decal, Parallax Mapping
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 LTC_06

Related shaders

guest

0 Comments
Oldest
Newest Most Voted