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