#ifndef ANIME_CLOUDS_INCLUDED #define ANIME_CLOUDS_INCLUDED // Organic 2D cloud field shared by the actual Sky shader and the fullscreen // ink composite. Broad domain-warped masses define the cloud, a second rotated // octave breaks up its contour into billows, and a quiet high-frequency layer // erodes the edge. Unlike a grid of radial primitives, no stage exposes a // circle or a square as the final silhouette. float cloud_hash(vec2 p) { // Sine-free hash: substantially cheaper when the field is sampled several // times to estimate the direction the sun meets the cloud. vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); } float cloud_noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); // Quintic interpolation keeps both the value and its slope continuous at // cell boundaries, so a density threshold cannot reveal the noise grid. vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); float a = cloud_hash(i); float b = cloud_hash(i + vec2(1.0, 0.0)); float c = cloud_hash(i + vec2(0.0, 1.0)); float d = cloud_hash(i + vec2(1.0, 1.0)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } float cloud_fbm(vec2 p) { mat2 turn = mat2(vec2(0.80, 0.60), vec2(-0.60, 0.80)); float value = cloud_noise(p) * 0.52; p = turn * p * 2.03 + vec2(9.17, 4.31); value += cloud_noise(p) * 0.27; p = turn * p * 2.07 + vec2(3.73, 12.61); value += cloud_noise(p) * 0.14; p = turn * p * 2.01 + vec2(15.19, 7.07); value += cloud_noise(p) * 0.07; return value; } float cloud_detail(vec2 p) { mat2 turn = mat2(vec2(0.86, 0.51), vec2(-0.51, 0.86)); return cloud_noise(turn * p * 2.75 + vec2(23.7, 6.1)) * 0.68 + cloud_noise(p * 5.20 + vec2(2.9, 31.4)) * 0.32; } float cloud_field(vec2 p) { // Low-frequency vector warp makes the mass curl and fork without making // the outline busy. The two components use unrelated offsets so this does // not merely slide the source noise along its own contour. vec2 domain = p * 0.34; vec2 warp = vec2( cloud_noise(domain + vec2(17.2, 4.8)), cloud_noise(domain + vec2(3.1, 29.6))) - 0.5; vec2 q = p + warp * 1.35; float mass = cloud_fbm(q * 0.52); float billows = cloud_fbm( mat2(vec2(0.74, 0.67), vec2(-0.67, 0.74)) * q * 1.18 + vec2(11.3, 8.7)); float erosion = cloud_detail(q); float density = mass * 0.70 + billows * 0.24 + erosion * 0.06; // A third, much broader field separates the weather into distinct banks. // Without this macro mask a perfectly organic contour can still connect // across half the dome and read as one enormous sheet instead of clouds. float islands = cloud_fbm(q * 0.24 + vec2(41.7, 18.3)); return density - (1.0 - islands) * 0.16; } float cloud_sun_response(vec2 p, vec3 sun_direction) { // Treat density as a painted height field. Sampling a short distance toward // and away from the sun tells us which edge faces it. This gives the cloud // a bright leading rim and a colored lee side, and naturally reverses when // the DirectionalLight turns. vec2 sun_xz = sun_direction.xz; float horizontal = length(sun_xz); vec2 axis = horizontal > 0.0001 ? sun_xz / horizontal : vec2(0.7071, 0.7071); float toward = cloud_field(p + axis * 0.075); float away = cloud_field(p - axis * 0.075); float directional_slope = (away - toward) / 0.15; // A high sun lights more of the cloud face; a low sun makes the directional // division stronger and leaves a broader colored underside. float overhead = clamp(sun_direction.y, 0.0, 1.0); return smoothstep(-0.22, 0.22, directional_slope + overhead * 0.08); } #endif