Files
Papay-Shooter/assets/shaders/anime_clouds.gdshaderinc
2026-08-02 02:20:02 -04:00

97 lines
3.5 KiB
Plaintext

#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