// 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;
}
}