OverviewFilesCommitsIssuesDocsReleasesPeers

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

202 lines · glsl ref:
#[compute]
#version 450

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(set = 0, binding = 0) uniform sampler2D depth_texture;
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2D cloud_texture;

layout(set = 1, binding = 0) uniform sampler3D perlin_worley_noise;
layout(set = 1, binding = 1) uniform sampler3D detail_noise;

layout(push_constant, std430) uniform Params {
	vec4 camera_pos_ymin;
	vec4 sun_dir_ymax;
	vec4 screen_time;
	vec4 cam_right;       // xyz = right, w = half_fov_tan
	vec4 cam_up_aspect;   // xyz = up, w = aspect
	vec4 cam_forward_pad; // xyz = forward
} p;

// --- Height gradient presets (Nubis / GPU Pro 7) ---

float height_gradient(float h, vec4 g) {
	return smoothstep(g.x, g.y, h) - smoothstep(g.z, g.w, h);
}

const vec4 STRATOCUMULUS = vec4(0.02, 0.20, 0.48, 0.625);
const vec4 CUMULUS       = vec4(0.01, 0.0625, 0.78, 1.0);

float remap(float v, float lo, float hi, float new_lo, float new_hi) {
	return new_lo + (v - lo) / (hi - lo) * (new_hi - new_lo);
}

vec2 intersect_slab(vec3 ro, vec3 rd, float y_lo, float y_hi) {
	if (abs(rd.y) < 1e-6) {
		if (ro.y >= y_lo && ro.y <= y_hi) return vec2(0.0, 1e20);
		else return vec2(1.0, 0.0);
	}
	float t0 = (y_lo - ro.y) / rd.y;
	float t1 = (y_hi - ro.y) / rd.y;
	if (t0 > t1) { float tmp = t0; t0 = t1; t1 = tmp; }
	return vec2(max(t0, 0.0), max(t1, 0.0));
}

// --- Procedural 3D noise ---

float hash31(vec3 p) {
	p = fract(p * vec3(443.897, 441.423, 437.195));
	p += dot(p, p.yzx + 19.19);
	return fract((p.x + p.y) * p.z);
}

float value_noise_3d(vec3 p) {
	vec3 i = floor(p);
	vec3 f = fract(p);
	f = f * f * (3.0 - 2.0 * f);
	return mix(
		mix(mix(hash31(i), hash31(i+vec3(1,0,0)), f.x),
			mix(hash31(i+vec3(0,1,0)), hash31(i+vec3(1,1,0)), f.x), f.y),
		mix(mix(hash31(i+vec3(0,0,1)), hash31(i+vec3(1,0,1)), f.x),
			mix(hash31(i+vec3(0,1,1)), hash31(i+vec3(1,1,1)), f.x), f.y),
		f.z);
}

float fbm_3d(vec3 p, int octaves) {
	float sum = 0.0, amp = 0.5, freq = 1.0;
	for (int i = 0; i < octaves; i++) {
		sum += value_noise_3d(p * freq) * amp;
		freq *= 2.0; amp *= 0.5;
	}
	return sum;
}

float worley_3d(vec3 p) {
	vec3 i = floor(p);
	vec3 f = fract(p);
	float md = 1.0;
	for (int z = -1; z <= 1; z++)
	for (int y = -1; y <= 1; y++)
	for (int x = -1; x <= 1; x++) {
		vec3 off = vec3(float(x), float(y), float(z));
		vec3 cell = i + off;
		vec3 pt = vec3(hash31(cell), hash31(cell+100.0), hash31(cell+200.0));
		md = min(md, length(off + pt - f));
	}
	return md;
}

// --- Cloud density ---

