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;
}
