XRoom_Unity/Assets/beantowel/Aerosol/Shaders/functions.hlsl
2025-05-31 10:20:20 +03:30

1844 lines
84 KiB
HLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2017 Eric Bruneton
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holders nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*
* Precomputed Atmospheric Scattering
* Copyright (c) 2008 INRIA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holders nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
/*<h2>atmosphere/functions.glsl</h2>
<p>This GLSL file contains the core functions that implement our atmosphere
model. It provides functions to compute the transmittance, the single scattering
and the second and higher orders of scattering, the ground irradiance, as well
as functions to store these in textures and to read them back. It uses physical
types and constants which are provided in two versions: a
<a href="definitions.glsl.html">GLSL version</a> and a
<a href="reference/definitions.h.html">C++ version</a>. This allows this file to
be compiled either with a GLSL compiler or with a C++ compiler (see the
<a href="../index.html">Introduction</a>).
<p>The functions provided in this file are organized as follows:
<ul>
<li><a href="#transmittance">Transmittance</a>
<ul>
<li><a href="#transmittance_computation">Computation</a></li>
<li><a href="#transmittance_precomputation">Precomputation</a></li>
<li><a href="#transmittance_lookup">Lookup</a></li>
</ul>
</li>
<li><a href="#single_scattering">Single scattering</a>
<ul>
<li><a href="#single_scattering_computation">Computation</a></li>
<li><a href="#single_scattering_precomputation">Precomputation</a></li>
<li><a href="#single_scattering_lookup">Lookup</a></li>
</ul>
</li>
<li><a href="#multiple_scattering">Multiple scattering</a>
<ul>
<li><a href="#multiple_scattering_computation">Computation</a></li>
<li><a href="#multiple_scattering_precomputation">Precomputation</a></li>
<li><a href="#multiple_scattering_lookup">Lookup</a></li>
</ul>
</li>
<li><a href="#irradiance">Ground irradiance</a>
<ul>
<li><a href="#irradiance_computation">Computation</a></li>
<li><a href="#irradiance_precomputation">Precomputation</a></li>
<li><a href="#irradiance_lookup">Lookup</a></li>
</ul>
</li>
<li><a href="#rendering">Rendering</a>
<ul>
<li><a href="#rendering_sky">Sky</a></li>
<li><a href="#rendering_aerial_perspective">Aerial perspective</a></li>
<li><a href="#rendering_ground">Ground</a></li>
</ul>
</li>
</ul>
<p>They use the following utility functions to avoid NaNs due to floating point
values slightly outside their theoretical bounds:
*/
#include "definitions.hlsl"
#include "header.hlsl"
Number ClampCosine(Number mu)
{
return clamp(mu, Number(-1.0), Number(1.0));
}
Length ClampDistance(Length d)
{
return max(d, 0.0 * m);
}
Length ClampRadius(IN(AtmosphereParameters) atmosphere, Length r)
{
return clamp(r, atmosphere.bottom_radius, atmosphere.top_radius);
}
Length SafeSqrt(Area a)
{
return sqrt(max(a, 0.0 * m2));
}
/*
<h3 id="transmittance">Transmittance</h3>
<p>As the light travels from a point $\bp$ to a point $\bq$ in the atmosphere,
it is partially absorbed and scattered out of its initial direction because of
the air molecules and the aerosol particles. Thus, the light arriving at $\bq$
is only a fraction of the light from $\bp$, and this fraction, which depends on
wavelength, is called the
<a href="https://en.wikipedia.org/wiki/Transmittance">transmittance</a>. The
following sections describe how we compute it, how we store it in a precomputed
texture, and how we read it back.
<h4 id="transmittance_computation">Computation</h4>
<p>For 3 aligned points $\bp$, $\bq$ and $\br$ inside the atmosphere, in this
order, the transmittance between $\bp$ and $\br$ is the product of the
transmittance between $\bp$ and $\bq$ and between $\bq$ and $\br$. In
particular, the transmittance between $\bp$ and $\bq$ is the transmittance
between $\bp$ and the nearest intersection $\bi$ of the half-line $[\bp,\bq)$
with the top or bottom atmosphere boundary, divided by the transmittance between
$\bq$ and $\bi$ (or 0 if the segment $[\bp,\bq]$ intersects the ground):
<svg width="340px" height="195px">
<style type="text/css"><![CDATA[
circle { fill: #000000; stroke: none; }
path { fill: none; stroke: #000000; }
text { font-size: 16px; font-style: normal; font-family: Sans; }
.vector { font-weight: bold; }
]]></style>
<path d="m 0,26 a 600,600 0 0 1 340,0"/>
<path d="m 0,110 a 520,520 0 0 1 340,0"/>
<path d="m 170,190 0,-30"/>
<path d="m 170,140 0,-130"/>
<path d="m 170,50 165,-33"/>
<path d="m 155,150 10,-10 10,10 10,-10"/>
<path d="m 155,160 10,-10 10,10 10,-10"/>
<path d="m 95,50 30,0"/>
<path d="m 95,190 30,0"/>
<path d="m 105,50 0,140" style="stroke-dasharray:8,2;"/>
<path d="m 100,55 5,-5 5,5"/>
<path d="m 100,185 5,5 5,-5"/>
<path d="m 170,25 a 25,25 0 0 1 25,20" style="stroke-dasharray:4,2;"/>
<path d="m 170,190 70,0"/>
<path d="m 235,185 5,5 -5,5"/>
<path d="m 165,125 5,-5 5,5"/>
<circle cx="170" cy="190" r="2.5"/>
<circle cx="170" cy="50" r="2.5"/>
<circle cx="320" cy="20" r="2.5"/>
<circle cx="270" cy="30" r="2.5"/>
<text x="155" y="45" class="vector">p</text>
<text x="265" y="45" class="vector">q</text>
<text x="320" y="15" class="vector">i</text>
<text x="175" y="185" class="vector">o</text>
<text x="90" y="125">r</text>
<text x="185" y="25">μ=cos(θ)</text>
<text x="240" y="185">x</text>
<text x="155" y="120">z</text>
</svg>
<p>Also, the transmittance between $\bp$ and $\bq$ and between $\bq$ and $\bp$
are the same. Thus, to compute the transmittance between arbitrary points, it
is sufficient to know the transmittance between a point $\bp$ in the atmosphere,
and points $\bi$ on the top atmosphere boundary. This transmittance depends on
only two parameters, which can be taken as the radius $r=\Vert\bo\bp\Vert$ and
the cosine of the "view zenith angle",
$\mu=\bo\bp\cdot\bp\bi/\Vert\bo\bp\Vert\Vert\bp\bi\Vert$. To compute it, we
first need to compute the length $\Vert\bp\bi\Vert$, and we need to know when
the segment $[\bp,\bi]$ intersects the ground.
<h5>Distance to the top atmosphere boundary</h5>
<p>A point at distance $d$ from $\bp$ along $[\bp,\bi)$ has coordinates
$[d\sqrt{1-\mu^2}, r+d\mu]^\top$, whose squared norm is $d^2+2r\mu d+r^2$.
Thus, by definition of $\bi$, we have
$\Vert\bp\bi\Vert^2+2r\mu\Vert\bp\bi\Vert+r^2=r_{\mathrm{top}}^2$,
from which we deduce the length $\Vert\bp\bi\Vert$:
*/
Length DistanceToTopAtmosphereBoundary(IN(AtmosphereParameters) atmosphere,
Length r, Number mu)
{
assert(r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
Area discriminant = r * r * (mu * mu - 1.0) + atmosphere.top_radius * atmosphere.top_radius;
return ClampDistance(-r * mu + SafeSqrt(discriminant));
}
/*
<p>We will also need, in the other sections, the distance to the bottom
atmosphere boundary, which can be computed in a similar way (this code assumes
that $[\bp,\bi)$ intersects the ground):
*/
Length DistanceToBottomAtmosphereBoundary(IN(AtmosphereParameters) atmosphere,
Length r, Number mu)
{
assert(r >= atmosphere.bottom_radius);
assert(mu >= -1.0 && mu <= 1.0);
Area discriminant = r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius;
return ClampDistance(-r * mu - SafeSqrt(discriminant));
}
/*
<h5>Intersections with the ground</h5>
<p>The segment $[\bp,\bi]$ intersects the ground when
$d^2+2r\mu d+r^2=r_{\mathrm{bottom}}^2$ has a solution with $d \ge 0$. This
requires the discriminant $r^2(\mu^2-1)+r_{\mathrm{bottom}}^2$ to be positive,
from which we deduce the following function:
*/
bool RayIntersectsGround(IN(AtmosphereParameters) atmosphere,
Length r, Number mu)
{
assert(r >= atmosphere.bottom_radius);
assert(mu >= -1.0 && mu <= 1.0);
return mu < 0.0 && r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius >= 0.0 * m2;
}
/*
<h5>Transmittance to the top atmosphere boundary</h5>
<p>We can now compute the transmittance between $\bp$ and $\bi$. From its
definition and the
<a href="https://en.wikipedia.org/wiki/Beer-Lambert_law">Beer-Lambert law</a>,
this involves the integral of the number density of air molecules along the
segment $[\bp,\bi]$, as well as the integral of the number density of aerosols
and the integral of the number density of air molecules that absorb light
(e.g. ozone) - along the same segment. These 3 integrals have the same form and,
when the segment $[\bp,\bi]$ does not intersect the ground, they can be computed
numerically with the help of the following auxilliary function (using the <a
href="https://en.wikipedia.org/wiki/Trapezoidal_rule">trapezoidal rule</a>):
*/
Number GetLayerDensity(IN(DensityProfileLayer) layer, Length altitude)
{
Number density = layer.exp_term * exp(layer.exp_scale * altitude) + layer.linear_term * altitude + layer.constant_term;
return clamp(density, Number(0.0), Number(1.0));
}
Number GetProfileDensity(IN(DensityProfile) profile, Length altitude)
{
return altitude < profile.layers[0].width ? GetLayerDensity(profile.layers[0], altitude) : GetLayerDensity(profile.layers[1], altitude);
}
Length ComputeOpticalLengthToTopAtmosphereBoundary(
IN(AtmosphereParameters) atmosphere, IN(DensityProfile) profile,
Length r, Number mu)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
// Number of intervals for the numerical integration.
const int SAMPLE_COUNT = 500;
// The integration step, i.e. the length of each integration interval.
Length dx = DistanceToTopAtmosphereBoundary(atmosphere, r, mu) / Number(SAMPLE_COUNT);
// Integration loop.
Length result = 0.0 * m;
for (int i = 0; i <= SAMPLE_COUNT; ++i) {
Length d_i = Number(i) * dx;
// Distance between the current sample point and the planet center.
Length r_i = sqrt(d_i * d_i + 2.0 * r * mu * d_i + r * r);
// Number density at the current sample point (divided by the number density
// at the bottom of the atmosphere, yielding a dimensionless number).
Number y_i = GetProfileDensity(profile, r_i - atmosphere.bottom_radius);
// Sample weight (from the trapezoidal rule).
Number weight_i = i == 0 || i == SAMPLE_COUNT ? 0.5 : 1.0;
result += y_i * weight_i * dx;
}
return result;
}
/*
<p>With this function the transmittance between $\bp$ and $\bi$ is now easy to
compute (we continue to assume that the segment does not intersect the ground):
*/
DimensionlessSpectrum ComputeTransmittanceToTopAtmosphereBoundary(
IN(AtmosphereParameters) atmosphere, Length r, Number mu)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
// return exp(-(...
return -(
atmosphere.rayleigh_scattering * ComputeOpticalLengthToTopAtmosphereBoundary(atmosphere, atmosphere.rayleigh_density, r, mu) + atmosphere.mie_extinction * ComputeOpticalLengthToTopAtmosphereBoundary(atmosphere, atmosphere.mie_density, r, mu) + atmosphere.absorption_extinction * ComputeOpticalLengthToTopAtmosphereBoundary(atmosphere, atmosphere.absorption_density, r, mu));
}
/*
<h4 id="transmittance_precomputation">Precomputation</h4>
<p>The above function is quite costly to evaluate, and a lot of evaluations are
needed to compute single and multiple scattering. Fortunately this function
depends on only two parameters and is quite smooth, so we can precompute it in a
small 2D texture to optimize its evaluation.
<p>For this we need a mapping between the function parameters $(r,\mu)$ and the
texture coordinates $(u,v)$, and vice-versa, because these parameters do not
have the same units and range of values. And even if it was the case, storing a
function $f$ from the $[0,1]$ interval in a texture of size $n$ would sample the
function at $0.5/n$, $1.5/n$, ... $(n-0.5)/n$, because texture samples are at
the center of texels. Therefore, this texture would only give us extrapolated
function values at the domain boundaries ($0$ and $1$). To avoid this we need
to store $f(0)$ at the center of texel 0 and $f(1)$ at the center of texel
$n-1$. This can be done with the following mapping from values $x$ in $[0,1]$ to
texture coordinates $u$ in $[0.5/n,1-0.5/n]$ - and its inverse:
*/
Number GetTextureCoordFromUnitRange(Number x, int texture_size)
{
return 0.5 / Number(texture_size) + x * (1.0 - 1.0 / Number(texture_size));
}
Number GetUnitRangeFromTextureCoord(Number u, int texture_size)
{
return (u - 0.5 / Number(texture_size)) / (1.0 - 1.0 / Number(texture_size));
}
/*
<p>Using these functions, we can now define a mapping between $(r,\mu)$ and the
texture coordinates $(u,v)$, and its inverse, which avoid any extrapolation
during texture lookups. In the <a href=
"http://evasion.inrialpes.fr/~Eric.Bruneton/PrecomputedAtmosphericScattering2.zip"
>original implementation</a> this mapping was using some ad-hoc constants chosen
for the Earth atmosphere case. Here we use a generic mapping, working for any
atmosphere, but still providing an increased sampling rate near the horizon.
Our improved mapping is based on the parameterization described in our
<a href="https://hal.inria.fr/inria-00288758/en">paper</a> for the 4D textures:
we use the same mapping for $r$, and a slightly improved mapping for $\mu$
(considering only the case where the view ray does not intersect the ground).
More precisely, we map $\mu$ to a value $x_{\mu}$ between 0 and 1 by considering
the distance $d$ to the top atmosphere boundary, compared to its minimum and
maximum values $d_{\mathrm{min}}=r_{\mathrm{top}}-r$ and
$d_{\mathrm{max}}=\rho+H$ (cf. the notations from the
<a href="https://hal.inria.fr/inria-00288758/en">paper</a> and the figure
below):
<svg width="505px" height="195px">
<style type="text/css"><![CDATA[
circle { fill: #000000; stroke: none; }
path { fill: none; stroke: #000000; }
text { font-size: 16px; font-style: normal; font-family: Sans; }
.vector { font-weight: bold; }
]]></style>
<path d="m 5,85 a 520,520 0 0 1 372,105"/>
<path d="m 5,5 a 600,600 0 0 1 490,185"/>
<path d="m 60,0 0,190"/>
<path d="m 60,65 180,-35"/>
<path d="m 55,5 5,-5 5,5"/>
<path d="m 55,60 5,5 5,-5"/>
<path d="m 55,70 5,-5 5,5"/>
<path d="m 60,40 a 25,25 0 0 1 25,20" style="stroke-dasharray:4,2;"/>
<path d="m 60,65 415,105"/>
<circle cx="60" cy="65" r="2.5"/>
<circle cx="240" cy="30" r="2.5"/>
<circle cx="180" cy="95" r="2.5"/>
<circle cx="475" cy="170" r="2.5"/>
<text x="20" y="40">d<tspan style="font-size:10px" dy="2">min</tspan></text>
<text x="35" y="70" class="vector">p</text>
<text x="35" y="125">r</text>
<text x="75" y="40">μ=cos(θ)</text>
<text x="120" y="75">ρ</text>
<text x="155" y="60">d</text>
<text x="315" y="125">H</text>
</svg>
<p>With these definitions, the mapping from $(r,\mu)$ to the texture coordinates
$(u,v)$ can be implemented as follows:
*/
float2 GetTransmittanceTextureUvFromRMu(IN(AtmosphereParameters) atmosphere,
Length r, Number mu)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
// Distance to top atmosphere boundary for a horizontal ray at ground level.
Length H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the horizon.
Length rho = SafeSqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the top atmosphere boundary for the ray (r,mu), and its minimum
// and maximum values over all mu - obtained for (r,1) and (r,mu_horizon).
Length d = DistanceToTopAtmosphereBoundary(atmosphere, r, mu);
Length d_min = atmosphere.top_radius - r;
Length d_max = rho + H;
Number x_mu = (d - d_min) / (d_max - d_min);
Number x_r = rho / H;
return float2(GetTextureCoordFromUnitRange(x_mu, TRANSMITTANCE_TEXTURE_WIDTH),
GetTextureCoordFromUnitRange(x_r, TRANSMITTANCE_TEXTURE_HEIGHT));
}
/*
<p>and the inverse mapping follows immediately:
*/
void GetRMuFromTransmittanceTextureUv(IN(AtmosphereParameters) atmosphere,
IN(float2) uv, OUT(Length) r, OUT(Number) mu)
{
assert(uv.x >= 0.0 && uv.x <= 1.0);
assert(uv.y >= 0.0 && uv.y <= 1.0);
Number x_mu = GetUnitRangeFromTextureCoord(uv.x, TRANSMITTANCE_TEXTURE_WIDTH);
Number x_r = GetUnitRangeFromTextureCoord(uv.y, TRANSMITTANCE_TEXTURE_HEIGHT);
// Distance to top atmosphere boundary for a horizontal ray at ground level.
Length H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the horizon, from which we can compute r:
Length rho = H * x_r;
r = sqrt(rho * rho + atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the top atmosphere boundary for the ray (r,mu), and its minimum
// and maximum values over all mu - obtained for (r,1) and (r,mu_horizon) -
// from which we can recover mu:
Length d_min = atmosphere.top_radius - r;
Length d_max = rho + H;
Length d = d_min + x_mu * (d_max - d_min);
mu = d == 0.0 * m ? Number(1.0) : (H * H - rho * rho - d * d) / (2.0 * r * d);
mu = ClampCosine(mu);
}
/*
<p>It is now easy to define a fragment shader function to precompute a texel of
the transmittance texture:
*/
DimensionlessSpectrum ComputeTransmittanceToTopAtmosphereBoundaryTexture(
IN(AtmosphereParameters) atmosphere, IN(float2) frag_coord)
{
const float2 TRANSMITTANCE_TEXTURE_SIZE = float2(TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT);
Length r;
Number mu;
GetRMuFromTransmittanceTextureUv(
atmosphere, frag_coord / TRANSMITTANCE_TEXTURE_SIZE, r, mu);
return ComputeTransmittanceToTopAtmosphereBoundary(atmosphere, r, mu);
}
/*
<h4 id="transmittance_lookup">Lookup</h4>
<p>With the help of the above precomputed texture, we can now get the
transmittance between a point and the top atmosphere boundary with a single
texture lookup (assuming there is no intersection with the ground):
*/
DimensionlessSpectrum GetTransmittanceToTopAtmosphereBoundary(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
Length r, Number mu)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
float2 uv = GetTransmittanceTextureUvFromRMu(atmosphere, r, mu);
return DimensionlessSpectrum(tex2D(transmittance_texture, uv).xyz);
}
/*
<p>Also, with $r_d=\Vert\bo\bq\Vert=\sqrt{d^2+2r\mu d+r^2}$ and $\mu_d=
\bo\bq\cdot\bp\bi/\Vert\bo\bq\Vert\Vert\bp\bi\Vert=(r\mu+d)/r_d$ the values of
$r$ and $\mu$ at $\bq$, we can get the transmittance between two arbitrary
points $\bp$ and $\bq$ inside the atmosphere with only two texture lookups
(recall that the transmittance between $\bp$ and $\bq$ is the transmittance
between $\bp$ and the top atmosphere boundary, divided by the transmittance
between $\bq$ and the top atmosphere boundary, or the reverse - we continue to
assume that the segment between the two points does not intersect the ground):
*/
DimensionlessSpectrum GetTransmittance(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
Length r, Number mu, Length d, bool ray_r_mu_intersects_ground)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
assert(d >= 0.0 * m);
Length r_d = ClampRadius(atmosphere, sqrt(d * d + 2.0 * r * mu * d + r * r));
Number mu_d = ClampCosine((r * mu + d) / r_d);
if (ray_r_mu_intersects_ground) {
return min(
exp(GetTransmittanceToTopAtmosphereBoundary(
atmosphere, transmittance_texture, r_d, -mu_d)
- GetTransmittanceToTopAtmosphereBoundary(
atmosphere, transmittance_texture, r, -mu)),
DimensionlessSpectrum(1.0, 1.0, 1.0));
} else {
return min(
exp(GetTransmittanceToTopAtmosphereBoundary(
atmosphere, transmittance_texture, r, mu)
- GetTransmittanceToTopAtmosphereBoundary(
atmosphere, transmittance_texture, r_d, mu_d)),
DimensionlessSpectrum(1.0, 1.0, 1.0));
}
}
/*
<p>where <code>ray_r_mu_intersects_ground</code> should be true iif the ray
defined by $r$ and $\mu$ intersects the ground. We don't compute it here with
<code>RayIntersectsGround</code> because the result could be wrong for rays
very close to the horizon, due to the finite precision and rounding errors of
floating point operations. And also because the caller generally has more robust
ways to know whether a ray intersects the ground or not (see below).
<p>Finally, we will also need the transmittance between a point in the
atmosphere and the Sun. The Sun is not a point light source, so this is an
integral of the transmittance over the Sun disc. Here we consider that the
transmittance is constant over this disc, except below the horizon, where the
transmittance is 0. As a consequence, the transmittance to the Sun can be
computed with <code>GetTransmittanceToTopAtmosphereBoundary</code>, times the
fraction of the Sun disc which is above the horizon.
<p>This fraction varies from 0 when the Sun zenith angle $\theta_s$ is larger
than the horizon zenith angle $\theta_h$ plus the Sun angular radius $\alpha_s$,
to 1 when $\theta_s$ is smaller than $\theta_h-\alpha_s$. Equivalently, it
varies from 0 when $\mu_s=\cos\theta_s$ is smaller than
$\cos(\theta_h+\alpha_s)\approx\cos\theta_h-\alpha_s\sin\theta_h$ to 1 when
$\mu_s$ is larger than
$\cos(\theta_h-\alpha_s)\approx\cos\theta_h+\alpha_s\sin\theta_h$. In between,
the visible Sun disc fraction varies approximately like a smoothstep (this can
be verified by plotting the area of <a
href="https://en.wikipedia.org/wiki/Circular_segment">circular segment</a> as a
function of its <a href="https://en.wikipedia.org/wiki/Sagitta_(geometry)"
>sagitta</a>). Therefore, since $\sin\theta_h=r_{\mathrm{bottom}}/r$, we can
approximate the transmittance to the Sun with the following function:
*/
DimensionlessSpectrum GetTransmittanceToSun(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
Length r, Number mu_s)
{
Number sin_theta_h = atmosphere.bottom_radius / r;
Number cos_theta_h = -sqrt(max(1.0 - sin_theta_h * sin_theta_h, 0.0));
return exp(GetTransmittanceToTopAtmosphereBoundary(
atmosphere, transmittance_texture, r, mu_s))
* smoothstep(-sin_theta_h * atmosphere.sun_angular_radius / rad,
sin_theta_h * atmosphere.sun_angular_radius / rad,
mu_s - cos_theta_h);
}
/*
<h3 id="single_scattering">Single scattering</h3>
<p>The single scattered radiance is the light arriving from the Sun at some
point after exactly one scattering event inside the atmosphere (which can be due
to air molecules or aerosol particles; we exclude reflections from the ground,
computed <a href="#irradiance">separately</a>). The following sections describe
how we compute it, how we store it in a precomputed texture, and how we read it
back.
<h4 id="single_scattering_computation">Computation</h4>
<p>Consider the Sun light scattered at a point $\bq$ by air molecules before
arriving at another point $\bp$ (for aerosols, replace "Rayleigh" with "Mie"
below):
<svg height="190px" width="340px">
<style type="text/css"><![CDATA[
circle { fill: #000000; stroke: none; }
path { fill: none; stroke: #000000; }
text { font-size: 16px; font-style: normal; font-family: Sans; }
.vector { font-weight: bold; }
]]></style>
<path d="m 0,66 a 600,600 0 0 1 340,0"/>
<path d="m 0,150 a 520,520 0 0 1 340,0"/>
<path d="m 170,180 0,-165"/>
<path d="m 250,180 30,-165"/>
<path d="m 170,90 -30,-60"/>
<path d="m 155,70 0,-10 8,6" />
<path d="m 270,70 -20,-40" style="stroke-width:2;"/>
<path d="m 170,90 100,-20" style="stroke-width:2;"/>
<path d="m 270,70 75,-15" />
<path d="m 170,65 a 25,25 0 0 1 25,20" style="stroke-dasharray:4,2;"/>
<path d="m 170,30 a 60,60 1 0 0 -26.8,6.3" style="stroke-dasharray:4,2;"/>
<path d="m 255,40 a 35,35 0 0 1 21,-3.2" style="stroke-dasharray:4,2;"/>
<path d="m 258,45 a 30,30 0 0 1 41,19" style="stroke-dasharray:4,2;"/>
<circle cx="170" cy="90" r="2.5"/>
<circle cx="270" cy="70" r="2.5"/>
<text x="155" y="105" class="vector">p</text>
<text x="275" y="85" class="vector">q</text>
<text x="130" y="70" class="vector">ω<tspan
dy="2" style="font-size:10px;font-weight:normal;">s</tspan></text>
<text x="155" y="164">r</text>
<text x="265" y="165">r<tspan dy="2" style="font-size:10px">d</tspan></text>
<text x="220" y="95">d</text>
<text x="190" y="65">μ</text>
<text x="145" y="25">μ<tspan dy="2" style="font-size:10px">s</tspan></text>
<text x="290" y="45">ν</text>
<text x="250" y="20">μ<tspan dy="2" style="font-size:10px">s,d</tspan></text>
</svg>
<p>The radiance arriving at $\bp$ is the product of:
<ul>
<li>the solar irradiance at the top of the atmosphere,</li>
<li>the transmittance between the Sun and $\bq$ (i.e. the fraction of the Sun
light at the top of the atmosphere that reaches $\bq$),</li>
<li>the Rayleigh scattering coefficient at $\bq$ (i.e. the fraction of the
light arriving at $\bq$ which is scattered, in any direction),</li>
<li>the Rayleigh phase function (i.e. the fraction of the scattered light at
$\bq$ which is actually scattered towards $\bp$),</li>
<li>the transmittance between $\bq$ and $\bp$ (i.e. the fraction of the light
scattered at $\bq$ towards $\bp$ that reaches $\bp$).</li>
</ul>
<p>Thus, by noting $\bw_s$ the unit direction vector towards the Sun, and with
the following definitions:
<ul>
<li>$r=\Vert\bo\bp\Vert$,</li>
<li>$d=\Vert\bp\bq\Vert$,</li>
<li>$\mu=(\bo\bp\cdot\bp\bq)/rd$,</li>
<li>$\mu_s=(\bo\bp\cdot\bw_s)/r$,</li>
<li>$\nu=(\bp\bq\cdot\bw_s)/d$</li>
</ul>
the values of $r$ and $\mu_s$ for $\bq$ are
<ul>
<li>$r_d=\Vert\bo\bq\Vert=\sqrt{d^2+2r\mu d +r^2}$,</li>
<li>$\mu_{s,d}=(\bo\bq\cdot\bw_s)/r_d=((\bo\bp+\bp\bq)\cdot\bw_s)/r_d=
(r\mu_s + d\nu)/r_d$</li>
</ul>
and the Rayleigh and Mie single scattering components can be computed as follows
(note that we omit the solar irradiance and the phase function terms, as well as
the scattering coefficients at the bottom of the atmosphere - we add them later
on for efficiency reasons):
*/
void ComputeSingleScatteringIntegrand(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
Length r, Number mu, Number mu_s, Number nu, Length d,
bool ray_r_mu_intersects_ground,
OUT(DimensionlessSpectrum) rayleigh, OUT(DimensionlessSpectrum) mie)
{
Length r_d = ClampRadius(atmosphere, sqrt(d * d + 2.0 * r * mu * d + r * r));
Number mu_s_d = ClampCosine((r * mu_s + d * nu) / r_d);
DimensionlessSpectrum transmittance = GetTransmittance(
atmosphere, transmittance_texture, r, mu, d,
ray_r_mu_intersects_ground)
* GetTransmittanceToSun(
atmosphere, transmittance_texture, r_d, mu_s_d);
rayleigh = transmittance * GetProfileDensity(atmosphere.rayleigh_density, r_d - atmosphere.bottom_radius);
mie = transmittance * GetProfileDensity(atmosphere.mie_density, r_d - atmosphere.bottom_radius);
}
/*
<p>Consider now the Sun light arriving at $\bp$ from a given direction $\bw$,
after exactly one scattering event. The scattering event can occur at any point
$\bq$ between $\bp$ and the intersection $\bi$ of the half-line $[\bp,\bw)$ with
the nearest atmosphere boundary. Thus, the single scattered radiance at $\bp$
from direction $\bw$ is the integral of the single scattered radiance from $\bq$
to $\bp$ for all points $\bq$ between $\bp$ and $\bi$. To compute it, we first
need the length $\Vert\bp\bi\Vert$:
*/
Length DistanceToNearestAtmosphereBoundary(IN(AtmosphereParameters) atmosphere,
Length r, Number mu, bool ray_r_mu_intersects_ground)
{
if (ray_r_mu_intersects_ground) {
return DistanceToBottomAtmosphereBoundary(atmosphere, r, mu);
} else {
return DistanceToTopAtmosphereBoundary(atmosphere, r, mu);
}
}
/*
<p>The single scattering integral can then be computed as follows (using
the <a href="https://en.wikipedia.org/wiki/Trapezoidal_rule">trapezoidal
rule</a>):
*/
void ComputeSingleScattering(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground,
OUT(IrradianceSpectrum) rayleigh, OUT(IrradianceSpectrum) mie)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
assert(mu_s >= -1.0 && mu_s <= 1.0);
assert(nu >= -1.0 && nu <= 1.0);
// Number of intervals for the numerical integration.
const int SAMPLE_COUNT = 50;
// The integration step, i.e. the length of each integration interval.
Length dx = DistanceToNearestAtmosphereBoundary(atmosphere, r, mu,
ray_r_mu_intersects_ground)
/ Number(SAMPLE_COUNT);
// Integration loop.
DimensionlessSpectrum rayleigh_sum = 0.0;
DimensionlessSpectrum mie_sum = 0.0;
for (int i = 0; i <= SAMPLE_COUNT; ++i) {
Length d_i = Number(i) * dx;
// The Rayleigh and Mie single scattering at the current sample point.
DimensionlessSpectrum rayleigh_i;
DimensionlessSpectrum mie_i;
ComputeSingleScatteringIntegrand(atmosphere, transmittance_texture,
r, mu, mu_s, nu, d_i, ray_r_mu_intersects_ground, rayleigh_i, mie_i);
// Sample weight (from the trapezoidal rule).
Number weight_i = (i == 0 || i == SAMPLE_COUNT) ? 0.5 : 1.0;
rayleigh_sum += rayleigh_i * weight_i;
mie_sum += mie_i * weight_i;
}
rayleigh = rayleigh_sum * dx * atmosphere.solar_irradiance * atmosphere.rayleigh_scattering;
mie = mie_sum * dx * atmosphere.solar_irradiance * atmosphere.mie_scattering;
}
/*
<p>Note that we added the solar irradiance and the scattering coefficient terms
that we omitted in <code>ComputeSingleScatteringIntegrand</code>, but not the
phase function terms - they are added at <a href="#rendering">render time</a>
for better angular precision. We provide them here for completeness:
*/
InverseSolidAngle RayleighPhaseFunction(Number nu)
{
InverseSolidAngle k = 3.0 / (16.0 * PI * sr);
return k * (1.0 + nu * nu);
}
InverseSolidAngle MiePhaseFunction(Number g, Number nu)
{
InverseSolidAngle k = 3.0 / (8.0 * PI * sr) * (1.0 - g * g) / (2.0 + g * g);
return k * (1.0 + nu * nu) / pow(1.0 + g * g - 2.0 * g * nu, 1.5);
}
/*
<h4 id="single_scattering_precomputation">Precomputation</h4>
<p>The <code>ComputeSingleScattering</code> function is quite costly to
evaluate, and a lot of evaluations are needed to compute multiple scattering.
We therefore want to precompute it in a texture, which requires a mapping from
the 4 function parameters to texture coordinates. Assuming for now that we have
4D textures, we need to define a mapping from $(r,\mu,\mu_s,\nu)$ to texture
coordinates $(u,v,w,z)$. The function below implements the mapping defined in
our <a href="https://hal.inria.fr/inria-00288758/en">paper</a>, with some small
improvements (refer to the paper and to the above figures for the notations):
<ul>
<li>the mapping for $\mu$ takes the minimal distance to the nearest atmosphere
boundary into account, to map $\mu$ to the full $[0,1]$ interval (the original
mapping was not covering the full $[0,1]$ interval).</li>
<li>the mapping for $\mu_s$ is more generic than in the paper (the original
mapping was using ad-hoc constants chosen for the Earth atmosphere case). It is
based on the distance to the top atmosphere boundary (for the sun rays), as for
the $\mu$ mapping, and uses only one ad-hoc (but configurable) parameter. Yet,
as the original definition, it provides an increased sampling rate near the
horizon.</li>
</ul>
*/
float4 GetScatteringTextureUvwzFromRMuMuSNu(IN(AtmosphereParameters) atmosphere,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
assert(mu_s >= -1.0 && mu_s <= 1.0);
assert(nu >= -1.0 && nu <= 1.0);
// Distance to top atmosphere boundary for a horizontal ray at ground level.
Length H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the horizon.
Length rho = SafeSqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius);
Number u_r = GetTextureCoordFromUnitRange(rho / H, SCATTERING_TEXTURE_R_SIZE);
// Discriminant of the quadratic equation for the intersections of the ray
// (r,mu) with the ground (see RayIntersectsGround).
Length r_mu = r * mu;
Area discriminant = r_mu * r_mu - r * r + atmosphere.bottom_radius * atmosphere.bottom_radius;
Number u_mu;
if (ray_r_mu_intersects_ground) {
// Distance to the ground for the ray (r,mu), and its minimum and maximum
// values over all mu - obtained for (r,-1) and (r,mu_horizon).
Length d = -r_mu - SafeSqrt(discriminant);
Length d_min = r - atmosphere.bottom_radius;
Length d_max = rho;
u_mu = 0.5 - 0.5 * GetTextureCoordFromUnitRange(d_max == d_min ? 0.0 : (d - d_min) / (d_max - d_min), SCATTERING_TEXTURE_MU_SIZE / 2);
} else {
// Distance to the top atmosphere boundary for the ray (r,mu), and its
// minimum and maximum values over all mu - obtained for (r,1) and
// (r,mu_horizon).
Length d = -r_mu + SafeSqrt(discriminant + H * H);
Length d_min = atmosphere.top_radius - r;
Length d_max = rho + H;
u_mu = 0.5 + 0.5 * GetTextureCoordFromUnitRange((d - d_min) / (d_max - d_min), SCATTERING_TEXTURE_MU_SIZE / 2);
}
Length d = DistanceToTopAtmosphereBoundary(
atmosphere, atmosphere.bottom_radius, mu_s);
Length d_min = atmosphere.top_radius - atmosphere.bottom_radius;
Length d_max = H;
Number a = (d - d_min) / (d_max - d_min);
Length D = DistanceToTopAtmosphereBoundary(
atmosphere, atmosphere.bottom_radius, atmosphere.mu_s_min);
Number A = (D - d_min) / (d_max - d_min);
// An ad-hoc function equal to 0 for mu_s = mu_s_min (because then d = D and
// thus a = A), equal to 1 for mu_s = 1 (because then d = d_min and thus
// a = 0), and with a large slope around mu_s = 0, to get more texture
// samples near the horizon.
Number u_mu_s = GetTextureCoordFromUnitRange(
max(1.0 - a / A, 0.0) / (1.0 + a), SCATTERING_TEXTURE_MU_S_SIZE);
Number u_nu = (nu + 1.0) / 2.0;
return float4(u_nu, u_mu_s, u_mu, u_r);
}
/*
<p>The inverse mapping follows immediately:
*/
void GetRMuMuSNuFromScatteringTextureUvwz(IN(AtmosphereParameters) atmosphere,
IN(float4) uvwz, OUT(Length) r, OUT(Number) mu, OUT(Number) mu_s,
OUT(Number) nu, OUT(bool) ray_r_mu_intersects_ground)
{
assert(uvwz.x >= 0.0 && uvwz.x <= 1.0);
assert(uvwz.y >= 0.0 && uvwz.y <= 1.0);
assert(uvwz.z >= 0.0 && uvwz.z <= 1.0);
assert(uvwz.w >= 0.0 && uvwz.w <= 1.0);
// Distance to top atmosphere boundary for a horizontal ray at ground level.
Length H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the horizon.
Length rho = H * GetUnitRangeFromTextureCoord(uvwz.w, SCATTERING_TEXTURE_R_SIZE);
r = sqrt(rho * rho + atmosphere.bottom_radius * atmosphere.bottom_radius);
if (uvwz.z < 0.5) {
// Distance to the ground for the ray (r,mu), and its minimum and maximum
// values over all mu - obtained for (r,-1) and (r,mu_horizon) - from which
// we can recover mu:
Length d_min = r - atmosphere.bottom_radius;
Length d_max = rho;
Length d = d_min + (d_max - d_min) * GetUnitRangeFromTextureCoord(1.0 - 2.0 * uvwz.z, SCATTERING_TEXTURE_MU_SIZE / 2);
mu = d == 0.0 * m ? Number(-1.0) : ClampCosine(-(rho * rho + d * d) / (2.0 * r * d));
ray_r_mu_intersects_ground = true;
} else {
// Distance to the top atmosphere boundary for the ray (r,mu), and its
// minimum and maximum values over all mu - obtained for (r,1) and
// (r,mu_horizon) - from which we can recover mu:
Length d_min = atmosphere.top_radius - r;
Length d_max = rho + H;
Length d = d_min + (d_max - d_min) * GetUnitRangeFromTextureCoord(2.0 * uvwz.z - 1.0, SCATTERING_TEXTURE_MU_SIZE / 2);
mu = d == 0.0 * m ? Number(1.0) : ClampCosine((H * H - rho * rho - d * d) / (2.0 * r * d));
ray_r_mu_intersects_ground = false;
}
Number x_mu_s = GetUnitRangeFromTextureCoord(uvwz.y, SCATTERING_TEXTURE_MU_S_SIZE);
Length d_min = atmosphere.top_radius - atmosphere.bottom_radius;
Length d_max = H;
Length D = DistanceToTopAtmosphereBoundary(
atmosphere, atmosphere.bottom_radius, atmosphere.mu_s_min);
Number A = (D - d_min) / (d_max - d_min);
Number a = (A - x_mu_s * A) / (1.0 + x_mu_s * A);
Length d = d_min + min(a, A) * (d_max - d_min);
mu_s = d == 0.0 * m ? Number(1.0) : ClampCosine((H * H - d * d) / (2.0 * atmosphere.bottom_radius * d));
nu = ClampCosine(uvwz.x * 2.0 - 1.0);
}
/*
<p>We assumed above that we have 4D textures, which is not the case in practice.
We therefore need a further mapping, between 3D and 4D texture coordinates. The
function below expands a 3D texel coordinate into a 4D texture coordinate, and
then to $(r,\mu,\mu_s,\nu)$ parameters. It does so by "unpacking" two texel
coordinates from the $x$ texel coordinate. Note also how we clamp the $\nu$
parameter at the end. This is because $\nu$ is not a fully independent variable:
its range of values depends on $\mu$ and $\mu_s$ (this can be seen by computing
$\mu$, $\mu_s$ and $\nu$ from the cartesian coordinates of the zenith, view and
sun unit direction vectors), and the previous functions implicitely assume this
(their assertions can break if this constraint is not respected).
*/
void GetRMuMuSNuFromScatteringTextureFragCoord(
IN(AtmosphereParameters) atmosphere, IN(float3) frag_coord,
OUT(Length) r, OUT(Number) mu, OUT(Number) mu_s, OUT(Number) nu,
OUT(bool) ray_r_mu_intersects_ground)
{
const float4 SCATTERING_TEXTURE_SIZE = float4(
SCATTERING_TEXTURE_NU_SIZE - 1,
SCATTERING_TEXTURE_MU_S_SIZE,
SCATTERING_TEXTURE_MU_SIZE,
SCATTERING_TEXTURE_R_SIZE);
Number frag_coord_nu = floor(frag_coord.x / Number(SCATTERING_TEXTURE_MU_S_SIZE));
Number frag_coord_mu_s = fmod(frag_coord.x, Number(SCATTERING_TEXTURE_MU_S_SIZE));
float4 uvwz = float4(frag_coord_nu, frag_coord_mu_s, frag_coord.y, frag_coord.z) / SCATTERING_TEXTURE_SIZE;
GetRMuMuSNuFromScatteringTextureUvwz(
atmosphere, uvwz, r, mu, mu_s, nu, ray_r_mu_intersects_ground);
// Clamp nu to its valid range of values, given mu and mu_s.
nu = clamp(nu, mu * mu_s - sqrt((1.0 - mu * mu) * (1.0 - mu_s * mu_s)),
mu * mu_s + sqrt((1.0 - mu * mu) * (1.0 - mu_s * mu_s)));
}
/*
<p>With this mapping, we can finally write a function to precompute a texel of
the single scattering in a 3D texture:
*/
void ComputeSingleScatteringTexture(IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture, IN(float3) frag_coord,
OUT(IrradianceSpectrum) rayleigh, OUT(IrradianceSpectrum) mie)
{
Length r;
Number mu;
Number mu_s;
Number nu;
bool ray_r_mu_intersects_ground;
GetRMuMuSNuFromScatteringTextureFragCoord(atmosphere, frag_coord,
r, mu, mu_s, nu, ray_r_mu_intersects_ground);
ComputeSingleScattering(atmosphere, transmittance_texture,
r, mu, mu_s, nu, ray_r_mu_intersects_ground, rayleigh, mie);
}
/*
<h4 id="single_scattering_lookup">Lookup</h4>
<p>With the help of the above precomputed texture, we can now get the scattering
between a point and the nearest atmosphere boundary with two texture lookups (we
need two 3D texture lookups to emulate a single 4D texture lookup with
quadrilinear interpolation; the 3D texture coordinates are computed using the
inverse of the 3D-4D mapping defined in
<code>GetRMuMuSNuFromScatteringTextureFragCoord</code>):
*/
TEMPLATE(AbstractSpectrum)
AbstractSpectrum GetScattering(
IN(AtmosphereParameters) atmosphere,
IN(AbstractScatteringTexture TEMPLATE_ARGUMENT(AbstractSpectrum))
scattering_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground)
{
float4 uvwz = GetScatteringTextureUvwzFromRMuMuSNu(
atmosphere, r, mu, mu_s, nu, ray_r_mu_intersects_ground);
Number tex_coord_x = uvwz.x * Number(SCATTERING_TEXTURE_NU_SIZE - 1);
Number tex_x = floor(tex_coord_x);
Number lerp = tex_coord_x - tex_x;
float3 uvw0 = float3((tex_x + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE),
uvwz.z, uvwz.w);
float3 uvw1 = float3((tex_x + 1.0 + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE),
uvwz.z, uvwz.w);
return AbstractSpectrum(tex3D(scattering_texture, uvw0).xyz * (1.0 - lerp) + tex3D(scattering_texture, uvw1).xyz * lerp);
}
/*
<p>Finally, we provide here a convenience lookup function which will be useful
in the next section. This function returns either the single scattering, with
the phase functions included, or the $n$-th order of scattering, with $n>1$. It
assumes that, if <code>scattering_order</code> is strictly greater than 1, then
<code>multiple_scattering_texture</code> corresponds to this scattering order,
with both Rayleigh and Mie included, as well as all the phase function terms.
*/
RadianceSpectrum GetScattering(
IN(AtmosphereParameters) atmosphere,
IN(ReducedScatteringTexture) single_rayleigh_scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
IN(ScatteringTexture) multiple_scattering_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground,
int scattering_order)
{
if (scattering_order == 1) {
IrradianceSpectrum rayleigh = GetScattering(
atmosphere, single_rayleigh_scattering_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
IrradianceSpectrum mie = GetScattering(
atmosphere, single_mie_scattering_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
return rayleigh * RayleighPhaseFunction(nu) + mie * MiePhaseFunction(atmosphere.mie_phase_function_g, nu);
} else {
return GetScattering(
atmosphere, multiple_scattering_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
}
}
/*
<h3 id="multiple_scattering">Multiple scattering</h3>
<p>The multiply scattered radiance is the light arriving from the Sun at some
point in the atmosphere after two or more <i>bounces</i> (where a bounce is
either a scattering event or a reflection from the ground). The following
sections describe how we compute it, how we store it in a precomputed texture,
and how we read it back.
<p>Note that, as for single scattering, we exclude here the light paths whose
<i>last</i> bounce is a reflection on the ground. The contribution from these
paths is computed separately, at rendering time, in order to take the actual
ground albedo into account (for intermediate reflections on the ground, which
are precomputed, we use an average, uniform albedo).
<h4 id="multiple_scattering_computation">Computation</h4>
<p>Multiple scattering can be decomposed into the sum of double scattering,
triple scattering, etc, where each term corresponds to the light arriving from
the Sun at some point in the atmosphere after <i>exactly</i> 2, 3, etc bounces.
Moreover, each term can be computed from the previous one. Indeed, the light
arriving at some point $\bp$ from direction $\bw$ after $n$ bounces is an
integral over all the possible points $\bq$ for the last bounce, which involves
the light arriving at $\bq$ from any direction, after $n-1$ bounces.
<p>This description shows that each scattering order requires a triple integral
to be computed from the previous one (one integral over all the points $\bq$
on the segment from $\bp$ to the nearest atmosphere boundary in direction $\bw$,
and a nested double integral over all directions at each point $\bq$).
Therefore, if we wanted to compute each order "from scratch", we would need a
triple integral for double scattering, a sextuple integral for triple
scattering, etc. This would be clearly inefficient, because of all the redundant
computations (the computations for order $n$ would basically redo all the
computations for all previous orders, leading to quadratic complexity in the
total number of orders). Instead, it is much more efficient to proceed as
follows:
<ul>
<li>precompute single scattering in a texture (as described above),</li>
<li>for $n \ge 2$:
<ul>
<li>precompute the $n$-th scattering in a texture, with a triple integral whose
integrand uses lookups in the $(n-1)$-th scattering texture</li>
</ul>
</li>
</ul>
<p>This strategy avoids many redundant computations but does not eliminate all
of them. Consider for instance the points $\bp$ and $\bp'$ in the figure below,
and the computations which are necessary to compute the light arriving at these
two points from direction $\bw$ after $n$ bounces. These computations involve,
in particular, the evaluation of the radiance $L$ which is scattered at $\bq$ in
direction $-\bw$, and coming from all directions after $n-1$ bounces:
<svg width="340px" height="150px">
<style type="text/css"><![CDATA[
circle { fill: #000000; stroke: none; }
path { fill: none; stroke: #000000; }
text { font-size: 16px; font-style: normal; font-family: Sans; }
.vector { font-weight: bold; }
]]></style>
<path d="m 0,26 a 600,600 0 0 1 340,0"/>
<path d="m 0,110 a 520,520 0 0 1 340,0"/>
<path d="m 170,140 0,-135"/>
<path d="m 20,80 200,-40" />
<path d="m 209,39 11,1 -10,5" />
<circle cx="70" cy="70" r="2.5"/>
<circle cx="120" cy="60" r="2.5"/>
<circle cx="170" cy="50" r="2.5"/>
<text x="65" y="60" class="vector">p</text>
<text x="175" y="65" class="vector">q</text>
<text x="225" y="35" class="vector">ω</text>
<text x="115" y="50" class="vector">p<tspan
style="font-weight:normal;">'</tspan></text>
</svg>
<p>Therefore, if we computed the n-th scattering with a triple integral as
described above, we would compute $L$ redundantly (in fact, for all points $\bp$
between $\bq$ and the nearest atmosphere boundary in direction $-\bw$). To avoid
this, and thus increase the efficiency of the multiple scattering computations,
we refine the above algorithm as follows:
<ul>
<li>precompute single scattering in a texture (as described above),</li>
<li>for $n \ge 2$:
<ul>
<li>for each point $\bq$ and direction $\bw$, precompute the light which is
scattered at $\bq$ towards direction $-\bw$, coming from any direction after
$n-1$ bounces (this involves only a double integral, whose integrand uses
lookups in the $(n-1)$-th scattering texture),</li>
<li>for each point $\bp$ and direction $\bw$, precompute the light coming from
direction $\bw$ after $n$ bounces (this involves only a single integral, whose
integrand uses lookups in the texture computed at the previous line)</li>
</ul>
</li>
</ul>
<p>To get a complete algorithm, we must now specify how we implement the two
steps in the above loop. This is what we do in the rest of this section.
<h5 id="multiple_scattering_first_step">First step</h5>
<p>The first step computes the radiance which is scattered at some point $\bq$
inside the atmosphere, towards some direction $-\bw$. Furthermore, we assume
that this scattering event is the $n$-th bounce.
<p>This radiance is the integral over all the possible incident directions
$\bw_i$, of the product of
<ul>
<li>the incident radiance $L_i$ arriving at $\bq$ from direction $\bw_i$ after
$n-1$ bounces, which is the sum of:
<ul>
<li>a term given by the precomputed scattering texture for the $(n-1)$-th
order,</li>
<li>if the ray $[\bq, \bw_i)$ intersects the ground at $\br$, the contribution
from the light paths with $n-1$ bounces and whose last bounce is at $\br$, i.e.
on the ground (these paths are excluded, by definition, from our precomputed
textures, but we must take them into account here since the bounce on the ground
is followed by a bounce at $\bq$). This contribution, in turn, is the product
of:
<ul>
<li>the transmittance between $\bq$ and $\br$,</li>
<li>the (average) ground albedo,</li>
<li>the <a href="https://www.cs.princeton.edu/~smr/cs348c-97/surveypaper.html"
>Lambertian BRDF</a> $1/\pi$,</li>
<li>the irradiance received on the ground after $n-2$ bounces. We explain in the
<a href="#irradiance">next section</a> how we precompute it in a texture. For
now, we assume that we can use the following function to retrieve this
irradiance from a precomputed texture:
</li>
</ul>
</li>
</ul>
</li>
</ul>
*/
IrradianceSpectrum GetIrradiance(
IN(AtmosphereParameters) atmosphere,
IN(IrradianceTexture) irradiance_texture,
Length r, Number mu_s);
/*
<ul>
<li>the scattering coefficient at $\bq$,</li>
<li>the scattering phase function for the directions $\bw$ and $\bw_i$</li>
</ul>
This leads to the following implementation (where
<code>multiple_scattering_texture</code> is supposed to contain the $(n-1)$-th
order of scattering, if $n>2$, <code>irradiance_texture</code> is the irradiance
received on the ground after $n-2$ bounces, and <code>scattering_order</code> is
equal to $n$):</li>
*/
RadianceDensitySpectrum ComputeScatteringDensity(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(ReducedScatteringTexture) single_rayleigh_scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
IN(ScatteringTexture) multiple_scattering_texture,
IN(IrradianceTexture) irradiance_texture,
Length r, Number mu, Number mu_s, Number nu, int scattering_order)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
assert(mu_s >= -1.0 && mu_s <= 1.0);
assert(nu >= -1.0 && nu <= 1.0);
assert(scattering_order >= 2);
// Compute unit direction vectors for the zenith, the view direction omega and
// and the sun direction omega_s, such that the cosine of the view-zenith
// angle is mu, the cosine of the sun-zenith angle is mu_s, and the cosine of
// the view-sun angle is nu. The goal is to simplify computations below.
float3 zenith_direction = float3(0.0, 0.0, 1.0);
float3 omega = float3(sqrt(1.0 - mu * mu), 0.0, mu);
Number sun_dir_x = omega.x == 0.0 ? 0.0 : (nu - mu * mu_s) / omega.x;
Number sun_dir_y = sqrt(max(1.0 - sun_dir_x * sun_dir_x - mu_s * mu_s, 0.0));
float3 omega_s = float3(sun_dir_x, sun_dir_y, mu_s);
const int SAMPLE_COUNT = 16;
const Angle dphi = pi / Number(SAMPLE_COUNT);
const Angle dtheta = pi / Number(SAMPLE_COUNT);
RadianceDensitySpectrum rayleigh_mie = 0.0 * watt_per_cubic_meter_per_sr_per_nm;
// Nested loops for the integral over all the incident directions omega_i.
for (int l = 0; l < SAMPLE_COUNT; ++l) {
Angle theta = (Number(l) + 0.5) * dtheta;
Number cos_theta = cos(theta);
Number sin_theta = sin(theta);
bool ray_r_theta_intersects_ground = RayIntersectsGround(atmosphere, r, cos_theta);
// The distance and transmittance to the ground only depend on theta, so we
// can compute them in the outer loop for efficiency.
Length distance_to_ground = 0.0 * m;
DimensionlessSpectrum transmittance_to_ground = 0.0;
DimensionlessSpectrum ground_albedo = 0.0;
if (ray_r_theta_intersects_ground) {
distance_to_ground = DistanceToBottomAtmosphereBoundary(atmosphere, r, cos_theta);
transmittance_to_ground = GetTransmittance(atmosphere, transmittance_texture, r, cos_theta,
distance_to_ground, true /* ray_intersects_ground */);
ground_albedo = atmosphere.ground_albedo;
}
for (int m = 0; m < 2 * SAMPLE_COUNT; ++m) {
Angle phi = (Number(m) + 0.5) * dphi;
float3 omega_i = float3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta);
SolidAngle domega_i = (dtheta / rad) * (dphi / rad) * sin(theta) * sr;
// The radiance L_i arriving from direction omega_i after n-1 bounces is
// the sum of a term given by the precomputed scattering texture for the
// (n-1)-th order:
Number nu1 = dot(omega_s, omega_i);
RadianceSpectrum incident_radiance = GetScattering(atmosphere,
single_rayleigh_scattering_texture, single_mie_scattering_texture,
multiple_scattering_texture, r, omega_i.z, mu_s, nu1,
ray_r_theta_intersects_ground, scattering_order - 1);
// and of the contribution from the light paths with n-1 bounces and whose
// last bounce is on the ground. This contribution is the product of the
// transmittance to the ground, the ground albedo, the ground BRDF, and
// the irradiance received on the ground after n-2 bounces.
float3 ground_normal = normalize(zenith_direction * r + omega_i * distance_to_ground);
IrradianceSpectrum ground_irradiance = GetIrradiance(
atmosphere, irradiance_texture, atmosphere.bottom_radius,
dot(ground_normal, omega_s));
incident_radiance += transmittance_to_ground * ground_albedo * (1.0 / (PI * sr)) * ground_irradiance;
// The radiance finally scattered from direction omega_i towards direction
// -omega is the product of the incident radiance, the scattering
// coefficient, and the phase function for directions omega and omega_i
// (all this summed over all particle types, i.e. Rayleigh and Mie).
Number nu2 = dot(omega, omega_i);
Number rayleigh_density = GetProfileDensity(
atmosphere.rayleigh_density, r - atmosphere.bottom_radius);
Number mie_density = GetProfileDensity(
atmosphere.mie_density, r - atmosphere.bottom_radius);
rayleigh_mie += incident_radiance * (atmosphere.rayleigh_scattering * rayleigh_density * RayleighPhaseFunction(nu2) + atmosphere.mie_scattering * mie_density * MiePhaseFunction(atmosphere.mie_phase_function_g, nu2)) * domega_i;
}
}
return rayleigh_mie;
}
/*
<h5 id="multiple_scattering_second_step">Second step</h5>
<p>The second step to compute the $n$-th order of scattering is to compute for
each point $\bp$ and direction $\bw$, the radiance coming from direction $\bw$
after $n$ bounces, using a texture precomputed with the previous function.
<p>This radiance is the integral over all points $\bq$ between $\bp$ and the
nearest atmosphere boundary in direction $\bw$ of the product of:
<ul>
<li>a term given by a texture precomputed with the previous function, namely
the radiance scattered at $\bq$ towards $\bp$, coming from any direction after
$n-1$ bounces,</li>
<li>the transmittance betweeen $\bp$ and $\bq$</li>
</ul>
Note that this excludes the light paths with $n$ bounces and whose last
bounce is on the ground, on purpose. Indeed, we chose to exclude these paths
from our precomputed textures so that we can compute them at render time
instead, using the actual ground albedo.
<p>The implementation for this second step is straightforward:
*/
RadianceSpectrum ComputeMultipleScattering(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(ScatteringDensityTexture) scattering_density_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu >= -1.0 && mu <= 1.0);
assert(mu_s >= -1.0 && mu_s <= 1.0);
assert(nu >= -1.0 && nu <= 1.0);
// Number of intervals for the numerical integration.
const int SAMPLE_COUNT = 50;
// The integration step, i.e. the length of each integration interval.
Length dx = DistanceToNearestAtmosphereBoundary(
atmosphere, r, mu, ray_r_mu_intersects_ground)
/ Number(SAMPLE_COUNT);
// Integration loop.
RadianceSpectrum rayleigh_mie_sum = 0.0 * watt_per_square_meter_per_sr_per_nm;
for (int i = 0; i <= SAMPLE_COUNT; ++i) {
Length d_i = Number(i) * dx;
// The r, mu and mu_s parameters at the current integration point (see the
// single scattering section for a detailed explanation).
Length r_i = ClampRadius(atmosphere, sqrt(d_i * d_i + 2.0 * r * mu * d_i + r * r));
Number mu_i = ClampCosine((r * mu + d_i) / r_i);
Number mu_s_i = ClampCosine((r * mu_s + d_i * nu) / r_i);
// The Rayleigh and Mie multiple scattering at the current sample point.
RadianceSpectrum rayleigh_mie_i = GetScattering(
atmosphere, scattering_density_texture, r_i, mu_i, mu_s_i, nu,
ray_r_mu_intersects_ground)
* GetTransmittance(
atmosphere, transmittance_texture, r, mu, d_i,
ray_r_mu_intersects_ground)
* dx;
// Sample weight (from the trapezoidal rule).
Number weight_i = (i == 0 || i == SAMPLE_COUNT) ? 0.5 : 1.0;
rayleigh_mie_sum += rayleigh_mie_i * weight_i;
}
return rayleigh_mie_sum;
}
/*
<h4 id="multiple_scattering_precomputation">Precomputation</h4>
<p>As explained in the <a href="#multiple_scattering">overall algorithm</a> to
compute multiple scattering, we need to precompute each order of scattering in a
texture to save computations while computing the next order. And, in order to
store a function in a texture, we need a mapping from the function parameters to
texture coordinates. Fortunately, all the orders of scattering depend on the
same $(r,\mu,\mu_s,\nu)$ parameters as single scattering, so we can simple reuse
the mappings defined for single scattering. This immediately leads to the
following simple functions to precompute a texel of the textures for the
<a href="#multiple_scattering_first_step">first</a> and
<a href="#multiple_scattering_second_step">second</a> steps of each iteration
over the number of bounces:
*/
RadianceDensitySpectrum ComputeScatteringDensityTexture(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(ReducedScatteringTexture) single_rayleigh_scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
IN(ScatteringTexture) multiple_scattering_texture,
IN(IrradianceTexture) irradiance_texture,
IN(float3) frag_coord, int scattering_order)
{
Length r;
Number mu;
Number mu_s;
Number nu;
bool ray_r_mu_intersects_ground;
GetRMuMuSNuFromScatteringTextureFragCoord(atmosphere, frag_coord,
r, mu, mu_s, nu, ray_r_mu_intersects_ground);
return ComputeScatteringDensity(atmosphere, transmittance_texture,
single_rayleigh_scattering_texture, single_mie_scattering_texture,
multiple_scattering_texture, irradiance_texture, r, mu, mu_s, nu,
scattering_order);
}
RadianceSpectrum ComputeMultipleScatteringTexture(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(ScatteringDensityTexture) scattering_density_texture,
IN(float3) frag_coord, OUT(Number) nu)
{
Length r;
Number mu;
Number mu_s;
bool ray_r_mu_intersects_ground;
GetRMuMuSNuFromScatteringTextureFragCoord(atmosphere, frag_coord,
r, mu, mu_s, nu, ray_r_mu_intersects_ground);
return ComputeMultipleScattering(atmosphere, transmittance_texture,
scattering_density_texture, r, mu, mu_s, nu,
ray_r_mu_intersects_ground);
}
/*
<h4 id="multiple_scattering_lookup">Lookup</h4>
<p>Likewise, we can simply reuse the lookup function <code>GetScattering</code>
implemented for single scattering to read a value from the precomputed textures
for multiple scattering. In fact, this is what we did above in the
<code>ComputeScatteringDensity</code> and <code>ComputeMultipleScattering</code>
functions.
<h3 id="irradiance">Ground irradiance</h3>
<p>The ground irradiance is the Sun light received on the ground after $n \ge 0$
bounces (where a bounce is either a scattering event or a reflection on the
ground). We need this for two purposes:
<ul>
<li>while precomputing the $n$-th order of scattering, with $n \ge 2$, in order
to compute the contribution of light paths whose $(n-1)$-th bounce is on the
ground (which requires the ground irradiance after $n-2$ bounces - see the
<a href="#multiple_scattering_computation">Multiple scattering</a>
section),</li>
<li>at rendering time, to compute the contribution of light paths whose last
bounce is on the ground (these paths are excluded, by definition, from our
precomputed scattering textures)</li>
</ul>
<p>In the first case we only need the ground irradiance for horizontal surfaces
at the bottom of the atmosphere (during precomputations we assume a perfectly
spherical ground with a uniform albedo). In the second case, however, we need
the ground irradiance for any altitude and any surface normal, and we want to
precompute it for efficiency. In fact, as described in our
<a href="https://hal.inria.fr/inria-00288758/en">paper</a> we precompute it only
for horizontal surfaces, at any altitude (which requires only 2D textures,
instead of 4D textures for the general case), and we use approximations for
non-horizontal surfaces.
<p>The following sections describe how we compute the ground irradiance, how we
store it in a precomputed texture, and how we read it back.
<h4 id="irradiance_computation">Computation</h4>
<p>The ground irradiance computation is different for the direct irradiance,
i.e. the light received directly from the Sun, without any intermediate bounce,
and for the indirect irradiance (at least one bounce). We start here with the
direct irradiance.
<p>The irradiance is the integral over an hemisphere of the incident radiance,
times a cosine factor. For the direct ground irradiance, the incident radiance
is the Sun radiance at the top of the atmosphere, times the transmittance
through the atmosphere. And, since the Sun solid angle is small, we can
approximate the transmittance with a constant, i.e. we can move it outside the
irradiance integral, which can be performed over (the visible fraction of) the
Sun disc rather than the hemisphere. Then the integral becomes equivalent to the
ambient occlusion due to a sphere, also called a view factor, which is given in
<a href="http://webserver.dmt.upm.es/~isidoro/tc3/Radiation%20View%20factors.pdf
">Radiative view factors</a> (page 10). For a small solid angle, these complex
equations can be simplified as follows:
*/
IrradianceSpectrum ComputeDirectIrradiance(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
Length r, Number mu_s)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu_s >= -1.0 && mu_s <= 1.0);
Number alpha_s = atmosphere.sun_angular_radius / rad;
// Approximate average of the cosine factor mu_s over the visible fraction of
// the Sun disc.
Number average_cosine_factor = mu_s < -alpha_s ? 0.0 : (mu_s > alpha_s ? mu_s : (mu_s + alpha_s) * (mu_s + alpha_s) / (4.0 * alpha_s));
return atmosphere.solar_irradiance * exp(GetTransmittanceToTopAtmosphereBoundary(atmosphere, transmittance_texture, r, mu_s)) * average_cosine_factor;
}
/*
<p>For the indirect ground irradiance the integral over the hemisphere must be
computed numerically. More precisely we need to compute the integral over all
the directions $\bw$ of the hemisphere, of the product of:
<ul>
<li>the radiance arriving from direction $\bw$ after $n$ bounces,
<li>the cosine factor, i.e. $\omega_z$</li>
</ul>
This leads to the following implementation (where
<code>multiple_scattering_texture</code> is supposed to contain the $n$-th
order of scattering, if $n>1$, and <code>scattering_order</code> is equal to
$n$):</li>
*/
IrradianceSpectrum ComputeIndirectIrradiance(
IN(AtmosphereParameters) atmosphere,
IN(ReducedScatteringTexture) single_rayleigh_scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
IN(ScatteringTexture) multiple_scattering_texture,
Length r, Number mu_s, int scattering_order)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu_s >= -1.0 && mu_s <= 1.0);
assert(scattering_order >= 1);
const int SAMPLE_COUNT = 32;
const Angle dphi = pi / Number(SAMPLE_COUNT);
const Angle dtheta = pi / Number(SAMPLE_COUNT);
IrradianceSpectrum result = 0.0 * watt_per_square_meter_per_nm;
float3 omega_s = float3(sqrt(1.0 - mu_s * mu_s), 0.0, mu_s);
for (int j = 0; j < SAMPLE_COUNT / 2; ++j) {
Angle theta = (Number(j) + 0.5) * dtheta;
for (int i = 0; i < 2 * SAMPLE_COUNT; ++i) {
Angle phi = (Number(i) + 0.5) * dphi;
float3 omega = float3(cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta));
SolidAngle domega = (dtheta / rad) * (dphi / rad) * sin(theta) * sr;
Number nu = dot(omega, omega_s);
result += GetScattering(atmosphere, single_rayleigh_scattering_texture,
single_mie_scattering_texture, multiple_scattering_texture,
r, omega.z, mu_s, nu, false /* ray_r_theta_intersects_ground */,
scattering_order)
* omega.z * domega;
}
}
return result;
}
/*
<h4 id="irradiance_precomputation">Precomputation</h4>
<p>In order to precompute the ground irradiance in a texture we need a mapping
from the ground irradiance parameters to texture coordinates. Since we
precompute the ground irradiance only for horizontal surfaces, this irradiance
depends only on $r$ and $\mu_s$, so we need a mapping from $(r,\mu_s)$ to
$(u,v)$ texture coordinates. The simplest, affine mapping is sufficient here,
because the ground irradiance function is very smooth:
*/
float2 GetIrradianceTextureUvFromRMuS(IN(AtmosphereParameters) atmosphere,
Length r, Number mu_s)
{
assert(r >= atmosphere.bottom_radius && r <= atmosphere.top_radius);
assert(mu_s >= -1.0 && mu_s <= 1.0);
Number x_r = (r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius);
Number x_mu_s = mu_s * 0.5 + 0.5;
return float2(GetTextureCoordFromUnitRange(x_mu_s, IRRADIANCE_TEXTURE_WIDTH),
GetTextureCoordFromUnitRange(x_r, IRRADIANCE_TEXTURE_HEIGHT));
}
/*
<p>The inverse mapping follows immediately:
*/
void GetRMuSFromIrradianceTextureUv(IN(AtmosphereParameters) atmosphere,
IN(float2) uv, OUT(Length) r, OUT(Number) mu_s)
{
assert(uv.x >= 0.0 && uv.x <= 1.0);
assert(uv.y >= 0.0 && uv.y <= 1.0);
Number x_mu_s = GetUnitRangeFromTextureCoord(uv.x, IRRADIANCE_TEXTURE_WIDTH);
Number x_r = GetUnitRangeFromTextureCoord(uv.y, IRRADIANCE_TEXTURE_HEIGHT);
r = atmosphere.bottom_radius + x_r * (atmosphere.top_radius - atmosphere.bottom_radius);
mu_s = ClampCosine(2.0 * x_mu_s - 1.0);
}
/*
<p>It is now easy to define a fragment shader function to precompute a texel of
the ground irradiance texture, for the direct irradiance:
*/
IrradianceSpectrum ComputeDirectIrradianceTexture(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(float2) frag_coord)
{
Length r;
Number mu_s;
GetRMuSFromIrradianceTextureUv(
atmosphere, frag_coord / IRRADIANCE_TEXTURE_SIZE, r, mu_s);
return ComputeDirectIrradiance(atmosphere, transmittance_texture, r, mu_s);
}
/*
<p>and the indirect one:
*/
IrradianceSpectrum ComputeIndirectIrradianceTexture(
IN(AtmosphereParameters) atmosphere,
IN(ReducedScatteringTexture) single_rayleigh_scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
IN(ScatteringTexture) multiple_scattering_texture,
IN(float2) frag_coord, int scattering_order)
{
Length r;
Number mu_s;
GetRMuSFromIrradianceTextureUv(
atmosphere, frag_coord / IRRADIANCE_TEXTURE_SIZE, r, mu_s);
return ComputeIndirectIrradiance(atmosphere,
single_rayleigh_scattering_texture, single_mie_scattering_texture,
multiple_scattering_texture, r, mu_s, scattering_order);
}
/*
<h4 id="irradiance_lookup">Lookup</h4>
<p>Thanks to these precomputed textures, we can now get the ground irradiance
with a single texture lookup:
*/
IrradianceSpectrum GetIrradiance(
IN(AtmosphereParameters) atmosphere,
IN(IrradianceTexture) irradiance_texture,
Length r, Number mu_s)
{
float2 uv = GetIrradianceTextureUvFromRMuS(atmosphere, r, mu_s);
return IrradianceSpectrum(tex2D(irradiance_texture, uv).xyz);
}
/*
<h3 id="rendering">Rendering</h3>
<p>Here we assume that the transmittance, scattering and irradiance textures
have been precomputed, and we provide functions using them to compute the sky
color, the aerial perspective, and the ground radiance.
<p>More precisely, we assume that the single Rayleigh scattering, without its
phase function term, plus the multiple scattering terms (divided by the Rayleigh
phase function for dimensional homogeneity) are stored in a
<code>scattering_texture</code>. We also assume that the single Mie scattering
is stored, without its phase function term:
<ul>
<li>either separately, in a <code>single_mie_scattering_texture</code> (this
option was not provided our <a href=
"http://evasion.inrialpes.fr/~Eric.Bruneton/PrecomputedAtmosphericScattering2.zip"
>original implementation</a>),</li>
<li>or, if the <code>COMBINED_SCATTERING_TEXTURES</code> preprocessor
macro is defined, in the <code>scattering_texture</code>. In this case, which is
only available with a GLSL compiler, Rayleigh and multiple scattering are stored
in the RGB channels, and the red component of the single Mie scattering is
stored in the alpha channel).</li>
</ul>
<p>In the second case, the green and blue components of the single Mie
scattering are extrapolated as described in our
<a href="https://hal.inria.fr/inria-00288758/en">paper</a>, with the following
function:
*/
float3 GetExtrapolatedSingleMieScattering(
IN(AtmosphereParameters) atmosphere, IN(float4) scattering)
{
// Algebraically this can never be negative, but rounding errors can produce
// that effect for sufficiently short view rays.
if (scattering.r <= 0.0) {
return 0.0;
}
return scattering.rgb * scattering.a / scattering.r * (atmosphere.rayleigh_scattering.r / atmosphere.mie_scattering.r) * (atmosphere.mie_scattering / atmosphere.rayleigh_scattering);
}
/*
<p>We can then retrieve all the scattering components (Rayleigh + multiple
scattering on one side, and single Mie scattering on the other side) with the
following function, based on
<a href="#single_scattering_lookup"><code>GetScattering</code></a> (we duplicate
some code here, instead of using two calls to <code>GetScattering</code>, to
make sure that the texture coordinates computation is shared between the lookups
in <code>scattering_texture</code> and
<code>single_mie_scattering_texture</code>):
*/
IrradianceSpectrum GetCombinedScattering(
IN(AtmosphereParameters) atmosphere,
IN(ReducedScatteringTexture) scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
Length r, Number mu, Number mu_s, Number nu,
bool ray_r_mu_intersects_ground,
OUT(IrradianceSpectrum) single_mie_scattering)
{
float4 uvwz = GetScatteringTextureUvwzFromRMuMuSNu(
atmosphere, r, mu, mu_s, nu, ray_r_mu_intersects_ground);
Number tex_coord_x = uvwz.x * Number(SCATTERING_TEXTURE_NU_SIZE - 1);
Number tex_x = floor(tex_coord_x);
Number lerp = tex_coord_x - tex_x;
float3 uvw0 = float3((tex_x + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE),
uvwz.z, uvwz.w);
float3 uvw1 = float3((tex_x + 1.0 + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE),
uvwz.z, uvwz.w);
float4 combined_scattering = tex3D(scattering_texture, uvw0) * (1.0 - lerp) + tex3D(scattering_texture, uvw1) * lerp;
IrradianceSpectrum scattering = IrradianceSpectrum(combined_scattering.xyz);
single_mie_scattering = GetExtrapolatedSingleMieScattering(atmosphere, combined_scattering);
return scattering;
}
/*
<h4 id="rendering_sky">Sky</h4>
<p>To render the sky we simply need to display the sky radiance, which we can
get with a lookup in the precomputed scattering tex2D(s), multiplied by the
phase function terms that were omitted during precomputation. We can also return
the transmittance of the atmosphere (which we can get with a single lookup in
the precomputed transmittance texture), which is needed to correctly render the
objects in space (such as the Sun and the Moon). This leads to the following
function, where most of the computations are used to correctly handle the case
of viewers outside the atmosphere, and the case of light shafts:
*/
RadianceSpectrum GetSkyRadiance(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(ReducedScatteringTexture) scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
Position camera, IN(Direction) view_ray, Length shadow_length,
IN(Direction) sun_direction, OUT(DimensionlessSpectrum) transmittance)
{
// Compute the distance to the top atmosphere boundary along the view ray,
// assuming the viewer is in space (or NaN if the view ray does not intersect
// the atmosphere).
Length r = length(camera);
Length rmu = dot(camera, view_ray);
Length distance_to_top_atmosphere_boundary = -rmu - sqrt(rmu * rmu - r * r + atmosphere.top_radius * atmosphere.top_radius);
// If the viewer is in space and the view ray intersects the atmosphere, move
// the viewer to the top atmosphere boundary (along the view ray):
if (distance_to_top_atmosphere_boundary > 0.0 * m) {
camera = camera + view_ray * distance_to_top_atmosphere_boundary;
r = atmosphere.top_radius;
rmu += distance_to_top_atmosphere_boundary;
} else if (r > atmosphere.top_radius) {
// If the view ray does not intersect the atmosphere, simply return 0.
transmittance = 1.0;
return 0.0 * watt_per_square_meter_per_sr_per_nm;
}
// Compute the r, mu, mu_s and nu parameters needed for the texture lookups.
Number mu = rmu / r;
Number mu_s = dot(camera, sun_direction) / r;
Number nu = dot(view_ray, sun_direction);
bool ray_r_mu_intersects_ground = RayIntersectsGround(atmosphere, r, mu);
transmittance = ray_r_mu_intersects_ground ? 0.0 : exp(GetTransmittanceToTopAtmosphereBoundary(atmosphere, transmittance_texture, r, mu));
IrradianceSpectrum single_mie_scattering;
IrradianceSpectrum scattering;
if (shadow_length == 0.0 * m) {
scattering = GetCombinedScattering(
atmosphere, scattering_texture, single_mie_scattering_texture,
r, mu, mu_s, nu, ray_r_mu_intersects_ground,
single_mie_scattering);
} else {
// Case of light shafts (shadow_length is the total length noted l in our
// paper): we omit the scattering between the camera and the point at
// distance l, by implementing Eq. (18) of the paper (shadow_transmittance
// is the T(x,x_s) term, scattering is the S|x_s=x+lv term).
Length d = shadow_length;
Length r_p = ClampRadius(atmosphere, sqrt(d * d + 2.0 * r * mu * d + r * r));
Number mu_p = (r * mu + d) / r_p;
Number mu_s_p = (r * mu_s + d * nu) / r_p;
scattering = GetCombinedScattering(
atmosphere, scattering_texture, single_mie_scattering_texture,
r_p, mu_p, mu_s_p, nu, ray_r_mu_intersects_ground,
single_mie_scattering);
DimensionlessSpectrum shadow_transmittance = GetTransmittance(atmosphere, transmittance_texture,
r, mu, shadow_length, ray_r_mu_intersects_ground);
scattering = scattering * shadow_transmittance;
single_mie_scattering = single_mie_scattering * shadow_transmittance;
}
return scattering * RayleighPhaseFunction(nu) + single_mie_scattering * MiePhaseFunction(atmosphere.mie_phase_function_g, nu);
}
/*
<h4 id="rendering_aerial_perspective">Aerial perspective</h4>
<p>To render the aerial perspective we need the transmittance and the scattering
between two points (i.e. between the viewer and a point on the ground, which can
at an arbibrary altitude). We already have a function to compute the
transmittance between two points (using 2 lookups in a texture which only
contains the transmittance to the top of the atmosphere), but we don't have one
for the scattering between 2 points. Hopefully, the scattering between 2 points
can be computed from two lookups in a texture which contains the scattering to
the nearest atmosphere boundary, as for the transmittance (except that here the
two lookup results must be subtracted, instead of divided). This is what we
implement in the following function (the initial computations are used to
correctly handle the case of viewers outside the atmosphere):
*/
RadianceSpectrum GetSkyRadianceToPoint(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(ReducedScatteringTexture) scattering_texture,
IN(ReducedScatteringTexture) single_mie_scattering_texture,
Position camera, IN(Position) position, Length shadow_length,
IN(Direction) sun_direction, OUT(DimensionlessSpectrum) transmittance)
{
// Compute the distance to the top atmosphere boundary along the view ray,
// assuming the viewer is in space (or NaN if the view ray does not intersect
// the atmosphere).
Direction view_ray = normalize(position - camera);
Length r = length(camera);
Length rmu = dot(camera, view_ray);
Length distance_to_top_atmosphere_boundary = -rmu - sqrt(rmu * rmu - r * r + atmosphere.top_radius * atmosphere.top_radius);
// If the viewer is in space and the view ray intersects the atmosphere, move
// the viewer to the top atmosphere boundary (along the view ray):
if (distance_to_top_atmosphere_boundary > 0.0 * m) {
camera = camera + view_ray * distance_to_top_atmosphere_boundary;
r = atmosphere.top_radius;
rmu += distance_to_top_atmosphere_boundary;
}
// Compute the r, mu, mu_s and nu parameters for the first texture lookup.
Number mu = rmu / r;
Number mu_s = dot(camera, sun_direction) / r;
Number nu = dot(view_ray, sun_direction);
Length d = length(position - camera);
bool ray_r_mu_intersects_ground = RayIntersectsGround(atmosphere, r, mu);
transmittance = GetTransmittance(atmosphere, transmittance_texture,
r, mu, d, ray_r_mu_intersects_ground);
IrradianceSpectrum single_mie_scattering;
IrradianceSpectrum scattering = GetCombinedScattering(
atmosphere, scattering_texture, single_mie_scattering_texture,
r, mu, mu_s, nu, ray_r_mu_intersects_ground,
single_mie_scattering);
// Compute the r, mu, mu_s and nu parameters for the second texture lookup.
// If shadow_length is not 0 (case of light shafts), we want to ignore the
// scattering along the last shadow_length meters of the view ray, which we
// do by subtracting shadow_length from d (this way scattering_p is equal to
// the S|x_s=x_0-lv term in Eq. (17) of our paper).
d = max(d - shadow_length, 0.0 * m);
Length r_p = ClampRadius(atmosphere, sqrt(d * d + 2.0 * r * mu * d + r * r));
Number mu_p = (r * mu + d) / r_p;
Number mu_s_p = (r * mu_s + d * nu) / r_p;
IrradianceSpectrum single_mie_scattering_p;
IrradianceSpectrum scattering_p = GetCombinedScattering(
atmosphere, scattering_texture, single_mie_scattering_texture,
r_p, mu_p, mu_s_p, nu, ray_r_mu_intersects_ground,
single_mie_scattering_p);
// Combine the lookup results to get the scattering between camera and point.
DimensionlessSpectrum shadow_transmittance = transmittance;
if (shadow_length > 0.0 * m) {
// This is the T(x,x_s) term in Eq. (17) of our paper, for light shafts.
shadow_transmittance = GetTransmittance(atmosphere, transmittance_texture,
r, mu, d, ray_r_mu_intersects_ground);
}
scattering = scattering - shadow_transmittance * scattering_p;
single_mie_scattering = single_mie_scattering - shadow_transmittance * single_mie_scattering_p;
single_mie_scattering = GetExtrapolatedSingleMieScattering(
atmosphere, float4(scattering, single_mie_scattering.r));
// Hack to avoid rendering artifacts when the sun is below the horizon.
single_mie_scattering = single_mie_scattering * smoothstep(Number(0.0), Number(0.01), mu_s);
return scattering * RayleighPhaseFunction(nu) + single_mie_scattering * MiePhaseFunction(atmosphere.mie_phase_function_g, nu);
}
/*
<h4 id="rendering_ground">Ground</h4>
<p>To render the ground we need the irradiance received on the ground after 0 or
more bounce(s) in the atmosphere or on the ground. The direct irradiance can be
computed with a lookup in the transmittance texture,
via <code>GetTransmittanceToSun</code>, while the indirect irradiance is given
by a lookup in the precomputed irradiance texture (this texture only contains
the irradiance for horizontal surfaces; we use the approximation defined in our
<a href="https://hal.inria.fr/inria-00288758/en">paper</a> for the other cases).
The function below returns the direct and indirect irradiances separately:
*/
IrradianceSpectrum GetSunAndSkyIrradiance(
IN(AtmosphereParameters) atmosphere,
IN(TransmittanceTexture) transmittance_texture,
IN(IrradianceTexture) irradiance_texture,
IN(Position) position, IN(Direction) normal, IN(Direction) sun_direction,
OUT(IrradianceSpectrum) sky_irradiance)
{
Length r = length(position);
Number mu_s = dot(position, sun_direction) / r;
// Indirect irradiance (approximated if the surface is not horizontal).
sky_irradiance = GetIrradiance(atmosphere, irradiance_texture, r, mu_s) * (1.0 + dot(normal, position) / r) * 0.5;
// Direct irradiance.
return atmosphere.solar_irradiance * GetTransmittanceToSun(atmosphere, transmittance_texture, r, mu_s) * max(dot(normal, sun_direction), 0.0);
}