Heightmap Voxel Traversal
Starting point for something like Daniel Schroeder’s voxel displacement rendering. His method is likely more sophisticated but if you’re looking for the same basic effect then this should suffice. For an improvement, I think quadtree mipmaps would work great with this approach.
Normals can either be calculated per-face, calculated per-voxel, or read from a normal map. Depth offset is disabled by default, displacement effects like this are rather finnicky when it comes to UVs.
Shader code
shader_type spatial;
// Normal mode
//#define FACE_NORMALS // Per-face normals
#define BUMP_MAPPING // Per-voxel normals calculated from height map
//#define NORMAL_MAPPING // Per-voxel normals from normal map
// Depth offset - Requires base mesh UV to have unit gradient in view space
//#define DEPTH_OFFSET
uniform sampler2D albedo_map : source_color, filter_nearest;
#ifdef NORMAL_MAPPING
uniform sampler2D normal_map : hint_normal, filter_nearest;
#endif
uniform sampler2D height_map : filter_nearest;
uniform vec2 uv_tiling = vec2(1.0);
uniform vec2 uv_offset = vec2(0.0);
group_uniforms VoxelTraversal;
/**
* Automatically scale depth to keep voxels cubic
*/
uniform bool auto_depth = true;
uniform float depth = 0.125;
uniform int levels : hint_range(1, 256) = 8;
uniform int max_steps : hint_range(8, 256) = 64;
group_uniforms;
void vertex() {
UV = (UV + uv_offset) * uv_tiling;
}
void fragment() {
// Ray in tangent space
mat3 tangent_view_matrix = mat3(TANGENT, -BINORMAL, -NORMAL);
vec3 ray_origin = vec3(UV, 0.0);
vec3 ray_direction = -VIEW * tangent_view_matrix;
vec3 ray_sign = sign(ray_direction);
// Scale ray by texture size and depth
ivec2 i_grid_size = ivec2(textureSize(height_map, 0));
vec2 f_grid_size = vec2(i_grid_size);
float f_levels = float(levels);
float depth_scale = auto_depth ? f_levels / max(f_grid_size.x * uv_tiling.x, f_grid_size.y * uv_tiling.y) : depth;
ray_origin.xy *= f_grid_size;
ray_direction.xy *= f_grid_size * uv_tiling;
ray_direction.z /= depth_scale;
// p - texture sample point
ivec2 p = ivec2(ray_origin.xy);
ivec2 delta_p = ivec2(ray_sign.xy);
// t - distance along ray
float t = 0.0;
vec2 delta_t = abs(1.0 / ray_direction.xy);
vec2 t_max = (ray_sign.xy * (vec2(p) - ray_origin.xy) + (ray_sign.xy * 0.5) + 0.5) * delta_t;
bool hit = false;
int axis = 2;
float voxel_last_z = 0.0;
for (int i = 0; i < max_steps; i++) {
float ray_z = ray_direction.z * t;
// Hit top of voxel
if (voxel_last_z < ray_z) {
t = voxel_last_z / ray_direction.z;
p[axis] -= delta_p[axis];
axis = 2;
hit = true;
break;
}
// Hit side of voxel or bounds
vec4 voxel = texelFetch(height_map, p % i_grid_size, 0);
float voxel_z = floor((1.0 - voxel.r) * f_levels) / f_levels;
if (voxel_z <= ray_z) {
hit = true;
break;
}
axis = t_max.x < t_max.y ? 0 : 1;
t = t_max[axis];
t_max[axis] += delta_t[axis];
p[axis] += delta_p[axis];
voxel_last_z = voxel_z;
}
// If no voxel was found, default to mid-plane
if (!hit) {
t = (depth_scale * 0.5) / ray_direction.z;
p = ivec2(ray_origin.xy + ray_direction.xy * t);
axis = 2;
}
vec2 offset_uv = vec2(p.xy) / f_grid_size.xy;
// Surface properties
ALBEDO = texture(albedo_map, offset_uv).rgb;
ROUGHNESS = 1.0;
SPECULAR = 0.0;
#ifdef FACE_NORMALS
NORMAL = vec3(0.0);
NORMAL[axis] = -ray_sign[axis];
NORMAL = tangent_view_matrix * NORMAL;
#endif
#ifdef BUMP_MAPPING
vec2 pixel = 1.0 / f_grid_size;
float up = texture(height_map, offset_uv + vec2(0.0, pixel.y)).r;
float down = texture(height_map, offset_uv - vec2(0.0, pixel.y)).r;
float right = texture(height_map, offset_uv + vec2(pixel.x, 0.0)).r;
float left = texture(height_map, offset_uv - vec2(pixel.x, 0.0)).r;
NORMAL_MAP = normalize(vec3(vec2(left - right, down - up) * depth_scale / length(pixel), -1.0));
NORMAL_MAP = NORMAL_MAP * 0.5 + 0.5;
#endif
#ifdef NORMAL_MAPPING
NORMAL_MAP = texture(normal_map, offset_uv).rgb;
#endif
#ifdef DEPTH_OFFSET
if (false) discard;
vec3 view_pos = VERTEX - VIEW * t;
vec4 clip_pos = PROJECTION_MATRIX * vec4(view_pos, 1.0);
clip_pos /= clip_pos.w;
LIGHT_VERTEX = view_pos;
DEPTH = clip_pos.z;
#endif
}
