#[compute]
#version 450
#define MAX_VIEWS 2
#include "godot/scene_data_inc.glsl"
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(set = 0, binding = 0, std140) uniform SceneDataBlock {
SceneData data;
SceneData prev_data;
} scene_data_block;
layout(rgba16f, set = 0, binding = 1) uniform image2D color_image;
layout(set = 0, binding = 2) uniform sampler2D depth_texture;
// Cloud volume data
layout(set = 1, binding = 0) uniform sampler3D cloud_sdf;
layout(set = 1, binding = 1) uniform sampler3D perlworlnoise;
layout(push_constant, std430) uniform Params {
vec2 raster_size;
float view;
float density_scale;
vec4 volume_pos; // xyz = world center, w = wind_time
vec4 volume_size; // xyz = half extents, w = erosion_strength
vec4 sun_dir; // xyz = direction toward sun, w = noise_freq
} params;
// Ray-box intersection in world space
vec2 intersect_aabb(vec3 ro, vec3 rd, vec3 box_min, vec3 box_max) {
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 beer_powder(float d) {
return 2.0 * exp(-d) * (1.0 - exp(-2.0 * d));
}
float hg_phase(float cos_theta, float g) {
float g2 = g * g;
return (1.0 - g2) / (4.0 * 3.14159265 * pow(1.0 + g2 - 2.0 * g * cos_theta, 1.5));
}
void main() {
ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
ivec2 size = ivec2(params.raster_size);
int view = int(params.view);
if (uv.x >= size.x || uv.y >= size.y) return;
// --- Reconstruct world-space ray from SceneData ---
vec2 uv_norm = (vec2(uv) + 0.5) / params.raster_size;
vec2 ndc = uv_norm * 2.0 - 1.0;
vec4 view_pos_near = scene_data_block.data.inv_projection_matrix * vec4(ndc, 1.0, 1.0);
vec4 view_pos_far = scene_data_block.data.inv_projection_matrix * vec4(ndc, 0.0, 1.0);
vec3 view_near = view_pos_near.xyz / view_pos_near.w;
vec3 view_far = view_pos_far.xyz / view_pos_far.w;
// Canonical mat3x4 → mat4 reconstruction (scene_forward_clustered.glsl:235-238)
mat3x4 ivm = scene_data_block.data.inv_view_matrix;
mat4 inv_view = transpose(mat4(ivm[0], ivm[1], ivm[2], vec4(0.0, 0.0, 0.0, 1.0)));
vec3 cam_pos = inv_view[3].xyz;
mat3 inv_view_rot = mat3(inv_view);
vec3 view_dir = normalize(view_far - view_near);
vec3 rd = normalize(inv_view_rot * view_dir);
// --- Scene depth ---
float raw_depth = texture(depth_texture, uv_norm).r;
float scene_dist = 1e20;
if (raw_depth > 0.0001) {
vec4 scene_clip = vec4(ndc, raw_depth, 1.0);
vec4 scene_view_pos = scene_data_block.data.inv_projection_matrix * scene_clip;
vec3 sv = scene_view_pos.xyz / scene_view_pos.w;
vec3 scene_world = (inv_view * vec4(sv, 1.0)).xyz;
scene_dist = length(scene_world - cam_pos);
}
// --- Intersect ray with cloud volume AABB ---
vec3 box_min = params.volume_pos.xyz - params.volume_size.xyz;
vec3 box_max = params.volume_pos.xyz + params.volume_size.xyz;
vec2 t = intersect_aabb(cam_pos, rd, box_min, box_max);
if (t.x >= t.y || t.y <= 0.0) return;
float max_dist = min(t.y, scene_dist);
if (t.x >= max_dist) return;
// --- Ray march through cloud volume ---
vec3 sun_dir = params.sun_dir.xyz;
float cos_theta = dot(rd, sun_dir);
float phase = max(hg_phase(cos_theta, 0.7), hg_phase(cos_theta, -0.2));
float wind_time = params.volume_pos.w;
float transmittance = 1.0;
vec3 light_energy = vec3(0.0);
float ray_length = max_dist - t.x;
// Scale steps by ray length: ~10m per step, clamped 32-192
int steps = clamp(int(ray_length / 10.0), 32, 192);
float step_size = ray_length / float(steps);
// Jitter ray start to reduce banding
float jitter = fract(sin(dot(vec2(uv), vec2(12.9898, 78.233))) * 43758.5453);
for (int i = 0; i < steps; i++) {
if (transmittance < 0.01) break;
float tc = t.x + (float(i) + jitter) * step_size;
vec3 pos = cam_pos + rd * tc;
// Map world position to volume UVW [0,1]
vec3 uvw = (pos - box_min) / (box_max - box_min);
// Drift the SDF sample slowly — cloud structure morphs over time
vec3 sdf_drift = vec3(wind_time * 2.0, 0.0, wind_time * 0.7);
vec3 sdf_uvw = uvw + sdf_drift / (box_max - box_min);
// Sample dimensional profile
float sdf = texture(cloud_sdf, sdf_uvw).r;
if (sdf <= 0.001) continue;
// Negative-space noise: invisible field that carves dissolving channels
// Samples at very low frequency so it creates large-scale gaps that drift
vec3 neg_pos = pos * 0.001 + vec3(wind_time * 1.5, wind_time * 0.3, wind_time * 0.8);
float neg_noise = textureLod(perlworlnoise, neg_pos, 0.0).b;
// Where neg_noise is low (<0.3), carve a hole in the cloud
float neg_mask = smoothstep(0.15, 0.35, neg_noise);
sdf *= neg_mask;
if (sdf <= 0.001) continue;
// Flat bottom (uses original uvw)
float height_frac = uvw.y;
float bottom_fade = smoothstep(0.15, 0.35, height_frac);
sdf *= bottom_fade;
if (sdf <= 0.001) continue;
// --- DENSITY FROM NOISE ---
vec3 wind_offset = vec3(wind_time * 10.0, 0.0, wind_time * 3.0);
vec3 sample_pos = pos - wind_offset;
// Curl turbulence: distort sample position for organic swirls
float freq = params.sun_dir.w; // noise_freq from inspector
float strength = params.volume_size.w; // erosion_strength from inspector
vec4 turb = textureLod(perlworlnoise, sample_pos * freq * 0.5, 0.0);
vec3 curl = (turb.rgb - 0.5) * 2.0;
sample_pos += curl * (20.0 / max(freq, 0.001));
// Three octaves at freq, freq*3, freq*8
vec4 n1 = textureLod(perlworlnoise, sample_pos * freq, 0.0);
vec4 n2 = textureLod(perlworlnoise, sample_pos * freq * 3.0, 0.0);
vec4 n3 = textureLod(perlworlnoise, sample_pos * freq * 8.0, 0.0);
// FBM blend: large shape + medium bumps + fine detail
float n_raw = n1.a * 0.55 + n2.a * 0.30 + n3.a * 0.15;
float n = clamp((n_raw - 0.07) / 0.93, 0.0, 1.0);
n = pow(n, 1.3);
// Erode sphere: noise pushes boundary inward
float erosion_factor = strength * (1.0 - sdf * 0.4);
float erosion = (1.0 - n) * erosion_factor;
float density = max(sdf - erosion, 0.0) * params.density_scale;
// Edge fade at box boundaries
vec3 edge = smoothstep(vec3(0.0), vec3(0.06), uvw) * smoothstep(vec3(0.0), vec3(0.06), vec3(1.0) - uvw);
density *= edge.x * edge.y * edge.z;
if (density < 0.001) continue;
// Light march (6 steps toward sun)
float light_od = 0.0;
float l_step = 0.04 * length(box_max - box_min);
vec3 lp = pos;
for (int j = 0; j < 6; j++) {
lp += sun_dir * l_step;
vec3 l_uvw = (lp - box_min) / (box_max - box_min);
if (any(lessThan(l_uvw, vec3(0))) || any(greaterThan(l_uvw, vec3(1)))) break;
light_od += max(texture(cloud_sdf, l_uvw).r, 0.0);
}
float optical = light_od * params.density_scale * l_step * 0.01;
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);
float ms_volume = sdf * beer_total;
float ambient_potential = pow(max(1.0 - sdf, 0.0), 0.5);
vec3 ambient = mix(vec3(0.2, 0.22, 0.28), vec3(0.5, 0.6, 0.8), height_frac) * ambient_potential * 0.4;
vec3 direct = vec3(1.0, 0.95, 0.85) * (ms_volume * 0.5 + beer_powder(optical * 0.3) * 0.15);
vec3 silver = vec3(1.0, 0.95, 0.85) * beer_total * beer_total * phase * 0.2;
vec3 col = direct + silver + ambient;
float st = exp(-density * step_size * 0.01);
light_energy += col * transmittance * (1.0 - st);
transmittance *= st;
}
float alpha = 1.0 - transmittance;
if (alpha < 0.005) return;
// Composite over scene
vec4 scene_color = imageLoad(color_image, uv);
vec3 final_color = light_energy + scene_color.rgb * transmittance;
imageStore(color_image, uv, vec4(final_color, 1.0));
}