1844 lines
84 KiB
HLSL
1844 lines
84 KiB
HLSL
/**
|
||
* 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);
|
||
} |