Cone Step Mapping

Cone step mapping based on the paper by Jonathan “LoneSock” Dummer, “Cone Step Mapping: An Iterative Ray-Heightfield Intersection Algorithm”.

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

shader_type spatial;
render_mode skip_vertex_transform;

group_uniforms _Transform;
uniform vec3 _Position = vec3(0.0);
uniform vec3 _Rotation = vec3(0.0);
uniform vec3 _Scale = vec3(1.0);

group_uniforms _Albedo;
uniform vec4 _DiffuseColor : source_color = vec4(1.0);
uniform sampler2D _DiffuseTexture : source_color, filter_nearest_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_nearest_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_nearest_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_nearest_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_mipmap, repeat_enable;
uniform float _NormalMapScale : hint_range(-16.0, 16.0, 1e-3) = 1.0;

group_uniforms _ConeStepMapping;
uniform float _TextureSide = 2048.0;
uniform sampler2D _ConeStepMap : filter_linear, repeat_enable;
uniform float _HeightScale : hint_range(0.0, 1.0, 1e-3) = 0.0;
uniform float _MaxSteps : hint_range(1.0, 1024.0, 1.0) = 256.0;

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 _parallax_height;
varying float _height_scale;

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

mat4 translation_matrix(
	in vec3 position
)
{
	return mat4(
	vec4(1, 0, 0, 0),
	vec4(0, 1, 0, 0),
	vec4(0, 0, 1, 0),
	vec4(position.x, position.y, position.z, 1)
	);
}

mat4 rotation_matrix(
	in vec3 angle // In radians.
)
{
	mat4 x = mat4(
		vec4(1, 0, 0, 0),
		vec4(0,  cos(angle.x), sin(angle.x), 0),
		vec4(0, -sin(angle.x), cos(angle.x), 0),
		vec4(0, 0, 0, 1)
	);
	
	mat4 y = mat4(
		vec4(cos(angle.y), 0, -sin(angle.y), 0),
		vec4(0, 1, 0, 0),
		vec4(sin(angle.y), 0,  cos(angle.y), 0),
		vec4(0, 0, 0, 1)
	);
	
	mat4 z = mat4(
		vec4( cos(angle.z), sin(angle.z), 0, 0),
		vec4(-sin(angle.z), cos(angle.z), 0, 0),
		vec4(0, 0, 1, 0),
		vec4(0, 0, 0, 1)
	);
	
	// Godot Default Rotation Order (YXZ).
	return y * x * z;
}

mat4 escalation_matrix(
	in vec3 scale
)
{
	return mat4(
	vec4(scale.x, 0, 0, 0),
	vec4(0, scale.y, 0, 0),
	vec4(0, 0, scale.z, 0),
	vec4(0, 0, 0, 1)
	);
}

vec2 step_cone_mapping_exact(
	in vec2 st,
	in vec3 view_ts,
	in sampler2D cone_step_map,
	in float texture_side,
	in float height_scale,
	inout float current_layer_height
)
{
	float w = 1.0 / texture_side;
	
	float view_lenght = length(view_ts.xy);
	
	vec4 current_cone_step_map = texture(cone_step_map, st);
	
	float scale = 0.0;
	
	while ((1.0 - view_ts.z * scale) > current_cone_step_map.r) {
		scale += w + (1.0 - view_ts.z * scale - current_cone_step_map.r) / (view_ts.z + view_lenght / (current_cone_step_map.g * current_cone_step_map.g));
		current_cone_step_map = texture(cone_step_map, st + view_ts.xy * scale * height_scale);
	}
	
	current_layer_height = view_ts.z * scale;
	
	scale -= w;
	
	return st + view_ts.xy * scale * height_scale;
}

vec2 step_cone_mapping_loop(
	in vec2 st,
	in vec3 view_ts,
	in int max_steps,
	in sampler2D cone_step_map,
	in float texture_side,
	in float height_scale,
	inout float current_layer_height
)
{
	float w = 1.0 / texture_side;
	
	float view_lenght = length(view_ts.xy);
	
	vec4 current_cone_step_map = texture(cone_step_map, st);
	
	float scale = 0.0;
	
	for (int i = 0; i < max_steps; ++i) {
		if ((1.0 - view_ts.z * scale) <= current_cone_step_map.r) {
			break;
		}
		
		scale += w + (1.0 - view_ts.z * scale - current_cone_step_map.r) / (view_ts.z + view_lenght / (current_cone_step_map.g * current_cone_step_map.g));
		current_cone_step_map = texture(cone_step_map, st + view_ts.xy * scale * height_scale);
	}
	
	current_layer_height = view_ts.z * scale;
	
	scale -= w;
	
	return st + view_ts.xy * scale * height_scale;
}

