Steep Parallax Mapping + Hard Parallax Shadows

If you found this helpful, please consider supporting me by purchasing Parallax Relief Mapping on itch.io. Your support is very important.

Shader code
// ElSuicio, 2026.
// GODOT v4.6.1.stable.
// x.com/ElSuicio
// github.com/ElSuicio
// Contact email [interdreamsoft@gmail.com]

shader_type spatial;

const float INV_PI = 0.31830988618379067154;

group_uniforms _Albedo;
uniform vec4 _DiffuseColor : source_color = vec4(1.0);
uniform sampler2D _DiffuseTexture : source_color, filter_linear_mipmap, repeat_enable;

group_uniforms _Metallic;
uniform float _Metallic : hint_range(0.0, 1.0, 1e-3) = 0.0;
uniform float _Specular : hint_range(0.0, 1.0, 1e-3) = 0.5;
uniform sampler2D _MetallicTexture : hint_default_white, filter_linear_mipmap, repeat_enable;

group_uniforms _Roughness;
uniform float _Roughness : hint_range(0.0, 1.0, 1e-3) = 1.0;
uniform sampler2D _RoughnessTexture : hint_roughness_r, filter_linear_mipmap, repeat_enable;

group_uniforms _Emission;
uniform vec4 _EmissionColor : source_color = vec4(vec3(0.0), 1.0);
uniform sampler2D _EmissionTexture : source_color, hint_default_black, filter_linear_mipmap, repeat_enable;
uniform float _EmissionEnergyMultiplier : hint_range(0.0, 100.0, 1e-3) = 0.0;

group_uniforms _NormalMap;
uniform sampler2D _NormalMap : hint_roughness_normal, filter_nearest, repeat_enable;
uniform float _NormalMapScale : hint_range(-16.0, 16.0, 1e-3) = 1.0;

group_uniforms SteepParallaxMapping;
uniform sampler2D _HeightMap : hint_default_black, filter_nearest, repeat_enable;
uniform float _HeightScale : hint_range(0.0, 1.0, 1e-3) = 0.0;
uniform float _NumLayers : hint_range(2.0, 128.0, 1.0) = 16.0;

group_uniforms SteepParallaxMapping.SelfShadow;
uniform float _ShadowSteps : hint_range(0.0, 1024.0, 1.0) = 16.0;
uniform float _ShadowBias : hint_range(0.0, 1.0, 1e-3) = 0.01;

group_uniforms _UV;
uniform vec2 _Tiling = vec2(1.0, 1.0);
uniform vec2 _Offset = vec2(0.0, 0.0);

varying mat3 TBN;
varying mat3 TBN_TRANSPOSE;

varying vec2 _parallax_uv;
varying float _height_scale;
varying float _parallax_height;

vec2 tiling_and_offset(
	in vec2 st,
	in vec2 tiling,
	in vec2 offset
)
{
	return vec2(st.x * tiling.x + offset.x, st.y * tiling.y + offset.y);
}

vec2 steep_parallax_mapping(
	in vec2 st,
	in vec3 view_ts,
	in float num_layers,
	in sampler2D height_map,
	in float height_scale,
	inout float current_layer_height
	)
{
	/* Basic Steep Parallax Mapping */
	float layer_height = 1.0 / num_layers;
	
	vec2 p = view_ts.xy * height_scale;
	vec2 delta_st = p / num_layers;
	
	vec2 current_st = st;
	
	float current_height_map_value = 1.0 - texture(height_map, current_st).r;
	
	while(current_layer_height < current_height_map_value)
	{
		current_layer_height += layer_height;
		current_st += delta_st;
		current_height_map_value = 1.0 - texture(height_map, current_st).r;
	}
	
	return current_st;
}

float schlick_fresnel(
	in float u
)
{
	float m = clamp(1.0 - u, 0.0, 1.0);
	return m * m * m * m * m; // pow(m, 5.0).
}

