Atmosphere shader
Skill tdimino/claude-code-minoan/skills/design-media/atmosphere-shader
A curated ~/.claude/ configuration for professional development workflows — 90+ skills, 46 hooks, and CLI tools
npx -y skills add tdimino/claude-code-minoan --skill atmosphere-shaderAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Generate physically-based atmospheric scattering shaders — sky domes, planetary atmospheres, LUT-optimized pipelines, depth-aware post-processing. Four modes — sky-dome, atmosphere-post, planet, lut. Triggers on atmospheric scattering, sky shader, sunset rendering, planet atmosphere, Rayleigh scattering, Mie scattering, volumetric sky, sky dome, atmosphere post-processing, aerial perspective, transmittance LUT, sky rendering, realistic sky, planetary rendering.
SKILL.md
11.2 KB, as published. Nobody here has run it
Atmosphere Shader Skill
Render physically-accurate skies, sunsets, and planetary atmospheres through atmospheric scattering — Rayleigh, Mie, and Ozone models composed via raymarching in GLSL fragment shaders. Four progressive modes from flat backdrop to LUT-optimized planetary pipeline.
Modes
| Mode | Output | Complexity |
|---|---|---|
sky-dome | Single-pass fragment shader, flat sky backdrop | Entry point |
atmosphere-post | Depth-aware post-processing effect with atmospheric fog | Intermediate |
planet | Spherical atmosphere shell around a mesh, logarithmic depth | Advanced |
lut | Transmittance + Sky View + Aerial Perspective LUTs, FBO pipeline | Production |
Gotchas — What Claude Gets Wrong
These are the highest-signal items in this skill. Every one is a concrete, reproducible failure mode.
1. Wrong scattering coefficients
Claude invents plausible-looking but physically incorrect beta values. Use these exact constants for Earth:
const vec3 BETA_R = vec3(5.8e-3, 13.5e-3, 33.1e-3); // Rayleigh scattering (1/km)
const float BETA_M_SCATTER = 21e-3; // Mie scattering
const float BETA_M_EXT = 21e-3 * 1.1; // Mie extinction (scatter + absorb)
const vec3 BETA_OZONE_ABS = vec3(0.65e-3, 1.881e-3, 0.085e-3); // Ozone absorption
const float RAYLEIGH_SCALE_HEIGHT = 8.0; // km
const float MIE_SCALE_HEIGHT = 1.2; // km
const float MIE_G = 0.76; // Henyey-Greenstein anisotropy
const float ATMOSPHERE_HEIGHT = 100.0; // km (Karman line)
See references/scattering-coefficients.md for Mars and custom planet parameter sets.
2. Missing nested light march
Claude implements only the view-ray loop, producing a blue gradient but no sunsets. The critical missing piece: at each sample point along the primary ray, a secondary march toward the sun accumulates sunOD (optical depth between sample and sun). Without this, tau only contains view-direction extinction and the sky is uniformly blue regardless of sun angle.
// WRONG — view ray only, no sunset
vec3 tau = BETA_R * viewODR;
// RIGHT — view ray + sun path
vec3 sunOD = lightMarch(h, sunDirection.y);
vec3 tau = BETA_R * (viewODR + sunOD.x)
+ BETA_M_EXT * (viewODM + sunOD.y)
+ BETA_OZONE_ABS * (viewODO + sunOD.z);
The light march denominator uses a +0.15 offset to prevent infinite path length at exact horizon angles: float denom = max(sunY + 0.15, 0.04);
3. Incorrect logarithmic depth reconstruction
At planetary scale, a linear depth buffer causes z-fighting between atmosphere and surface. Enable logarithmicDepthBuffer: true in the R3F Canvas, then decode in the shader:
float logDepthToViewZ(float depth) {
return -(pow(2.0, depth * log2(cameraFar + 1.0)) - 1.0);
}
Claude defaults to depth * (far - near) + near, which is the linear formula and fails at planetary distances.
4. No LUT pipeline knowledge
Without guidance, Claude attempts the full nested raymarch (24 primary steps x 8 light march steps = 192 texture fetches per pixel) at screen resolution. The LUT approach precomputes transmittance into a 250x64 texture, then downstream LUTs sample it with a single texture2D call. See references/hillaire-lut-pipeline.md.
5. Wrong phase function normalization
The Mie phase function (Henyey-Greenstein) has a (2.0 + g*g) term in the denominator that Claude drops:
// WRONG — missing (2 + gg) denominator
float miePhase(float mu) {
float gg = MIE_G * MIE_G;
return (1.0 - gg) / pow(1.0 + gg - 2.0 * MIE_G * mu, 1.5);
}
// RIGHT
float miePhase(float mu) {
float gg = MIE_G * MIE_G;
float num = 3.0 * (1.0 - gg) * (1.0 + mu * mu);
float den = 8.0 * PI * (2.0 + gg) * pow(max(1.0 + gg - 2.0 * MIE_G * mu, 1e-4), 1.5);
return num / den;
}
6. Missing ozone
Claude's atmospheric models skip ozone entirely, losing the purple twilight shift and the correct sky-blue (as opposed to pure Rayleigh blue). Ozone peaks at ~25 km altitude with ~15 km width, absorbs but does not scatter:
float ozoneDensity(float h) {
return max(0.0, 1.0 - abs(h - 25.0) / 15.0);
}
Sky Dome Mode
Single-pass fragment shader. Rayleigh + Mie + Ozone scattering with nested light march. Suitable as a full-screen backdrop.
Architecture
- Cast ray per pixel from camera position
- Step through atmosphere volume (24 primary steps)
- At each step: accumulate Rayleigh, Mie, Ozone optical depth
- At each step: light march toward sun (8 steps) for sun transmittance
- Combine:
scattering = SUN_INTENSITY * (phaseR * BETA_R * sumR + phaseM * BETA_M * sumM) - Horizon mask:
smoothstep(-0.12, 0.05, skyDir.y)to blend to space below horizon - Tonemap:
ACESFilm(color)
Density functions, phase functions, and transmittance: see gotchas #1, #5, #6 above — those contain the correct implementations with wrong/right comparisons.
Atmosphere Post-Processing Mode
Depth-aware post-processing effect. Reconstructs world-space position from the depth buffer, marches through the scene volume, applies atmospheric fog that thickens with distance.
World-Space Reconstruction
vec3 getWorldPosition(vec2 uv, float depth) {
float clipZ = depth * 2.0 - 1.0;
vec4 clip = vec4(uv * 2.0 - 1.0, clipZ, 1.0);
vec4 view = projectionMatrixInverse * clip;
vec4 world = viewMatrixInverse * view;
return world.xyz / world.w;
}
Depth-Driven Step Sizing
float sceneDepth = depthToRayDistance(uv, depth);
bool isBackground = depth >= 1.0 - 1e-7;
if (isBackground) {
sceneDepth = atmosphereHeight * 8.0;
}
float stepSize = sceneDepth / float(PRIMARY_STEPS);
Nearby geometry gets dense sampling; distant sky rays distribute steps over a larger volume.
Ground-ray early termination: if rayDir.y < -1e-5, compute tGround = observerAltitude / max(-rayDir.y, 1e-4) and cap rayEnd to prevent marching below the surface.
R3F Integration
Three uniforms from the scene: depthBuffer, projectionMatrixInverse, viewMatrixInverse. Enable logarithmicDepthBuffer: true on the Canvas gl prop for planetary scale. FBOs for LUTs use dedicated WebGLRenderTarget instances rendered to off-screen scenes.
Planet Mode
Spherical atmosphere shell. Uses ray-sphere intersection to define atmosphere entry/exit, logarithmic depth buffer for planetary scale.
Ray-Sphere Intersection
vec2 raySphereIntersect(vec3 ro, vec3 rd, vec3 center, float radius) {
vec3 oc = ro - center;
float b = dot(oc, rd);
float c = dot(oc, oc) - radius * radius;
float disc = b * b - c;
if (disc < 0.0) return vec2(-1.0);
float sq = sqrt(disc);
return vec2(-b - sq, -b + sq);
}
Atmosphere Segment Clipping
Three cases: ray misses atmosphere, ray hits planet surface, scene object occludes planet.
vec2 atmosphereHit = raySphereIntersect(ro, rd, vec3(0.0), atmosphereRadius);
vec2 planetHit = raySphereIntersect(ro, rd, vec3(0.0), planetRadius);
float atmosphereNear = max(atmosphereHit.x, 0.0);
float atmosphereFar = atmosphereHit.y;
if (planetHit.x > 0.0) {
atmosphereFar = min(atmosphereFar, planetHit.x);
if (sceneDepth < planetHit.x - 2.0) {
atmosphereFar = min(atmosphereFar, sceneDepth);
}
} else {
atmosphereFar = min(atmosphereFar, sceneDepth);
}
Eclipse Handling
Compare angular radii and separation of sun/moon from each sample point. Three cases: no overlap, moon >= sun angular size (total/annular), moon < sun (partial). Multiply transmittance by this value at each sample.
float sunVisibility(vec3 point) {
vec3 sunDir = normalize(sunDirection);
vec3 toMoon = moonPosition - point;
float moonDist = length(toMoon);
vec3 moonDir = normalize(toMoon);
if (moonDist <= 1e-5 || dot(sunDir, moonDir) < 0.9) return 1.0;
float angularSep = acos(clamp(dot(sunDir, moonDir), -1.0, 1.0));
float sunAngularRadius = SUN_RADIUS / SUN_DISTANCE;
float moonAngularRadius = moonRadius / moonDist;
float outerEdge = sunAngularRadius + moonAngularRadius;
if (moonAngularRadius >= sunAngularRadius) {
float innerEdge = moonAngularRadius - sunAngularRadius;
return max(0.075, smoothstep(innerEdge, outerEdge, angularSep));
}
float innerEdge = sunAngularRadius - moonAngularRadius;
float minVis = clamp(1.0 - (moonAngularRadius * moonAngularRadius)
/ (sunAngularRadius * sunAngularRadius), 0.0, 1.0);
return mix(minVis, 1.0, smoothstep(innerEdge, outerEdge, angularSep));
}
LUT Mode
Precompute expensive scattering into textures, compose in a final pass. Based on Hillaire (2020).
Pipeline
Transmittance LUT (250x64)
|
+---------+---------+
| |
Sky View LUT Aerial Perspective LUT
(256x128) (screen resolution)
| |
+---------+---------+
|
Composition Pass
Each LUT is rendered to a dedicated FBO, passed as a uniform to downstream passes.
Transmittance LUT Parameterization
- x-axis:
mu = cos(zenith)from -1 (toward ground) to +1 (toward space) - y-axis: altitude from
planetRadiustoatmosphereRadius - Output:
vec3(exp(-tau))— surviving light fraction per channel
Composition
// Geometry pixels: blend original color with atmospheric haze
color = color * aerialPerspective.a + aerialPerspective.rgb;
// Background pixels: replace with sky color
color = sampleSkyViewLUT(rayDir, planetCenter);
color = ACESFilm(color);
color = pow(color, vec3(1.0 / 2.2));
See references/hillaire-lut-pipeline.md for full LUT shader implementations.
Scope Boundaries
This skill covers volumetric atmospheric light transport. It does NOT cover:
- Volumetric clouds (separate rendering concern)
- Domain warping / procedural effects (that's
rocaille-shader) - Particle systems (that's
threejs-particle-canvas) - TSL/WebGPU API reference (that's
webgpu-threejs-tsl) - CSS-based sky gradients or surface effects (that's
grainient)
Reference Documentation
references/scattering-coefficients.md— Earth, Mars, and custom planet parameter tablesreferences/hillaire-lut-pipeline.md— LUT architecture from Hillaire (2020)
Attribution
- Atmospheric scattering model — Nishita et al., "Display of the Earth Taking into Account Atmospheric Scattering" (SIGGRAPH 1993)
- LUT-based approach — Sebastian Hillaire, "A Scalable and Production Ready Sky and Atmosphere Rendering Technique" (EGSR 2020)
- Implementation reference — Maxime Heckel, "On Rendering the Sky, Sunsets, and Planets" (2026)
- Production reference — three-geospatial by Shota Matsuda