Rendering pretty sky in Around The World game
Rendering a Pretty Sky in Around The World
Around The World, Part 34: A pretty sky
Published Mon, 12 Oct 2026 by Thomas ten Cate
Game Development | Around the World
This entry is part of an ongoing series documenting the creation of Around The World, a game centered on maritime exploration and discovery.
Despite previous progress, playtesting revealed that the visual synthesis of the sea, sky, and clouds wasn't quite hitting the mark. The overall composition felt "off."
☁️ Light Scattering in Clouds
In the previous installment, I utilized a lookup table (LUT) generated via povray. This table mapped the amount of light scattered out of clouds based on two variables:
- Horizontal Axis: Light angle.
- Vertical Axis: Cloud density.
However, I had lingering doubts about the accuracy of this data:
"I am beginning to suspect that povray doesn’t actually simulate multiple scattering at all; the documentation does not say yes or no either way."
Regardless of how I adjusted the intervals or samples, the base of the spheres remained too dark. While I initially resisted the urge to build a custom tool, curiosity eventually won. I developed a multithreaded, recursive Monte Carlo ray tracer using Rust in just a few hundred lines of code.
While this tool was "dumb"—hard-coded specifically to render a single, uniform-density spherical cloud—it served its purpose: it confirmed that povray does not simulate multiple scattering.
Comparison of Scattering Methods
| Feature | povray LUT | Custom Rust Tracer LUT |
|---|---|---|
| Scattering Type | Single Scattering | Multiple Scattering |
| Light Distribution | Concentrated/Dark bottoms | Uniform/Brighter bottoms |
| Visual Result | "Metallic" appearance | Soft, natural clouds |
| Low Density | Similar results | Similar results |
As shown in the comparison, at low densities (thin clouds), the difference is negligible because there are fewer opportunities for a light ray to scatter more than once. However, at higher densities, the difference is stark.
By implementing the new LUT, the "metallic" sheen vanished, the cloud bottoms brightened, and the overall lighting became significantly smoother.
🪄 One Weird Trick: Indirect Lighting
In the real world, clouds aren't solely illuminated by direct sunlight; they also scatter indirect sunlight from the surrounding sky. Because this ambient light is typically blue, clouds should exhibit a blueish tint.
I avoided adding a static blue value because the clouds must dynamically react to the sky's color during sunrise, sunset, or nighttime. The solution lay in how Godot handles light rendering.
The Logic of Indirect Light
Godot samples indirect light from cube maps containing radiance and irradiance. For diffuse materials, the engine typically samples these maps in the direction of the surface normal.
- The Problem: At the bottom of a cloud, the normal points down toward the ground, resulting in very little indirect light.
- The Reality: Clouds receive indirect light from above, which then scatters through the volume.
The Hack: What if we force the normal to point upward toward the sky?
The results were subtle but transformative:
- Daytime: Clouds gained a blueish hue, blending seamlessly with the atmosphere and appearing more distant.
- Sunset: Instead of just having orange bottoms (from direct Rayleigh outscattering), the clouds turned pink.
This pink hue is the result of mixing , with the green channel being suppressed.
🌌 Atmospheric Scattering
I noticed an unnatural yellowing of the sky near the horizon, even when the sun was at its zenith.
Initially, I thought the outscattering was overpowering the inscattering and tried adding a fake multiple inscattering term. Ultimately, the fix was simpler: changing the order of operations during the raymarch.
Raymarch Order Comparison
Old Method (Incorrect):
- Add (blueish) light scattered into the ray segment.
- Attenuate by outscattering over the ray segment (reddening).
New Method (Correct):
- Attenuate by outscattering over the ray segment (reddening).
- Add (blueish) light scattered into the ray segment.
// Conceptual Raymarch Logic
for (float t = 0; t < max_dist; t += step) {
// New Order:
color = attenuate(color, outscattering);
color += calculate_inscattering();
}
Theoretically, this difference should vanish if the number of integration steps is increased (as it's a numerical integration artifact), but in practice, it didn't. I suspect there is still a deeper underlying issue, but the visual result is much improved.
🌍 Horizon Bending
To achieve realism, I used physical parameters from Earth, including:
- Atmospheric density
- Atmospheric height
- Planet size
However, there is a scaling discrepancy: the planet in my game is approximately smaller than Earth.
Implementation Checklist
- Implement Multiple Scattering LUT via Rust
- Flip surface normals for indirect sky light
- Reorder raymarch attenuation/addition
- Resolve numerical integration discrepancy
- Adjust horizon bending for scale