Atmospheric Scattering - Rendering a planet-size digital world with dynamic time of day
November 7, 2022 Technology
We want to create a planet-size digital place with dynamic time of day. Atmospheric scattering is an indispensable effect for rendering a realistic planet. It's the most important part of the sky, one of our primary light sources.
"The sky color gives key indications about the hour of the day, and the aerial perspective gives an important cue to evaluate distances." - [3]
Author: Ruby (DongJi) Render Programmer
How do we implement it?
We do it iteratively.
Iteration 1 - Reconstruct the position from the depth buffer
We do it in a compute shader. To sample from the depth buffer, we also need to create an SRV (Shader Resource View) with R32_FLOAT format from the depth buffer.
First, we can use UV and depth to get the position in clip space. Then we use the inverse (local) view projection matrix to get the position in world space.
See Figure 1, code is provided in Appendix A.
To test the result, we use the reconstructed position to generate the fog.
Iteration 2 - Implement Nishita93 model with brute force ray marching
We decided to start with the Nishita93 model [1]. It's a single scattering model.
We do ray marching in two directions: view direction and light direction. For view direction, if some part of it does not get the light from the sun, other parts can still receive and transfer the energies. For light direction, if we notice that any part of the ray is occluded, we can immediately stop the calculation and throw away the accumulated results since the light from the sun won't be able to do any contribution in this case. So we should go through it thoroughly.
Parameters
Planet radius: We use 6371 km.
Planet center: We use (0, 0, 0) since we only have one planet for now. We can use different values in the future.
Atmosphere height: We use 80 km as the height/thickness of the atmosphere, where the mesosphere locates.
Density scale height (Rayleigh): We use 7994 m.
Density scale height (Mie): We use 1200 m.
Scattering coefficient (Rayleigh): We calculate to get this parameter on the C++ side. First, we use the index of refraction of the atmosphere (at sea level) n = 1.0003 on the C++ side. First, we use the index of refraction of the atmosphere (at sea level) and the Rayleigh particles' molecular number density of the atmosphere (at sea level) Ns = 2.55e25 to calculate the Rayleigh constant. Then we use this Rayleigh constant and wavelength = (680, 550, 440) nm [4] to calculate the scattering coefficient. Code is provided in Appendix B.
Scattering coefficient (Mie): We use (0.00002, 0.00002, 0.00002) [3].
Implementation
We launch the 1st ray, starting from the camera position along the view direction. Then we get the intersection points between this ray and the outer atmosphere. Now we will get the path we want ray marching on.
See Figure 3. V is the camera position. P is the fragment position which we reconstructed in the 1st iteration. Inear and Ifar are the intersections. If V is outside the atmosphere, we will pick Inear as the path's starting point. If V is inside the atmosphere, we will pick it as the path's starting point. In either case, we choose the more progressed point (either V or Inear) on the ray as the start point. Similarly, between P and Ifar, we pick the less progressed one on the ray as the path's endpoint.
Be aware that the path could be an invalid one. For example, see Figure 4; in this case, Inear should be the start point, and P should be the endpoint, which makes the path an invalid one along the ray direction. If the path on the 1st ray is invalid, just exit the calculation since there's no scattering contribution.
Code is provided in Appendix D.
Now that we have the 1st ray, we can start to do the marching and integrate our in-scattering. For each step, we first get the density ratio at that point.
Then, starting from the point, we also calculate the optical depth along the sunlight direction. To get the optical depth along the sunlight direction, we launch the 2nd ray, which starts from the point and along the sunlight direction. If the planet blocks this ray, then there will be no contribution from this ray, see Figure 5 (a). Otherwise, we just need to clamp this ray inside the outer atmosphere, see Figure 5 (b). After the 2nd ray is prepared, we do ray marching and integrate the density ratio to get the optical depth. Code is provided in Appendix E.
We then use the density ratio and optical depth to compute the in-scattering at this point and integrate it. After the integration, we apply different corresponding phase functions to the Rayleigh in-scattering and Mie in-scattering parts. Code is provided in Appendix F.
Finally, we calculate and combine the in-scattering, out-scattering and the lighting data in the light buffer to get our final scattering result. See Figure 6; now, the planet is starting to look good. Code is provided in Appendix G.
Iteration 3 - Precomputation & minor improvements
First, we start by implementing the process of precomputation.
We decided to start with the 2D lookup table proposed by O'Neil in 2004, referenced below [2]. The lookup table is generated in another compute shader. We map UV.x from [0, 1] to [altitude on the ground, altitude at the top of the atmosphere] and map UV.y from [0, 1] to [cos(PI), cos(0)] (zenith angle). In the end, we store the related density ratio and the optical depth (along the direction) in the lookup table. Code is provided in Appendix H.
Performance is around 10 times faster with precomputation. See one of the previous test cases in Figure 7. In this case, the original version costs 6.64 ms, while the new pre-computation version only costs 0.69 ms.
Then we add the sun disk. Having a sun disk is important as it is how it looks in the real world. See the lovely view of the sun disk in Figure 8. A fun fact is that the angular size of the Sun and the Moon are pretty close (~ 0.5o = 30 arcmin) if viewed from the Earth's surface [4].
Finally, we use a configurable extra-terrestrial solar irradiance with 3 constants [5]. We multiply 3 spectrum samples L(λr), L(λg), and L(λb) by these 3 constants to get the RGB color. This gives us a relatively accurate approximation compared to spectral rendering with a large number of wavelengths [6]. See the difference between before and after in Figure 9.
- In computer graphics, spectral rendering is a technique in which a scene's light transport is modelled with actual wavelengths [7].
Future improvements
The next iteration aims to add multiple scattering, and some parameter values (such as the thickness of the atmosphere) may be adjusted. We also plan to extend the method to support non-opaque objects (transparent, volumetric, etc.).
For the challenges/features we see in the future, we need to think about how to integrate the weather with atmospheric scattering on our planet. I assume this step will be challenging.
If you are interested in joining us to tackle these challenges or even invent an entirely novel solution, then check out our career page.
Reference
[1] Nishita 1993, Display of The Earth Taking into Account Atmospheric Scattering -
http://nishitalab.org/user/nis/cdrom/sig93_nis.pdf
[2] O’Neil, Accurate Atmospheric Scattering - https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-16-accurate-atmospheric-scattering
[3] Bruneton & Neyret 2008, Precomputed Atmospheric Scattering - https://hal.inria.fr/inria-00288758/document
[4] Angular Diameter - https://www.astronomy.swin.edu.au/cosmos/A/Angular+Diameter
[5] Precomputed Atmospheric Scattering: a New Implementation - https://ebruneton.github.io/precomputed_atmospheric_scattering/
[6] A Qualitative and Quantitative Evaluation of 8 Clear Sky Models - https://arxiv.org/pdf/1612.04336.pdf
[7] Spectral rendering - Spectral rendering
Appendix A
float3 get_world_space_local_position(float2 p_uv, float p_depth, float4x4 p_inv_view_proj_local)
{
// Get position in clip space
float4 l_pos_cs = float4(p_uv * 2.0 - 1.0, p_depth, 1.0);
l_pos_cs.y = -l_pos_cs.y;
// Get position in camera local world space
float4 l_pos_ws_local = mul(l_pos_cs, p_inv_view_proj_local);
return l_pos_ws_local.xyz / l_pos_ws_local.w;
}
Appendix B
//=========================================================================
// @brief Get the Rayleigh/Mie particle polarisability constant
// @param p_n, The index of refraction of the atmosphere at sea level
// @param p_ns, The Rayleigh/Mie particles' molecular number density of the atmosphere at sea level
// @ref [Nishita 1993, Display of The Earth Taking into Account Atmospheric Scattering] : Equation 1
float get_particle_polarisability_constant(float p_n, float p_ns)
{
float l_n2_minus_1 = p_n * p_n - 1.0f;
float l_k = 2.0f * M_PI * M_PI * l_n2_minus_1 * l_n2_minus_1 / 3.0f / p_ns;
return l_k;
}
Appendix C
//-----------------------------------------------------------------------------
// Return the scale of the ray direction (near, far)
float2 ray_sphere_intersect(float3 p_ray_origin, float3 p_ray_dir,
float3 p_sphere_center, float p_sphere_radius)
{
p_ray_origin -= p_sphere_center;
float l_a = dot(p_ray_dir, p_ray_dir);
float l_b = 2.0f * dot(p_ray_origin, p_ray_dir);
float l_c = dot(p_ray_origin, p_ray_origin) - p_sphere_radius * p_sphere_radius;
float l_d = l_b * l_b - 4.0f * l_a * l_c;
if (l_d < 0)
{
return -1;
}
else
{
l_d = sqrt(l_d);
return float2(-l_b - l_d, -l_b + l_d) / (2.0f * l_a);
}
}
Appendix D
// Get initial attributes of the ray
float3 l_ray_start = l_camera.m_camera_pos;
float3 l_ray_dir = normalize(l_pos_ws_local);
float l_ray_length = length(l_pos_ws_local);
// Get intersections with the outer atmosphere (near, far)
float2 l_intersections = ray_sphere_intersect(l_ray_start, l_ray_dir, g_push_constants.m_planet_center, g_push_constants.m_planet_radius + g_push_constants.m_atmosphere_height);
// Get starting point and end point of the ray
float l_ray_start_scale = max(l_intersections.x, 0.0f);
float l_ray_end_scale = min(l_intersections.y, l_ray_length);
// Return if there is no intersection with the atmosphere or the ray is invalid
if (l_ray_start_scale >= l_ray_end_scale)
{
l_rt[p_dispatch_thread_id.xy].xyz = pack_lighting(get_sun_disc_mask(l_light_dir, l_ray_dir) * l_light_color);
return;
}
Appendix E
//-----------------------------------------------------------------------------
// Get optical depth along the light direction
float2 get_optical_depth_along_light_direction(float3 p_ray_start, float3 p_ray_dir, float3 p_planet_center, float p_planet_radius,
float p_atmosphere_height, float2 p_density_scale_height, uint p_sample_count)
{
// Get the intersection with the planet
float2 l_intersection = ray_sphere_intersect(p_ray_start, p_ray_dir, p_planet_center, p_planet_radius);
// Intersect with the planet
if (l_intersection.x > 0)
{
return 1e20;
}
// Get the intersection with the outer atmosphere
l_intersection = ray_sphere_intersect(p_ray_start, p_ray_dir, p_planet_center, p_planet_radius + p_atmosphere_height);
float l_ray_end = l_intersection.y;
// Compute the optical depth along the ray
float l_step_size = l_ray_end / p_sample_count;
float2 l_optical_depth = 0.0f;
for (uint l_i = 0; l_i < p_sample_count; l_i++)
{
// Sample in the middle of the segment
float3 l_position = p_ray_start + (l_i + 0.5f) * l_step_size * p_ray_dir;
float2 l_density_ratio = get_density_ratio(l_position, p_planet_center, p_planet_radius, p_density_scale_height);
l_optical_depth += l_density_ratio * l_step_size;
}
return l_optical_depth;
}
Appendix F
//-----------------------------------------------------------------------------
// Phase function for Rayleigh scattering
// [Nishita 1993, Display of The Earth Taking into Account Atmospheric Scattering] : Equation 5
// We divided by an extra 4 pi since we want to use the scattering coefficient in the final scattering equation
float phase_function_rayleigh(float p_cos_theta)
{
return (3.0f / (16.0f * M_PI)) * (1 + p_cos_theta * p_cos_theta);
}
//-----------------------------------------------------------------------------
// Phase function for Mie scattering
// Cornette-Shanks phase function
// [Nishita 1993, Display of The Earth Taking into Account Atmospheric Scattering] : Equation 5
// We divided by an extra 4 pi since we want to use the scattering coefficient in the final scattering equation
float phase_function_mie(float p_cos_theta, float p_mie_g)
{
float l_mie_g2 = p_mie_g * p_mie_g;
return 1.5f * 1.0f / (4.0f * M_PI)
* (1.0f - l_mie_g2) * (1.0f + p_cos_theta * p_cos_theta)
* pow(1.0f + l_mie_g2 - 2.0f * p_mie_g * p_cos_theta, -3.0f / 2.0f) / (2.0f + l_mie_g2);
}
Appendix G
// Calculate the in-scattering
float3 l_light_inscattering = (l_integrated_inscattering_rayleigh * g_push_constants.m_rayleigh_scattering_coefficient + l_integrated_inscattering_mie * g_push_constants.m_mie_scattering_coefficient)
* g_push_constants.m_solar_irradiance * l_light_color;
// Calculate the out-scattering (without reflected light from earth)
float3 l_light_extinction = exp(-(l_optical_depth_camera_to_point.x * g_push_constants.m_rayleigh_scattering_coefficient + l_optical_depth_camera_to_point.y * g_push_constants.m_mie_scattering_coefficient));
float3 l_lighting = unpack_lighting(l_rt[p_dispatch_thread_id.xy].xyz);
// Final scattering result
// [Nishita 1993, Display of The Earth Taking into Account Atmospheric Scattering] : Equation 9
l_lighting = l_lighting * l_light_extinction + l_light_inscattering;
Appendix H
// UV to altitude and cosine theta
float l_altitude = l_uv.x * g_push_constants.m_atmosphere_height;
float l_cos_theta = -1.0f + l_uv.y * 2.0f;
// Get density ratio of current point
float2 l_density_ratio = exp(-l_altitude.xx / g_push_constants.m_density_scale_height);
// Get optical depth along the light direction
float3 l_ray_start = float3(0.0f, l_altitude + g_push_constants.m_planet_radius, 0.0f);
float3 l_ray_dir = normalize(float3(sqrt(1.0f - l_cos_theta * l_cos_theta), l_cos_theta, 0.0f));
float2 l_optical_depth = get_optical_depth_along_light_direction(l_ray_start, l_ray_dir, float3(0.0f, 0.0f, 0.0f),
g_push_constants.m_planet_radius, g_push_constants.m_atmosphere_height,
g_push_constants.m_density_scale_height, g_push_constants.m_sample_count);
l_rt[p_dispatch_thread_id.xy] = float4(l_density_ratio, l_optical_depth);
Share: Twitter Facebook Copy link Next article
Keep me posted
Stay updated on our journey to create massive, procedurally generated open worlds.
For more latest news and updates you can also follow us on Twitter