OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / addons / nubis / shaders / cloud_raymarch_spatial.gdshader

119 lines · gdshader ref:
// Cloud chunk ray-march shader — renders a 3D density texture
// as a volumetric cloud on a box mesh. Uses cull_front so fragments
// fire on the back face (camera sees through the volume).
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_front, unshaded;

uniform sampler3D density_tex : filter_linear_mipmap, repeat_disable;
uniform sampler2D depth_texture : hint_depth_texture, filter_linear_mipmap;
uniform float density_scale : hint_range(0.0, 20.0) = 5.0;
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;

// Box bounds in local space (unit cube centered at origin)
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);
}

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

// Henyey-Greenstein phase function
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));
}

void fragment() {
	// Camera position in LOCAL space of this box
	vec3 ro = (inverse(MODEL_MATRIX) * INV_VIEW_MATRIX * vec4(0, 0, 0, 1)).xyz;
	// Fragment position in local space (back face due to cull_front)
	vec3 frag_local = (inverse(MODEL_MATRIX) * INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
	vec3 rd = normalize(frag_local - ro);

	// Intersect ray with unit box
	vec2 t = intersect_box(ro, rd);
	if (t.x >= t.y) discard;

	// Scene depth — terminate ray if it hits geometry
	float raw_depth = textureLod(depth_texture, SCREEN_UV, 0.0).r;
	// Convert depth to linear distance
	vec4 clip_pos = vec4(SCREEN_UV * 2.0 - 1.0, raw_depth, 1.0);
	vec4 view_pos = INV_PROJECTION_MATRIX * clip_pos;
	float scene_depth = -view_pos.z / view_pos.w;
	// Current fragment depth in view space
	float frag_depth = -VERTEX.z;
	// Max march distance limited by scene geometry
	float max_t = t.y;
	if (raw_depth > 0.0001) {
		float depth_ratio = scene_depth / frag_depth;
		max_t = min(max_t, t.x + (t.y - t.x) * clamp(depth_ratio, 0.0, 1.0));
	}

	// March through the box
	int steps = 48;
	float step_size = (max_t - t.x) / float(steps);
	float transmittance = 1.0;
	vec3 light_energy = vec3(0.0);

	// Sun direction in local space
	vec3 local_sun = normalize((inverse(MODEL_MATRIX) * vec4(sun_direction, 0.0)).xyz);
	float cos_theta = dot(rd, local_sun);
	float phase = max(hg_phase(cos_theta, 0.6), hg_phase(cos_theta, -0.2));

	for (int i = 0; i < steps; i++) {
		if (transmittance < 0.01) break;

		float tc = t.x + (float(i) + 0.5) * step_size;
		vec3 pos = ro + rd * tc;

		// Sample density (UVW maps [-0.5, 0.5] to [0, 1])
		vec3 uvw = pos + 0.5;
		float density = texture(density_tex, uvw).r;
		if (density < 0.001) continue;

		float sample_ext = density_scale * density;
		float height_frac = uvw.y;

		// Light march (4 steps toward sun)
		float light_density = 0.0;
		float light_step = 0.15;
		for (int j = 1; j <= 4; j++) {
			vec3 lp = pos + local_sun * light_step * float(j);
			vec3 luv = lp + 0.5;
			if (all(greaterThanEqual(luv, vec3(0))) && all(lessThanEqual(luv, vec3(1)))) {
				light_density += texture(density_tex, luv).r;
			}
		}

		float lt = beer_powder(light_density * density_scale * light_step);
		float ambient = mix(0.15, 0.4, height_frac);
		vec3 col = sun_color * lt * phase * sun_energy + vec3(0.4, 0.5, 0.7) * ambient;

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

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

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