float sample_cloud_density(vec3 pos, float y_min, float y_max, float weather) {
	float slab_thickness = y_max - y_min;
	float height_frac = clamp((pos.y - y_min) / slab_thickness, 0.0, 1.0);

	vec3 sp = pos * 0.002;
	float perlin = fbm_3d(sp, 4);
	float worley = 1.0 - worley_3d(sp * 2.0);
	float base_cloud = perlin * 0.7 + worley * 0.3;

	float gradient = mix(
		height_gradient(height_frac, CUMULUS),
		height_gradient(height_frac, STRATOCUMULUS),
		weather);
	base_cloud *= gradient;

	float coverage = mix(0.2, 0.85, weather);
	base_cloud = remap(base_cloud, 1.0 - coverage, 1.0, 0.0, 1.0);
	base_cloud *= coverage;

	if (base_cloud > 0.0) {
		float detail = fbm_3d(pos * 0.01, 3);
		float erosion = mix(detail, 1.0 - detail, clamp(height_frac * 3.0, 0.0, 1.0));
		base_cloud = remap(base_cloud, erosion * 0.15, 1.0, 0.0, 1.0);
	}
	return clamp(base_cloud, 0.0, 1.0);
}

// --- Lighting ---

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 pixel = ivec2(gl_GlobalInvocationID.xy);
	ivec2 screen = ivec2(p.screen_time.xy);
	if (pixel.x >= screen.x || pixel.y >= screen.y) return;

	vec3 cam_pos = p.camera_pos_ymin.xyz;
	float y_min = p.camera_pos_ymin.w;
	float y_max = p.sun_dir_ymax.w;
	vec3 sun_dir = p.sun_dir_ymax.xyz;
	float weather = p.screen_time.w;

	// Reconstruct world-space ray from camera basis vectors.
	vec2 uv = (vec2(pixel) + 0.5) / vec2(screen);
	vec2 ndc = uv * 2.0 - 1.0;
	ndc.y = -ndc.y;  // Vulkan Y-flip: UV top=0 but NDC top=+1
	float half_fov_tan = p.cam_right.w;
	float aspect = p.cam_up_aspect.w;
	vec3 rd = normalize(
		p.cam_forward_pad.xyz
		+ p.cam_right.xyz * ndc.x * half_fov_tan * aspect
		+ p.cam_up_aspect.xyz * ndc.y * half_fov_tan
	);

	vec2 t = intersect_slab(cam_pos, rd, y_min, y_max);
	if (t.x >= t.y || t.y <= 0.0) {
		imageStore(cloud_texture, pixel, vec4(0.0));
		return;
	}
	t.y = min(t.y, 5000.0);

	int steps = 64;
	float step_size = (t.y - t.x) / float(steps);
	float transmittance = 1.0;
	vec3 light_energy = vec3(0.0);
	float extinction = 0.04;

	float cos_theta = dot(rd, sun_dir);
	float phase = hg_phase(cos_theta, 0.6);

	for (int i = 0; i < steps; i++) {
		if (transmittance < 0.01) break;

		float tc = t.x + (float(i) + 0.5) * step_size;
		vec3 pos = cam_pos + rd * tc;

		float density = sample_cloud_density(pos, y_min, y_max, weather);
		if (density < 0.001) continue;

		float sample_ext = extinction * density;
		float height_frac = clamp((pos.y - y_min) / (y_max - y_min), 0.0, 1.0);

		// Light march (5 steps toward sun)
		float light_density = 0.0;
		float light_step = (y_max - y_min) * 0.15;
		for (int j = 0; j < 5; j++) {
			vec3 lp = pos + sun_dir * light_step * float(j + 1);
			light_density += sample_cloud_density(lp, y_min, y_max, weather);
		}
		float lt = beer_powder(light_density * extinction * light_step);
		float ms = lt + 0.5 * beer_powder(light_density * extinction * light_step * 0.5);

		float ambient = mix(0.15, 0.4, height_frac);
		vec3 sun_col = vec3(1.0, 0.95, 0.85);
		vec3 amb_col = vec3(0.4, 0.5, 0.7);
		vec3 col = sun_col * ms * phase + amb_col * ambient;

		float st = exp(-sample_ext * step_size);
		light_energy += col * transmittance * (1.0 - st);
		transmittance *= st;
	}

	imageStore(cloud_texture, pixel, vec4(light_energy, 1.0 - transmittance));
}