Atmospheric Rendering (Visual Scattering)
Issue: #741 Parent Epic: #429 (Planetary Terrain System)
Summary
Render atmospheric scattering visuals for bodies with atmospheres. Uses a translucent atmosphere shell mesh with a custom GLSL shader that computes single-scattering (Rayleigh + Mie) along the view ray. Produces both the orbital haze rim and the interior sky gradient during atmospheric entry.
Separate from #502 (atmospheric drag physics).
Phase 1 Scope
- Earth only (extensible to Mars, Titan, Venus later)
- Both views: cockpit view (3D) and map view (3D)
- Full sky transition: haze rim from orbit + sky gradient inside atmosphere
- Night-side glow: atmosphere visible on dark limb (forward-scattered light)
Atmosphere Shell Geometry
A transparent sphere mesh slightly larger than the body, representing the atmosphere’s outer boundary:
atmosphereRadius = bodyRadius + atmosphereHeight
For Earth:
- bodyRadius: 6,371,000 m
- atmosphereHeight: 100,000 m (visual boundary, Karman line equivalent)
- atmosphereRadius: 6,471,000 m
The shell uses an IcosahedronGeometry (subdivision level 4, ~2560 faces) for
uniform face distribution. Attached as a child of the body mesh so it inherits
position/rotation transforms.
Shader Design
Approach: Single-Pass Analytical Scattering
A custom ShaderMaterial with transparent: true, side: THREE.BackSide,
depthWrite: false, and additive-style blending. Renders the back faces of the
atmosphere shell so the shader runs for every pixel where the atmosphere is visible.
Uniforms
| Uniform | Type | Description |
|---|---|---|
uSunDirection |
vec3 | Normalized sun direction in body-local space |
uCameraPos |
vec3 | Camera position in body-local space |
uBodyRadius |
float | Planet surface radius (scaled) |
uAtmoRadius |
float | Atmosphere outer radius (scaled) |
uRayleighCoeffs |
vec3 | Rayleigh scattering coefficients (RGB, wavelength-dependent) |
uMieCoeff |
float | Mie scattering coefficient |
uMieG |
float | Mie phase function asymmetry (Henyey-Greenstein g) |
uScaleHeight |
float | Atmosphere scale height (normalized to atmosphere thickness) |
uSunIntensity |
float | Sun light intensity multiplier |
Earth Parameters
Rayleigh coefficients: vec3(5.5e-6, 13.0e-6, 22.4e-6) // RGB, per meter
Mie coefficient: 21e-6
Mie g: 0.758 (forward-scattering asymmetry)
Scale height (Rayleigh): 8,500 m
Scale height (Mie): 1,200 m
Sun intensity: 22.0
Vertex Shader
Passes world-space position to fragment shader. Computes view ray origin and direction in body-local coordinates.
Fragment Shader Algorithm
- Ray-sphere intersection: Compute entry/exit points of view ray through atmosphere shell and body surface.
- Optical depth integration: March along the view ray in ~8-16 steps.
At each sample point:
- Compute altitude above surface
- Compute local density:
rho = exp(-altitude / scaleHeight) - Accumulate optical depth along view ray
- Compute optical depth from sample point toward sun (secondary ray, ~4 steps)
- Compute total transmittance (Beer-Lambert:
exp(-tau * scatterCoeffs)) - Accumulate in-scattered light (Rayleigh + Mie phase functions)
- Phase functions:
- Rayleigh:
3/(16*pi) * (1 + cos^2(theta)) - Mie (Henyey-Greenstein):
(1-g^2) / (4*pi * (1+g^2-2g*cos(theta))^1.5)
- Rayleigh:
- Output:
vec4(totalInScattered, 1.0 - averageTransmittance)- RGB = accumulated scattered light color
- Alpha = opacity from optical depth (thicker atmosphere = more opaque)
Camera Inside Atmosphere
When the camera is below atmosphereRadius, the ray starts at the camera
position (not the shell surface). The shader detects this via
length(uCameraPos) < uAtmoRadius and adjusts the integration start point.
This produces the sky gradient effect during descent.
Night-Side Behavior
The shader naturally handles night-side glow: at the terminator and slightly beyond, the secondary sun-ray still has partial transmittance through the atmosphere, producing a colored rim on the dark limb. No special case needed – the ray marching integration handles this physically.
Integration Points
Cockpit View (cockpitMeshes.js)
In updateBody(), when creating a body mesh for Earth:
- Create atmosphere shell geometry and shader material
- Attach as child of body mesh (
body.add(atmoMesh)) - Tag with
userData.isAtmosphere = true
Each frame, update uSunDirection and uCameraPos uniforms based on
current sun and camera positions relative to the body.
Map View (mapBodies.js)
In ensureBody(), same approach: create atmosphere child mesh for Earth.
Uniforms updated in the map view’s render loop.
Uniform Updates
A shared function updateAtmosphereUniforms(atmoMesh, bodyWorldPos, sunWorldPos, cameraWorldPos) handles the per-frame uniform updates for both views. Lives in a new atmosphere.js module.
Module Structure
services/web-client/src/atmosphere.js
// Atmosphere visual rendering — scattering shell mesh and shader
// Per-body atmosphere visual parameters
const ATMO_PARAMS = {
Earth: {
height: 100_000, // visual atmosphere height (m)
rayleighCoeffs: [5.5e-6, 13.0e-6, 22.4e-6],
mieCoeff: 21e-6,
mieG: 0.758,
rayleighScaleHeight: 8500,
mieScaleHeight: 1200,
sunIntensity: 22.0,
},
};
export function createAtmosphereMesh(bodyName, bodyRadius, scale) { ... }
export function updateAtmosphereUniforms(atmoMesh, bodyWorldPos, sunWorldPos, cameraWorldPos) { ... }
Performance Considerations
- Ray march steps: 8 view-ray + 4 sun-ray = 12 texture-free samples per fragment
- Only renders for bodies with atmospheres (Earth only in Phase 1)
- Back-face rendering means fragments only generated where atmosphere is visible
- At orbital distances, the atmosphere subtends few pixels – cheap
- At close range (inside atmosphere), more pixels but still manageable with 8 steps
depthWrite: falseprevents z-fighting with body surface
Acceptance Criteria
- Blue haze rim visible around Earth when viewed from orbit (cockpit view)
- Blue haze rim visible around Earth in map view
- Sky transitions to blue gradient when camera descends into atmosphere
- Sunset/sunrise coloring visible near terminator (Mie forward scattering)
- Faint atmosphere glow visible on night-side limb
- No atmosphere rendered on Moon, Mars, or other airless bodies
- No visible seam or z-fighting between atmosphere and body surface
- Performance: <2ms GPU time for atmosphere rendering (single body)