OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / addons / nubis_clouds / shaders / cloud_sdf_raymarch.gdshader

216 lines · gdshader ref:
// Nubis Cubed-inspired cloud ray-march shader
// Uses dimensional profile (SDF) for step acceleration and noise blend control.
// Renders on a box mesh (spatial shader, cull_front for back-face entry).
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_front, unshaded;

// Cloud SDF / dimensional profile (3D texture from cloud volume)
uniform sampler3D cloud_sdf : filter_linear, repeat_disable;

// Pre-baked noise for up-rez detail
uniform sampler3D perlworlnoise : filter_linear_mipmap, repeat_enable;
uniform sampler3D worlnoise : filter_linear_mipmap, repeat_enable;

// Scene depth for terrain occlusion
uniform sampler2D depth_texture : hint_depth_texture, filter_linear_mipmap;

// Parameters
uniform float density_scale : hint_range(0.0, 200.0) = 50.0;
uniform float noise_scale : hint_range(0.0001, 0.01) = 0.001;
uniform float detail_erosion : hint_range(0.0, 0.5) = 0.2;
uniform float fade_t : hint_range(0.0, 1.0) = 1.0;
uniform vec3 sun_direction = vec3(0.5, 0.7, 0.3);
uniform vec3 sun_color : source_color = vec3(1.0, 0.95, 0.85);
uniform float sun_energy : hint_range(0.0, 5.0) = 1.2;

const vec3 BOX_MIN = vec3(-0.5);
const vec3 BOX_MAX = vec3(0.5);

// Ray-box intersection (local space)
vec2 intersect_box(vec3 ro, vec3 rd) {
	vec3 inv_rd = 1.0 / rd;
	vec3 t0 = (BOX_MIN - ro) * inv_rd;
	vec3 t1 = (BOX_MAX - ro) * inv_rd;
	vec3 tmin = min(t0, t1);
	vec3 tmax = max(t0, t1);
	float near = max(max(tmin.x, tmin.y), tmin.z);
	float far = min(min(tmax.x, tmax.y), tmax.z);
	return vec2(max(near, 0.0), far);
}

float remap(float v, float lo, float hi, float nlo, float nhi) {
	return nlo + (v - lo) / (hi - lo) * (nhi - nlo);
}

// Beer-Powder transmittance
float beer_powder(float d) {
	return 2.0 * exp(-d) * (1.0 - exp(-2.0 * d));
}

// Henyey-Greenstein phase
float hg_phase(float cos_theta, float g) {
	float g2 = g * g;
	return (1.0 - g2) / (4.0 * PI * pow(1.0 + g2 - 2.0 * g * cos_theta, 1.5));
}

// Nubis Cubed up-rez: use SDF dimensional profile to blend noise detail
float uprez_density(vec3 world_pos, float dimensional_profile, float detail_type) {
	if (dimensional_profile <= 0.0) return 0.0;

	// Sample noise with wind offset (could add TIME here)
	vec3 noise_pos = world_pos * noise_scale;
	vec4 noise = textureLod(perlworlnoise, noise_pos, 0.0);

	// Wispy noise: blend low→high frequency over dimensional profile
	// At edges (low profile): high freq wisps. At core (high profile): low freq structure.
	float wispy_noise = mix(noise.r, noise.g, dimensional_profile);

	// Billowy noise: same principle but with Worley
	vec3 detail = textureLod(worlnoise, noise_pos * 3.0, 0.0).rgb;
	float billowy_type_gradient = pow(dimensional_profile, 0.25);
	float billowy_noise = mix(detail.g * 0.3, detail.b * 0.3, billowy_type_gradient);

	// Blend wispy and billowy by detail type
	float noise_composite = mix(wispy_noise, billowy_noise, detail_type);

	// Erode the dimensional profile with the noise composite
	float uprezed = remap(dimensional_profile, noise_composite * detail_erosion, 1.0, 0.0, 1.0);

	// Scale density — keep it simple, sharpen later if needed
	return max(uprezed, 0.0) * density_scale;
}

