OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / spom_probe.gdshader

144 lines · gdshader ref:
// 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
}