void brdf(
	in vec3 n,
	in vec3 l,
	in vec3 v,
	in vec3 c,
	in float w,
	in float m,
	in float s,
	inout float fd,
	inout vec3 fs
)
{
	float NdotL = dot(n, l); // cos(theta_l) == cos(theta_i).
	
	if(NdotL < 0.0)
	{
		return;
	}
	
	float NdotV = min(max(dot(n, v), 1e-3), 1.0); // cos(theta_v) == cos(theta_r).
	
	vec3 h = normalize(v + l);
	
	float HdotN = dot(h, n); // cos(theta_h).
	float HdotL = dot(h, l); // cos(theta_d).
	
	float alpha = w * w;
	float alpha2 = alpha * alpha;
	
	/* Burley + Trowbridge-Reitz-GGX */
	float FD_l = schlick_fresnel(NdotL), FD_v = schlick_fresnel(NdotV);
	float FD90 = 0.5 + 2.0 * w * HdotL * HdotL;
	
	fd = INV_PI * mix(1.0, FD90, FD_l) * mix(1.0, FD90, FD_v) * NdotL;
	
	/* Normal Distribution Function (GGX) */
	float t = 1.0 + (alpha2 - 1.0) * HdotN * HdotN;
	float D = alpha2 / (PI * t * t);
	
	/* Geometric Function (Implicit) */
	//float G = 0.5 / mix(2.0 * NdotL * NdotV, NdotL + NdotV, alpha);
	
	/* Geometric Function (Smith-GGX) */
	float GL = 1.0 / (NdotL + sqrt(alpha2 + (1.0 - alpha2) * (NdotL * NdotL)));
	float GV = 1.0 / (NdotV + sqrt(alpha2 + (1.0 - alpha2) * (NdotV * NdotV)));
	
	float G = GL * GV;
	
	/* Fresnel Function (Schlick’s Approximation) */
	vec3 f0 = mix(vec3(s * 0.08), c, vec3(m));
	vec3 F = f0 + (1.0 - f0) * schlick_fresnel(HdotL);
	
	fs = vec3(D * G * F * NdotL);
}

float hard_parallax_shadow(
	in vec2 st,
	in vec3 light_ts,
	in float shadow_steps,
	in float shadow_bias,
	in sampler2D height_map,
	in float height_scale,
	in float parallax_height
)
{
	if(light_ts.z <= 1e-3)
	{
		return 0.0;
	}
	
	float layer_height = 1.0 / shadow_steps;
	
	vec2 delta_st = ((light_ts.xy / light_ts.z) * height_scale) / shadow_steps;
	
	vec2 current_st = st;
	float current_layer_height = (1.0 - parallax_height) + shadow_bias;
	
	float current_height_map_value = texture(height_map, current_st).r;
	
	for(int i = 0; i <= int(shadow_steps); i++)
	{
		current_st -= delta_st;
		current_layer_height += layer_height;
		
		current_height_map_value = texture(height_map, current_st).r;
		
		if(current_layer_height <= current_height_map_value)
		{
			break;
		}
	}
	
	return step(current_height_map_value - current_layer_height, 0.0);
}

void fragment()
{
	/* Parallax UV */
	_parallax_uv = tiling_and_offset(UV, _Tiling, _Offset);
	
	/* TBN */
	TBN = mat3(-TANGENT, BINORMAL, NORMAL);
	TBN_TRANSPOSE = transpose(TBN);
	
	/* View in Tangent Space */
	vec3 view_ts = TBN_TRANSPOSE * VIEW;
	
	/* Steep Parallax Mapping */
	_height_scale = _HeightScale * 0.5;
	_parallax_height = 0.0;
	_parallax_uv = steep_parallax_mapping(_parallax_uv, view_ts, _NumLayers, _HeightMap, _height_scale, _parallax_height);
	
	/* Diffuse Color */
	vec4 albedo = _DiffuseColor * texture(_DiffuseTexture, _parallax_uv);
	ALBEDO = albedo.rgb;
	//ALPHA = albedo.a;
	
	/* Metallic */
	float metallic = _Metallic * texture(_MetallicTexture, _parallax_uv).b;
	METALLIC = metallic;
	SPECULAR = _Specular;
	
	/* Roughness */
	float roughness = _Roughness * texture(_RoughnessTexture, _parallax_uv).g;
	ROUGHNESS = roughness;
	
	/* Emission */
	vec4 emission = _EmissionColor + texture(_EmissionTexture, _parallax_uv); // Emission operator add.
	//vec4 emission = _EmissionColor * texture(_EmissionTexture, _parallax_uv); // Emission operator multiply.
	EMISSION = emission.rgb * _EmissionEnergyMultiplier;
	
	/* Normal Mapping */
	NORMAL_MAP = texture(_NormalMap, _parallax_uv).xyz;
	NORMAL_MAP_DEPTH = _NormalMapScale;
}

void light()
{
	vec3 n = normalize(NORMAL);
	vec3 l = normalize(LIGHT);
	vec3 v = normalize(VIEW);
	
	float fd = 0.0;
	vec3 fs = vec3(0.0);
	
	/* Light in Tangent Space */
	vec3 light_ts = TBN_TRANSPOSE * LIGHT;
	
	/* Burley + Trowbridge-Reitz-GGX */
	// https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf
	brdf(n, l, v, ALBEDO, ROUGHNESS, METALLIC, _Specular, fd, fs);
	
	/* Hard Parallax Shadow */
	float shadow = hard_parallax_shadow(_parallax_uv, light_ts, _ShadowSteps, _ShadowBias, _HeightMap, _height_scale, _parallax_height);
	
	vec3 radiance = LIGHT_COLOR * ATTENUATION;
	
	DIFFUSE_LIGHT += radiance * fd * shadow;
	SPECULAR_LIGHT += radiance * fs * shadow;
}
Live Preview
Tags
3d, parallax
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 ElSuicio

Related shaders

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments