OverviewFilesCommitsIssuesDocsReleasesPeers

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

235 lines · gdshader ref:
// Nubis volumetric cloud sky shader
// Based on Clay John's Godot cloud demo (Schneider/HZD approach)
// Uses pre-baked 3D noise textures for correct cloud shapes
shader_type sky;
render_mode use_half_res_pass;

uniform sampler3D perlworlnoise : filter_linear_mipmap, repeat_enable;
uniform sampler3D worlnoise : filter_linear_mipmap, repeat_enable;
uniform sampler2D weather_map : filter_linear_mipmap, repeat_enable;

uniform float cloud_density_mult : hint_range(0.01, 1.0) = 0.05;
uniform float cloud_coverage : hint_range(0.0, 1.0) = 0.25;
uniform float cloud_time_scale : hint_range(0.0, 20.0) = 1.0;
uniform float cloud_time_offset : hint_range(0.0, 1000.0, 0.5) = 0.0;
uniform vec2 cloud_direction = vec2(1.0, 0.0);

uniform float cloud_bottom : hint_range(100.0, 5000.0) = 1500.0;
uniform float cloud_top : hint_range(500.0, 10000.0) = 4000.0;
uniform float aerial_density : hint_range(0.0, 0.001) = 0.00015;
uniform float precipitation : hint_range(0.0, 1.0) = 0.0;

const float GROUND_RADIUS = 6000000.0;

// --- Utility ---

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

float henyey_greenstein(float cos_theta, float g) {
	float k = 3.0 / (8.0 * PI) * (1.0 - g * g) / (2.0 + g * g);
	return k * (1.0 + cos_theta * cos_theta) / pow(1.0 + g * g - 2.0 * g * cos_theta, 1.5);
}

float hash(vec3 p) {
	p = fract(p * 0.3183099 + 0.1);
	p *= 17.0;
	return fract(p.x * p.y * p.z * (p.x + p.y + p.z));
}

float intersect_sphere(vec3 pos, vec3 dir, float r) {
	float a = dot(dir, dir);
	float b = 2.0 * dot(dir, pos);
	float c = dot(pos, pos) - r * r;
	float d = sqrt(max(b * b - 4.0 * a * c, 0.0));
	return max(-b - d, -b + d) / (2.0 * a);
}

// --- Height gradient (Schneider) ---

float get_height_fraction(float pos_length) {
	return clamp((pos_length - (GROUND_RADIUS + cloud_bottom)) /
		(cloud_top - cloud_bottom), 0.0, 1.0);
}

float density_height_gradient(float height_frac, float cloud_type) {
	const vec4 STRATUS = vec4(0.02, 0.05, 0.09, 0.11);
	const vec4 STRATOCUMULUS = vec4(0.02, 0.2, 0.48, 0.625);
	const vec4 CUMULUS = vec4(0.01, 0.0625, 0.65, 1.0);  // wider top fade (was 0.78)
	const vec4 CUMULONIMBUS = vec4(0.0, 0.04, 0.90, 1.0);
	float st = 1.0 - clamp(cloud_type * 2.0, 0.0, 1.0);
	float sc = 1.0 - abs(cloud_type - 0.5) * 2.0;
	float cu = clamp(cloud_type - 0.5, 0.0, 1.0) * 2.0;
	vec4 g = STRATUS * st + STRATOCUMULUS * sc + CUMULUS * cu;
	// Blend toward cumulonimbus when precipitation is high
	g = mix(g, CUMULONIMBUS, precipitation * precipitation);
	float base = smoothstep(g.x, g.y, height_frac) - smoothstep(g.z, g.w, height_frac);
	// Anvil: horizontal spread above 85% for storm clouds
	float anvil = smoothstep(0.85, 0.95, height_frac) * precipitation * 0.5;
	return max(base, anvil);
}

// --- Cloud density (Schneider method, exact Clay John approach) ---

float cloud_density_fn(vec3 p, vec3 weather, float mip) {
	// Wind: base shape scrolls with time. cloud_time_offset for manual control.
	float wind_time = (TIME + cloud_time_offset) * cloud_time_scale;
	p.x += 10.0 * wind_time;
	float height_frac = get_height_fraction(length(p));

	vec4 n = textureLod(perlworlnoise, p * 0.00008, mip - 2.0);
	float fbm = n.g * 0.625 + n.b * 0.25 + n.a * 0.125;
	float g = density_height_gradient(height_frac, weather.r);
	float base_cloud = remap(n.r, -(1.0 - fbm), 1.0, 0.0, 1.0);
	float weather_coverage = cloud_coverage * weather.b;
	// Schneider remap with smoothstep softening at the boundary
	float shaped = base_cloud * g;
	float threshold = 1.0 - weather_coverage;
	base_cloud = remap(shaped, threshold, 1.0, 0.0, 1.0) * weather_coverage;
	// Smooth the hard 0-boundary: S-curve fade over 10% range
	base_cloud = max(base_cloud, 0.0);
	base_cloud *= smoothstep(threshold - 0.05, threshold + 0.05, shaped);

	// Curl turbulence: cheap hash-based (no extra texture lookups)
	vec3 curl_pos = p * 0.00015;
	float curl_a = hash(curl_pos);
	float curl_b = hash(curl_pos + vec3(7.3, 0.0, 0.0));
	float curl_c = hash(curl_pos + vec3(0.0, 0.0, 11.7));
	// Curl of hash field = divergence-free distortion
	p.x += (curl_a - curl_c) * 80.0;
	p.z += (curl_b - curl_a) * 80.0;

	// Detail erosion — wind shear: detail scrolls faster than base
	p.xy -= 40.0 * wind_time;
	vec3 hn = textureLod(worlnoise, p * 0.001, mip).rgb;
	float hfbm = hn.r * 0.625 + hn.g * 0.25 + hn.b * 0.125;
	hfbm = mix(hfbm, 1.0 - hfbm, clamp(height_frac * 4.0, 0.0, 1.0));
	base_cloud = remap(base_cloud, hfbm * 0.2 * height_frac, 1.0, 0.0, 1.0);

	return pow(clamp(base_cloud, 0.0, 1.0), (1.0 - height_frac) * 0.8 + 0.5);
}

// --- Simple atmosphere ---

vec3 atmosphere(vec3 dir) {
	float horizon = 1.0 - max(dir.y, 0.0);
	horizon *= horizon;
	vec3 zenith = vec3(0.25, 0.45, 0.75);
	vec3 horiz = vec3(0.4, 0.55, 0.75);  // less bright — reduced horizon band
	vec3 ground = vec3(0.35, 0.35, 0.38);  // neutral grey, not brown
	vec3 col = mix(zenith, horiz, horizon);
	if (dir.y < 0.0) col = mix(horiz, ground, min(-dir.y * 3.0, 1.0));

	float sd = dot(dir, LIGHT0_DIRECTION);
	col += vec3(1.0, 0.95, 0.8) * smoothstep(0.9995, 0.9999, sd) * 50.0;
	col += vec3(1.0, 0.8, 0.5) * pow(max(sd, 0.0), 8.0) * 0.3;
	return col;
}

// --- March ---

vec4 march_clouds(vec3 dir, vec3 camera) {
	vec3 cam_pos = vec3(0.0, GROUND_RADIUS, 0.0) + vec3(camera.x * 0.1, camera.y * 0.2, camera.z * 0.1);
	float shell_bottom = GROUND_RADIUS + cloud_bottom;
	float shell_top = GROUND_RADIUS + cloud_top;

	float t0 = intersect_sphere(cam_pos, dir, shell_bottom);
	float t1 = intersect_sphere(cam_pos, dir, shell_top);
	if (t0 < 0.0 || t1 < 0.0) return vec4(0.0);

	vec3 start = cam_pos + dir * t0;
	vec3 end_pt = cam_pos + dir * t1;
	float shell_dist = length(end_pt - start);

	// Fixed step size in world space: ~50m per step.
	// Looking up (short path): fewer steps but same resolution per step.
	// Looking at horizon (long path): more steps, capped at 128.
	float target_step = 50.0;
	int steps = clamp(int(shell_dist / target_step), 16, 128);
	vec3 ray_step = dir * shell_dist / float(steps);
	float ss = length(ray_step);

	float T = 1.0;
	float alpha = 0.0;
	vec3 L = vec3(0.0);
	vec3 p = start + hash(start * 10.0) * ss;

	float lss = (cloud_top - cloud_bottom) / 36.0;
	vec3 ldir = normalize(LIGHT0_DIRECTION);
	float cos_theta = dot(ldir, normalize(dir));
	float phase = max(henyey_greenstein(cos_theta, 0.6), henyey_greenstein(cos_theta, -0.2));

	float weather_scale = 0.00006;
	float wind_t = 0.003 * (TIME + cloud_time_offset) * cloud_time_scale;
	vec2 weather_pos = vec2(wind_t * cloud_direction.x, wind_t * cloud_direction.y);

	vec3 sun_color = LIGHT0_COLOR * LIGHT0_ENERGY * ss * 0.1;
	vec3 ambient_color = atmosphere(normalize(vec3(1.0, 1.0, 0.0))) * 0.5;
	float total_dist = 0.0;

	for (int i = 0; i < steps; i++) {
		p += ray_step;
		total_dist += ss;
		vec3 weather = texture(weather_map, p.xz * weather_scale + 0.5 + weather_pos).xyz;
		float t = cloud_density_fn(p, weather, 0.0);
		float dt = exp(-cloud_density_mult * t * ss);
		T *= dt;

		if (t > 0.0) {
			// Light march (6 steps toward sun)
			vec3 lp = p;
			float cd = 0.0;
			for (int j = 0; j < 6; j++) {
				lp += ldir * lss;
				vec3 lw = texture(weather_map, lp.xz * weather_scale + 0.5 + weather_pos).xyz;
				cd += cloud_density_fn(lp, lw, float(j));
			}

			// Beer-Powder (original Clay John, no modifications)
			float optical = cloud_density_mult * cd * lss;
			float beers = exp(-optical);
			float beers2 = exp(-optical * 0.25) * 0.7;
			float beers_total = max(beers, beers2);

			float height_frac = get_height_fraction(length(p));
			// Precipitation darkening: darken base at bottom of cloud
			float precip_dark = 1.0 - precipitation * 0.4 * (1.0 - height_frac);
			vec3 ambient = mix(ambient_color * 0.3, vec3(1.0), smoothstep(0.0, 1.0, height_frac))
				* cloud_density_mult * ambient_color;

			alpha += (1.0 - dt) * (1.0 - alpha);
			L += (ambient + beers_total * sun_color * phase * alpha) * T * t * precip_dark;
		}

		if (alpha > 0.99) break;
	}

	// Aerial perspective: distant clouds fade toward sky color
	vec3 aerial_col = atmosphere(dir);
	float aerial_fade = 1.0 - exp(-total_dist * aerial_density);
	L = mix(L, aerial_col * max(alpha, 0.1), aerial_fade);

	return clamp(vec4(L, alpha), 0.0, 1.0);
}

void sky() {
	if (AT_HALF_RES_PASS) {
		if (EYEDIR.y > 0.0) {
			vec4 cloud = march_clouds(EYEDIR, POSITION);
			COLOR = cloud.rgb;
			ALPHA = cloud.a;
		} else {
			COLOR = vec3(0.0);
			ALPHA = 0.0;
		}
	} else {
		vec3 sky = atmosphere(EYEDIR);
		vec4 cloud = HALF_RES_COLOR;
		if (cloud.a > 0.005) {
			sky = mix(sky, cloud.rgb, cloud.a);
		}
		COLOR = sky;
	}
}