float pixel_depth_offset(
	in vec2 base_st,
	in vec3 view_ts,
	in vec3 vertex_vs,
	in float height_scale,
	in float parallax_height,
	in mat3 tbn,
	in mat4 projection_matrix
)
{
	vec3 dpdx = dFdx(vertex_vs);
	vec3 dpdy = dFdy(vertex_vs);
	
	vec2 dstdx = dFdx(base_st);
	vec2 dstdy = dFdy(base_st);
	
	float scale_x = length(dpdx) / max(length(dstdx), 1e-5);
	float scale_y = length(dpdy) / max(length(dstdy), 1e-5);
	
	float mesh_scale = (scale_x + scale_y) * 0.5;
	
	float height = parallax_height * height_scale * mesh_scale;
	
	vec3 parallax_offset_ts = view_ts * (height / view_ts.z);
	vec3 parallax_offset_vs = tbn * parallax_offset_ts;
	
	vec3 disp_pos_vs = vertex_vs - parallax_offset_vs;
	
	vec4 disp_pos_proj = projection_matrix * vec4(disp_pos_vs, 1.0);
	disp_pos_proj.xyz /= disp_pos_proj.w;
	
	return disp_pos_proj.z;
}

void vertex()
{
	/* UV */
	UV = tiling_and_offset(UV, _Tiling, _Offset);
	
	/* Translation */
	mat4 TRANSLATION_MATRIX = translation_matrix(_Position);
	
	/* Rotation */
	mat4 ROTATION_MATRIX = rotation_matrix(radians(_Rotation));
	
	/* Scale */
	mat4 ESCALATION_MATRIX = escalation_matrix(_Scale);
	
	/* Vertex */
	vec4 vertex_os = vec4(VERTEX, 1.0);
	vertex_os = ESCALATION_MATRIX * vertex_os;
	vertex_os = ROTATION_MATRIX * vertex_os;
	vec4 vertex_ws = MODEL_MATRIX * vertex_os;
	vertex_ws = TRANSLATION_MATRIX * vertex_ws;
	vec4 vertex_vs = VIEW_MATRIX * vertex_ws;
	VERTEX = vertex_vs.xyz;
	
	/* Normal */
	vec4 normal_os = vec4(NORMAL, 0.0);
	normal_os = ROTATION_MATRIX * normal_os;
	vec4 normal_ws = MODEL_MATRIX * normal_os;
	vec4 normal_vs = VIEW_MATRIX * normal_ws;
	NORMAL = normalize(normal_vs.xyz);
	
	/* Binormal */
	vec4 binormal_os = vec4(BINORMAL, 0.0);
	binormal_os = ROTATION_MATRIX * binormal_os;
	vec4 binormal_ws = MODEL_MATRIX * binormal_os;
	vec4 binormal_vs = VIEW_MATRIX * binormal_ws;
	BINORMAL = normalize(binormal_vs.xyz);
	
	/* Tangent */
	TANGENT = normalize(cross(BINORMAL, NORMAL));
	
	/* TBN */
	TBN = mat3(-TANGENT, BINORMAL, NORMAL);
	TBN_TRANSPOSE = transpose(TBN);
	
	/* Projection */
	vec4 vertex_proj = PROJECTION_MATRIX * vertex_vs;
	POSITION = vertex_proj;
}

void fragment() {
	/* Parallax UV */
	_parallax_uv = UV;
	
	/* View in Tangent Space */
	vec3 view_ts = TBN_TRANSPOSE * VIEW;
	
	/* Cone Step Mapping */
	_parallax_height = 0.0;
	_height_scale = _HeightScale * 0.5;
	
	//_parallax_uv = step_cone_mapping_exact(_parallax_uv, view_ts, _ConeStepMap,  _TextureSide, _height_scale, _parallax_height);
	_parallax_uv = step_cone_mapping_loop(_parallax_uv, view_ts, int(_MaxSteps), _ConeStepMap, _TextureSide, _height_scale, _parallax_height);
	
	/* Pixel Depth Offset (PDO) */
	DEPTH = pixel_depth_offset(UV, view_ts, VERTEX, _height_scale, _parallax_height, TBN, PROJECTION_MATRIX);
	
	/* 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;
}
Live Preview
Tags
3d, parallax
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 ElSuicio

Related shaders

guest

2 Comments
Oldest
Newest Most Voted
tentabrobpy
1 month ago

Nice to see an implementation of this, how are you generating the cone map?