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