Implement dynamic atmosphere

https://github.com/Fewes/MinimalAtmosphere
This commit is contained in:
reo 2025-09-18 23:23:53 +03:00
parent 9905ffd26b
commit 7038343b19
9 changed files with 373 additions and 29 deletions

View file

@ -1,34 +1,157 @@
#version 330 core
in vec3 v_normal;
in vec3 v_world_normal;
in vec2 v_tex;
in vec3 v_position;
in vec3 v_world_pos;
out vec4 frag_color;
uniform vec3 u_light;
uniform sampler2D tex;
uniform vec3 color; // base colour factor (acts as solid colour when no texture)
uniform vec3 color; // base colour factor
uniform vec3 camera_pos; // camera world position
uniform vec3 light_dir; // directional light direction (from light towards scene)
uniform vec3 light_color; // directional light color
// -------------------------------------
// Constants and helpers (ported from atmosphere.cginc)
const float PI = 3.14159265359;
const float PLANET_RADIUS = 6371000.0;
const float ATMOSPHERE_HEIGHT = 100000.0;
const float RAYLEIGH_HEIGHT = (ATMOSPHERE_HEIGHT * 0.08);
const float MIE_HEIGHT = (ATMOSPHERE_HEIGHT * 0.012);
const float EXPOSURE = 20.0;
// Coefficients (1e-6 scaling kept)
const vec3 C_RAYLEIGH = vec3(5.802, 13.558, 33.100) * 1e-6;
const vec3 C_MIE = vec3(3.996, 3.996, 3.996) * 1e-6;
const vec3 C_OZONE = vec3(0.650, 1.881, 0.085) * 1e-6;
float saturate(float x) { return clamp(x, 0.0, 1.0); }
vec2 saturate(vec2 x) { return clamp(x, vec2(0.0), vec2(1.0)); }
vec3 saturate(vec3 x) { return clamp(x, vec3(0.0), vec3(1.0)); }
float AtmosphereHeight(vec3 positionWS) {
return length(positionWS - vec3(0.0, -PLANET_RADIUS, 0.0)) - PLANET_RADIUS;
}
float DensityRayleigh(float h) { return exp(-max(0.0, h / RAYLEIGH_HEIGHT)); }
float DensityMie (float h) { return exp(-max(0.0, h / MIE_HEIGHT)); }
float DensityOzone (float h) { return max(0.0, 1.0 - abs(h - 25000.0) / 15000.0); }
vec3 AtmosphereDensity(float h) { return vec3(DensityRayleigh(h), DensityMie(h), DensityOzone(h)); }
// Sphere intersection with atmosphere shell
vec2 SphereIntersection(vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius) {
vec3 o = rayStart - sphereCenter;
float a = dot(rayDir, rayDir);
float b = 2.0 * dot(o, rayDir);
float c = dot(o, o) - (sphereRadius * sphereRadius);
float d = b*b - 4.0*a*c;
if (d < 0.0) return vec2(-1.0);
d = sqrt(d);
return vec2(-b - d, -b + d) / (2.0 * a);
}
vec2 AtmosphereIntersection(vec3 rayStart, vec3 rayDir) {
return SphereIntersection(rayStart, rayDir, vec3(0.0, -PLANET_RADIUS, 0.0), PLANET_RADIUS + ATMOSPHERE_HEIGHT);
}
float PhaseRayleigh(float costh) {
return 3.0 * (1.0 + costh*costh) / (16.0 * PI);
}
float PhaseMie(float costh, float g) {
g = min(g, 0.9381);
float k = 1.55*g - 0.55*g*g*g;
float kcosth = k*costh;
return (1.0 - k*k) / ((4.0*PI) * (1.0 - kcosth) * (1.0 - kcosth));
}
vec3 IntegrateOpticalDepth(vec3 rayStart, vec3 rayDir) {
vec2 intersection = AtmosphereIntersection(rayStart, rayDir);
float rayLength = intersection.y;
int sampleCount = 8;
float stepSize = rayLength / float(sampleCount);
vec3 opticalDepth = vec3(0.0);
for (int i = 0; i < sampleCount; ++i) {
vec3 localPosition = rayStart + rayDir * (float(i) + 0.5) * stepSize;
float localHeight = AtmosphereHeight(localPosition);
vec3 localDensity = AtmosphereDensity(localHeight);
opticalDepth += localDensity * stepSize;
}
return opticalDepth;
}
vec3 Absorb(vec3 opticalDepth) {
// Mie absorbs slightly more than it scatters (~10%)
return exp(-(opticalDepth.x * C_RAYLEIGH + opticalDepth.y * C_MIE * 1.1 + opticalDepth.z * C_OZONE));
}
vec3 IntegrateScattering(vec3 rayStart, vec3 rayDir, float rayLength, vec3 lightDir, vec3 lightColor, out vec3 transmittance) {
float rayHeight = AtmosphereHeight(rayStart);
float sampleDistributionExponent = 1.0 + saturate(1.0 - rayHeight / ATMOSPHERE_HEIGHT) * 8.0;
vec2 intersection = AtmosphereIntersection(rayStart, rayDir);
rayLength = min(rayLength, intersection.y);
if (intersection.x > 0.0) {
rayStart += rayDir * intersection.x;
rayLength -= intersection.x;
}
float costh = dot(rayDir, lightDir);
float phaseR = PhaseRayleigh(costh);
float phaseM = PhaseMie(costh, 0.85);
int sampleCount = 64;
vec3 opticalDepth = vec3(0.0);
vec3 rayleigh = vec3(0.0);
vec3 mie = vec3(0.0);
float prevRayTime = 0.0;
for (int i = 0; i < sampleCount; ++i) {
float t = pow(float(i) / float(sampleCount), sampleDistributionExponent) * rayLength;
float stepSize = (t - prevRayTime);
vec3 localPosition = rayStart + rayDir * t;
float localHeight = AtmosphereHeight(localPosition);
vec3 localDensity = AtmosphereDensity(localHeight);
opticalDepth += localDensity * stepSize;
vec3 viewTransmittance = Absorb(opticalDepth);
vec3 opticalDepthLight = IntegrateOpticalDepth(localPosition, lightDir);
vec3 lightTransmittance = Absorb(opticalDepthLight);
rayleigh += viewTransmittance * lightTransmittance * phaseR * localDensity.x * stepSize;
mie += viewTransmittance * lightTransmittance * phaseM * localDensity.y * stepSize;
prevRayTime = t;
}
transmittance = Absorb(opticalDepth);
return (rayleigh * C_RAYLEIGH + mie * C_MIE) * lightColor * EXPOSURE;
}
void main() {
// Combine base texture (or constant white) with colour factor supplied by CPU.
// Base albedo
vec3 base_col = texture(tex, v_tex).rgb * color;
vec3 ambient_color = base_col * 0.2;
vec3 diffuse_color = base_col * 0.6;
vec3 specular_color = vec3(1.0);
// Lighting setup
vec3 L = normalize(light_dir);
vec3 N = normalize(v_world_normal);
// u_light is the direction **from the light towards the fragment**.
float diffuse = max(dot(normalize(v_normal), normalize(u_light)), 0.0);
// Directional light transmittance (planet shadow)
vec3 lightTransmittance = Absorb(IntegrateOpticalDepth(v_world_pos, L));
vec3 camera_dir = normalize(-v_position);
vec3 half_dir = normalize(normalize(u_light) + camera_dir);
float specular = pow(max(dot(half_dir, normalize(v_normal)), 0.0), 16.0);
// Rough ambient by sampling sky upwards
vec3 tmp;
vec3 ambient = IntegrateScattering(v_world_pos, vec3(0.0, 1.0, 0.0), 1.0/0.0, L, light_color, tmp);
vec3 result = ambient_color + diffuse * diffuse_color + specular * specular_color;
// Lambert + atmospheric directional lighting
float NdotL = max(0.0, dot(N, L));
vec3 lit = base_col * NdotL * (ambient + light_color * lightTransmittance);
// Convert from linear to sRGB for display (approximate γ-correction)
result = pow(result, vec3(1.0 / 2.2));
// View-ray scattering and transmittance between surface and camera
vec3 V = camera_pos - v_world_pos;
float rayLength = length(V);
V = V / max(rayLength, 1e-6);
vec3 transmittance;
vec3 scattering = IntegrateScattering(camera_pos, -V, rayLength, L, light_color, transmittance);
vec3 result = lit * transmittance + scattering;
frag_color = vec4(result, 1.0);
}