OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / addons / nubis / shaders / cloud_compositor.glsl

215 lines · glsl ref:
#[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));
}