
Documentation/Rendering
Why the sky is blue, and how to render it
REACT's sky is not a gradient or a skybox. It is the solution to a physics problem: how does sunlight scatter through 60 km of air? This article covers the two scattering processes that matter, the equation the renderer solves, and the precomputation trick that makes it nearly free at runtime.
- 10 min
- 4 figures
- Updated June 2026
60 km
atmosphere top
256 x 64
transmittance LUT
4
scatter orders
0.7 ms
runtime cost
Sunlight reaching your eye through the atmosphere has been redirected by two very different populations of particles. Air molecules are thousands of times smaller than the wavelength of visible light, and scatter according to Rayleigh theory. Aerosols, meaning dust, salt and water droplets, are about the same size as the wavelength, and scatter according to Mie theory. Every visual property of the sky comes from the interplay of these two.
The Rayleigh part carries the color. Its strength falls off with the fourth power of wavelength, which is a brutally steep curve:
Eq 01
each symbol, explained —
The scattering coefficient tells you what fraction of light gets scattered per meter of travel. Because of the fourth power, 440 nm blue light scatters about 5.7 times more strongly than 680 nm red light.
Look up on a clear day and you see sunlight that scattered into your eye from the side: mostly blue, by the curve above. Look at the sun at the horizon and you see what is left after a long slant path through the air: the blue has been scattered away en route, leaving red. One curve explains both the noon sky and the sunset, which is exactly the kind of economy you want from a model.
Mie scattering from aerosols is nearly wavelength independent, which is why haze looks white. What makes it visually distinctive is its direction: aerosols throw light strongly forward, producing the bright glow around the sun. REACT models this with the Henyey-Greenstein phase function, where a single parameter g controls how forward-biased the scattering is.
Putting it together: the light arriving at the camera along a view ray is the sum, over every point on that ray, of sunlight that survived the trip into the atmosphere, scattered toward the camera at that point, and then survived the trip back out. Survival is governed by transmittance, which is Beer-Lambert attenuation along the path:
Eq 02
each symbol, explained —
Transmittance between two points is an exponential decay over the integrated scattering and absorption along the path. Every term in the sky integral is wrapped in two of these, one for the sun leg and one for the camera leg.
Evaluating this nested integral by brute force for every pixel every frame is hopeless: 32 view samples times a transmittance integral each is thousands of operations per pixel. The standard solution, which REACT adopts from Bruneton and Neyret, is to notice that the integrals depend on only a handful of parameters, like altitude and the angles to the sun. Anything that depends on few parameters can be precomputed into a lookup table.
When a planet loads, REACT bakes its atmosphere into a small set of textures. Transmittance from any altitude in any direction becomes one 2D table. Single scattering becomes a compact 3D table. Multiple scattering, light that bounced through the air more than once, is computed by iterating on the previous tables three more times. It accounts for most of the sky's brightness at dusk, so skipping it is not an option.
Transmittance LUT
256 x 64 table of attenuation from every altitude and view angle to space.
Single scattering
One bounce of sunlight, integrated using the transmittance table.
Multiple scattering
Orders 2 to 4, each iteration feeding on the last. Most of the dusk sky lives here.
Sky-view LUT
Rebuilt per frame for the current sun: a cheap latitude-longitude map of the whole sky.
Per-pixel apply
Two fetches per pixel, plus aerial perspective on distant terrain.
The exact tables, their resolutions and what they are parameterized by:
| Table | Resolution | Parameterized by | Rebuilt |
|---|---|---|---|
| Transmittance | 256 x 64, RGB16F | altitude, view-zenith angle | when the atmosphere itself changes |
| Scattering | 32 x 128 x 32, RGBA16F | altitude, view-zenith, sun-zenith | when the atmosphere itself changes |
| Multiple scattering | 32 x 32, RGB16F | altitude, sun-zenith | with the scattering table, orders 2 to 4 |
| Sky-view | 192 x 108, RGB16F | current sun and camera position | every frame, in 0.05 ms |
| Aerial perspective | 32 x 32 x 16 froxels | screen position, depth | every frame, alongside sky-view |
At runtime, evaluating the entire sky comes down to a handful of texture fetches and two phase functions:
shaders/sky.frag (simplified)
vec3 SkyRadiance(vec3 eye, vec3 dir, vec3 sunDir) {
ScatterParams p = ParamsFor(eye, dir, sunDir);
float mu = dot(dir, sunDir);
vec3 rayleigh = texture(u_scatterLUT, p.uvw).rgb * RayleighPhase(mu);
vec3 mie = texture(u_mieLUT, p.uvw).rgb * HGPhase(mu, a_mie_g);
return (rayleigh + mie) * u_sunIrradiance;
}Because the tables are parameterized by altitude, the same data serves a hiker on the surface and a ship in orbit. The earthrise shot at the top of this page and a sunset on the ground are the same lookup tables, sampled from two ends.
The clouds living inside this sky are their own system, built on Schneider and Vos's Horizon Zero Dawn cloudscapes and the Nubis follow-ups: density fields carved from Worley's cellular noise layered with Perlin, raymarched in 64 steps, with their powder-sugar darkening at the sun-facing edges. The shafts of light they cast come from Mitchell's post-process volumetric scattering, which is why god rays through a cloud gap cost a fraction of a millisecond rather than a real volumetric pass.
Note
The LUTs only rebuild when the atmosphere itself changes, for example a different planet or a density slider in the editor. Sun movement is free: the sun angle is a lookup coordinate, not baked data.
Known issue
From very high orbit the sun disc can clip against the atmosphere's outer shell for a few frames at dawn. Cosmetic, rare, and on the list.
Everything in this article is tunable from the in-engine console:
| Variable | Default | Description |
|---|---|---|
| a_scatter_orders | 4 | Number of scattering orders precomputed. Order 1 alone makes dusk too dark; beyond 4 the change is unmeasurable. |
| a_view_samples | 32 | Samples along the view ray during LUT generation. Affects bake time, not runtime cost. |
| a_mie_g | 0.76 | Henyey-Greenstein asymmetry for aerosols. Higher values tighten the glow around the sun. |
| a_clouds | 1 | Toggle the volumetric cloud raymarch, the most expensive part of the sky at 0.4 of its 0.7 ms. |
| a_aerial_perspective | 1 | Apply the scattering LUTs to distant terrain so far mountains haze out physically. |
| a_show_luts | 0 | Debug: draw the transmittance and scattering tables as overlays. |
Further reading
- [01]Bruneton, E., Neyret, F. (2008). Precomputed Atmospheric Scattering. Computer Graphics Forum (EGSR).
- [02]Hillaire, S. (2020). A Scalable and Production Ready Sky and Atmosphere Rendering Technique. Computer Graphics Forum (EGSR).
- [03]Nishita, T., Sirai, T., Tadamura, K., Nakamae, E. (1993). Display of the Earth Taking into Account Atmospheric Scattering. SIGGRAPH.
- [04]Schneider, A., Vos, N. (2015). The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn. SIGGRAPH Advances in Real-Time Rendering.
- [05]Worley, S. (1996). A Cellular Texture Basis Function. SIGGRAPH.
- [06]Mitchell, K. (2007). Volumetric Light Scattering as a Post-Process. GPU Gems 3, Addison-Wesley.