void fragment() {
	// Camera in local space
	vec3 ro = (inverse(MODEL_MATRIX) * vec4((INV_VIEW_MATRIX * vec4(0, 0, 0, 1)).xyz, 1.0)).xyz;
	vec3 frag_world = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
	vec3 frag_local = (inverse(MODEL_MATRIX) * vec4(frag_world, 1.0)).xyz;
	vec3 rd = normalize(frag_local - ro);

	vec2 t = intersect_box(ro, rd);
	if (t.x >= t.y) discard;

	// --- DEPTH OCCLUSION: stop ray where scene geometry is closer ---
	float raw_depth = textureLod(depth_texture, SCREEN_UV, 0.0).r;
	if (raw_depth > 0.0001) {
		// Linearize scene depth to view-space Z (negative)
		vec4 clip_pos = vec4(SCREEN_UV * 2.0 - 1.0, raw_depth, 1.0);
		vec4 view_pos = INV_PROJECTION_MATRIX * clip_pos;
		float scene_z = -view_pos.z / view_pos.w;
		// Back face fragment depth (this fragment, view space)
		float back_z = -VERTEX.z;
		// If scene is closer than the back face, limit the ray
		if (scene_z < back_z && back_z > 0.001) {
			// Approximate: scale t.y proportionally to how far into the box the scene is
			float depth_frac = scene_z / back_z;
			t.y = t.x + (t.y - t.x) * clamp(depth_frac, 0.0, 1.0);
		}
	}

	// Sun in local space
	vec3 local_sun = normalize((inverse(MODEL_MATRIX) * vec4(sun_direction, 0.0)).xyz);
	float cos_theta = dot(rd, local_sun);
	// Triple-lobe phase: forward scatter + dynamic backscatter + weak backscatter
	float phase = max(max(hg_phase(cos_theta, 0.7), hg_phase(cos_theta, 0.4)), hg_phase(cos_theta, -0.2));

	// March
	float transmittance = 1.0;
	vec3 light_energy = vec3(0.0);
	float current_t = t.x;

	for (int i = 0; i < 128; i++) {
		if (transmittance < 0.01 || current_t >= t.y) break;

		vec3 pos = ro + rd * current_t;
		vec3 uvw = pos + 0.5;

		// Sample dimensional profile (SDF)
		float sdf = texture(cloud_sdf, uvw).r;

		if (sdf <= 0.001) {
			current_t += 0.03;
			continue;
		}

		// --- BOX EDGE FADE: smooth density to 0 at box boundaries ---
		vec3 edge = smoothstep(vec3(0.0), vec3(0.06), uvw) * smoothstep(vec3(0.0), vec3(0.06), vec3(1.0) - uvw);
		float edge_fade = edge.x * edge.y * edge.z;

		// --- DEPTH-DEPENDENT DETAIL (#9) ---
		vec3 world_pos = (MODEL_MATRIX * vec4(pos, 1.0)).xyz;
		vec3 np = world_pos * noise_scale;
		vec4 noise = textureLod(perlworlnoise, np, 0.0);
		// Wisps at edges (low sdf), billows at core (high sdf)
		float wispy = mix(noise.r, noise.g, sdf);
		vec3 detail = textureLod(worlnoise, np * 3.0, 0.0).rgb;
		float billowy = mix(detail.g, detail.b, pow(sdf, 0.25));
		float noise_composite = mix(wispy, billowy, 0.5);

		// Erode SDF with depth-dependent noise
		float eroded = sdf - noise_composite * detail_erosion * (1.0 - sdf * 0.5);
		float density = max(eroded, 0.0) * density_scale * edge_fade;

		if (density < 0.001) {
			current_t += 0.01;
			continue;
		}

		float step_size = 0.012;
		float height_frac = uvw.y;

		// --- LIGHT MARCH ---
		float light_optical_depth = 0.0;
		float light_step = 0.04;
		vec3 light_pos = pos;
		for (int j = 0; j < 6; j++) {
			light_pos += local_sun * light_step;
			vec3 l_uvw = light_pos + 0.5;
			if (any(lessThan(l_uvw, vec3(0))) || any(greaterThan(l_uvw, vec3(1)))) break;
			float l_sdf = texture(cloud_sdf, l_uvw).r;
			light_optical_depth += max(l_sdf, 0.0);
		}

		float optical = light_optical_depth * density_scale * light_step;

		// === NUBIS CUBED LIGHTING (from SIGGRAPH 2023 PDF) ===

		// Beer-Lambert with sun-angle relaxation (p.139)
		float sun_relax = mix(0.25, 0.65, smoothstep(0.0, 0.9, cos_theta));
		float beer = exp(-optical * sun_relax);
		float beer2 = exp(-optical * sun_relax * 0.25) * 0.7;
		float beer_total = max(beer, beer2);

		// Beer-Powder (direct light only)
		float powder = beer_powder(optical * 0.3);

		// Multi-scatter volume (p.139): dimensional profile IS scatter probability
		float ms_volume = sdf * beer_total;

		// Silver lining: HG phase at thin edges (high beer_total = thin)
		float edge_brightness = beer_total * beer_total;

		// Ambient scattering (p.144): pow(1 - dimensional_profile, 0.5)
		float ambient_potential = pow(max(1.0 - sdf, 0.0), 0.5);
		vec3 ambient_sky = vec3(0.5, 0.6, 0.8);
		vec3 ambient_ground = vec3(0.2, 0.22, 0.28);
		vec3 ambient_color = mix(ambient_ground, ambient_sky, height_frac) * ambient_potential * 0.4;

		// Direct sun + multi-scatter glow + silver lining + ambient
		vec3 direct = sun_color * (ms_volume * 0.5 + powder * 0.15) * sun_energy;
		vec3 silver = sun_color * edge_brightness * phase * sun_energy * 0.2;
		vec3 col = direct + silver + ambient_color;

		float st = exp(-density * step_size);
		light_energy += col * transmittance * (1.0 - st);
		transmittance *= st;

		current_t += step_size;
	}

	float alpha = (1.0 - transmittance) * fade_t;
	if (alpha < 0.005) discard;

	ALBEDO = light_energy / max(alpha, 0.01);
	ALPHA = alpha;
}