// SPOM / fast-POM EVALUATION shader (probe only — not the level shader). One quad, five modes:
// 0 normal map only (baseline)
// 1 POM: angle-adaptive linear search + last-step interpolation (Godot deep parallax,
// scene/resources/material.cpp ~1517, but with the physically right xy/z offset)
// 2 "FPOM": 1 + distance fade to mode 0 + MAX-MIP empty-space skipping (quadtree
// displacement, Drobot 2009) before a short linear refine
// 3 SPOM: 1 + discard where the hit leaves the face's UV bounds -> carved silhouette
// 4 SPOM + DEPTH write (correct intersections; costs early-z)
// Relief is INWARD (white = top = the face plane), so silhouettes carve, never grow.
//
// MODE IS A COMPILE-TIME VARIANT (#define MODE n, injected by _probe_spom.gd), not a
// uniform: `discard` and a DEPTH write change the PIPELINE (no early-z) for every pixel
// of a shader that merely CONTAINS them, so a shared uber-shader would charge all modes
// SPOM's costs and hide the very difference being measured.
shader_type spatial;
#ifndef MODE
#define MODE 1
#endif
render_mode depth_draw_opaque, cull_back;
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2D normal_tex : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable;
uniform sampler2D height_tex : filter_linear_mipmap, repeat_enable;
// Pyramid of the MIN DEPTH (= 1 - max height) per cell, built on the CPU (max of 2x2).
uniform sampler2D mindepth_tex : filter_nearest_mipmap, repeat_enable;
// MODE 5: the POM code (mode 1) PRESENT but switched off by a uniform — what a
// uniform-toggled tier costs the players who leave it off (register pressure etc.).
#if MODE == 5
uniform bool pom_on = false;
#define RUN_POM pom_on
const int mode = 1;
#else
#define RUN_POM (mode != 0)
const int mode = MODE;
#endif
uniform float depth_m = 0.03; // relief depth, metres
uniform vec2 tile_m = vec2(2.4, 1.2); // metres per texture repeat (u, v)
uniform vec2 quad_m = vec2(4.0, 2.0); // the quad's size, metres
uniform int min_steps = 8;
uniform int max_steps = 32;
uniform float fade_start = 8.0; // FPOM: full relief nearer than this
uniform float fade_end = 14.0; // FPOM: plain normal map beyond this
uniform int top_level = 6; // FPOM: coarsest pyramid level used
uniform vec2 tex_size = vec2(1024.0, 512.0); // height map pixels (cells are per-axis)
uniform bool dx_normals = true; // pack normals are DirectX (+G down)
float depth_at(vec2 uv, vec2 dx, vec2 dy) {
return 1.0 - textureGrad(height_tex, uv, dx, dy).r;
}
void fragment() {
vec2 base_uv = UV * quad_m / tile_m; // continuous, in tiles
vec2 dx = dFdx(base_uv);
vec2 dy = dFdy(base_uv);
vec2 uv = base_uv;
float hit_t = 0.0; // depth of the hit, 0..1 of depth_m
vec3 view_dir = normalize(normalize(-VERTEX + EYE_OFFSET) * mat3(TANGENT, -BINORMAL, NORMAL));
float relief = 1.0;
if (mode == 2) {
relief = 1.0 - smoothstep(fade_start, fade_end, length(VERTEX));
}
if (RUN_POM && relief > 0.0) {
// Offset per unit depth, in tiles: physically view.xy / view.z (clamped at grazing).
vec2 p = view_dir.xy / max(view_dir.z, 0.12) * (depth_m * relief) / tile_m;
vec2 dir = -p; // uv moves this much per 1.0 of depth
float t = 0.0;
if (mode == 2) {
// QUADTREE / MAX-MIP SKIPPING: in a coarse cell whose shallowest point is
// deeper than the ray, jump straight to that depth or the cell's exit.
int level = top_level;
for (int i = 0; i < 48 && level >= 0; i++) {
vec2 cur = base_uv + dir * t;
float md = textureLod(mindepth_tex, cur, float(level)).r;
if (t < md) {
vec2 cs = exp2(float(level)) / tex_size;
vec2 cell = floor(cur / cs);
vec2 bound = (cell + step(vec2(0.0), dir)) * cs;
vec2 tx = (bound - cur) / (dir + sign(dir) * 1e-6 + vec2(1e-9));
float t_exit = min(abs(tx.x), abs(tx.y)) + 1e-4;
if (md <= t + t_exit) {
t = md;
level -= 1; // reached the cell floor: look finer
} else {
t += t_exit; // crossed into the next cell
}
} else {
level -= 1;
}
if (t >= 1.0) { t = 1.0; break; }
}
// short linear refine around the coarse hit
float lt = max(t - 0.06, 0.0);
float step_t = 0.015;
float d = depth_at(base_uv + dir * lt, dx, dy);
for (int i = 0; i < 6 && lt < d; i++) {
lt += step_t;
d = depth_at(base_uv + dir * lt, dx, dy);
}
float before = depth_at(base_uv + dir * (lt - step_t), dx, dy) - (lt - step_t);
float after = d - lt;
float w = after / (after - before + 1e-6);
t = mix(lt, lt - step_t, clamp(w, 0.0, 1.0));
} else {
float n = mix(float(max_steps), float(min_steps), abs(view_dir.z));
float layer = 1.0 / n;
float d = depth_at(base_uv, dx, dy);
for (int i = 0; i < 128 && t < d; i++) {
t += layer;
d = depth_at(base_uv + dir * t, dx, dy);
}
float before = depth_at(base_uv + dir * (t - layer), dx, dy) - (t - layer);
float after = d - t;
float w = after / (after - before + 1e-6);
t = mix(t, t - layer, clamp(w, 0.0, 1.0));
}
hit_t = t;
uv = base_uv + dir * t;
#if MODE >= 3
// SILHOUETTE: the hit left this face -> nothing here, let what is behind show.
vec2 lim = quad_m / tile_m;
if (uv.x < 0.0 || uv.y < 0.0 || uv.x > lim.x || uv.y > lim.y) {
discard;
}
#endif
}
ALBEDO = textureGrad(albedo_tex, uv, dx, dy).rgb;
vec3 n = textureGrad(normal_tex, uv, dx, dy).rgb;
if (dx_normals) {
n.g = 1.0 - n.g; // Godot reads OpenGL (+G up) normal maps
}
NORMAL_MAP = n;
ROUGHNESS = 0.8;
#if MODE == 4
// The hit is deeper along the view ray: depth below the face / cos(view angle).
// (A shader that writes DEPTH must write it on EVERY path, or depth is undefined and
// the face z-fights what is behind it — seen as 4x4 noise when this was a uniform.)
float along = hit_t * depth_m * relief / max(view_dir.z, 0.12);
vec3 hit = VERTEX + normalize(VERTEX) * along;
vec4 c = PROJECTION_MATRIX * vec4(hit, 1.0);
DEPTH = c.z / c.w;
#endif
}