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

  1. Ray-sphere intersection: Compute entry/exit points of view ray through atmosphere shell and body surface.
  2. 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)
  3. 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)
  4. 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:

  1. Create atmosphere shell geometry and shader material
  2. Attach as child of body mesh (body.add(atmoMesh))
  3. 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: false prevents 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)

Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.