diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21b61f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +output/** diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..33ea466 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "external/dimensional_types"] + path = external/dimensional_types + url = https://github.com/ebruneton/dimensional_types +[submodule "external/progress_bar"] + path = external/progress_bar + url = https://github.com/ebruneton/progress_bar +[submodule "external/minpng"] + path = external/minpng + url = https://github.com/jrmuizel/minpng diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bfad612 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +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 holder 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 HOLDER 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..607adb9 --- /dev/null +++ b/Makefile @@ -0,0 +1,125 @@ +# 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 holder 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 HOLDER 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. + +GPP := g++ +GPP_FLAGS := -Wall -Wmain -pedantic -pedantic-errors -std=c++11 +INCLUDE_FLAGS := \ + -I. -Iexternal -Iexternal/dimensional_types -Iexternal/progress_bar +DEBUG_FLAGS := -g +RELEASE_FLAGS := -DNDEBUG -O3 -fexpensive-optimizations + +DIRS := atmosphere tools +HEADERS := $(shell find $(DIRS) -name "*.h") +SOURCES := $(shell find $(DIRS) -name "*.cc") +GLSL_SOURCES := $(shell find $(DIRS) -name "*.glsl") +DOC_SOURCES := $(HEADERS) $(SOURCES) $(GLSL_SOURCES) index + +all: lint doc test integration_test demo + +# cpplint can be installed with "pip install cpplint". +# We exclude runtime/references checking for functions.h and model_test.cc +# because we can't avoid using non-const references in these files, due to the +# constraints of double C++/GLSL compilation of functions.glsl. +# We also exclude build/c++11 checking for docgen_main.cc to allow the use of +# . +lint: $(HEADERS) $(SOURCES) + cpplint --exclude=tools/docgen_main.cc \ + --exclude=atmosphere/reference/functions.h \ + --exclude=atmosphere/reference/model_test.cc --root=$(PWD) $^ + cpplint --filter=-runtime/references --root=$(PWD) \ + atmosphere/reference/functions.h \ + atmosphere/reference/model_test.cc + cpplint --filter=-build/c++11 --root=$(PWD) tools/docgen_main.cc + +doc: $(DOC_SOURCES:%=output/Doc/%.html) + +test: output/Debug/atmosphere_test + output/Debug/atmosphere_test + +integration_test: output/Release/atmosphere_integration_test + mkdir -p output/Doc/atmosphere/reference + output/Release/atmosphere_integration_test + +demo: output/Debug/atmosphere_demo + output/Debug/atmosphere_demo + +clean: + rm -f $(GLSL_SOURCES:%=%.inc) + rm -rf output/Debug output/Release output/Doc + +output/Doc/%.html: % output/Debug/tools/docgen tools/docgen_template.html + mkdir -p $(@D) + output/Debug/tools/docgen $< tools/docgen_template.html $@ + +output/Debug/tools/docgen: output/Debug/tools/docgen_main.o + $(GPP) $< -o $@ + +output/Debug/atmosphere_test: \ + output/Debug/atmosphere/reference/functions.o \ + output/Debug/atmosphere/reference/functions_test.o \ + output/Debug/external/dimensional_types/test/test_main.o + $(GPP) $^ -o $@ + +output/Release/atmosphere_integration_test: \ + output/Release/atmosphere/model.o \ + output/Release/atmosphere/reference/functions.o \ + output/Release/atmosphere/reference/model.o \ + output/Release/atmosphere/reference/model_test.o \ + output/Release/external/dimensional_types/test/test_main.o \ + output/Release/external/progress_bar/util/progress_bar.o + $(GPP) $^ -pthread -lGLEW -lglut -lGL -o $@ + +output/Debug/atmosphere_demo: \ + output/Debug/atmosphere/demo/demo.o \ + output/Debug/atmosphere/demo/demo_main.o \ + output/Debug/atmosphere/model.o + $(GPP) $^ -pthread -lGLEW -lglut -lGL -o $@ + +output/Debug/%.o: %.cc + mkdir -p $(@D) + $(GPP) $(GPP_FLAGS) $(INCLUDE_FLAGS) $(DEBUG_FLAGS) -c $< -o $@ + +output/Release/%.o: %.cc + mkdir -p $(@D) + $(GPP) $(GPP_FLAGS) $(INCLUDE_FLAGS) $(RELEASE_FLAGS) -c $< -o $@ + +output/Debug/atmosphere/model.o output/Release/atmosphere/model.o: \ + atmosphere/definitions.glsl.inc \ + atmosphere/functions.glsl.inc + +output/Debug/atmosphere/reference/model_test.o \ +output/Release/atmosphere/reference/model_test.o: \ + atmosphere/definitions.glsl.inc \ + atmosphere/reference/model_test.glsl.inc + +output/Debug/atmosphere/demo/demo.o output/Release/atmosphere/demo/demo.o: \ + atmosphere/demo/demo.glsl.inc + +%.glsl.inc: %.glsl + sed -e '1i std::string $(*F)_glsl = R"***(' -e '$$a )***";' $< > $@ + diff --git a/README.md b/README.md index 7db14fe..ac0eb7e 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# precomputed_atmospheric_scattering \ No newline at end of file +## Synopsis + +This project provides a new implementation of our [Precomputed Atmospheric +Scattering](https://hal.inria.fr/inria-00288758/en) paper. This new +implementation uses more descriptive function and variable names, adds +extensive comments and documentation, unit tests, and more. + +## License + +This project is released under the BSD license. diff --git a/atmosphere/constants.h b/atmosphere/constants.h new file mode 100644 index 0000000..7070d52 --- /dev/null +++ b/atmosphere/constants.h @@ -0,0 +1,177 @@ +/** + * 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. + */ + +/*

atmosphere/constants.h

+ +

This file defines the size of the precomputed texures used in our atmosphere +model. It also provides tabulated values of the CIE color matching functions and the conversion matrix from the XYZ to the +sRGB color spaces (which are +needed to convert the spectral radiance samples computed by our algorithm to +sRGB luminance values). +*/ + +#ifndef ATMOSPHERE_CONSTANTS_H_ +#define ATMOSPHERE_CONSTANTS_H_ + +namespace atmosphere { + +constexpr int TRANSMITTANCE_TEXTURE_WIDTH = 256; +constexpr int TRANSMITTANCE_TEXTURE_HEIGHT = 64; + +constexpr int SCATTERING_TEXTURE_R_SIZE = 32; +constexpr int SCATTERING_TEXTURE_MU_SIZE = 128; +constexpr int SCATTERING_TEXTURE_MU_S_SIZE = 32; +constexpr int SCATTERING_TEXTURE_NU_SIZE = 8; + +constexpr int SCATTERING_TEXTURE_WIDTH = + SCATTERING_TEXTURE_NU_SIZE * SCATTERING_TEXTURE_MU_S_SIZE; +constexpr int SCATTERING_TEXTURE_HEIGHT = SCATTERING_TEXTURE_MU_SIZE; +constexpr int SCATTERING_TEXTURE_DEPTH = SCATTERING_TEXTURE_R_SIZE; + +constexpr int IRRADIANCE_TEXTURE_WIDTH = 64; +constexpr int IRRADIANCE_TEXTURE_HEIGHT = 16; + +// The conversion factor between watts and lumens. +constexpr double MAX_LUMINOUS_EFFICACY = 683.0; + +// Values from "CIE (1931) 2-deg color matching functions", see +// "http://web.archive.org/web/20081228084047/ +// http://www.cvrl.org/database/data/cmfs/ciexyz31.txt". +constexpr double CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[380] = { + 360, 0.000129900000, 0.000003917000, 0.000606100000, + 365, 0.000232100000, 0.000006965000, 0.001086000000, + 370, 0.000414900000, 0.000012390000, 0.001946000000, + 375, 0.000741600000, 0.000022020000, 0.003486000000, + 380, 0.001368000000, 0.000039000000, 0.006450001000, + 385, 0.002236000000, 0.000064000000, 0.010549990000, + 390, 0.004243000000, 0.000120000000, 0.020050010000, + 395, 0.007650000000, 0.000217000000, 0.036210000000, + 400, 0.014310000000, 0.000396000000, 0.067850010000, + 405, 0.023190000000, 0.000640000000, 0.110200000000, + 410, 0.043510000000, 0.001210000000, 0.207400000000, + 415, 0.077630000000, 0.002180000000, 0.371300000000, + 420, 0.134380000000, 0.004000000000, 0.645600000000, + 425, 0.214770000000, 0.007300000000, 1.039050100000, + 430, 0.283900000000, 0.011600000000, 1.385600000000, + 435, 0.328500000000, 0.016840000000, 1.622960000000, + 440, 0.348280000000, 0.023000000000, 1.747060000000, + 445, 0.348060000000, 0.029800000000, 1.782600000000, + 450, 0.336200000000, 0.038000000000, 1.772110000000, + 455, 0.318700000000, 0.048000000000, 1.744100000000, + 460, 0.290800000000, 0.060000000000, 1.669200000000, + 465, 0.251100000000, 0.073900000000, 1.528100000000, + 470, 0.195360000000, 0.090980000000, 1.287640000000, + 475, 0.142100000000, 0.112600000000, 1.041900000000, + 480, 0.095640000000, 0.139020000000, 0.812950100000, + 485, 0.057950010000, 0.169300000000, 0.616200000000, + 490, 0.032010000000, 0.208020000000, 0.465180000000, + 495, 0.014700000000, 0.258600000000, 0.353300000000, + 500, 0.004900000000, 0.323000000000, 0.272000000000, + 505, 0.002400000000, 0.407300000000, 0.212300000000, + 510, 0.009300000000, 0.503000000000, 0.158200000000, + 515, 0.029100000000, 0.608200000000, 0.111700000000, + 520, 0.063270000000, 0.710000000000, 0.078249990000, + 525, 0.109600000000, 0.793200000000, 0.057250010000, + 530, 0.165500000000, 0.862000000000, 0.042160000000, + 535, 0.225749900000, 0.914850100000, 0.029840000000, + 540, 0.290400000000, 0.954000000000, 0.020300000000, + 545, 0.359700000000, 0.980300000000, 0.013400000000, + 550, 0.433449900000, 0.994950100000, 0.008749999000, + 555, 0.512050100000, 1.000000000000, 0.005749999000, + 560, 0.594500000000, 0.995000000000, 0.003900000000, + 565, 0.678400000000, 0.978600000000, 0.002749999000, + 570, 0.762100000000, 0.952000000000, 0.002100000000, + 575, 0.842500000000, 0.915400000000, 0.001800000000, + 580, 0.916300000000, 0.870000000000, 0.001650001000, + 585, 0.978600000000, 0.816300000000, 0.001400000000, + 590, 1.026300000000, 0.757000000000, 0.001100000000, + 595, 1.056700000000, 0.694900000000, 0.001000000000, + 600, 1.062200000000, 0.631000000000, 0.000800000000, + 605, 1.045600000000, 0.566800000000, 0.000600000000, + 610, 1.002600000000, 0.503000000000, 0.000340000000, + 615, 0.938400000000, 0.441200000000, 0.000240000000, + 620, 0.854449900000, 0.381000000000, 0.000190000000, + 625, 0.751400000000, 0.321000000000, 0.000100000000, + 630, 0.642400000000, 0.265000000000, 0.000049999990, + 635, 0.541900000000, 0.217000000000, 0.000030000000, + 640, 0.447900000000, 0.175000000000, 0.000020000000, + 645, 0.360800000000, 0.138200000000, 0.000010000000, + 650, 0.283500000000, 0.107000000000, 0.000000000000, + 655, 0.218700000000, 0.081600000000, 0.000000000000, + 660, 0.164900000000, 0.061000000000, 0.000000000000, + 665, 0.121200000000, 0.044580000000, 0.000000000000, + 670, 0.087400000000, 0.032000000000, 0.000000000000, + 675, 0.063600000000, 0.023200000000, 0.000000000000, + 680, 0.046770000000, 0.017000000000, 0.000000000000, + 685, 0.032900000000, 0.011920000000, 0.000000000000, + 690, 0.022700000000, 0.008210000000, 0.000000000000, + 695, 0.015840000000, 0.005723000000, 0.000000000000, + 700, 0.011359160000, 0.004102000000, 0.000000000000, + 705, 0.008110916000, 0.002929000000, 0.000000000000, + 710, 0.005790346000, 0.002091000000, 0.000000000000, + 715, 0.004109457000, 0.001484000000, 0.000000000000, + 720, 0.002899327000, 0.001047000000, 0.000000000000, + 725, 0.002049190000, 0.000740000000, 0.000000000000, + 730, 0.001439971000, 0.000520000000, 0.000000000000, + 735, 0.000999949300, 0.000361100000, 0.000000000000, + 740, 0.000690078600, 0.000249200000, 0.000000000000, + 745, 0.000476021300, 0.000171900000, 0.000000000000, + 750, 0.000332301100, 0.000120000000, 0.000000000000, + 755, 0.000234826100, 0.000084800000, 0.000000000000, + 760, 0.000166150500, 0.000060000000, 0.000000000000, + 765, 0.000117413000, 0.000042400000, 0.000000000000, + 770, 0.000083075270, 0.000030000000, 0.000000000000, + 775, 0.000058706520, 0.000021200000, 0.000000000000, + 780, 0.000041509940, 0.000014990000, 0.000000000000, + 785, 0.000029353260, 0.000010600000, 0.000000000000, + 790, 0.000020673830, 0.000007465700, 0.000000000000, + 795, 0.000014559770, 0.000005257800, 0.000000000000, + 800, 0.000010253980, 0.000003702900, 0.000000000000, + 805, 0.000007221456, 0.000002607800, 0.000000000000, + 810, 0.000005085868, 0.000001836600, 0.000000000000, + 815, 0.000003581652, 0.000001293400, 0.000000000000, + 820, 0.000002522525, 0.000000910930, 0.000000000000, + 825, 0.000001776509, 0.000000641530, 0.000000000000, + 830, 0.000001251141, 0.000000451810, 0.000000000000, +}; + +// The conversion matrix from XYZ to linear sRGB color spaces. +// Values from https://en.wikipedia.org/wiki/SRGB. +constexpr double XYZ_TO_SRGB[9] = { + +3.2406, -1.5372, -0.4986, + -0.9689, +1.8758, +0.0415, + +0.0557, -0.2040, +1.0570 +}; + +} // namespace atmosphere + +#endif // ATMOSPHERE_CONSTANTS_H_ diff --git a/atmosphere/definitions.glsl b/atmosphere/definitions.glsl new file mode 100644 index 0000000..9a892c6 --- /dev/null +++ b/atmosphere/definitions.glsl @@ -0,0 +1,215 @@ +/** + * 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. + */ + +/*

atmosphere/definitions.glsl

+ +

This GLSL file defines the physical types and constants which are used in the +main functions of our atmosphere model, in +such a way that they can be compiled by a GLSL compiler (a +C++ equivalent of this file +provides the same types and constants in C++, to allow the same functions to be +compiled by a C++ compiler - see the Introduction). + +

Physical quantities

+ +

The physical quantities we need for our atmosphere model are +radiometric and +photometric +quantities. In GLSL we can't define custom numeric types to enforce the +homogeneity of expressions at compile time, so we define all the physical +quantities as float, with preprocessor macros (there is no +typedef in GLSL). + +

We start with six base quantities: length, wavelength, angle, solid angle, +power and luminous power (wavelength is also a length, but we distinguish the +two for increased clarity). +*/ + +#define Length float +#define Wavelength float +#define Angle float +#define SolidAngle float +#define Power float +#define LuminousPower float + +/* +

From this we "derive" the irradiance, radiance, spectral irradiance, +spectral radiance, luminance, etc, as well pure numbers, area, volume, etc (the +actual derivation is done in the C++ +equivalent of this file). +*/ + +#define Number float +#define Area float +#define Volume float +#define NumberDensity float +#define Irradiance float +#define Radiance float +#define SpectralPower float +#define SpectralIrradiance float +#define SpectralRadiance float +#define SpectralRadianceDensity float +#define ScatteringCoefficient float +#define InverseSolidAngle float +#define LuminousIntensity float +#define Luminance float +#define Illuminance float + +/* +

We also need vectors of physical quantities, mostly to represent functions +depending on the wavelength. In this case the vector elements correspond to +values of a function at some predefined wavelengths. Again, in GLSL we can't +define custom vector types to enforce the homogeneity of expressions at compile +time, so we define these vector types as vec3, with preprocessor +macros. The full definitions are given in the +C++ equivalent of this file). +*/ + +// A generic function from Wavelength to some other type. +#define AbstractSpectrum vec3 +// A function from Wavelength to Number. +#define DimensionlessSpectrum vec3 +// A function from Wavelength to SpectralPower. +#define PowerSpectrum vec3 +// A function from Wavelength to SpectralIrradiance. +#define IrradianceSpectrum vec3 +// A function from Wavelength to SpectralRadiance. +#define RadianceSpectrum vec3 +// A function from Wavelength to SpectralRadianceDensity. +#define RadianceDensitySpectrum vec3 +// A function from Wavelength to ScaterringCoefficient. +#define ScatteringSpectrum vec3 + +// A position in 3D (3 length values). +#define Position vec3 +// A unit direction vector in 3D (3 unitless values). +#define Direction vec3 +// A vector of 3 luminance values. +#define Luminance3 vec3 +// A vector of 3 illuminance values. +#define Illuminance3 vec3 + +/* +

Finally, we also need precomputed textures containing physical quantities in +each texel. Since we can't define custom sampler types to enforce the +homogeneity of expressions at compile time in GLSL, we define these texture +types as sampler2D and sampler3D, with preprocessor +macros. The full definitions are given in the +C++ equivalent of this file). +*/ + +#define TransmittanceTexture sampler2D +#define AbstractScatteringTexture sampler3D +#define ReducedScatteringTexture sampler3D +#define ScatteringTexture sampler3D +#define ScatteringDensityTexture sampler3D +#define IrradianceTexture sampler2D + +/* +

Physical units

+ +

We can then define the units for our six base physical quantities: +meter (m), nanometer (nm), radian (rad), steradian (sr), watt (watt) and lumen +(lm): +*/ + +const Length m = 1.0; +const Wavelength nm = 1.0; +const Angle rad = 1.0; +const SolidAngle sr = 1.0; +const Power watt = 1.0; +const LuminousPower lm = 1.0; + +/* +

From which we can derive the units for some derived physical quantities, +as well as some derived units (kilometer km, kilocandela kcd, degree deg): +*/ + +const float PI = 3.14159265358979323846; + +const Length km = 1000.0 * m; +const Area m2 = m * m; +const Volume m3 = m * m * m; +const Angle pi = PI * rad; +const Angle deg = pi / 180.0; +const Irradiance watt_per_square_meter = watt / m2; +const Radiance watt_per_square_meter_per_sr = watt / (m2 * sr); +const SpectralIrradiance watt_per_square_meter_per_nm = watt / (m2 * nm); +const SpectralRadiance watt_per_square_meter_per_sr_per_nm = + watt / (m2 * sr * nm); +const SpectralRadianceDensity watt_per_cubic_meter_per_sr_per_nm = + watt / (m3 * sr * nm); +const LuminousIntensity cd = lm / sr; +const LuminousIntensity kcd = 1000.0 * cd; +const Luminance cd_per_square_meter = cd / m2; +const Luminance kcd_per_square_meter = kcd / m2; + +/* +

Atmosphere parameters

+ +

Using the above types, we can now define the parameters of our atmosphere +model: +*/ + +struct AtmosphereParameters { + // The solar irradiance at the top of the atmosphere. + IrradianceSpectrum solar_irradiance; + // The sun's angular radius. + Angle sun_angular_radius; + // The distance between the planet center and the bottom of the atmosphere. + Length bottom_radius; + // The distance between the planet center and the top of the atmosphere. + Length top_radius; + // The scale height of air molecules, meaning that their density is + // proportional to exp(-h / rayleigh_scale_height), with h the altitude + // (with the bottom of the atmosphere at altitude 0). + Length rayleigh_scale_height; + // The scattering coefficient of air molecules at the bottom of the + // atmosphere, as a function of wavelength. + ScatteringSpectrum rayleigh_scattering; + // The scale height of aerosols, meaning that their density is proportional + // to exp(-h / mie_scale_height), with h the altitude. + Length mie_scale_height; + // The scattering coefficient of aerosols at the bottom of the atmosphere, + // as a function of wavelength. + ScatteringSpectrum mie_scattering; + // The extinction coefficient of aerosols at the bottom of the atmosphere, + // as a function of wavelength. + ScatteringSpectrum mie_extinction; + // The asymetry parameter for the Cornette-Shanks phase function for the + // aerosols. + Number mie_phase_function_g; + // The average albedo of the ground. + DimensionlessSpectrum ground_albedo; + // The cosine of the maximum Sun zenith angle for which atmospheric scattering + // must be precomputed (for maximum precision, use the smallest Sun zenith + // angle yielding negligible sky light radiance values. For instance, for the + // Earth case, 102 degrees is a good choice - yielding mu_s_min = -0.2). + Number mu_s_min; +}; diff --git a/atmosphere/demo/demo.cc b/atmosphere/demo/demo.cc new file mode 100644 index 0000000..77641e3 --- /dev/null +++ b/atmosphere/demo/demo.cc @@ -0,0 +1,470 @@ +/** + * 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. + */ + +/*

atmosphere/demo/demo.cc

+ +

This file, together with the shader demo.glsl, +shows how the API provided in model.h can be used +in practice. It implements the Demo class whose header is defined +in demo.h (note that most of the following code is +independent of our atmosphere model. The only part which is related to it is the +InitModel method). +*/ + +#include "atmosphere/demo/demo.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace atmosphere { +namespace demo { + +/* +

Our demo application renders a simple scene made of a purely spherical planet +and a large sphere (of 1km radius) lying on it. This scene is displayed by +rendering a full screen quad, with a fragment shader computing the color of each +pixel by "ray tracing" (which is very simple here because the scene consists of +only two spheres). The vertex shader is thus very simple, and is provided in the +following constant. The fragment shader is more complex, and is defined in the +separate file demo.glsl (which is included here as +a string literal via the generated file demo.glsl.inc): +*/ + +namespace { + +constexpr double kSunAngularRadius = 0.00935 / 2.0; +constexpr double kSunSolidAngle = 2.0 * M_PI * (1.0 - cos(kSunAngularRadius)); +constexpr double kLengthUnitInMeters = 1000.0; + +const char* kVertexShader = R"( + #version 330 + uniform mat4 model_from_view; + uniform mat4 view_from_clip; + layout(location = 0) in vec4 vertex; + out vec3 view_ray; + void main() { + view_ray = + (model_from_view * vec4((view_from_clip * vertex).xyz, 0.0)).xyz; + gl_Position = vertex; + })"; + +#include "atmosphere/demo/demo.glsl.inc" + +static std::map INSTANCES; + +} // anonymous namespace + +/* +

The class constructor is straightforward and completely independent of our +atmosphere model (which is initialized in the separate method +InitModel). It's main role is to create the demo window and to set +up the event handler callbacks (it does so in such a way that several Demo +instances can be created at the same time, using the INSTANCES +global variable): +*/ + +Demo::Demo(int viewport_width, int viewport_height) : + use_constant_solar_spectrum_(false), + use_combined_textures_(true), + use_luminance_(true), + do_white_balance_(false), + show_help_(true), + program_(0), + view_distance_meters_(9000.0), + view_zenith_angle_radians_(1.47), + view_azimuth_angle_radians_(-0.1), + sun_zenith_angle_radians_(1.3), + sun_azimuth_angle_radians_(2.9), + exposure_(10.0) { + glutInitWindowSize(viewport_width, viewport_height); + window_id_ = glutCreateWindow("Atmosphere Demo"); + INSTANCES[window_id_] = this; + glewInit(); + + glutDisplayFunc([]() { + INSTANCES[glutGetWindow()]->HandleRedisplayEvent(); + }); + glutReshapeFunc([](int width, int height) { + INSTANCES[glutGetWindow()]->HandleReshapeEvent(width, height); + }); + glutKeyboardFunc([](unsigned char key, int x, int y) { + INSTANCES[glutGetWindow()]->HandleKeyboardEvent(key); + }); + glutMouseFunc([](int button, int state, int x, int y) { + INSTANCES[glutGetWindow()]->HandleMouseClickEvent(button, state, x, y); + }); + glutMotionFunc([](int x, int y) { + INSTANCES[glutGetWindow()]->HandleMouseDragEvent(x, y); + }); + glutMouseWheelFunc([](int button, int dir, int x, int y) { + INSTANCES[glutGetWindow()]->HandleMouseWheelEvent(dir); + }); + + InitModel(); +} + +/* +

The destructor is even simpler: +*/ + +Demo::~Demo() { + glDeleteProgram(program_); + INSTANCES.erase(window_id_); +} + +/* +

The "real" initialization work, which is specific to our atmosphere model, +is done in the following method. It starts with the creation of an atmosphere +Model instance, with parameters corresponding the to Earth +atmosphere: +*/ + +void Demo::InitModel() { + // Values from "Reference Solar Spectral Irradiance: ASTM G-173", ETR column + // (see http://rredc.nrel.gov/solar/spectra/am1.5/ASTMG173/ASTMG173.html), + // summed and averaged in each bin (e.g. the value for 360nm is the average + // of the ASTM G-173 values for all wavelengths between 360 and 370nm). + constexpr int kLambdaMin = 360; + constexpr int kLambdaMax = 830; + constexpr double kSolarIrradiance[48] = { + 1.11776, 1.14259, 1.01249, 1.14716, 1.72765, 1.73054, 1.6887, 1.61253, + 1.91198, 2.03474, 2.02042, 2.02212, 1.93377, 1.95809, 1.91686, 1.8298, + 1.8685, 1.8931, 1.85149, 1.8504, 1.8341, 1.8345, 1.8147, 1.78158, 1.7533, + 1.6965, 1.68194, 1.64654, 1.6048, 1.52143, 1.55622, 1.5113, 1.474, 1.4482, + 1.41018, 1.36775, 1.34188, 1.31429, 1.28303, 1.26758, 1.2367, 1.2082, + 1.18737, 1.14683, 1.12362, 1.1058, 1.07124, 1.04992 + }; + // Wavelength independent solar irradiance "spectrum" (not physically + // realistic, but was used in the original implementation). + constexpr double kConstantSolarIrradiance = 1.5; + constexpr double kBottomRadius = 6360000.0; + constexpr double kTopRadius = 6420000.0; + constexpr double kRayleigh = 1.24062e-6; + constexpr double kRayleighScaleHeight = 8000.0; + constexpr double kMieScaleHeight = 1200.0; + constexpr double kMieAngstromAlpha = 0.0; + constexpr double kMieAngstromBeta = 5.328e-3; + constexpr double kMieSingleScatteringAlbedo = 0.9; + constexpr double kMiePhaseFunctionG = 0.8; + constexpr double kGroundAlbedo = 0.1; + constexpr double kMaxSunZenithAngle = 102.0 / 180.0 * M_PI; + + std::vector wavelengths; + std::vector solar_irradiance; + std::vector rayleigh_scattering; + std::vector mie_scattering; + std::vector mie_extinction; + std::vector ground_albedo; + for (int l = kLambdaMin; l <= kLambdaMax; l += 10) { + double lambda = static_cast(l) * 1e-3; // micro-meters + double mie = + kMieAngstromBeta / kMieScaleHeight * pow(lambda, -kMieAngstromAlpha); + wavelengths.push_back(l); + if (use_constant_solar_spectrum_) { + solar_irradiance.push_back(kConstantSolarIrradiance); + } else { + solar_irradiance.push_back(kSolarIrradiance[(l - kLambdaMin) / 10]); + } + rayleigh_scattering.push_back(kRayleigh * pow(lambda, -4)); + mie_scattering.push_back(mie * kMieSingleScatteringAlbedo); + mie_extinction.push_back(mie); + ground_albedo.push_back(kGroundAlbedo); + } + + model_.reset(new Model(wavelengths, solar_irradiance, kSunAngularRadius, + kBottomRadius, kTopRadius, kRayleighScaleHeight, rayleigh_scattering, + kMieScaleHeight, mie_scattering, mie_extinction, kMiePhaseFunctionG, + ground_albedo, kMaxSunZenithAngle, kLengthUnitInMeters, + use_combined_textures_)); + model_->Init(); + +/* +

Then, it creates and compiles the vertex and fragment shaders used to render +our demo scene, and link them with the Model's atmosphere shader +to get the final scene rendering program: +*/ + + GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex_shader, 1, &kVertexShader, NULL); + glCompileShader(vertex_shader); + + const std::string fragment_shader_str = + "#version 330\n" + + std::string(use_luminance_ ? "#define USE_LUMINANCE\n" : "") + + demo_glsl; + const char* fragment_shader_source = fragment_shader_str.c_str(); + GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL); + glCompileShader(fragment_shader); + + if (program_ != 0) { + glDeleteProgram(program_); + } + program_ = glCreateProgram(); + glAttachShader(program_, vertex_shader); + glAttachShader(program_, fragment_shader); + glAttachShader(program_, model_->GetShader()); + glLinkProgram(program_); + glDetachShader(program_, vertex_shader); + glDetachShader(program_, fragment_shader); + glDetachShader(program_, model_->GetShader()); + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + +/* +

Finally, it sets the uniforms of this program that can be set once and for +all (in our case this includes the Model's texture uniforms, +because our demo app does not have any texture of its own): +*/ + + glUseProgram(program_); + model_->SetProgramUniforms(program_, 0, 1, 2, 3); + double white_point_r = 1.0; + double white_point_g = 1.0; + double white_point_b = 1.0; + if (do_white_balance_) { + Model::ConvertSpectrumToLinearSrgb(wavelengths, solar_irradiance, + &white_point_r, &white_point_g, &white_point_b); + double white_point = (white_point_r + white_point_g + white_point_b) / 3.0; + white_point_r /= white_point; + white_point_g /= white_point; + white_point_b /= white_point; + } + glUniform3f(glGetUniformLocation(program_, "white_point"), + white_point_r, white_point_g, white_point_b); + glUniform3f(glGetUniformLocation(program_, "earth_center"), + 0.0, 0.0, -kBottomRadius / kLengthUnitInMeters); + glUniform3f(glGetUniformLocation(program_, "sun_radiance"), + kSolarIrradiance[0] / kSunSolidAngle, + kSolarIrradiance[1] / kSunSolidAngle, + kSolarIrradiance[2] / kSunSolidAngle); + glUniform2f(glGetUniformLocation(program_, "sun_size"), + tan(kSunAngularRadius), + cos(kSunAngularRadius)); + + // This sets 'view_from_clip', which only depends on the window size. + HandleReshapeEvent(glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)); +} + +/* +

The scene rendering method simply sets the uniforms related to the camera +position and to the Sun direction, and then draws a full screen quad (and +optionally a help screen). +*/ + +void Demo::HandleRedisplayEvent() const { + // Unit vectors of the camera frame, expressed in world space. + float cos_z = cos(view_zenith_angle_radians_); + float sin_z = sin(view_zenith_angle_radians_); + float cos_a = cos(view_azimuth_angle_radians_); + float sin_a = sin(view_azimuth_angle_radians_); + float ux[3] = { -sin_a, cos_a, 0.0 }; + float uy[3] = { -cos_z * cos_a, -cos_z * sin_a, sin_z }; + float uz[3] = { sin_z * cos_a, sin_z * sin_a, cos_z }; + float l = view_distance_meters_ / kLengthUnitInMeters; + + // Transform matrix from camera frame to world space. + float model_from_view[16] = { + ux[0], uy[0], uz[0], uz[0] * l, + ux[1], uy[1], uz[1], uz[1] * l, + ux[2], uy[2], uz[2], uz[2] * l, + 0.0, 0.0, 0.0, 1.0 + }; + + glUniform3f(glGetUniformLocation(program_, "camera"), + model_from_view[3], + model_from_view[7], + model_from_view[11]); + glUniform1f(glGetUniformLocation(program_, "exposure"), + use_luminance_ ? exposure_ * 1e-5 : exposure_); + glUniformMatrix4fv(glGetUniformLocation(program_, "model_from_view"), + 1, true, model_from_view); + glUniform3f(glGetUniformLocation(program_, "sun_direction"), + cos(sun_azimuth_angle_radians_) * sin(sun_zenith_angle_radians_), + sin(sun_azimuth_angle_radians_) * sin(sun_zenith_angle_radians_), + cos(sun_zenith_angle_radians_)); + + glBegin(GL_TRIANGLE_STRIP); + glVertex4f(-1.0, -1.0, 0.0, 1.0); + glVertex4f(+1.0, -1.0, 0.0, 1.0); + glVertex4f(-1.0, +1.0, 0.0, 1.0); + glVertex4f(+1.0, +1.0, 0.0, 1.0); + glEnd(); + + if (show_help_) { + std::stringstream help; + help << "\nMouse:\n" + << " drag, CTRL+drag, wheel: view and sun directions\n" + << "Keys:\n" + << " h: help\n" + << " s: solar spectrum (currently: " + << (use_constant_solar_spectrum_ ? "constant" : "realistic") << ")\n" + << " t: combine textures (currently: " + << (use_combined_textures_ ? "on" : "off") << ")\n" + << " l: use luminance (currently: " + << (use_luminance_ ? "on" : "off") << ")\n" + << " w: white balance (currently: " + << (do_white_balance_ ? "on" : "off") << ")\n" + << " +/-: increase/decrease exposure (" << exposure_ << ")\n" + << " 1-9: predefined views\n"; + glUseProgram(0); + glColor3f(1.0, 0.0, 0.0); + glRasterPos2f(-0.99, 1.0); + glutBitmapString(GLUT_BITMAP_9_BY_15, + (const unsigned char*) help.str().c_str()); + glUseProgram(program_); + } + + glutSwapBuffers(); + glutPostRedisplay(); +} + +/* +

The other event handling methods are also straightforward, and do not +interact with the atmosphere model: +*/ + +void Demo::HandleReshapeEvent(int viewport_width, int viewport_height) { + glViewport(0, 0, viewport_width, viewport_height); + + constexpr float kFovY = 50.0 / 180.0 * M_PI; + constexpr float kTanFovY = tan(kFovY / 2.0); + float aspect_ratio = static_cast(viewport_width) / viewport_height; + + // Transform matrix from clip space to camera space. + float view_from_clip[16] = { + kTanFovY * aspect_ratio, 0.0, 0.0, 0.0, + 0.0, kTanFovY, 0.0, 0.0, + 0.0, 0.0, 0.0, -1.0, + 0.0, 0.0, 1.0, 1.0 + }; + glUniformMatrix4fv(glGetUniformLocation(program_, "view_from_clip"), 1, true, + view_from_clip); +} + +void Demo::HandleKeyboardEvent(unsigned char key) { + if (key == 27) { + glutDestroyWindow(window_id_); + } else if (key == 'h') { + show_help_ = !show_help_; + } else if (key == 's') { + use_constant_solar_spectrum_ = !use_constant_solar_spectrum_; + } else if (key == 't') { + use_combined_textures_ = !use_combined_textures_; + } else if (key == 'l') { + use_luminance_ = !use_luminance_; + } else if (key == 'w') { + do_white_balance_ = !do_white_balance_; + } else if (key == '+') { + exposure_ *= 1.1; + } else if (key == '-') { + exposure_ /= 1.1; + } else if (key == '1') { + SetView(9000.0, 1.47, 0.0, 1.3, 3.0, 10.0); + } else if (key == '2') { + SetView(9000.0, 1.47, 0.0, 1.564, -3.0, 10.0); + } else if (key == '3') { + SetView(7000.0, 1.57, 0.0, 1.54, -2.96, 10.0); + } else if (key == '4') { + SetView(7000.0, 1.57, 0.0, 1.328, -3.044, 10.0); + } else if (key == '5') { + SetView(9000.0, 1.39, 0.0, 1.2, 0.7, 10.0); + } else if (key == '6') { + SetView(9000.0, 1.5, 0.0, 1.628, 1.05, 200.0); + } else if (key == '7') { + SetView(7000.0, 1.43, 0.0, 1.57, 1.34, 40.0); + } else if (key == '8') { + SetView(2.7e6, 0.81, 0.0, 1.57, 2.0, 10.0); + } else if (key == '9') { + SetView(1.2e7, 0.0, 0.0, 0.93, -2.0, 10.0); + } + if (key == 's' || key == 't' || key == 'l' || key == 'w') { + InitModel(); + } +} + +void Demo::HandleMouseClickEvent( + int button, int state, int mouse_x, int mouse_y) { + previous_mouse_x_ = mouse_x; + previous_mouse_y_ = mouse_y; + is_ctrl_key_pressed_ = (glutGetModifiers() & GLUT_ACTIVE_CTRL) != 0; + + if ((button == 3) || (button == 4)) { + if (state == GLUT_DOWN) { + HandleMouseWheelEvent(button == 3 ? 1 : -1); + } + } +} + +void Demo::HandleMouseDragEvent(int mouse_x, int mouse_y) { + constexpr double kScale = 500.0; + if (is_ctrl_key_pressed_) { + sun_zenith_angle_radians_ -= (previous_mouse_y_ - mouse_y) / kScale; + sun_zenith_angle_radians_ = + std::max(0.0, std::min(M_PI, sun_zenith_angle_radians_)); + sun_azimuth_angle_radians_ += (previous_mouse_x_ - mouse_x) / kScale; + } else { + view_zenith_angle_radians_ += (previous_mouse_y_ - mouse_y) / kScale; + view_zenith_angle_radians_ = + std::max(0.0, std::min(M_PI / 2.0, view_zenith_angle_radians_)); + view_azimuth_angle_radians_ += (previous_mouse_x_ - mouse_x) / kScale; + } + previous_mouse_x_ = mouse_x; + previous_mouse_y_ = mouse_y; +} + +void Demo::HandleMouseWheelEvent(int mouse_wheel_direction) { + if (mouse_wheel_direction < 0) { + view_distance_meters_ *= 1.05; + } else { + view_distance_meters_ /= 1.05; + } +} + +void Demo::SetView(double view_distance_meters, + double view_zenith_angle_radians, double view_azimuth_angle_radians, + double sun_zenith_angle_radians, double sun_azimuth_angle_radians, + double exposure) { + view_distance_meters_ = view_distance_meters; + view_zenith_angle_radians_ = view_zenith_angle_radians; + view_azimuth_angle_radians_ = view_azimuth_angle_radians; + sun_zenith_angle_radians_ = sun_zenith_angle_radians; + sun_azimuth_angle_radians_ = sun_azimuth_angle_radians; + exposure_ = exposure; +} + +} // namespace demo +} // namespace atmosphere diff --git a/atmosphere/demo/demo.glsl b/atmosphere/demo/demo.glsl new file mode 100644 index 0000000..ffa3c48 --- /dev/null +++ b/atmosphere/demo/demo.glsl @@ -0,0 +1,385 @@ +/** + * 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. + */ + +/*

atmosphere/demo/demo.glsl

+ +

This GLSL fragment shader is used to render our demo scene, which consists of +a sphere S on a purely spherical planet P. It is rendered by "ray tracing", i.e. +the vertex shader outputs the view ray direction, and the fragment shader +computes the intersection of this ray with the spheres S and P to produce the +final pixels. The fragment shader also computes the intersection of the light +rays with the sphere S, to compute shadows, as well as the intersections of the +view ray with the shadow volume of S, in order to compute light shafts. + +

Our fragment shader has the following inputs and outputs: +*/ + +uniform vec3 camera; +uniform float exposure; +uniform vec3 white_point; +uniform vec3 earth_center; +uniform vec3 sun_direction; +uniform vec3 sun_radiance; +uniform vec2 sun_size; +in vec3 view_ray; +layout(location = 0) out vec3 color; + +/* +

It uses the following constants, as well as the following atmosphere +rendering functions, defined externally (by the Model's +GetShader() shader). The USE_LUMINANCE option is used +to select either the functions returning radiance values, or those returning +luminance values (see model.h). +*/ + +const float PI = 3.14159265; +const vec3 kSphereCenter = vec3(0.0, 0.0, 1.0); +const float kSphereRadius = 1.0; +const vec3 kSphereAlbedo = vec3(0.8); +const vec3 kGroundAlbedo = vec3(0.0, 0.0, 0.04); + +#ifdef USE_LUMINANCE +#define GetSkyRadiance GetSkyLuminance +#define GetSkyRadianceToPoint GetSkyLuminanceToPoint +#define GetSunAndSkyIrradiance GetSunAndSkyIlluminance +#endif + +vec3 GetSkyRadiance(vec3 camera, vec3 view_ray, float shadow_length, + vec3 sun_direction, out vec3 transmittance); +vec3 GetSkyRadianceToPoint(vec3 camera, vec3 point, float shadow_length, + vec3 sun_direction, out vec3 transmittance); +vec3 GetSunAndSkyIrradiance( + vec3 p, vec3 normal, vec3 sun_direction, out vec3 sky_irradiance); + +/*

Shadows and light shafts

+ +

The functions to compute shadows and light shafts must be defined before we +can use them in the main shader function, so we define them first. Testing if +a point is in the shadow of the sphere S is equivalent to test if the +corresponding light ray intersects the sphere, which is very simple to do. +However, this is only valid for a punctual light source, which is not the case +of the Sun. In the following function we compute an approximate (and biased) +soft shadow by taking the angular size of the Sun into account: +*/ + +float GetSunVisibility(vec3 point, vec3 sun_direction) { + vec3 p = point - kSphereCenter; + float p_dot_v = dot(p, sun_direction); + float p_dot_p = dot(p, p); + float ray_sphere_center_squared_distance = p_dot_p - p_dot_v * p_dot_v; + float distance_to_intersection = -p_dot_v - sqrt( + kSphereRadius * kSphereRadius - ray_sphere_center_squared_distance); + if (distance_to_intersection > 0.0) { + // Compute the distance between the view ray and the sphere, and the + // corresponding (tangent of the) subtended angle. Finally, use this to + // compute an approximate sun visibility. + float ray_sphere_distance = + kSphereRadius - sqrt(ray_sphere_center_squared_distance); + float ray_sphere_angular_distance = -ray_sphere_distance / p_dot_v; + return smoothstep(1.0, 0.0, ray_sphere_angular_distance / sun_size.x); + } + return 1.0; +} + +/* +

The sphere also partially occludes the sky light, and we approximate this +effect with an ambient occlusion factor. The ambient occlusion factor due to a +sphere is given in Radiation View Factors (Isidoro Martinez, 1995). In the simple case where +the sphere is fully visible, it is given by the following function: +*/ + +float GetSkyVisibility(vec3 point) { + vec3 p = point - kSphereCenter; + float p_dot_p = dot(p, p); + return + 1.0 + p.z / sqrt(p_dot_p) * kSphereRadius * kSphereRadius / p_dot_p; +} + +/* +

To compute light shafts we need the intersections of the view ray with the +shadow volume of the sphere S. Since the Sun is not a punctual light source this +shadow volume is not a cylinder but a cone (for the umbra, plus another cone for +the penumbra, but we ignore it here): + + + + + + + + + + + + + + + + + + + + + + p + q + s + v + R + r + ρ + d + δ + α + + +

Noting, as in the above figure, $\bp$ the camera position, $\bv$ and $\bs$ +the unit view ray and sun direction vectors and $R$ the sphere radius (supposed +to be centered on the origin), the point at distance $d$ from the camera is +$\bq=\bp+d\bv$. This point is at a distance $\delta=-\bq\cdot\bs$ from the +sphere center along the umbra cone axis, and at a distance $r$ from this axis +given by $r^2=\bq\cdot\bq-\delta^2$. Finally, at distance $\delta$ along the +axis the umbra cone has radius $\rho=R-\delta\tan\alpha$, where $\alpha$ is +the Sun's angular radius. The point at distance $d$ from the camera is on the +shadow cone only if $r^2=\rho^2$, i.e. only if +\begin{equation} +(\bp+d\bv)\cdot(\bp+d\bv)-((\bp+d\bv)\cdot\bs)^2= +(R+((\bp+d\bv)\cdot\bs)\tan\alpha)^2 +\end{equation} +Developping this gives a quadratic equation for $d$: +\begin{equation} +ad^2+2bd+c=0 +\end{equation} +where +

+From this we deduce the two possible solutions for $d$, which must be clamped to +the actual shadow part of the mathematical cone (i.e. the slab between the +sphere center and the cone apex or, in other words, the points for which +$\delta$ is between $0$ and $R/\tan\alpha$). The following function implements +these equations: +*/ + +void GetSphereShadowInOut(vec3 view_direction, vec3 sun_direction, + out float d_in, out float d_out) { + vec3 pos = camera - kSphereCenter; + float pos_dot_sun = dot(pos, sun_direction); + float view_dot_sun = dot(view_direction, sun_direction); + float k = sun_size.x; + float l = 1.0 + k * k; + float a = 1.0 - l * view_dot_sun * view_dot_sun; + float b = dot(pos, view_direction) - l * pos_dot_sun * view_dot_sun - + k * kSphereRadius * view_dot_sun; + float c = dot(pos, pos) - l * pos_dot_sun * pos_dot_sun - + 2.0 * k * kSphereRadius * pos_dot_sun - kSphereRadius * kSphereRadius; + float discriminant = b * b - a * c; + if (discriminant > 0.0) { + d_in = max(0.0, (-b - sqrt(discriminant)) / a); + d_out = (-b + sqrt(discriminant)) / a; + // The values of d for which delta is equal to 0 and kSphereRadius / k. + float d_base = -pos_dot_sun / view_dot_sun; + float d_apex = -(pos_dot_sun + kSphereRadius / k) / view_dot_sun; + if (view_dot_sun > 0.0) { + d_in = max(d_in, d_apex); + d_out = a > 0.0 ? min(d_out, d_base) : d_base; + } else { + d_in = a > 0.0 ? max(d_in, d_base) : d_base; + d_out = min(d_out, d_apex); + } + } else { + d_in = 0.0; + d_out = 0.0; + } +} + +/*

Main shading function

+ +

Using these functions we can now implement the main shader function, which +computes the radiance from the scene for a given view ray. This function first +tests if the view ray intersects the sphere S. If so it computes the sun and +sky light received by the sphere at the intersection point, combines this with +the sphere BRDF and the aerial perspective between the camera and the sphere. +It then does the same with the ground, i.e. with the planet sphere P, and then +computes the sky radiance and transmittance. Finally, all these terms are +composited together (an opacity is also computed for each object, using an +approximate view cone - sphere intersection factor) to get the final radiance. + +

We start with the computation of the intersections of the view ray with the +shadow volume of the sphere, because they are needed to get the aerial +perspective for the sphere and the planet: +*/ + +void main() { + // Normalized view direction vector. + vec3 view_direction = normalize(view_ray); + // Tangent of the angle subtended by this fragment. + float fragment_angular_size = + length(dFdx(view_ray) + dFdy(view_ray)) / length(view_ray); + + float shadow_in; + float shadow_out; + GetSphereShadowInOut(view_direction, sun_direction, shadow_in, shadow_out); + + // Hack to fade out light shafts when the Sun is very close to the horizon. + float lightshaft_fadein_hack = smoothstep( + 0.02, 0.04, dot(normalize(camera - earth_center), sun_direction)); + +/* +

We then test whether the view ray intersects the sphere S or not. If it does, +we compute an approximate (and biased) opacity value, using the same +approximation as in GetSunVisibility: +*/ + + // Compute the distance between the view ray line and the sphere center, + // and the distance between the camera and the intersection of the view + // ray with the sphere (or NaN if there is no intersection). + vec3 p = camera - kSphereCenter; + float p_dot_v = dot(p, view_direction); + float p_dot_p = dot(p, p); + float ray_sphere_center_squared_distance = p_dot_p - p_dot_v * p_dot_v; + float distance_to_intersection = -p_dot_v - sqrt( + kSphereRadius * kSphereRadius - ray_sphere_center_squared_distance); + + // Compute the radiance reflected by the sphere, if the ray intersects it. + float sphere_alpha = 0.0; + vec3 sphere_radiance = vec3(0.0); + if (distance_to_intersection > 0.0) { + // Compute the distance between the view ray and the sphere, and the + // corresponding (tangent of the) subtended angle. Finally, use this to + // compute the approximate analytic antialiasing factor sphere_alpha. + float ray_sphere_distance = + kSphereRadius - sqrt(ray_sphere_center_squared_distance); + float ray_sphere_angular_distance = -ray_sphere_distance / p_dot_v; + sphere_alpha = + min(ray_sphere_angular_distance / fragment_angular_size, 1.0); + +/* +

We can then compute the intersection point and its normal, and use them to +get the sun and sky irradiance received at this point. The reflected radiance +follows, by multiplying the irradiance with the sphere BRDF: +*/ + vec3 point = camera + view_direction * distance_to_intersection; + vec3 normal = normalize(point - kSphereCenter); + + // Compute the radiance reflected by the sphere. + vec3 sky_irradiance; + vec3 sun_irradiance = GetSunAndSkyIrradiance( + point - earth_center, normal, sun_direction, sky_irradiance); + sphere_radiance = + kSphereAlbedo * (1.0 / PI) * (sun_irradiance + sky_irradiance); + +/* +

Finally, we take into account the aerial perspective between the camera and +the sphere, which depends on the length of this segment which is in shadow: +*/ + float shadow_length = + max(0.0, min(shadow_out, distance_to_intersection) - shadow_in) * + lightshaft_fadein_hack; + vec3 transmittance; + vec3 in_scatter = GetSkyRadianceToPoint(camera - earth_center, + point - earth_center, shadow_length, sun_direction, transmittance); + sphere_radiance = sphere_radiance * transmittance + in_scatter; + } + +/* +

In the following we repeat the same steps as above, but for the planet sphere +P instead of the sphere S (a smooth opacity is not really needed here, so we +don't compute it. Note also how we modulate the sun and sky irradiance received +on the ground by the sun and sky visibility factors): +*/ + + // Compute the distance between the view ray line and the Earth center, + // and the distance between the camera and the intersection of the view + // ray with the ground (or NaN if there is no intersection). + p = camera - earth_center; + p_dot_v = dot(p, view_direction); + p_dot_p = dot(p, p); + float ray_earth_center_squared_distance = p_dot_p - p_dot_v * p_dot_v; + distance_to_intersection = -p_dot_v - sqrt( + earth_center.z * earth_center.z - ray_earth_center_squared_distance); + + // Compute the radiance reflected by the ground, if the ray intersects it. + float ground_alpha = 0.0; + vec3 ground_radiance = vec3(0.0); + if (distance_to_intersection > 0.0) { + vec3 point = camera + view_direction * distance_to_intersection; + vec3 normal = normalize(point - earth_center); + + // Compute the radiance reflected by the ground. + vec3 sky_irradiance; + vec3 sun_irradiance = GetSunAndSkyIrradiance( + point - earth_center, normal, sun_direction, sky_irradiance); + ground_radiance = kGroundAlbedo * (1.0 / PI) * ( + sun_irradiance * GetSunVisibility(point, sun_direction) + + sky_irradiance * GetSkyVisibility(point)); + + float shadow_length = + max(0.0, min(shadow_out, distance_to_intersection) - shadow_in) * + lightshaft_fadein_hack; + vec3 transmittance; + vec3 in_scatter = GetSkyRadianceToPoint(camera - earth_center, + point - earth_center, shadow_length, sun_direction, transmittance); + ground_radiance = ground_radiance * transmittance + in_scatter; + ground_alpha = 1.0; + } + +/* +

Finally, we compute the radiance and transmittance of the sky, and composite +together, from back to front, the radiance and opacities of all the ojects of +the scene: +*/ + + // Compute the radiance of the sky. + float shadow_length = max(0.0, shadow_out - shadow_in) * + lightshaft_fadein_hack; + vec3 transmittance; + vec3 radiance = GetSkyRadiance( + camera - earth_center, view_direction, shadow_length, sun_direction, + transmittance); + + // If the view ray intersects the Sun, add the Sun radiance. + if (dot(view_direction, sun_direction) > sun_size.y) { + radiance = radiance + transmittance * sun_radiance; + } + radiance = mix(radiance, ground_radiance, ground_alpha); + radiance = mix(radiance, sphere_radiance, sphere_alpha); + color = + pow(vec3(1.0) - exp(-radiance / white_point * exposure), vec3(1.0 / 2.2)); +} diff --git a/atmosphere/demo/demo.h b/atmosphere/demo/demo.h new file mode 100644 index 0000000..cc3a8ae --- /dev/null +++ b/atmosphere/demo/demo.h @@ -0,0 +1,91 @@ +/** + * 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. + */ + +/*

atmosphere/demo/demo.h

+ +

Our demo application consists of a single class, whose header is defined +below. Besides a constructor and an initialization method, this class has one +method per user interface event, and a few fields to store the current rendering +options, the current camera and Sun parameters, as well as references to the +atmosphere model and to the GLSL program used to render the scene: +*/ + +#ifndef ATMOSPHERE_DEMO_DEMO_H_ +#define ATMOSPHERE_DEMO_DEMO_H_ + +#include + +#include "atmosphere/model.h" + +namespace atmosphere { +namespace demo { + +class Demo { + public: + Demo(int viewport_width, int viewport_height); + ~Demo(); + + private: + void InitModel(); + void HandleRedisplayEvent() const; + void HandleReshapeEvent(int viewport_width, int viewport_height); + void HandleKeyboardEvent(unsigned char key); + void HandleMouseClickEvent(int button, int state, int mouse_x, int mouse_y); + void HandleMouseDragEvent(int mouse_x, int mouse_y); + void HandleMouseWheelEvent(int mouse_wheel_direction); + void SetView(double view_distance_meters, double view_zenith_angle_radians, + double view_azimuth_angle_radians, double sun_zenith_angle_radians, + double sun_azimuth_angle_radians, double exposure); + + bool use_constant_solar_spectrum_; + bool use_combined_textures_; + bool use_luminance_; + bool do_white_balance_; + bool show_help_; + + std::unique_ptr model_; + unsigned int program_; + int window_id_; + + double view_distance_meters_; + double view_zenith_angle_radians_; + double view_azimuth_angle_radians_; + double sun_zenith_angle_radians_; + double sun_azimuth_angle_radians_; + double exposure_; + + int previous_mouse_x_; + int previous_mouse_y_; + bool is_ctrl_key_pressed_; +}; + +} // namespace demo +} // namespace atmosphere + +#endif // ATMOSPHERE_DEMO_DEMO_H_ diff --git a/atmosphere/demo/demo_main.cc b/atmosphere/demo/demo_main.cc new file mode 100644 index 0000000..51cf8d8 --- /dev/null +++ b/atmosphere/demo/demo_main.cc @@ -0,0 +1,53 @@ +/** + * 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. + */ + +/*

atmosphere/demo/demo_main.cc

+ +

This very simple file provides the main() function of our demo +application. +*/ + +#include "atmosphere/demo/demo.h" + +#include +#include + +#include + +using atmosphere::demo::Demo; + +int main(int argc, char** argv) { + glutInit(&argc, argv); + glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); + glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_CONTINUE_EXECUTION); + + std::unique_ptr demo(new Demo(1024, 576)); + glutMainLoop(); + return 0; +} diff --git a/atmosphere/functions.glsl b/atmosphere/functions.glsl new file mode 100644 index 0000000..b2cfd68 --- /dev/null +++ b/atmosphere/functions.glsl @@ -0,0 +1,1863 @@ +/** + * 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. + */ + +/*

atmosphere/functions.glsl

+ +

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 +GLSL version and a +C++ version. This allows this file to +be compiled either with a GLSL compiler or with a C++ compiler (see the +Introduction). + +

The functions provided in this file are organized as follows: +

+ +

They use the following utility functions to avoid NaNs due to floating point +values slightly outside their theoretical bounds: +*/ + +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)); +} + +/* +

Transmittance

+ +

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 +transmittance. The +following sections describe how we compute it, how we store it in a precomputed +texture, and how we read it back. + +

Computation

+ +

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): + + + + + + + + + + + + + + + + + + + + + + + + p + q + i + o + r + μ=cos(θ) + x + z + + +

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. + +

Distance to the top atmosphere boundary
+ +

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)); +} + +/* +

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)); +} + +/* +

Intersections with the ground
+ +

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; +} + +/* +

Transmittance to the top atmosphere boundary
+ +

We can now compute the transmittance between $\bp$ and $\bi$. From its +definition and the +Beer-Lambert law, +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 +along this segment. Both 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 trapezoidal rule): +*/ + +Length ComputeOpticalLengthToTopAtmosphereBoundary( + IN(AtmosphereParameters) atmosphere, Length scale_height, + 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 = exp(-(r_i - atmosphere.bottom_radius) / scale_height); + // 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; +} + +/* +

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(-( + atmosphere.rayleigh_scattering * + ComputeOpticalLengthToTopAtmosphereBoundary( + atmosphere, atmosphere.rayleigh_scale_height, r, mu) + + atmosphere.mie_extinction * + ComputeOpticalLengthToTopAtmosphereBoundary( + atmosphere, atmosphere.mie_scale_height, r, mu))); +} + +/* +

Precomputation

+ +

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. + +

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)); +} + +/* +

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 original implementation 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 +paper 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 +paper and the figure +below): + + + + + + + + + + + + + + + + + dmin + p + r + μ=cos(θ) + ρ + d + H + + +

With these definitions, the mapping from $(r,\mu)$ to the texture coordinates +$(u,v)$ can be implemented as follows: +*/ + +vec2 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 vec2(GetTextureCoordFromUnitRange(x_mu, TRANSMITTANCE_TEXTURE_WIDTH), + GetTextureCoordFromUnitRange(x_r, TRANSMITTANCE_TEXTURE_HEIGHT)); +} + +/* +

and the inverse mapping follows immediately: +*/ + +void GetRMuFromTransmittanceTextureUv(IN(AtmosphereParameters) atmosphere, + IN(vec2) 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); +} + +/* +

It is now easy to define a fragment shader function to precompute a texel of +the transmittance texture: +*/ + +DimensionlessSpectrum ComputeTransmittanceToTopAtmosphereBoundaryTexture( + IN(AtmosphereParameters) atmosphere, IN(vec2) gl_frag_coord) { + const vec2 TRANSMITTANCE_TEXTURE_SIZE = + vec2(TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT); + Length r; + Number mu; + GetRMuFromTransmittanceTextureUv( + atmosphere, gl_frag_coord / TRANSMITTANCE_TEXTURE_SIZE, r, mu); + return ComputeTransmittanceToTopAtmosphereBoundary(atmosphere, r, mu); +} + +/* +

Lookup

+ +

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); + vec2 uv = GetTransmittanceTextureUvFromRMu(atmosphere, r, mu); + return DimensionlessSpectrum(texture(transmittance_texture, uv)); +} + +/* +

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( + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r_d, -mu_d) / + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r, -mu), + DimensionlessSpectrum(1.0)); + } else { + return min( + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r, mu) / + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r_d, mu_d), + DimensionlessSpectrum(1.0)); + } +} + +/* +

where ray_r_mu_intersects_ground should be true iif the ray +defined by $r$ and $\mu$ intersects the ground. We don't compute it here with +RayIntersectsGround 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). + +

Single scattering

+ +

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 separately). The following sections describe +how we compute it, how we store it in a precomputed texture, and how we read it +back. + +

Computation

+ +

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): + + + + + + + + + + + + + + + + + + + p + q + ωs + r + rd + d + μ + μs + ν + μs,d + + +

The radiance arriving at $\bp$ is the product of: +

+ +

Thus, by noting $\bw_s$ the unit direction vector towards the Sun, and with +the following definitions: +

+the values of $r$ and $\mu_s$ for $\bq$ are + +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); + + if (RayIntersectsGround(atmosphere, r_d, mu_s_d)) { + rayleigh = DimensionlessSpectrum(0.0); + mie = DimensionlessSpectrum(0.0); + } else { + DimensionlessSpectrum transmittance = + GetTransmittance( + atmosphere, transmittance_texture, r, mu, d, + ray_r_mu_intersects_ground) * + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r_d, mu_s_d); + rayleigh = transmittance * exp( + -(r_d - atmosphere.bottom_radius) / atmosphere.rayleigh_scale_height); + mie = transmittance * exp( + -(r_d - atmosphere.bottom_radius) / atmosphere.mie_scale_height); + } +} + +/* +

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); + } +} + +/* +

The single scattering integral can then be computed as follows (using +the trapezoidal +rule): +*/ + +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 = DimensionlessSpectrum(0.0); + DimensionlessSpectrum mie_sum = DimensionlessSpectrum(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; +} + +/* +

Note that we added the solar irradiance and the scattering coefficient terms +that we omitted in ComputeSingleScatteringIntegrand, but not the +phase function terms - they are added at render time +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); +} + +/* +

Precomputation

+ +

The ComputeSingleScattering 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 paper, with some small +improvements (refer to the paper and to the above figures for the notations): +

+*/ + +vec4 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); + Number A = + -2.0 * atmosphere.mu_s_min * atmosphere.bottom_radius / (d_max - d_min); + 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 vec4(u_nu, u_mu_s, u_mu, u_r); +} + +/* +

The inverse mapping follows immediately: +*/ + +void GetRMuMuSNuFromScatteringTextureUvwz(IN(AtmosphereParameters) atmosphere, + IN(vec4) 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; + Number A = + -2.0 * atmosphere.mu_s_min * atmosphere.bottom_radius / (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); +} + +/* +

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(vec3) gl_frag_coord, + OUT(Length) r, OUT(Number) mu, OUT(Number) mu_s, OUT(Number) nu, + OUT(bool) ray_r_mu_intersects_ground) { + const vec4 SCATTERING_TEXTURE_SIZE = vec4( + SCATTERING_TEXTURE_NU_SIZE - 1, + SCATTERING_TEXTURE_MU_S_SIZE, + SCATTERING_TEXTURE_MU_SIZE, + SCATTERING_TEXTURE_R_SIZE); + Number frag_coord_nu = + floor(gl_frag_coord.x / Number(SCATTERING_TEXTURE_MU_S_SIZE)); + Number frag_coord_mu_s = + mod(gl_frag_coord.x, Number(SCATTERING_TEXTURE_MU_S_SIZE)); + vec4 uvwz = + vec4(frag_coord_nu, frag_coord_mu_s, gl_frag_coord.y, gl_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))); +} + +/* +

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(vec3) gl_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, gl_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); +} + +/* +

Lookup

+ +

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 +GetRMuMuSNuFromScatteringTextureFragCoord): +*/ + +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) { + vec4 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; + vec3 uvw0 = vec3((tex_x + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE), + uvwz.z, uvwz.w); + vec3 uvw1 = vec3((tex_x + 1.0 + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE), + uvwz.z, uvwz.w); + return AbstractSpectrum(texture(scattering_texture, uvw0) * (1.0 - lerp) + + texture(scattering_texture, uvw1) * lerp); +} + +/* +

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 scattering_order is strictly greater than 1, then +multiple_scattering_texture 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); + } +} + +/* +

Multiple scattering

+ +

The multiply scattered radiance is the light arriving from the Sun at some +point in the atmosphere after two or more bounces (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. + +

Note that, as for single scattering, we exclude here the light paths whose +last 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). + +

Computation

+ +

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 exactly 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. + +

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: +

+ +

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: + + + + + + + + + + + + p + q + ω + 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: +

+ +

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. + +

First step
+ +

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. + +

This radiance is the integral over all the possible incident directions +$\bw_i$, of the product of +

+*/ + +IrradianceSpectrum GetIrradiance( + IN(AtmosphereParameters) atmosphere, + IN(IrradianceTexture) irradiance_texture, + Length r, Number mu_s); + +/* + +This leads to the following implementation (where +multiple_scattering_texture is supposed to contain the $(n-1)$-th +order of scattering, if $n>2$, irradiance_texture is the irradiance +received on the ground after $n-2$ bounces, and scattering_order is +equal to $n$): +*/ + +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. + vec3 zenith_direction = vec3(0.0, 0.0, 1.0); + vec3 omega = vec3(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)); + vec3 omega_s = vec3(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 = + RadianceDensitySpectrum(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 = DimensionlessSpectrum(0.0); + DimensionlessSpectrum ground_albedo = DimensionlessSpectrum(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; + vec3 omega_i = + vec3(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. + vec3 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 = exp( + -(r - atmosphere.bottom_radius) / atmosphere.rayleigh_scale_height); + Number mie_density = exp( + -(r - atmosphere.bottom_radius) / atmosphere.mie_scale_height); + 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; +} + +/* +
Second step
+ +

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. + +

This radiance is the integral over all points $\bq$ between $\bp$ and the +nearest atmosphere boundary in direction $\bw$ of the product of: +

+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. + +

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 = + RadianceSpectrum(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; +} + +/* +

Precomputation

+ +

As explained in the overall algorithm 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 +first and +second 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(vec3) gl_frag_coord, int scattering_order) { + Length r; + Number mu; + Number mu_s; + Number nu; + bool ray_r_mu_intersects_ground; + GetRMuMuSNuFromScatteringTextureFragCoord(atmosphere, gl_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(vec3) gl_frag_coord, OUT(Number) nu) { + Length r; + Number mu; + Number mu_s; + bool ray_r_mu_intersects_ground; + GetRMuMuSNuFromScatteringTextureFragCoord(atmosphere, gl_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); +} + +/* +

Lookup

+ +

Likewise, we can simply reuse the lookup function GetScattering +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 +ComputeScatteringDensity and ComputeMultipleScattering +functions. + +

Ground irradiance

+ +

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: +

+ +

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 +paper 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. + +

The following sections describe how we compute the ground irradiance, how we +store it in a precomputed texture, and how we read it back. + +

Computation

+ +

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. + +

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 this radiance is zero outside the small solid +angle of the Sun, we can approximate the irradiance integral with the Sun +radiance, times the Sun solid angle (yielding the solar irradiance), times the +transmittance and the cosine factor for the Sun direction, i.e. $\mu_s$. This +yields the following implementation: +*/ + +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); + + return atmosphere.solar_irradiance * + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r, mu_s) * max(mu_s, 0.0); +} + +/* +

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: +

+This leads to the following implementation (where +multiple_scattering_texture is supposed to contain the $n$-th +order of scattering, if $n>1$, and scattering_order is equal to +$n$): +*/ + +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 = + IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm); + vec3 omega_s = vec3(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; + bool ray_r_theta_intersects_ground = + RayIntersectsGround(atmosphere, r, cos(theta)); + for (int i = 0; i < 2 * SAMPLE_COUNT; ++i) { + Angle phi = (Number(i) + 0.5) * dphi; + vec3 omega = + vec3(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, ray_r_theta_intersects_ground, + scattering_order) * + omega.z * domega; + } + } + return result; +} + +/* +

Precomputation

+ +

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: +*/ + +vec2 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 vec2(GetTextureCoordFromUnitRange(x_mu_s, IRRADIANCE_TEXTURE_WIDTH), + GetTextureCoordFromUnitRange(x_r, IRRADIANCE_TEXTURE_HEIGHT)); +} + +/* +

The inverse mapping follows immediately: +*/ + +void GetRMuSFromIrradianceTextureUv(IN(AtmosphereParameters) atmosphere, + IN(vec2) 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); +} + +/* +

It is now easy to define a fragment shader function to precompute a texel of +the ground irradiance texture, for the direct irradiance: +*/ + +const vec2 IRRADIANCE_TEXTURE_SIZE = + vec2(IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT); + +IrradianceSpectrum ComputeDirectIrradianceTexture( + IN(AtmosphereParameters) atmosphere, + IN(TransmittanceTexture) transmittance_texture, + IN(vec2) gl_frag_coord) { + Length r; + Number mu_s; + GetRMuSFromIrradianceTextureUv( + atmosphere, gl_frag_coord / IRRADIANCE_TEXTURE_SIZE, r, mu_s); + return ComputeDirectIrradiance(atmosphere, transmittance_texture, r, mu_s); +} + +/* +

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(vec2) gl_frag_coord, int scattering_order) { + Length r; + Number mu_s; + GetRMuSFromIrradianceTextureUv( + atmosphere, gl_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); +} + +/* +

Lookup

+ +

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) { + vec2 uv = GetIrradianceTextureUvFromRMuS(atmosphere, r, mu_s); + return IrradianceSpectrum(texture(irradiance_texture, uv)); +} + +/* +

Rendering

+ +

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. + +

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 +scattering_texture. We also assume that the single Mie scattering +is stored, without its phase function term: +

+ +

In the second case, the green and blue components of the single Mie +scattering are extrapolated as described in our +paper, with the following +function: +*/ + +#ifdef COMBINED_SCATTERING_TEXTURES +vec3 GetExtrapolatedSingleMieScattering( + IN(AtmosphereParameters) atmosphere, IN(vec4) scattering) { + if (scattering.r == 0.0) { + return vec3(0.0); + } + return scattering.rgb * scattering.a / scattering.r * + (atmosphere.rayleigh_scattering.r / atmosphere.mie_scattering.r) * + (atmosphere.mie_scattering / atmosphere.rayleigh_scattering); +} +#endif + +/* +

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 +GetScattering (we duplicate +some code here, instead of using two calls to GetScattering, to +make sure that the texture coordinates computation is shared between the lookups +in scattering_texture and +single_mie_scattering_texture): +*/ + +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) { + vec4 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; + vec3 uvw0 = vec3((tex_x + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE), + uvwz.z, uvwz.w); + vec3 uvw1 = vec3((tex_x + 1.0 + uvwz.y) / Number(SCATTERING_TEXTURE_NU_SIZE), + uvwz.z, uvwz.w); +#ifdef COMBINED_SCATTERING_TEXTURES + vec4 combined_scattering = + texture(scattering_texture, uvw0) * (1.0 - lerp) + + texture(scattering_texture, uvw1) * lerp; + IrradianceSpectrum scattering = IrradianceSpectrum(combined_scattering); + single_mie_scattering = + GetExtrapolatedSingleMieScattering(atmosphere, combined_scattering); +#else + IrradianceSpectrum scattering = IrradianceSpectrum( + texture(scattering_texture, uvw0) * (1.0 - lerp) + + texture(scattering_texture, uvw1) * lerp); + single_mie_scattering = IrradianceSpectrum( + texture(single_mie_scattering_texture, uvw0) * (1.0 - lerp) + + texture(single_mie_scattering_texture, uvw1) * lerp); +#endif + return scattering; +} + +/* +

Sky

+ +

To render the sky we simply need to display the sky radiance, which we can +get with a lookup in the precomputed scattering texture(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; + } + // If the view ray does not intersect the atmosphere, simply return 0. + if (r > atmosphere.top_radius) { + transmittance = DimensionlessSpectrum(1.0); + return RadianceSpectrum(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 ? DimensionlessSpectrum(0.0) : + 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); +} + +/* +

Aerial perspective

+ +

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) point, 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(point - 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(point - 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; +#ifdef COMBINED_SCATTERING_TEXTURES + single_mie_scattering = GetExtrapolatedSingleMieScattering( + atmosphere, vec4(scattering, single_mie_scattering.r)); +#endif + + // 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); +} + +/* +

Ground

+ +

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, 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 +paper for the other cases). + +

Note that it is useful here to take the angular size of the sun into account. +With a punctual light source (as we assumed in all the above functions), the +direct irradiance on a slanted surface would be discontinuous when the sun +moves across the horizon. With an area light source this discontinuity issue +disappears because the visible sun area decreases continously as the sun moves +across the horizon. + +

Taking the angular size of the sun into account, without approximations, is +quite complex because the visible sun area is restricted both by the distant +horizon and by the local surface. Here we ignore the masking by the local +surface, and we approximate the masking by the horizon with a +smoothstep. + +

The smoothstep approximation is justified as follows. When the sun, of +angular radius $\alpha_s$, is at an angle $\alpha$ above the horizon (we assume +that $\alpha$ is between $-\alpha_s$ and $\alpha_s$), the fraction $f$ of its +surface which is visible can be computed from the area of a circular segment: +$f(\alpha)=(\theta-\sin\theta)/2\pi$, with +$\theta=2\arccos(-\alpha/\alpha_s)$. The smoothstep approximation is justified +by the fact that $f$, expressed as a function of the cosine of the sun zenith +angle, $\mu_s=\sin\alpha\approx\alpha$, is quite similar to +smoothstep(-alpha_s, alpha_s, mu_s). + +

The function below returns the direct and indirect irradiances separately, +and takes the angular size of the sun into account by using the above +approximation: +*/ + +IrradianceSpectrum GetSunAndSkyIrradiance( + IN(AtmosphereParameters) atmosphere, + IN(TransmittanceTexture) transmittance_texture, + IN(IrradianceTexture) irradiance_texture, + IN(Position) point, IN(Direction) normal, IN(Direction) sun_direction, + OUT(IrradianceSpectrum) sky_irradiance) { + Length r = length(point); + Number mu_s = dot(point, 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, point) / r) * 0.5; + + // Direct irradiance. + return atmosphere.solar_irradiance * + GetTransmittanceToTopAtmosphereBoundary( + atmosphere, transmittance_texture, r, mu_s) * + smoothstep(-atmosphere.sun_angular_radius / rad, + atmosphere.sun_angular_radius / rad, + mu_s) * + max(dot(normal, sun_direction), 0.0); +} diff --git a/atmosphere/model.cc b/atmosphere/model.cc new file mode 100644 index 0000000..da04bab --- /dev/null +++ b/atmosphere/model.cc @@ -0,0 +1,929 @@ +/** + * 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. + */ + +/*

atmosphere/model.cc

+ +

This file implements the API of our atmosphere +model. Its main role is to precompute the transmittance, scattering and +irradiance textures. The GLSL functions to precompute them are provided in +functions.glsl, but they are not sufficient. +They must be used in fully functional shaders and programs, and these programs +must be called in the correct order, with the correct input and output textures +(via framebuffer objects), to precompute each scattering order in sequence, as +described in Algorithm 4.1 of +our paper. This is the role +of the following C++ code. +*/ + +#include "atmosphere/model.h" + +#include + +#include +#include +#include +#include + +#include "atmosphere/constants.h" + +/* +

The rest of this file is organized in 3 parts: +

+ +

Shader definitions

+ +

In order to precompute a texture we attach it to a framebuffer object (FBO) +and we render a full quad in this FBO. For this we need a basic vertex shader: +*/ + +namespace atmosphere { + +namespace { + +const char* kVertexShader = R"( + #version 330 + layout(location = 0) in vec2 vertex; + void main() { + gl_Position = vec4(vertex, 0.0, 1.0); + })"; + +/* +

a basic geometry shader (only for 3D textures, to specify in which layer we +want to write): +*/ + +const char* kGeometryShader = R"( + #version 330 + #extension GL_EXT_geometry_shader4 : enable + layout(triangles) in; + layout(triangle_strip, max_vertices = 3) out; + uniform int layer; + void main() { + gl_Position = gl_PositionIn[0]; + gl_Layer = layer; + EmitVertex(); + gl_Position = gl_PositionIn[1]; + gl_Layer = layer; + EmitVertex(); + gl_Position = gl_PositionIn[2]; + gl_Layer = layer; + EmitVertex(); + EndPrimitive(); + })"; + +/* +

and a fragment shader, which depends on the texture we want to compute. This +is the role of the following shaders, which simply wrap the precomputation +functions from functions.glsl in complete +shaders (with a main function and a proper declaration of the +shader inputs and outputs). Note that these strings must be concatenated with +definitions.glsl and functions.glsl (provided as C++ +string literals by the generated .glsl.inc files), as well as with +a definition of the ATMOSPHERE constant - containing the atmosphere +parameters, to really get a complete shader: +*/ + +#include "atmosphere/definitions.glsl.inc" +#include "atmosphere/functions.glsl.inc" + +const char* kComputeTransmittanceShader = R"( + layout(location = 0) out vec3 transmittance; + void main() { + transmittance = ComputeTransmittanceToTopAtmosphereBoundaryTexture( + ATMOSPHERE, gl_FragCoord.xy); + })"; + +const char* kComputeDirectIrradianceShader = R"( + layout(location = 0) out vec3 delta_irradiance; + layout(location = 1) out vec3 irradiance; + uniform sampler2D transmittance_texture; + void main() { + delta_irradiance = ComputeDirectIrradianceTexture( + ATMOSPHERE, transmittance_texture, gl_FragCoord.xy); + irradiance = vec3(0.0); + })"; + +const char* kComputeSingleScatteringShader = R"( + layout(location = 0) out vec3 delta_rayleigh; + layout(location = 1) out vec3 delta_mie; + layout(location = 2) out vec4 scattering; + uniform sampler2D transmittance_texture; + uniform int layer; + void main() { + ComputeSingleScatteringTexture( + ATMOSPHERE, transmittance_texture, vec3(gl_FragCoord.xy, layer + 0.5), + delta_rayleigh, delta_mie); + scattering = vec4(delta_rayleigh.rgb, delta_mie.r); + })"; + +const char* kComputeScatteringDensityShader = R"( + layout(location = 0) out vec3 scattering_density; + uniform sampler2D transmittance_texture; + uniform sampler3D single_rayleigh_scattering_texture; + uniform sampler3D single_mie_scattering_texture; + uniform sampler3D multiple_scattering_texture; + uniform sampler2D irradiance_texture; + uniform int scattering_order; + uniform int layer; + void main() { + scattering_density = ComputeScatteringDensityTexture( + ATMOSPHERE, transmittance_texture, single_rayleigh_scattering_texture, + single_mie_scattering_texture, multiple_scattering_texture, + irradiance_texture, vec3(gl_FragCoord.xy, layer + 0.5), + scattering_order); + })"; + +const char* kComputeIndirectIrradianceShader = R"( + layout(location = 0) out vec3 delta_irradiance; + layout(location = 1) out vec3 irradiance; + uniform sampler3D single_rayleigh_scattering_texture; + uniform sampler3D single_mie_scattering_texture; + uniform sampler3D multiple_scattering_texture; + uniform int scattering_order; + void main() { + delta_irradiance = ComputeIndirectIrradianceTexture( + ATMOSPHERE, single_rayleigh_scattering_texture, + single_mie_scattering_texture, multiple_scattering_texture, + gl_FragCoord.xy, scattering_order - 1); + irradiance = delta_irradiance; + })"; + +const char* kComputeMultipleScatteringShader = R"( + layout(location = 0) out vec3 delta_multiple_scattering; + layout(location = 1) out vec4 scattering; + uniform sampler2D transmittance_texture; + uniform sampler3D scattering_density_texture; + uniform int layer; + void main() { + float nu; + delta_multiple_scattering = ComputeMultipleScatteringTexture( + ATMOSPHERE, transmittance_texture, scattering_density_texture, + vec3(gl_FragCoord.xy, layer + 0.5), nu); + scattering = vec4( + delta_multiple_scattering.rgb / RayleighPhaseFunction(nu), 0.0); + })"; + +/* +

We finally need a shader implementing the GLSL functions exposed in our API, +which can be done by calling the corresponding functions in +functions.glsl, with the precomputed +texture arguments taken from uniform variables (note also the +*_RADIANCE_TO_LUMINANCE conversion constants in the last functions: +they are computed in the second part below, and their definitions are +concatenated to this GLSL code to get a fully functional shader). +*/ + +const char* kAtmosphereShader = R"( + uniform sampler2D transmittance_texture; + uniform sampler3D scattering_texture; + uniform sampler3D single_mie_scattering_texture; + uniform sampler2D irradiance_texture; + RadianceSpectrum GetSkyRadiance( + Position camera, Direction view_ray, Length shadow_length, + Direction sun_direction, out DimensionlessSpectrum transmittance) { + return GetSkyRadiance(ATMOSPHERE, transmittance_texture, + scattering_texture, single_mie_scattering_texture, + camera, view_ray, shadow_length, sun_direction, transmittance); + } + RadianceSpectrum GetSkyRadianceToPoint( + Position camera, Position point, Length shadow_length, + Direction sun_direction, out DimensionlessSpectrum transmittance) { + return GetSkyRadianceToPoint(ATMOSPHERE, transmittance_texture, + scattering_texture, single_mie_scattering_texture, + camera, point, shadow_length, sun_direction, transmittance); + } + IrradianceSpectrum GetSunAndSkyIrradiance( + Position p, Direction normal, Direction sun_direction, + out IrradianceSpectrum sky_irradiance) { + return GetSunAndSkyIrradiance(ATMOSPHERE, transmittance_texture, + irradiance_texture, p, normal, sun_direction, sky_irradiance); + } + Luminance3 GetSkyLuminance( + Position camera, Direction view_ray, Length shadow_length, + Direction sun_direction, out DimensionlessSpectrum transmittance) { + return GetSkyRadiance(camera, view_ray, shadow_length, sun_direction, + transmittance) * SKY_SPECTRAL_RADIANCE_TO_LUMINANCE; + } + Luminance3 GetSkyLuminanceToPoint( + Position camera, Position point, Length shadow_length, + Direction sun_direction, out DimensionlessSpectrum transmittance) { + return GetSkyRadianceToPoint(camera, point, shadow_length, sun_direction, + transmittance) * SKY_SPECTRAL_RADIANCE_TO_LUMINANCE; + } + Illuminance3 GetSunAndSkyIlluminance( + Position p, Direction normal, Direction sun_direction, + out IrradianceSpectrum sky_irradiance) { + IrradianceSpectrum sun_irradiance = + GetSunAndSkyIrradiance(p, normal, sun_direction, sky_irradiance); + sky_irradiance *= SKY_SPECTRAL_RADIANCE_TO_LUMINANCE; + return sun_irradiance * SUN_SPECTRAL_RADIANCE_TO_LUMINANCE; + })"; + +/*

Utility classes and functions

+ +

To compile and link these shaders into programs, and to set their uniforms, +we use the following utility class: +*/ + +class Program { + public: + Program( + const std::string& vertex_shader_source, + const std::string& fragment_shader_source) + : Program(vertex_shader_source, "", fragment_shader_source) { + } + + Program( + const std::string& vertex_shader_source, + const std::string& geometry_shader_source, + const std::string& fragment_shader_source) { + program_ = glCreateProgram(); + + const char* source; + source = vertex_shader_source.c_str(); + GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex_shader, 1, &source, NULL); + glCompileShader(vertex_shader); + CheckShader(vertex_shader); + glAttachShader(program_, vertex_shader); + + GLuint geometry_shader = 0; + if (!geometry_shader_source.empty()) { + source = geometry_shader_source.c_str(); + geometry_shader = glCreateShader(GL_GEOMETRY_SHADER); + glShaderSource(geometry_shader, 1, &source, NULL); + glCompileShader(geometry_shader); + CheckShader(geometry_shader); + glAttachShader(program_, geometry_shader); + } + + source = fragment_shader_source.c_str(); + GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment_shader, 1, &source, NULL); + glCompileShader(fragment_shader); + CheckShader(fragment_shader); + glAttachShader(program_, fragment_shader); + + glLinkProgram(program_); + CheckProgram(program_); + + glDetachShader(program_, vertex_shader); + glDeleteShader(vertex_shader); + if (!geometry_shader_source.empty()) { + glDetachShader(program_, geometry_shader); + glDeleteShader(geometry_shader); + } + glDetachShader(program_, fragment_shader); + glDeleteShader(fragment_shader); + } + + ~Program() { + glDeleteProgram(program_); + } + + void Use() const { + glUseProgram(program_); + } + + void BindInt(const std::string& uniform_name, int value) const { + glUniform1i(glGetUniformLocation(program_, uniform_name.c_str()), value); + } + + void BindTexture2d(const std::string& sampler_uniform_name, GLuint texture, + GLuint texture_unit) const { + glActiveTexture(GL_TEXTURE0 + texture_unit); + glBindTexture(GL_TEXTURE_2D, texture); + BindInt(sampler_uniform_name, texture_unit); + } + + void BindTexture3d(const std::string& sampler_uniform_name, GLuint texture, + GLuint texture_unit) const { + glActiveTexture(GL_TEXTURE0 + texture_unit); + glBindTexture(GL_TEXTURE_3D, texture); + BindInt(sampler_uniform_name, texture_unit); + } + + private: + static void CheckShader(GLuint shader) { + GLint compile_status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status); + if (compile_status == GL_FALSE) { + PrintShaderLog(shader); + } + assert(compile_status == GL_TRUE); + } + + static void PrintShaderLog(GLuint shader) { + GLint log_length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length); + if (log_length > 0) { + std::unique_ptr log_data(new char[log_length]); + glGetShaderInfoLog(shader, log_length, &log_length, log_data.get()); + std::cerr << "compile log = " + << std::string(log_data.get(), log_length) << std::endl; + } + } + + static void CheckProgram(GLuint program) { + GLint link_status; + glGetProgramiv(program, GL_LINK_STATUS, &link_status); + if (link_status == GL_FALSE) { + PrintProgramLog(program); + } + assert(link_status == GL_TRUE); + assert(glGetError() == 0); + } + + static void PrintProgramLog(GLuint program) { + GLint log_length; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length); + if (log_length > 0) { + std::unique_ptr log_data(new char[log_length]); + glGetProgramInfoLog(program, log_length, &log_length, log_data.get()); + std::cerr << "link log = " + << std::string(log_data.get(), log_length) << std::endl; + } + } + + GLuint program_; +}; + +/* +

We also need functions to allocate the precomputed textures on GPU: +*/ + +GLuint NewTexture2d(int width, int height) { + GLuint texture; + glGenTextures(1, &texture); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + // 16F precision for the transmittance gives artifacts. + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, width, height, 0, + GL_RGB, GL_FLOAT, NULL); + return texture; +} + +GLuint NewTexture3d(int width, int height, int depth, GLenum format) { + GLuint texture; + glGenTextures(1, &texture); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_3D, texture); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + if (format == GL_RGBA) { + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA16F, width, height, depth, 0, + format, GL_FLOAT, NULL); + } else { + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB16F, width, height, depth, 0, + format, GL_FLOAT, NULL); + } + return texture; +} + +/* +

and a function to draw a full screen quad in an offscreen framebuffer: +*/ + +void DrawQuad() { + glBegin(GL_TRIANGLE_STRIP); + glVertex2f(-1.0, -1.0); + glVertex2f(+1.0, -1.0); + glVertex2f(-1.0, +1.0); + glVertex2f(+1.0, +1.0); + glEnd(); +} + +/* +

Finally, we need a utility function to compute the value of the conversion +constants *_RADIANCE_TO_LUMINANCE, used above to convert the +spectral results into luminance values. These are the constants k_r, k_g, k_b +described in Section 14.3 of A +Qualitative and Quantitative Evaluation of 8 Clear Sky Models. + +

Computing their value requires an integral of a function times a CIE color +matching function. Thus, we first need functions to interpolate an arbitrary +function (specified by some samples), and a CIE color matching function +(specified by tabulated values), at an arbitrary wavelength. This is the purpose +of the following two functions: +*/ + +constexpr int kLambdaMin = 360; +constexpr int kLambdaMax = 830; + +double CieColorMatchingFunctionTableValue(double wavelength, int column) { + if (wavelength <= kLambdaMin || wavelength >= kLambdaMax) { + return 0.0; + } + double u = (wavelength - kLambdaMin) / 5.0; + int row = static_cast(std::floor(u)); + assert(row >= 0 && row + 1 < 95); + assert(CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[4 * row] <= wavelength && + CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[4 * (row + 1)] >= wavelength); + u -= row; + return CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[4 * row + column] * (1.0 - u) + + CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[4 * (row + 1) + column] * u; +} + +double Interpolate( + const std::vector& wavelengths, + const std::vector& wavelength_function, + double wavelength) { + assert(wavelength_function.size() == wavelengths.size()); + if (wavelength < wavelengths[0]) { + return wavelength_function[0]; + } + for (unsigned int i = 0; i < wavelengths.size() - 1; ++i) { + if (wavelength < wavelengths[i + 1]) { + double u = + (wavelength - wavelengths[i]) / (wavelengths[i + 1] - wavelengths[i]); + return + wavelength_function[i] * (1.0 - u) + wavelength_function[i + 1] * u; + } + } + return wavelength_function[wavelength_function.size() - 1]; +} + +/* +

We can then implement a utility function to compute the "spectral radiance to +luminance" conversion constants (see Section 14.3 in A Qualitative and Quantitative +Evaluation of 8 Clear Sky Models for their definitions): +*/ + +// The returned constants are in lumen.nm / watt. +void ComputeSpectralRadianceToLuminanceFactors( + const std::vector& wavelengths, + const std::vector& solar_irradiance, + double lambda_power, double* k_r, double* k_g, double* k_b) { + *k_r = 0.0; + *k_g = 0.0; + *k_b = 0.0; + double solar_r = Interpolate(wavelengths, solar_irradiance, Model::kLambdaR); + double solar_g = Interpolate(wavelengths, solar_irradiance, Model::kLambdaG); + double solar_b = Interpolate(wavelengths, solar_irradiance, Model::kLambdaB); + int dlambda = 1; + for (int lambda = kLambdaMin; lambda < kLambdaMax; lambda += dlambda) { + double x_bar = CieColorMatchingFunctionTableValue(lambda, 1); + double y_bar = CieColorMatchingFunctionTableValue(lambda, 2); + double z_bar = CieColorMatchingFunctionTableValue(lambda, 3); + const double* xyz2srgb = XYZ_TO_SRGB; + double r_bar = + xyz2srgb[0] * x_bar + xyz2srgb[1] * y_bar + xyz2srgb[2] * z_bar; + double g_bar = + xyz2srgb[3] * x_bar + xyz2srgb[4] * y_bar + xyz2srgb[5] * z_bar; + double b_bar = + xyz2srgb[6] * x_bar + xyz2srgb[7] * y_bar + xyz2srgb[8] * z_bar; + double irradiance = Interpolate(wavelengths, solar_irradiance, lambda); + *k_r += r_bar * irradiance / solar_r * + pow(lambda / Model::kLambdaR, lambda_power); + *k_g += g_bar * irradiance / solar_g * + pow(lambda / Model::kLambdaG, lambda_power); + *k_b += b_bar * irradiance / solar_b * + pow(lambda / Model::kLambdaB, lambda_power); + } + *k_r *= MAX_LUMINOUS_EFFICACY * dlambda; + *k_g *= MAX_LUMINOUS_EFFICACY * dlambda; + *k_b *= MAX_LUMINOUS_EFFICACY * dlambda; +} + +} // anonymous namespace + +/*

Model implementation

+ +

Using the above utility functions and classes, we can now implement the +constructor of the Model class. This constructor generates a piece +of GLSL code that defines an ATMOSPHERE constant containing the +atmosphere parameters (we use constants instead of uniforms to enable constant +folding and propagation optimizations in the GLSL compiler), concatenated with +functions.glsl, and with +ATMOSPHERE_SHADER, to get the shader exposed by our API in +GetShader. It also allocates the precomputed textures, but does not +initialize them. +*/ + +Model::Model( + const std::vector& wavelengths, + const std::vector& solar_irradiance, + const double sun_angular_radius, + double bottom_radius, + double top_radius, + double rayleigh_scale_height, + const std::vector& rayleigh_scattering, + double mie_scale_height, + const std::vector& mie_scattering, + const std::vector& mie_extinction, + double mie_phase_function_g, + const std::vector& ground_albedo, + double max_sun_zenith_angle, + double length_unit_in_meters, + bool combine_scattering_textures) { + auto to_string = [&wavelengths](const std::vector& v, double scale) { + double r = Interpolate(wavelengths, v, kLambdaR) * scale; + double g = Interpolate(wavelengths, v, kLambdaG) * scale; + double b = Interpolate(wavelengths, v, kLambdaB) * scale; + return "vec3(" + std::to_string(r) + "," + std::to_string(g) + "," + + std::to_string(b) + ")"; + }; + double sky_k_r, sky_k_g, sky_k_b; + ComputeSpectralRadianceToLuminanceFactors(wavelengths, solar_irradiance, + -3 /* lambda_power */, &sky_k_r, &sky_k_g, &sky_k_b); + double sun_k_r, sun_k_g, sun_k_b; + ComputeSpectralRadianceToLuminanceFactors(wavelengths, solar_irradiance, + 0 /* lambda_power */, &sun_k_r, &sun_k_g, &sun_k_b); + glsl_header_ = + "#version 330\n" + "#define IN(x) const in x\n" + "#define OUT(x) out x\n" + "#define TEMPLATE(x)\n" + "#define TEMPLATE_ARGUMENT(x)\n" + "#define assert(x)\n" + "const int TRANSMITTANCE_TEXTURE_WIDTH = " + + std::to_string(TRANSMITTANCE_TEXTURE_WIDTH) + ";\n" + + "const int TRANSMITTANCE_TEXTURE_HEIGHT = " + + std::to_string(TRANSMITTANCE_TEXTURE_HEIGHT) + ";\n" + + "const int SCATTERING_TEXTURE_R_SIZE = " + + std::to_string(SCATTERING_TEXTURE_R_SIZE) + ";\n" + + "const int SCATTERING_TEXTURE_MU_SIZE = " + + std::to_string(SCATTERING_TEXTURE_MU_SIZE) + ";\n" + + "const int SCATTERING_TEXTURE_MU_S_SIZE = " + + std::to_string(SCATTERING_TEXTURE_MU_S_SIZE) + ";\n" + + "const int SCATTERING_TEXTURE_NU_SIZE = " + + std::to_string(SCATTERING_TEXTURE_NU_SIZE) + ";\n" + + "const int IRRADIANCE_TEXTURE_WIDTH = " + + std::to_string(IRRADIANCE_TEXTURE_WIDTH) + ";\n" + + "const int IRRADIANCE_TEXTURE_HEIGHT = " + + std::to_string(IRRADIANCE_TEXTURE_HEIGHT) + ";\n" + + (combine_scattering_textures ? + "#define COMBINED_SCATTERING_TEXTURES\n" : "") + + definitions_glsl + + "const AtmosphereParameters ATMOSPHERE = AtmosphereParameters(\n" + + to_string(solar_irradiance, 1.0) + ",\n" + + std::to_string(sun_angular_radius) + ",\n" + + std::to_string(bottom_radius / length_unit_in_meters) + ",\n" + + std::to_string(top_radius / length_unit_in_meters) + ",\n" + + std::to_string( + rayleigh_scale_height / length_unit_in_meters) + ",\n" + + to_string(rayleigh_scattering, length_unit_in_meters) + ",\n" + + std::to_string(mie_scale_height / length_unit_in_meters) + ",\n" + + to_string(mie_scattering, length_unit_in_meters) + ",\n" + + to_string(mie_extinction, length_unit_in_meters) + ",\n" + + std::to_string(mie_phase_function_g) + ",\n" + + to_string(ground_albedo, 1.0) + ",\n" + + std::to_string(cos(max_sun_zenith_angle)) + ");\n" + + "const vec3 SKY_SPECTRAL_RADIANCE_TO_LUMINANCE = vec3(" + + std::to_string(sky_k_r) + "," + + std::to_string(sky_k_g) + "," + + std::to_string(sky_k_b) + ");\n" + + "const vec3 SUN_SPECTRAL_RADIANCE_TO_LUMINANCE = vec3(" + + std::to_string(sun_k_r) + "," + + std::to_string(sun_k_g) + "," + + std::to_string(sun_k_b) + ");\n" + + functions_glsl; + transmittance_texture_ = NewTexture2d( + TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT); + scattering_texture_ = NewTexture3d( + SCATTERING_TEXTURE_WIDTH, + SCATTERING_TEXTURE_HEIGHT, + SCATTERING_TEXTURE_DEPTH, + combine_scattering_textures ? GL_RGBA : GL_RGB); + if (combine_scattering_textures) { + optional_single_mie_scattering_texture_ = 0; + } else { + optional_single_mie_scattering_texture_ = NewTexture3d( + SCATTERING_TEXTURE_WIDTH, + SCATTERING_TEXTURE_HEIGHT, + SCATTERING_TEXTURE_DEPTH, + GL_RGB); + } + irradiance_texture_ = NewTexture2d( + IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT); + + std::string shader = glsl_header_ + kAtmosphereShader; + const char* source = shader.c_str(); + atmosphere_shader_ = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(atmosphere_shader_, 1, &source, NULL); + glCompileShader(atmosphere_shader_); +} + +/* +

The destructor is trivial: +*/ + +Model::~Model() { + glDeleteTextures(1, &transmittance_texture_); + glDeleteTextures(1, &scattering_texture_); + if (optional_single_mie_scattering_texture_ != 0) { + glDeleteTextures(1, &optional_single_mie_scattering_texture_); + } + glDeleteTextures(1, &irradiance_texture_); + glDeleteShader(atmosphere_shader_); +} + +/* +

The most complex method is the following, which precomputes the atmosphere +textures. This method first allocates the temporary resources it needs, then +performs the precomputations, and finally destroys the temporary resources. +Each phase is explained by the inline comments below. +*/ + +void Model::Init(unsigned int num_scattering_orders) { + // The precomputations require temporary textures, in particular to store the + // contribution of one scattering order, which is needed to compute the next + // order of scattering (the final precomputed textures store the sum of all + // the scattering orders). We allocate them here, and destroy them at the end + // of this method. + GLuint delta_irradiance_texture = NewTexture2d( + IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT); + GLuint delta_rayleigh_scattering_texture = NewTexture3d( + SCATTERING_TEXTURE_WIDTH, + SCATTERING_TEXTURE_HEIGHT, + SCATTERING_TEXTURE_DEPTH, + GL_RGB); + GLuint delta_mie_scattering_texture; + if (optional_single_mie_scattering_texture_ == 0) { + delta_mie_scattering_texture = NewTexture3d( + SCATTERING_TEXTURE_WIDTH, + SCATTERING_TEXTURE_HEIGHT, + SCATTERING_TEXTURE_DEPTH, + GL_RGB); + } else { + delta_mie_scattering_texture = optional_single_mie_scattering_texture_; + } + GLuint delta_scattering_density_texture = NewTexture3d( + SCATTERING_TEXTURE_WIDTH, + SCATTERING_TEXTURE_HEIGHT, + SCATTERING_TEXTURE_DEPTH, + GL_RGB); + GLuint delta_multiple_scattering_texture = delta_rayleigh_scattering_texture; + + // The precomputations also require a temporary framebuffer object, created + // here (and destroyed at the end of this method). + GLuint fbo; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glReadBuffer(GL_COLOR_ATTACHMENT0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + const GLuint kDrawBuffers[3] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2 + }; + + // Finally, the precomputations also require specific GLSL programs, for each + // precomputation step. We create and compile them here (they are + // automatically destroyed when this method returns, via the Program + // destructor). + Program compute_transmittance( + kVertexShader, glsl_header_ + kComputeTransmittanceShader); + Program compute_direct_irradiance( + kVertexShader, glsl_header_ + kComputeDirectIrradianceShader); + Program compute_single_scattering(kVertexShader, kGeometryShader, + glsl_header_ + kComputeSingleScatteringShader); + Program compute_scattering_density(kVertexShader, kGeometryShader, + glsl_header_ + kComputeScatteringDensityShader); + Program compute_indirect_irradiance( + kVertexShader, glsl_header_ + kComputeIndirectIrradianceShader); + Program compute_multiple_scattering(kVertexShader, kGeometryShader, + glsl_header_ + kComputeMultipleScatteringShader); + + // Compute the transmittance, and store it in transmittance_texture_. + glFramebufferTexture( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, transmittance_texture_, 0); + glViewport(0, 0, TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT); + compute_transmittance.Use(); + DrawQuad(); + + // Compute the direct irradiance, store it in delta_irradiance_texture, and + // initialize irradiance_texture_ with zeros (we don't want the direct + // irradiance in irradiance_texture_, but only the irradiance from the sky). + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + delta_irradiance_texture, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, + irradiance_texture_, 0); + glDrawBuffers(2, kDrawBuffers); + glViewport(0, 0, IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT); + compute_direct_irradiance.Use(); + compute_direct_irradiance.BindTexture2d( + "transmittance_texture", transmittance_texture_, 0); + DrawQuad(); + + // Compute the rayleigh and mie single scattering, and store them in + // delta_rayleigh_scattering_texture and delta_mie_scattering_texture, as well + // as in scattering_texture. + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + delta_rayleigh_scattering_texture, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, + delta_mie_scattering_texture, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, + scattering_texture_, 0); + glDrawBuffers(3, kDrawBuffers); + glViewport(0, 0, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT); + compute_single_scattering.Use(); + compute_single_scattering.BindTexture2d( + "transmittance_texture", transmittance_texture_, 0); + for (unsigned int layer = 0; layer < SCATTERING_TEXTURE_DEPTH; ++layer) { + compute_single_scattering.BindInt("layer", layer); + DrawQuad(); + } + + // Compute the 2nd, 3rd and 4th order of scattering, in sequence. + for (unsigned int scattering_order = 2; + scattering_order <= num_scattering_orders; + ++scattering_order) { + // Compute the scattering density, and store it in + // delta_scattering_density_texture. + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + delta_scattering_density_texture, 0); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, 0, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + glViewport(0, 0, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT); + compute_scattering_density.Use(); + compute_scattering_density.BindTexture2d( + "transmittance_texture", transmittance_texture_, 0); + compute_scattering_density.BindTexture3d( + "single_rayleigh_scattering_texture", + delta_rayleigh_scattering_texture, + 1); + compute_scattering_density.BindTexture3d( + "single_mie_scattering_texture", delta_mie_scattering_texture, 2); + compute_scattering_density.BindTexture3d( + "multiple_scattering_texture", delta_multiple_scattering_texture, 3); + compute_scattering_density.BindTexture2d( + "irradiance_texture", delta_irradiance_texture, 4); + compute_scattering_density.BindInt("scattering_order", scattering_order); + for (unsigned int layer = 0; layer < SCATTERING_TEXTURE_DEPTH; ++layer) { + compute_scattering_density.BindInt("layer", layer); + DrawQuad(); + } + + // Compute the indirect irradiance, store it in delta_irradiance_texture and + // accumulate it in irradiance_texture_. + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + delta_irradiance_texture, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, + irradiance_texture_, 0); + glDrawBuffers(2, kDrawBuffers); + glViewport(0, 0, IRRADIANCE_TEXTURE_WIDTH, IRRADIANCE_TEXTURE_HEIGHT); + compute_indirect_irradiance.Use(); + compute_indirect_irradiance.BindTexture3d( + "single_rayleigh_scattering_texture", + delta_rayleigh_scattering_texture, + 0); + compute_indirect_irradiance.BindTexture3d( + "single_mie_scattering_texture", delta_mie_scattering_texture, 1); + compute_indirect_irradiance.BindTexture3d( + "multiple_scattering_texture", delta_multiple_scattering_texture, 2); + compute_indirect_irradiance.BindInt("scattering_order", scattering_order); + glEnablei(GL_BLEND, 1); + glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD); + glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE); + DrawQuad(); + glDisablei(GL_BLEND, 1); + + // Compute the multiple scattering, store it in + // delta_multiple_scattering_texture, and accumulate it in + // scattering_texture_. + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + delta_multiple_scattering_texture, 0); + glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, + scattering_texture_, 0); + glDrawBuffers(2, kDrawBuffers); + glViewport(0, 0, SCATTERING_TEXTURE_WIDTH, SCATTERING_TEXTURE_HEIGHT); + compute_multiple_scattering.Use(); + compute_multiple_scattering.BindTexture2d( + "transmittance_texture", transmittance_texture_, 0); + compute_multiple_scattering.BindTexture3d( + "scattering_density_texture", delta_scattering_density_texture, 1); + glEnablei(GL_BLEND, 1); + glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD); + glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE); + for (unsigned int layer = 0; layer < SCATTERING_TEXTURE_DEPTH; ++layer) { + compute_multiple_scattering.BindInt("layer", layer); + DrawQuad(); + } + glDisablei(GL_BLEND, 1); + } + + // Delete the temporary resources allocated at the begining of this method. + glUseProgram(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &delta_scattering_density_texture); + if (optional_single_mie_scattering_texture_ == 0) { + glDeleteTextures(1, &delta_mie_scattering_texture); + } + glDeleteTextures(1, &delta_rayleigh_scattering_texture); + glDeleteTextures(1, &delta_irradiance_texture); + assert(glGetError() == 0); +} + +/* +

The SetProgramUniforms method is straightforward: it simply +binds the precomputed textures to the specified texture units, and then sets +the corresponding uniforms in the user provided program to the index of these +texture units. +*/ + +void Model::SetProgramUniforms(unsigned int program, + unsigned int transmittance_texture_unit, + unsigned int scattering_texture_unit, + unsigned int irradiance_texture_unit, + unsigned int single_mie_scattering_texture_unit) const { + glActiveTexture(GL_TEXTURE0 + transmittance_texture_unit); + glBindTexture(GL_TEXTURE_2D, transmittance_texture_); + glUniform1i(glGetUniformLocation(program, "transmittance_texture"), + transmittance_texture_unit); + + glActiveTexture(GL_TEXTURE0 + scattering_texture_unit); + glBindTexture(GL_TEXTURE_3D, scattering_texture_); + glUniform1i(glGetUniformLocation(program, "scattering_texture"), + scattering_texture_unit); + + glActiveTexture(GL_TEXTURE0 + irradiance_texture_unit); + glBindTexture(GL_TEXTURE_2D, irradiance_texture_); + glUniform1i(glGetUniformLocation(program, "irradiance_texture"), + irradiance_texture_unit); + + if (optional_single_mie_scattering_texture_ != 0) { + glActiveTexture(GL_TEXTURE0 + single_mie_scattering_texture_unit); + glBindTexture(GL_TEXTURE_3D, optional_single_mie_scattering_texture_); + glUniform1i(glGetUniformLocation(program, "single_mie_scattering_texture"), + single_mie_scattering_texture_unit); + } +} + +/* +

Finally, the utility method ConvertSpectrumToLinearSrgb is +implemented with a simple numerical integration of the given function, times +the CIE color matching funtions (with an integration step of 1nm), followed by +a matrix multiplication: +*/ + +void Model::ConvertSpectrumToLinearSrgb( + const std::vector& wavelengths, + const std::vector& spectrum, + double* r, double* g, double* b) { + double x = 0.0; + double y = 0.0; + double z = 0.0; + const int dlambda = 1; + for (int lambda = kLambdaMin; lambda < kLambdaMax; lambda += dlambda) { + double value = Interpolate(wavelengths, spectrum, lambda); + x += CieColorMatchingFunctionTableValue(lambda, 1) * value; + y += CieColorMatchingFunctionTableValue(lambda, 2) * value; + z += CieColorMatchingFunctionTableValue(lambda, 3) * value; + } + *r = MAX_LUMINOUS_EFFICACY * + (XYZ_TO_SRGB[0] * x + XYZ_TO_SRGB[1] * y + XYZ_TO_SRGB[2] * z) * dlambda; + *g = MAX_LUMINOUS_EFFICACY * + (XYZ_TO_SRGB[3] * x + XYZ_TO_SRGB[4] * y + XYZ_TO_SRGB[5] * z) * dlambda; + *b = MAX_LUMINOUS_EFFICACY * + (XYZ_TO_SRGB[6] * x + XYZ_TO_SRGB[7] * y + XYZ_TO_SRGB[8] * z) * dlambda; +} + +} // namespace atmosphere diff --git a/atmosphere/model.h b/atmosphere/model.h new file mode 100644 index 0000000..25a439b --- /dev/null +++ b/atmosphere/model.h @@ -0,0 +1,222 @@ +/** + * 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. + */ + +/*

atmosphere/model.h

+ +

This file defines the API to use our atmosphere model in OpenGL applications. +To use it: +

+ +

The shader returned by GetShader provides the following +functions (that you need to forward declare in your own shaders to be able to +compile them separately): + +

+// Returns the sky radiance along the segment from 'camera' to the nearest
+// atmosphere boundary in direction 'view_ray', as well as the transmittance
+// along this segment.
+vec3 GetSkyRadiance(vec3 camera, vec3 view_ray, double shadow_length,
+    vec3 sun_direction, out vec3 transmittance);
+
+// Returns the sky radiance along the segment from 'camera' to 'p', as well as
+// the transmittance along this segment.
+vec3 GetSkyRadianceToPoint(vec3 camera, vec3 p, double shadow_length,
+    vec3 sun_direction, out vec3 transmittance);
+
+// Returns the sun and sky irradiance received on a surface patch located at 'p'
+// and whose normal vector is 'normal'.
+vec3 GetSunAndSkyIrradiance(vec3 p, vec3 normal, vec3 sun_direction,
+    out vec3 sky_irradiance);
+
+// Returns the sky luminance along the segment from 'camera' to the nearest
+// atmosphere boundary in direction 'view_ray', as well as the transmittance
+// along this segment.
+vec3 GetSkyLuminance(vec3 camera, vec3 view_ray, double shadow_length,
+    vec3 sun_direction, out vec3 transmittance);
+
+// Returns the sky luminance along the segment from 'camera' to 'p', as well as
+// the transmittance along this segment.
+vec3 GetSkyLuminanceToPoint(vec3 camera, vec3 p, double shadow_length,
+    vec3 sun_direction, out vec3 transmittance);
+
+// Returns the sun and sky illuminance received on a surface patch located at
+// 'p' and whose normal vector is 'normal'.
+vec3 GetSunAndSkyIlluminance(vec3 p, vec3 normal, vec3 sun_direction,
+    out vec3 sky_illuminance);
+
+ +

where +

+ +

and where +

+ +

The concrete API definition is the following: +*/ + +#ifndef ATMOSPHERE_MODEL_H_ +#define ATMOSPHERE_MODEL_H_ + +#include +#include + +namespace atmosphere { + +class Model { + public: + Model( + // The wavelength values, in nanometers, and sorted in increasing order, for + // which the solar_irradiance, rayleigh_scattering, mie_scattering, + // mie_extinction and ground_albedo samples are provided. If your shaders + // use luminance values (as opposed to radiance values, see above), use a + // large number of wavelengths (e.g. between 15 and 50) to get accurate + // results (this number of wavelengths has absolutely no impact on the + // shader performance). + const std::vector& wavelengths, + // The solar irradiance at the top of the atmosphere, in W/m^2/nm. This + // vector must have the same size as the wavelength parameter. + const std::vector& solar_irradiance, + // The sun's angular radius, in radians. + double sun_angular_radius, + // The distance between the planet center and the bottom of the atmosphere, + // in m. + double bottom_radius, + // The distance between the planet center and the top of the atmosphere, + // in m. + double top_radius, + // The scale height of air molecules, in m, meaning that their density is + // proportional to exp(-h / rayleigh_scale_height), with h the altitude + // (with the bottom of the atmosphere at altitude 0). + double rayleigh_scale_height, + // The scattering coefficient of air molecules at the bottom of the + // atmosphere, as a function of wavelength, in m^-1. This vector must have + // the same size as the wavelength parameter. + const std::vector& rayleigh_scattering, + // The scale height of aerosols, in m, meaning that their density is + // proportional to exp(-h / mie_scale_height), with h the altitude. + double mie_scale_height, + // The scattering coefficient of aerosols at the bottom of the atmosphere, + // as a function of wavelength, in m^-1. This vector must have the same size + // as the wavelength parameter. + const std::vector& mie_scattering, + // The extinction coefficient of aerosols at the bottom of the atmosphere, + // as a function of wavelength, in m^-1. This vector must have the same size + // as the wavelength parameter. + const std::vector& mie_extinction, + // The asymetry parameter for the Cornette-Shanks phase function for the + // aerosols. + double mie_phase_function_g, + // The average albedo of the ground, as a function of wavelength. This + // vector must have the same size as the wavelength parameter. + const std::vector& ground_albedo, + // The maximum Sun zenith angle for which atmospheric scattering must be + // precomputed, in radians (for maximum precision, use the smallest Sun + // zenith angle yielding negligible sky light radiance values. For instance, + // for the Earth case, 102 degrees is a good choice). + double max_sun_zenith_angle, + // The length unit used in your shaders and meshes. This is the length unit + // which must be used when calling the atmosphere model shader functions. + double length_unit_in_meters, + // Whether to pack the (red component of the) single Mie scattering with the + // Rayleigh and multiple scattering in a single texture, or to store the + // (3 components of the) single Mie scattering in a separate texture. + bool combine_scattering_textures); + + ~Model(); + + void Init(unsigned int num_scattering_orders = 4); + + unsigned int GetShader() const { return atmosphere_shader_; } + + void SetProgramUniforms(unsigned int program, + unsigned int transmittance_texture_unit, + unsigned int scattering_texture_unit, + unsigned int irradiance_texture_unit, + unsigned int optional_single_mie_scattering_texture_unit = 0) const; + + // Utility method to convert a function of the wavelength to linear sRGB. + // 'wavelengths' and 'spectrum' must have the same size. The integral of + // 'spectrum' times each CIE_2_DEG_COLOR_MATCHING_FUNCTIONS (and times + // MAX_LUMINOUS_EFFICACY) is computed to get XYZ values, which are then + // converted to linear sRGB with the XYZ_TO_SRGB matrix. + static void ConvertSpectrumToLinearSrgb( + const std::vector& wavelengths, + const std::vector& spectrum, + double* r, double* g, double* b); + + static constexpr double kLambdaR = 680.0; + static constexpr double kLambdaG = 550.0; + static constexpr double kLambdaB = 440.0; + + private: + std::string glsl_header_; + unsigned int transmittance_texture_; + unsigned int scattering_texture_; + unsigned int optional_single_mie_scattering_texture_; + unsigned int irradiance_texture_; + unsigned int atmosphere_shader_; +}; + +} // namespace atmosphere + +#endif // ATMOSPHERE_MODEL_H_ diff --git a/atmosphere/reference/definitions.h b/atmosphere/reference/definitions.h new file mode 100644 index 0000000..b16f945 --- /dev/null +++ b/atmosphere/reference/definitions.h @@ -0,0 +1,252 @@ +/** + * 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. + */ + +/*

atmosphere/reference/definitions.h

+ +

This C++ file defines the physical types and constants which are used in the +main GLSL functions of our atmosphere +model, in such a way that they can be compiled by the C++ compiler. The GLSL equivalent of this file provides the same +types and constants in GLSL, to allow the same functions to be compiled by the +GLSL compiler. + +

The main purpose of this C++ compilation is to check the dimensional homogeneity of the GLSL expressions (see the +Introduction). For this we define the C++ physical +types by using generic templates parameterized by physical dimensions, +inspired from Boost.Unit, +and provided by the following included files: +*/ + +#ifndef ATMOSPHERE_REFERENCE_DEFINITIONS_H_ +#define ATMOSPHERE_REFERENCE_DEFINITIONS_H_ + +#include "atmosphere/constants.h" +#include "math/angle.h" +#include "math/binary_function.h" +#include "math/scalar.h" +#include "math/scalar_function.h" +#include "math/ternary_function.h" +#include "math/vector.h" + +namespace atmosphere { +namespace reference { + +/* +

Physical quantities

+ +

The physical quantities we need for our atmosphere model are +radiometric and +photometric +quantities. We start with six base quantities: angle, length, wavelength, solid +angle, power and luminous power (wavelength is also a length, but we distinguish +the two for increased clarity). +*/ + +typedef dimensional::Angle Angle; +typedef dimensional::Scalar<1, 0, 0, 0, 0> Length; +typedef dimensional::Scalar<0, 1, 0, 0, 0> Wavelength; +typedef dimensional::Scalar<0, 0, 1, 0, 0> SolidAngle; +typedef dimensional::Scalar<0, 0, 0, 1, 0> Power; +typedef dimensional::Scalar<0, 0, 0, 0, 1> LuminousPower; + +/* +

From this we derive the irradiance, radiance, spectral irradiance, +spectral radiance, luminance, etc, as well pure numbers, area, volume, etc. +*/ + +typedef dimensional::Scalar<0, 0, 0, 0, 0> Number; +typedef dimensional::Scalar<2, 0, 0, 0, 0> Area; +typedef dimensional::Scalar<3, 0, 0, 0, 0> Volume; +typedef dimensional::Scalar<-2, 0, 0, 1, 0> Irradiance; +typedef dimensional::Scalar<-2, 0, -1, 1, 0> Radiance; +typedef dimensional::Scalar<0, -1, 0, 1, 0> SpectralPower; +typedef dimensional::Scalar<-2, -1, 0, 1, 0> SpectralIrradiance; +typedef dimensional::Scalar<-2, -1, -1, 1, 0> SpectralRadiance; +typedef dimensional::Scalar<-3, -1, -1, 1, 0> SpectralRadianceDensity; +typedef dimensional::Scalar<-1, 0, 0, 0, 0> ScatteringCoefficient; +typedef dimensional::Scalar<0, 0, -1, 0, 0> InverseSolidAngle; +typedef dimensional::Scalar<-3, 0, 0, 0, 0> NumberDensity; +typedef dimensional::Scalar<0, 0, -1, 0, 1> LuminousIntensity; +typedef dimensional::Scalar<-2, 0, -1, 0, 1> Luminance; +typedef dimensional::Scalar<-2, 0, 0, 0, 1> Illuminance; + +/* +

We also need vectors of physical quantities, mostly to represent functions +depending on the wavelength. In this case the vector elements correspond to +values of a function at some predefined wavelengths. Here we use 47 predefined +wavelengths, uniformly distributed between 360 and 830 nanometers: +*/ + +template +using WavelengthFunction = dimensional::ScalarFunction< + 0, 1, 0, 0, 0, U1, U2, U3, U4, U5, 47, 360, 830>; + +// A function from Wavelength to Number. +typedef WavelengthFunction<0, 0, 0, 0, 0> DimensionlessSpectrum; +// A function from Wavelength to SpectralPower. +typedef WavelengthFunction<0, -1, 0, 1, 0> PowerSpectrum; +// A function from Wavelength to SpectralIrradiance. +typedef WavelengthFunction<-2, -1, 0, 1, 0> IrradianceSpectrum; +// A function from Wavelength to SpectralRadiance. +typedef WavelengthFunction<-2, -1, -1, 1, 0> RadianceSpectrum; +// A function from Wavelength to SpectralRadianceDensity. +typedef WavelengthFunction<-3, -1, -1, 1, 0> RadianceDensitySpectrum; +// A function from Wavelength to ScaterringCoefficient. +typedef WavelengthFunction<-1, 0, 0, 0, 0> ScatteringSpectrum; + +// A position in 3D (3 length values). +typedef dimensional::Vector3 Position; +// A unit direction vector in 3D (3 unitless values). +typedef dimensional::Vector3 Direction; +// A vector of 3 luminance values. +typedef dimensional::Vector3 Luminance3; +// A vector of 3 illuminance values. +typedef dimensional::Vector3 Illuminance3; + +/* +

Finally, we also need precomputed textures containing physical quantities in +each texel (the texture sizes are defined in +constants.h): +*/ + +typedef dimensional::BinaryFunction< + TRANSMITTANCE_TEXTURE_WIDTH, + TRANSMITTANCE_TEXTURE_HEIGHT, + DimensionlessSpectrum> TransmittanceTexture; + +template +using AbstractScatteringTexture = dimensional::TernaryFunction< + SCATTERING_TEXTURE_WIDTH, + SCATTERING_TEXTURE_HEIGHT, + SCATTERING_TEXTURE_DEPTH, + T>; + +typedef AbstractScatteringTexture + ReducedScatteringTexture; + +typedef AbstractScatteringTexture + ScatteringTexture; + +typedef AbstractScatteringTexture + ScatteringDensityTexture; + +typedef dimensional::BinaryFunction< + IRRADIANCE_TEXTURE_WIDTH, + IRRADIANCE_TEXTURE_HEIGHT, + IrradianceSpectrum> IrradianceTexture; + +/* +

Physical units

+ +

We can then define the units for our base physical quantities: +radians (rad), meter (m), nanometer (nm), steradian (sr), watt (watt) and lumen +(lm): +*/ + +constexpr Angle rad = dimensional::rad; +constexpr Length m = Length::Unit(); +constexpr Wavelength nm = Wavelength::Unit(); +constexpr SolidAngle sr = SolidAngle::Unit(); +constexpr Power watt = Power::Unit(); +constexpr LuminousPower lm = LuminousPower::Unit(); + +/* +

From which we can derive the units for some derived physical quantities, +as well as some derived units (degress deg, kilometer km, kilocandela kcd): +*/ + +constexpr double PI = dimensional::PI; +constexpr Angle pi = dimensional::pi; +constexpr Angle deg = dimensional::deg; +constexpr Length km = 1000.0 * m; +constexpr Area m2 = m * m; +constexpr Volume m3 = m * m * m; +constexpr Irradiance watt_per_square_meter = watt / m2; +constexpr Radiance watt_per_square_meter_per_sr = watt / (m2 * sr); +constexpr SpectralIrradiance watt_per_square_meter_per_nm = watt / (m2 * nm); +constexpr SpectralRadiance watt_per_square_meter_per_sr_per_nm = + watt / (m2 * sr * nm); +constexpr SpectralRadianceDensity watt_per_cubic_meter_per_sr_per_nm = + watt / (m3 * sr * nm); +constexpr LuminousIntensity cd = lm / sr; +constexpr LuminousIntensity kcd = 1000.0 * cd; +constexpr Luminance cd_per_square_meter = cd / m2; +constexpr Luminance kcd_per_square_meter = kcd / m2; + +/* +

Atmosphere parameters

+ +

Using the above types, we can now define the parameters of our atmosphere +model: +*/ + +struct AtmosphereParameters { + // The solar irradiance at the top of the atmosphere. + IrradianceSpectrum solar_irradiance; + // The sun's angular radius. + Angle sun_angular_radius; + // The distance between the planet center and the bottom of the atmosphere. + Length bottom_radius; + // The distance between the planet center and the top of the atmosphere. + Length top_radius; + // The scale height of air molecules, meaning that their density is + // proportional to exp(-h / rayleigh_scale_height), with h the altitude + // (with the bottom of the atmosphere at altitude 0). + Length rayleigh_scale_height; + // The scattering coefficient of air molecules at the bottom of the + // atmosphere, as a function of wavelength. + ScatteringSpectrum rayleigh_scattering; + // The scale height of aerosols, meaning that their density is proportional + // to exp(-h / mie_scale_height), with h the altitude. + Length mie_scale_height; + // The scattering coefficient of aerosols at the bottom of the atmosphere, + // as a function of wavelength. + ScatteringSpectrum mie_scattering; + // The extinction coefficient of aerosols at the bottom of the atmosphere, + // as a function of wavelength. + ScatteringSpectrum mie_extinction; + // The asymetry parameter for the Cornette-Shanks phase function for the + // aerosols. + Number mie_phase_function_g; + // The average albedo of the ground. + DimensionlessSpectrum ground_albedo; + // The cosine of the maximum Sun zenith angle for which atmospheric scattering + // must be precomputed (for maximum precision, use the smallest Sun zenith + // angle yielding negligible sky light radiance values. For instance, for the + // Earth case, 102 degrees is a good choice - yielding mu_s_min = -0.2). + Number mu_s_min; +}; + +} // namespace reference +} // namespace atmosphere + +#endif // ATMOSPHERE_REFERENCE_DEFINITIONS_H_ diff --git a/atmosphere/reference/functions.cc b/atmosphere/reference/functions.cc new file mode 100644 index 0000000..708c948 --- /dev/null +++ b/atmosphere/reference/functions.cc @@ -0,0 +1,57 @@ +/** + * 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. + */ + +/*

atmosphere/reference/functions.cc

+ +

This file "provides" the C++ implementation of our atmosphere model. In fact +the real implementation is provided in the +corresponding GLSL file, which is included here, +after the definition of the macros which are needed to be able to compile this +GLSL code as C++. +*/ + +#include "atmosphere/reference/functions.h" + +#include + +#define IN(x) const x& +#define OUT(x) x& +#define TEMPLATE(x) template +#define TEMPLATE_ARGUMENT(x) + +namespace atmosphere { +namespace reference { + +using std::max; +using std::min; + +#include "atmosphere/functions.glsl" + +} // namespace reference +} // namespace atmosphere diff --git a/atmosphere/reference/functions.h b/atmosphere/reference/functions.h new file mode 100644 index 0000000..5b35f50 --- /dev/null +++ b/atmosphere/reference/functions.h @@ -0,0 +1,245 @@ +/** + * 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. + */ + +/*

atmosphere/reference/functions.h

+ +

This file provides a C++ header for the GLSL +functions that implement our atmosphere model. The C++ "implementation" is +provided in functions.cc (this file simply +includes the GLSL file after defining the macros it depends on). The +documentation is provided in the GLSL file. +*/ + +#ifndef ATMOSPHERE_REFERENCE_FUNCTIONS_H_ +#define ATMOSPHERE_REFERENCE_FUNCTIONS_H_ + +#include "atmosphere/reference/definitions.h" + +namespace atmosphere { +namespace reference { + +typedef dimensional::vec2 vec2; +typedef dimensional::vec3 vec3; +typedef dimensional::vec4 vec4; + +// Transmittance. + +Length DistanceToTopAtmosphereBoundary( + const AtmosphereParameters& atmosphere, Length r, Number mu); + +Length DistanceToBottomAtmosphereBoundary( + const AtmosphereParameters& atmosphere, Length r, Number mu); + +bool RayIntersectsGround( + const AtmosphereParameters& atmosphere, Length r, Number mu); + +Length ComputeOpticalLengthToTopAtmosphereBoundary( + const AtmosphereParameters& atmosphere, Length scale_height, + Length r, Number mu); + +DimensionlessSpectrum ComputeTransmittanceToTopAtmosphereBoundary( + const AtmosphereParameters& atmosphere, Length r, Number mu); + +Number GetTextureCoordFromUnitRange(Number x, int texture_size); + +Number GetUnitRangeFromTextureCoord(Number u, int texture_size); + +vec2 GetTransmittanceTextureUvFromRMu(const AtmosphereParameters& atmosphere, + Length r, Number mu); + +void GetRMuFromTransmittanceTextureUv(const AtmosphereParameters& atmosphere, + const vec2& uv, Length& r, Number& mu); + +DimensionlessSpectrum ComputeTransmittanceToTopAtmosphereBoundaryTexture( + const AtmosphereParameters& atmosphere, const vec2& gl_frag_coord); + +DimensionlessSpectrum GetTransmittanceToTopAtmosphereBoundary( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + Length r, Number mu); + +DimensionlessSpectrum GetTransmittance( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + Length r, Number mu, Length d, bool ray_r_mu_intersects_ground); + +// Single scattering. + +void ComputeSingleScatteringIntegrand( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + Length r, Number mu, Number mu_s, Number nu, Length d, + bool ray_r_mu_intersects_ground, + DimensionlessSpectrum& rayleigh, DimensionlessSpectrum& mie); + +Length DistanceToNearestAtmosphereBoundary( + const AtmosphereParameters& atmosphere, Length r, Number mu, + bool ray_r_mu_intersects_ground); + +void ComputeSingleScattering( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + Length r, Number mu, Number mu_s, Number nu, + bool ray_r_mu_intersects_ground, + IrradianceSpectrum& rayleigh, IrradianceSpectrum& mie); + +InverseSolidAngle RayleighPhaseFunction(Number nu); +InverseSolidAngle MiePhaseFunction(Number g, Number nu); + +vec4 GetScatteringTextureUvwzFromRMuMuSNu( + const AtmosphereParameters& atmosphere, + Length r, Number mu, Number mu_s, Number nu, + bool ray_r_mu_intersects_ground); + +void GetRMuMuSNuFromScatteringTextureUvwz( + const AtmosphereParameters& atmosphere, const vec4& uvwz, + Length& r, Number& mu, Number& mu_s, Number& nu, + bool& ray_r_mu_intersects_ground); + +void ComputeSingleScatteringTexture(const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const vec3& gl_frag_coord, IrradianceSpectrum& rayleigh, + IrradianceSpectrum& mie); + +template +T GetScattering( + const AtmosphereParameters& atmosphere, + const AbstractScatteringTexture& scattering_texture, + Length r, Number mu, Number mu_s, Number nu, + bool ray_r_mu_intersects_ground); + +RadianceSpectrum GetScattering( + const AtmosphereParameters& atmosphere, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + Length r, Number mu, Number mu_s, Number nu, + bool ray_r_mu_intersects_ground, + int scattering_order); + +// Multiple scattering. + +RadianceDensitySpectrum ComputeScatteringDensity( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + const IrradianceTexture& irradiance_texture, + Length r, Number mu, Number mu_s, Number nu, + int scattering_order); + +RadianceSpectrum ComputeMultipleScattering( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const ScatteringDensityTexture& scattering_density_texture, + Length r, Number mu, Number mu_s, Number nu, + bool ray_r_mu_intersects_ground); + +RadianceDensitySpectrum ComputeScatteringDensityTexture( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + const IrradianceTexture& irradiance_texture, + const vec3& gl_frag_coord, int scattering_order); + +RadianceSpectrum ComputeMultipleScatteringTexture( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const ScatteringDensityTexture& scattering_density_texture, + const vec3& gl_frag_coord, Number& nu); + +// Ground irradiance. + +IrradianceSpectrum ComputeDirectIrradiance( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + Length r, Number mu_s); + +IrradianceSpectrum ComputeIndirectIrradiance( + const AtmosphereParameters& atmosphere, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + Length r, Number mu_s, int scattering_order); + +vec2 GetIrradianceTextureUvFromRMuS(const AtmosphereParameters& atmosphere, + Length r, Number mu_s); + +void GetRMuSFromIrradianceTextureUv(const AtmosphereParameters& atmosphere, + const vec2& uv, Length& r, Number& mu_s); + +IrradianceSpectrum ComputeDirectIrradianceTexture( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const vec2& gl_frag_coord); + +IrradianceSpectrum ComputeIndirectIrradianceTexture( + const AtmosphereParameters& atmosphere, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + const vec2& gl_frag_coord, int scattering_order); + +IrradianceSpectrum GetIrradiance( + const AtmosphereParameters& atmosphere, + const IrradianceTexture& irradiance_texture, + Length r, Number mu_s); + +// Rendering. + +RadianceSpectrum GetSkyRadiance( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const ReducedScatteringTexture& scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + Position camera, const Direction& view_ray, Length shadow_length, + const Direction& sun_direction, DimensionlessSpectrum& transmittance); + +RadianceSpectrum GetSkyRadianceToPoint( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const ReducedScatteringTexture& scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + Position camera, const Position& point, Length shadow_length, + const Direction& sun_direction, DimensionlessSpectrum& transmittance); + +IrradianceSpectrum GetSunAndSkyIrradiance( + const AtmosphereParameters& atmosphere, + const TransmittanceTexture& transmittance_texture, + const IrradianceTexture& irradiance_texture, + const Position& point, const Direction& normal, + const Direction& sun_direction, IrradianceSpectrum& sky_irradiance); + +} // namespace reference +} // namespace atmosphere + +#endif // ATMOSPHERE_REFERENCE_FUNCTIONS_H_ diff --git a/atmosphere/reference/functions_test.cc b/atmosphere/reference/functions_test.cc new file mode 100644 index 0000000..6b80d52 --- /dev/null +++ b/atmosphere/reference/functions_test.cc @@ -0,0 +1,1505 @@ +/** + * 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. + */ + +/*

atmosphere/reference/functions_test.cc

+ +

This file provides unit tests for the GLSL +functions that implement our atmosphere model. We start with the definition +of some (arbitrary) values for the atmosphere parameters: +*/ + +#include "atmosphere/reference/functions.h" + +#include +#include + +#include "atmosphere/reference/definitions.h" +#include "atmosphere/constants.h" +#include "test/test_case.h" + +namespace atmosphere { +namespace reference { + +constexpr double kEpsilon = 1e-3; + +constexpr SpectralIrradiance kSolarIrradiance = + 123.0 * watt_per_square_meter_per_nm; +constexpr Length kBottomRadius = 1000.0 * km; +constexpr Length kTopRadius = 1500.0 * km; +constexpr Length kScaleHeight = 60.0 * km; +constexpr Length kRayleighScaleHeight = 60.0 * km; +constexpr Length kMieScaleHeight = 30.0 * km; +constexpr ScatteringCoefficient kRayleighScattering = 0.001 / km; +constexpr ScatteringCoefficient kMieScattering = 0.0015 / km; +constexpr ScatteringCoefficient kMieExtinction = 0.002 / km; +constexpr Number kGroundAlbedo = 0.1; + +/* +

This helper function computes the cosine of the angle between the zenith and +the horizon, at some radius r. We will use it to test rays just above or below +the horizon. +*/ + +Number CosineOfHorizonZenithAngle(Length r) { + assert(r >= kBottomRadius); + return -sqrt(1.0 - (kBottomRadius / r) * (kBottomRadius / r)); +} + +/* +

Some unit tests need a precomputed texture as input, but we don't want to +precompute a whole texture for that, for efficiency reasons. Our solution is to +provide lazily computed textures instead, i.e. textures whose texels are +computed the first time we try to read them. The first type of texture we need +is a lazy transmittance texture (negative values mean "not yet computed"): +*/ + +class LazyTransmittanceTexture : + public dimensional::BinaryFunction { + public: + explicit LazyTransmittanceTexture( + const AtmosphereParameters& atmosphere_parameters) + : BinaryFunction(DimensionlessSpectrum(-1.0)), + atmosphere_parameters_(atmosphere_parameters) { + } + + virtual const DimensionlessSpectrum& Get(int i, int j) const { + int index = i + j * TRANSMITTANCE_TEXTURE_HEIGHT; + if (value_[index][0]() < 0.0) { + value_[index] = ComputeTransmittanceToTopAtmosphereBoundaryTexture( + atmosphere_parameters_, vec2(i + 0.5, j + 0.5)); + } + return value_[index]; + } + + void Clear() { + constexpr unsigned int n = + TRANSMITTANCE_TEXTURE_WIDTH * TRANSMITTANCE_TEXTURE_HEIGHT; + for (unsigned int i = 0; i < n; ++i) { + value_[i] = DimensionlessSpectrum(-1.0); + } + } + + private: + const AtmosphereParameters& atmosphere_parameters_; +}; + +/* +

We also need a lazy single scattering texture: +*/ + +class LazySingleScatteringTexture : + public dimensional::TernaryFunction { + public: + LazySingleScatteringTexture( + const AtmosphereParameters& atmosphere_parameters, + const TransmittanceTexture& transmittance_texture, + bool rayleigh) + : TernaryFunction(IrradianceSpectrum(-watt_per_square_meter_per_nm)), + atmosphere_parameters_(atmosphere_parameters), + transmittance_texture_(transmittance_texture), + rayleigh_(rayleigh) { + } + + virtual const IrradianceSpectrum& Get(int i, int j, int k) const { + int index = + i + SCATTERING_TEXTURE_WIDTH * (j + SCATTERING_TEXTURE_HEIGHT * k); + if (value_[index][0] < 0.0 * watt_per_square_meter_per_nm) { + IrradianceSpectrum rayleigh; + IrradianceSpectrum mie; + ComputeSingleScatteringTexture(atmosphere_parameters_, + transmittance_texture_, vec3(i + 0.5, j + 0.5, k + 0.5), + rayleigh, mie); + value_[index] = rayleigh_ ? rayleigh : mie; + } + return value_[index]; + } + + private: + const AtmosphereParameters& atmosphere_parameters_; + const TransmittanceTexture& transmittance_texture_; + const bool rayleigh_; +}; + +/* +

a lazy multiple scattering texture, for step 1: +*/ + +class LazyScatteringDensityTexture : + public dimensional::TernaryFunction { + public: + LazyScatteringDensityTexture( + const AtmosphereParameters& atmosphere_parameters, + const TransmittanceTexture& transmittance_texture, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + const IrradianceTexture& irradiance_texture, + const int order) + : TernaryFunction( + RadianceDensitySpectrum(-watt_per_cubic_meter_per_sr_per_nm)), + atmosphere_parameters_(atmosphere_parameters), + transmittance_texture_(transmittance_texture), + single_rayleigh_scattering_texture_(single_rayleigh_scattering_texture), + single_mie_scattering_texture_(single_mie_scattering_texture), + multiple_scattering_texture_(multiple_scattering_texture), + irradiance_texture_(irradiance_texture), + order_(order) { + } + + virtual const RadianceDensitySpectrum& Get(int i, int j, int k) const { + int index = + i + SCATTERING_TEXTURE_WIDTH * (j + SCATTERING_TEXTURE_HEIGHT * k); + if (value_[index][0] < 0.0 * watt_per_cubic_meter_per_sr_per_nm) { + value_[index] = ComputeScatteringDensityTexture( + atmosphere_parameters_, transmittance_texture_, + single_rayleigh_scattering_texture_, single_mie_scattering_texture_, + multiple_scattering_texture_, irradiance_texture_, + vec3(i + 0.5, j + 0.5, k + 0.5), order_); + } + return value_[index]; + } + + private: + const AtmosphereParameters& atmosphere_parameters_; + const TransmittanceTexture& transmittance_texture_; + const ReducedScatteringTexture& single_rayleigh_scattering_texture_; + const ReducedScatteringTexture& single_mie_scattering_texture_; + const ScatteringTexture& multiple_scattering_texture_; + const IrradianceTexture& irradiance_texture_; + const int order_; +}; + + +/* +

and step 2 of the multiple scattering computations: +*/ + +class LazyMultipleScatteringTexture : + public dimensional::TernaryFunction { + public: + LazyMultipleScatteringTexture( + const AtmosphereParameters& atmosphere_parameters, + const TransmittanceTexture& transmittance_texture, + const ScatteringDensityTexture& scattering_density_texture) + : TernaryFunction(RadianceSpectrum(-watt_per_square_meter_per_sr_per_nm)), + atmosphere_parameters_(atmosphere_parameters), + transmittance_texture_(transmittance_texture), + scattering_density_texture_(scattering_density_texture) { + } + + virtual const RadianceSpectrum& Get(int i, int j, int k) const { + int index = + i + SCATTERING_TEXTURE_WIDTH * (j + SCATTERING_TEXTURE_HEIGHT * k); + if (value_[index][0] < 0.0 * watt_per_square_meter_per_sr_per_nm) { + Number ignored; + value_[index] = ComputeMultipleScatteringTexture(atmosphere_parameters_, + transmittance_texture_, scattering_density_texture_, + vec3(i + 0.5, j + 0.5, k + 0.5), ignored); + } + return value_[index]; + } + + private: + const AtmosphereParameters& atmosphere_parameters_; + const TransmittanceTexture& transmittance_texture_; + const ScatteringDensityTexture& scattering_density_texture_; +}; + +/* +

and, finally, a lazy ground irradiance texture: +*/ + +class LazyIndirectIrradianceTexture : + public dimensional::BinaryFunction { + public: + LazyIndirectIrradianceTexture( + const AtmosphereParameters& atmosphere_parameters, + const ReducedScatteringTexture& single_rayleigh_scattering_texture, + const ReducedScatteringTexture& single_mie_scattering_texture, + const ScatteringTexture& multiple_scattering_texture, + int scattering_order) + : BinaryFunction(IrradianceSpectrum(-watt_per_square_meter_per_nm)), + atmosphere_parameters_(atmosphere_parameters), + single_rayleigh_scattering_texture_(single_rayleigh_scattering_texture), + single_mie_scattering_texture_(single_mie_scattering_texture), + multiple_scattering_texture_(multiple_scattering_texture), + scattering_order_(scattering_order) { + } + + virtual const IrradianceSpectrum& Get(int i, int j) const { + int index = i + j * IRRADIANCE_TEXTURE_HEIGHT; + if (value_[index][0] < 0.0 * watt_per_square_meter_per_nm) { + value_[index] = ComputeIndirectIrradianceTexture(atmosphere_parameters_, + single_rayleigh_scattering_texture_, + single_mie_scattering_texture_, + multiple_scattering_texture_, + vec2(i + 0.5, j + 0.5), + scattering_order_); + } + return value_[index]; + } + + private: + const AtmosphereParameters& atmosphere_parameters_; + const ReducedScatteringTexture& single_rayleigh_scattering_texture_; + const ReducedScatteringTexture& single_mie_scattering_texture_; + const ScatteringTexture& multiple_scattering_texture_; + int scattering_order_; +}; + +/* +

We can now define the unit tests themselves. Each test is an instance of the +following TestCase subclass, which has an +atmosphere_parameters_ field initialized from the above constants. +Note that a new instance of this class is created for each unit test. +*/ + +class FunctionsTest : public dimensional::TestCase { + public: + template + FunctionsTest(const std::string& name, T test) + : TestCase("FunctionsTest " + name, static_cast(test)) { + atmosphere_parameters_.solar_irradiance[0] = kSolarIrradiance; + atmosphere_parameters_.bottom_radius = kBottomRadius; + atmosphere_parameters_.top_radius = kTopRadius; + atmosphere_parameters_.rayleigh_scale_height = kRayleighScaleHeight; + atmosphere_parameters_.rayleigh_scattering[0] = kRayleighScattering; + atmosphere_parameters_.mie_scale_height = kMieScaleHeight; + atmosphere_parameters_.mie_scattering[0] = kMieScattering; + atmosphere_parameters_.mie_extinction[0] = kMieExtinction; + atmosphere_parameters_.ground_albedo[0] = kGroundAlbedo; + atmosphere_parameters_.mu_s_min = -1.0; + } + +/* +

Distance to the top atmosphere boundary: check that this distance is +$r_\mathrm{top}-r$ for a vertical ray ($\mu=1$), and +$\sqrt{r_\mathrm{top}^2-r^2}$ for a horizontal ray ($\mu=0$). +*/ + + void TestDistanceToTopAtmosphereBoundary() { + constexpr Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + // Vertical ray, looking top. + ExpectNear( + kTopRadius - r, + DistanceToTopAtmosphereBoundary(atmosphere_parameters_, r, 1.0), + 1.0 * m); + // Horizontal ray. + ExpectNear( + sqrt(kTopRadius * kTopRadius - r * r), + DistanceToTopAtmosphereBoundary(atmosphere_parameters_, r, 0.0), + 1.0 * m); + } + +/* +

Intersections with the ground: check that a vertical ray does not +intersect the ground, unless it is looking down. Likewise, check that a ray +looking slightly above the horizon ($\mu=\mu_{\mathrm{horiz}}+\epsilon$) does +not intersect the ground, but a ray looking slightly below the horizon +($\mu=\mu_{\mathrm{horiz}}-\epsilon$) does. +*/ + + void TestRayIntersectsGround() { + constexpr Length r = kBottomRadius * 0.9 + kTopRadius * 0.1; + Number mu_horizon = CosineOfHorizonZenithAngle(r); + ExpectFalse(RayIntersectsGround(atmosphere_parameters_, r, 1.0)); + ExpectFalse(RayIntersectsGround( + atmosphere_parameters_, r, mu_horizon + kEpsilon)); + ExpectTrue(RayIntersectsGround( + atmosphere_parameters_, r, mu_horizon - kEpsilon)); + ExpectTrue(RayIntersectsGround(atmosphere_parameters_, r, -1.0)); + } + +/* +

Optical length to the top atmosphere boundary: check that for a +vertical ray, looking up, the numerical integration in +ComputeOpticalLengthToTopAtmosphereBoundary gives the expected +result, which can be computed analytically: +$$ +\int_r^{r_\mathrm{top}} + \exp\left(-\frac{x-r_\mathrm{bottom}}{K}\right)\mathrm{d}x = +K\left[\exp\left(-\frac{r-r_\mathrm{bottom}}{K}\right)- +\exp\left(-\frac{r_\mathrm{top}-r_\mathrm{bottom}}{K}\right)\right] +$$ +where $K$ is the scale height. Likewise, check that for $K=\infty$ the optical +length to the top atmosphere boundary is the distance to the top atmosphere +boundary (using a horizontal ray). +*/ + + void TestComputeOpticalLengthToTopAtmosphereBoundary() { + constexpr Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + constexpr Length h_r = r - kBottomRadius; + constexpr Length h_top = kTopRadius - kBottomRadius; + // Vertical ray, looking top. + ExpectNear( + kScaleHeight * (exp(-h_r / kScaleHeight) - exp(-h_top / kScaleHeight)), + ComputeOpticalLengthToTopAtmosphereBoundary( + atmosphere_parameters_, kScaleHeight, r, 1.0), + 1.0 * m); + // Horizontal ray, no exponential density fall off. + ExpectNear( + sqrt(kTopRadius * kTopRadius - r * r), + ComputeOpticalLengthToTopAtmosphereBoundary(atmosphere_parameters_, + std::numeric_limits::infinity() * km, r, 0.0), + 1.0 * m); + } + +/* +

Transmittance to the top atmosphere boundary: check that for a +vertical ray, looking up, the numerical integration in +ComputeTransmittanceToTopAtmosphereBoundary gives the expected +result, which can be computed analytically (using the above equation for the +optical length). Likewise, check that for a horizontal ray, without Mie +scattering, and with a uniform density of air molecules (Rayleigh scale height +set to $\infty$), the optical depth is the Rayleigh scattering coefficient times +the distance to the top atmospere boundary. +*/ + + void TestComputeTransmittanceToTopAtmosphereBoundary() { + constexpr Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + constexpr Length h_r = r - kBottomRadius; + constexpr Length h_top = kTopRadius - kBottomRadius; + // Vertical ray, looking up. + Number rayleigh_optical_depth = kRayleighScattering * kRayleighScaleHeight * + (exp(-h_r / kRayleighScaleHeight) - exp(-h_top / kRayleighScaleHeight)); + Number mie_optical_depth = kMieExtinction * kMieScaleHeight * + (exp(-h_r / kMieScaleHeight) - exp(-h_top / kMieScaleHeight)); + ExpectNear( + exp(-(rayleigh_optical_depth + mie_optical_depth)), + ComputeTransmittanceToTopAtmosphereBoundary( + atmosphere_parameters_, r, 1.0)[0], + Number(kEpsilon)); + // Horizontal ray, uniform atmosphere without aerosols. + SetUniformAtmosphere(); + RemoveAerosols(); + ExpectNear( + exp(-kRayleighScattering * sqrt(kTopRadius * kTopRadius - r * r)), + ComputeTransmittanceToTopAtmosphereBoundary( + atmosphere_parameters_, r, 0.0)[0], + Number(kEpsilon)); + } + +/* +

Texture coordinates: check that for a texture of size $n$, the center +of texel 0 (at texel coordinate $0.5/n$) is mapped to 0, and that the center of +texel $n-1$ (at texel coordinate $(n-0.5)/n$) is mapped to 1 (and vice-versa). +Finally, check that the mapping function and its inverse are really inverse of +each other (i.e. their composition should give the identity function). +*/ + + void TestGetTextureCoordFromUnitRange() { + ExpectNear(0.5 / 10.0, GetTextureCoordFromUnitRange(0.0, 10)(), kEpsilon); + ExpectNear(9.5 / 10.0, GetTextureCoordFromUnitRange(1.0, 10)(), kEpsilon); + + ExpectNear(0.0, GetUnitRangeFromTextureCoord(0.5 / 10.0, 10)(), kEpsilon); + ExpectNear(1.0, GetUnitRangeFromTextureCoord(9.5 / 10.0, 10)(), kEpsilon); + + ExpectNear(1.0 / 3.0, GetUnitRangeFromTextureCoord( + GetTextureCoordFromUnitRange(1.0 / 3.0, 10), 10)(), kEpsilon); + } + +/* +

Mapping to transmittance texture coordinates: check that the boundary +values of $r$ ($r_\mathrm{bottom}$ and $r_\mathrm{top}$) and $\mu$ +($\mu_\mathrm{horiz}$ and $1$) are mapped to the centers of the boundary texels +of the transmittance texture. +*/ + + void TestGetTransmittanceTextureUvFromRMu() { + vec2 uv = GetTransmittanceTextureUvFromRMu( + atmosphere_parameters_, kTopRadius, 1.0); + ExpectNear(0.5 / TRANSMITTANCE_TEXTURE_WIDTH, uv.x(), kEpsilon); + ExpectNear(1.0 - 0.5 / TRANSMITTANCE_TEXTURE_HEIGHT, uv.y(), kEpsilon); + + Number top_mu_horizon = CosineOfHorizonZenithAngle(kTopRadius); + uv = GetTransmittanceTextureUvFromRMu( + atmosphere_parameters_, kTopRadius, top_mu_horizon); + ExpectNear(1.0 - 0.5 / TRANSMITTANCE_TEXTURE_WIDTH, uv.x(), kEpsilon); + ExpectNear(1.0 - 0.5 / TRANSMITTANCE_TEXTURE_HEIGHT, uv.y(), kEpsilon); + + uv = GetTransmittanceTextureUvFromRMu( + atmosphere_parameters_, kBottomRadius, 1.0); + ExpectNear(0.5 / TRANSMITTANCE_TEXTURE_WIDTH, uv.x(), kEpsilon); + ExpectNear(0.5 / TRANSMITTANCE_TEXTURE_HEIGHT, uv.y(), kEpsilon); + + uv = GetTransmittanceTextureUvFromRMu( + atmosphere_parameters_, kBottomRadius, 0.0); + ExpectNear(1.0 - 0.5 / TRANSMITTANCE_TEXTURE_WIDTH, uv.x(), kEpsilon); + ExpectNear(0.5 / TRANSMITTANCE_TEXTURE_HEIGHT, uv.y(), kEpsilon); + } + +/* +

Mapping from transmittance texture coordinates: check that the centers +of the boundary texels of the transmittance texture are mapped to the boundary +values of $r$ ($r_\mathrm{bottom}$ and $r_\mathrm{top}$) and $\mu$ +($\mu_{\mathrm{horiz}}$ and $1$). Finally, check that the mapping function and +its inverse are really inverse of each other (i.e. their composition should give +the identity function). +*/ + + void TestGetRMuFromTransmittanceTextureUv() { + Length r; + Number mu; + GetRMuFromTransmittanceTextureUv( + atmosphere_parameters_, + vec2(0.5 / TRANSMITTANCE_TEXTURE_WIDTH, + 1.0 - 0.5 / TRANSMITTANCE_TEXTURE_HEIGHT), + r, mu); + ExpectNear(kTopRadius, r, 1.0 * m); + ExpectNear(1.0, mu(), kEpsilon); + + GetRMuFromTransmittanceTextureUv( + atmosphere_parameters_, + vec2(1.0 - 0.5 / TRANSMITTANCE_TEXTURE_WIDTH, + 1.0 - 0.5 / TRANSMITTANCE_TEXTURE_HEIGHT), + r, mu); + ExpectNear(kTopRadius, r, 1.0 * m); + ExpectNear( + CosineOfHorizonZenithAngle(kTopRadius)(), + mu(), + kEpsilon); + + GetRMuFromTransmittanceTextureUv( + atmosphere_parameters_, + vec2(0.5 / TRANSMITTANCE_TEXTURE_WIDTH, + 0.5 / TRANSMITTANCE_TEXTURE_HEIGHT), + r, mu); + ExpectNear(kBottomRadius, r, 1.0 * m); + ExpectNear(1.0, mu(), kEpsilon); + + GetRMuFromTransmittanceTextureUv( + atmosphere_parameters_, + vec2(1.0 - 0.5 / TRANSMITTANCE_TEXTURE_WIDTH, + 0.5 / TRANSMITTANCE_TEXTURE_HEIGHT), + r, mu); + ExpectNear(kBottomRadius, r, 1.0 * m); + ExpectNear(0.0, mu(), kEpsilon); + + GetRMuFromTransmittanceTextureUv( + atmosphere_parameters_, + GetTransmittanceTextureUvFromRMu(atmosphere_parameters_, + kBottomRadius * 0.2 + kTopRadius * 0.8, 0.25), + r, mu); + ExpectNear(kBottomRadius * 0.2 + kTopRadius * 0.8, r, 1.0 * m); + ExpectNear(0.25, mu(), kEpsilon); + } + +/* +

Transmittance texture: check that we get the same transmittance to the +top atmosphere boundary (more or less $\epsilon$) whether we compute it directly +with ComputeTransmittanceToTopAtmosphereBoundary, or via a +bilinearly interpolated lookup in the precomputed transmittance texture. +*/ + + void TestGetTransmittanceToTopAtmosphereBoundary() { + LazyTransmittanceTexture transmittance_texture(atmosphere_parameters_); + + const Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + const Number mu = 0.4; + ExpectNear( + ComputeTransmittanceToTopAtmosphereBoundary( + atmosphere_parameters_, r, mu)[0], + GetTransmittanceToTopAtmosphereBoundary( + atmosphere_parameters_, transmittance_texture, r, mu)[0], + Number(kEpsilon)); + } + +/* +

Transmittance texture: check that GetTransmittance (which +combines two bilinearly interpolated lookups in the precomputed transmittance +texture) gives the expected result, in some cases where this result can be +computed analytically (when there are no aerosols and when the density of air +molecules is uniform, the optical depth is simply the Rayleigh scattering +coefficient times the distance travelled in the atmosphere). +*/ + + void TestComputeAndGetTransmittance() { + SetUniformAtmosphere(); + RemoveAerosols(); + LazyTransmittanceTexture transmittance_texture(atmosphere_parameters_); + + const Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + const Length d = (kTopRadius - kBottomRadius) * 0.1; + // Horizontal ray, from bottom atmosphere boundary. + ExpectNear( + exp(-kRayleighScattering * d), + GetTransmittance(atmosphere_parameters_, transmittance_texture, + kBottomRadius, 0.0, d, false /* ray_intersects_ground */)[0], + Number(kEpsilon)); + // Almost vertical ray, looking up. + ExpectNear( + exp(-kRayleighScattering * d), + GetTransmittance( + atmosphere_parameters_, transmittance_texture, r, 0.7, d, + false /* ray_intersects_ground */)[0], + Number(kEpsilon)); + // Almost vertical ray, looking down. + ExpectNear( + exp(-kRayleighScattering * d), + GetTransmittance( + atmosphere_parameters_, transmittance_texture, r, -0.7, d, + RayIntersectsGround(atmosphere_parameters_, r, -0.7))[0], + Number(kEpsilon)); + } + +/* +

Single scattering integrand: check that the computation in +ComputeSingleScatteringIntegrand (which uses a precomputed +transmittance texture) gives the expected result, in 3 cases where this result +can be computed analytically: +

+*/ + + void TestComputeSingleScatteringIntegrand() { + LazyTransmittanceTexture transmittance_texture(atmosphere_parameters_); + + // Vertical ray, from bottom to top atmosphere boundary, scattering at + // middle of ray, scattering angle equal to 0. + const Length h_top = kTopRadius - kBottomRadius; + const Length h = h_top / 2.0; + DimensionlessSpectrum rayleigh; + DimensionlessSpectrum mie; + ComputeSingleScatteringIntegrand( + atmosphere_parameters_, transmittance_texture, + kBottomRadius, 1.0, 1.0, 1.0, h, false, rayleigh, mie); + Number rayleigh_optical_depth = kRayleighScattering * kRayleighScaleHeight * + (1.0 - exp(-h_top / kRayleighScaleHeight)); + Number mie_optical_depth = kMieExtinction * kMieScaleHeight * + (1.0 - exp(-h_top / kMieScaleHeight)); + ExpectNear( + exp(-rayleigh_optical_depth - mie_optical_depth) * + exp(-h / kRayleighScaleHeight), + rayleigh[0], + Number(kEpsilon)); + ExpectNear( + exp(-rayleigh_optical_depth - mie_optical_depth) * + exp(-h / kMieScaleHeight), + mie[0], + Number(kEpsilon)); + + // Vertical ray, top to middle of atmosphere, scattering angle 180 degrees. + ComputeSingleScatteringIntegrand( + atmosphere_parameters_, transmittance_texture, + kTopRadius, -1.0, 1.0, -1.0, h, true, rayleigh, mie); + rayleigh_optical_depth = 2.0 * kRayleighScattering * kRayleighScaleHeight * + (exp(-h / kRayleighScaleHeight) - exp(-h_top / kRayleighScaleHeight)); + mie_optical_depth = 2.0 * kMieExtinction * kMieScaleHeight * + (exp(-h / kMieScaleHeight) - exp(-h_top / kMieScaleHeight)); + ExpectNear( + exp(-rayleigh_optical_depth - mie_optical_depth) * + exp(-h / kRayleighScaleHeight), + rayleigh[0], + Number(kEpsilon)); + ExpectNear( + exp(-rayleigh_optical_depth - mie_optical_depth) * + exp(-h / kMieScaleHeight), + mie[0], + Number(kEpsilon)); + + // Horizontal ray, from bottom to top atmosphere boundary, scattering at + // 50km, scattering angle equal to 0, uniform atmosphere, no aerosols. + transmittance_texture.Clear(); + SetUniformAtmosphere(); + RemoveAerosols(); + ComputeSingleScatteringIntegrand( + atmosphere_parameters_, transmittance_texture, + kBottomRadius, 0.0, 0.0, 1.0, 50.0 * km, false, rayleigh, mie); + rayleigh_optical_depth = kRayleighScattering * sqrt( + kTopRadius * kTopRadius - kBottomRadius * kBottomRadius); + ExpectNear( + exp(-rayleigh_optical_depth), + rayleigh[0], + Number(kEpsilon)); + } + +/* +

Distance to the nearest atmosphere boundary: check that this distance +is $r_\mathrm{top}-r$ for a vertical ray looking up ($\mu=1$), +$\sqrt{r_\mathrm{top}^2-r^2}$ for a horizontal ray ($\mu=0$), and +$r-r_\mathrm{bottom}$ for a vertical ray looking down ($\mu=-1$). +*/ + + void TestDistanceToNearestAtmosphereBoundary() { + constexpr Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + // Vertical ray, looking top. + ExpectNear( + kTopRadius - r, + DistanceToNearestAtmosphereBoundary( + atmosphere_parameters_, r, 1.0, + RayIntersectsGround(atmosphere_parameters_, r, 1.0)), + 1.0 * m); + // Horizontal ray. + ExpectNear( + sqrt(kTopRadius * kTopRadius - r * r), + DistanceToNearestAtmosphereBoundary( + atmosphere_parameters_, r, 0.0, + RayIntersectsGround(atmosphere_parameters_, r, 0.0)), + 1.0 * m); + // Vertical ray, looking down. + ExpectNear( + r - kBottomRadius, + DistanceToNearestAtmosphereBoundary( + atmosphere_parameters_, r, -1.0, + RayIntersectsGround(atmosphere_parameters_, r, -1.0)), + 1.0 * m); + } + +/* +

Single scattering: check that the numerical integration in +ComputeSingleScattering gives the expected result, in 2 cases where +the integral can be computed analytically: +

+*/ + + void TestComputeSingleScattering() { + LazyTransmittanceTexture transmittance_texture(atmosphere_parameters_); + + // Vertical ray, from bottom atmosphere boundary, scattering angle 0. + const Length h_top = kTopRadius - kBottomRadius; + IrradianceSpectrum rayleigh; + IrradianceSpectrum mie; + ComputeSingleScattering( + atmosphere_parameters_, transmittance_texture, + kBottomRadius, 1.0, 1.0, 1.0, false, rayleigh, mie); + Number rayleigh_optical_depth = kRayleighScattering * kRayleighScaleHeight * + (1.0 - exp(-h_top / kRayleighScaleHeight)); + Number mie_optical_depth = kMieExtinction * kMieScaleHeight * + (1.0 - exp(-h_top / kMieScaleHeight)); + // The relative error is about 1% here. + ExpectNear( + Number(1.0), + rayleigh[0] / (kSolarIrradiance * rayleigh_optical_depth * + exp(-rayleigh_optical_depth - mie_optical_depth)), + Number(10.0 * kEpsilon)); + ExpectNear( + Number(1.0), + mie[0] / (kSolarIrradiance * mie_optical_depth * kMieScattering / + kMieExtinction * exp(-rayleigh_optical_depth - mie_optical_depth)), + Number(10.0 * kEpsilon)); + + // Vertical ray, from top atmosphere boundary, scattering angle 180 degrees, + // no aerosols. + transmittance_texture.Clear(); + RemoveAerosols(); + ComputeSingleScattering( + atmosphere_parameters_, transmittance_texture, + kTopRadius, -1.0, 1.0, -1.0, true, rayleigh, mie); + ExpectNear( + Number(1.0), + rayleigh[0] / (kSolarIrradiance * + 0.5 * (1.0 - exp(-2.0 * kRayleighScaleHeight * kRayleighScattering * + (1.0 - exp(-h_top / kRayleighScaleHeight))))), + Number(2.0 * kEpsilon)); + ExpectNear(0.0, mie[0].to(watt_per_square_meter_per_nm), kEpsilon); + } + +/* +

Rayleigh and Mie phase functions: check that the integral of these +phase functions over all solid angles gives $1$. +*/ + + void TestPhaseFunctions() { + Number rayleigh_integral = 0.0; + Number mie_integral = 0.0; + const unsigned int N = 100; + for (unsigned int i = 0; i < N; ++i) { + Angle theta = (i + 0.5) * pi / N; + SolidAngle domega = sin(theta) * (PI / N) * (2.0 * PI) * sr; + rayleigh_integral = + rayleigh_integral + RayleighPhaseFunction(cos(theta)) * domega; + mie_integral = + mie_integral + MiePhaseFunction(0.8, cos(theta)) * domega; + } + ExpectNear(1.0, rayleigh_integral(), 2.0 * kEpsilon); + ExpectNear(1.0, mie_integral(), 2.0 * kEpsilon); + } + +/* +

Mapping to scattering texture coordinates: check that the boundary +values of $r$ ($r_\mathrm{top}$, $r_\mathrm{bottom}$), $\mu$ ($-1$, +$\mu_{\mathrm{horiz}}$ and $1$), and $\mu_s$ and $\nu$ ($-1$ and $1$), are +mapped to the centers of the boundary texels of the scattering texture. +*/ + + void TestGetScatteringTextureUvwzFromRMuMuSNu() { + ExpectNear( + 0.5 / SCATTERING_TEXTURE_R_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kBottomRadius, 0.0, 0.0, 0.0, false).w(), + kEpsilon); + ExpectNear( + 1.0 - 0.5 / SCATTERING_TEXTURE_R_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kTopRadius, 0.0, 0.0, 0.0, false).w(), + kEpsilon); + + const Length r = (kTopRadius + kBottomRadius) / 2.0; + const Number mu = CosineOfHorizonZenithAngle(r); + ExpectNear( + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, r, mu, 0.0, 0.0, true).z(), + kEpsilon); + ExpectNear( + 1.0 - 0.5 / SCATTERING_TEXTURE_MU_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, r, mu, 0.0, 0.0, false).z(), + kEpsilon); + ExpectTrue(GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, r, -1.0, 0.0, 0.0, true).z() < 0.5); + ExpectTrue(GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, r, 1.0, 0.0, 0.0, false).z() > 0.5); + + ExpectNear( + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kBottomRadius, 0.0, -1.0, 0.0, false).y(), + kEpsilon); + ExpectNear( + 1.0 - 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kBottomRadius, 0.0, 1.0, 0.0, false).y(), + kEpsilon); + + ExpectNear( + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kTopRadius, 0.0, -1.0, 0.0, false).y(), + kEpsilon); + ExpectNear( + 1.0 - 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kTopRadius, 0.0, 1.0, 0.0, false).y(), + kEpsilon); + + ExpectNear( + 0.0, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kBottomRadius, 0.0, 0.0, -1.0, false).x(), + kEpsilon); + ExpectNear( + 1.0, + GetScatteringTextureUvwzFromRMuMuSNu( + atmosphere_parameters_, kBottomRadius, 0.0, 0.0, 1.0, false).x(), + kEpsilon); + } + +/* +

Mapping from scattering texture coordinates: check that the centers of +the boundary texels of the scattering texture are mapped to the boundary values +of $r$ ($r_\mathrm{top}$, $r_\mathrm{bottom}$), $\mu$ ($-1$, +$\mu_{\mathrm{horiz}}$ and $1$), and $\mu_s$ and $\nu$ ($-1$ and $1$). Finally, +check that the mapping function and its inverse are really inverse of each other +(i.e. their composition should give the identity function). +*/ + + void TestGetRMuMuSNuFromScatteringTextureUvwz() { + Length r; + Number mu; + Number mu_s; + Number nu; + bool ray_r_mu_intersects_ground; + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + 0.5 / SCATTERING_TEXTURE_R_SIZE), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(kBottomRadius, r, 1.0 * m); + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + 1.0 - 0.5 / SCATTERING_TEXTURE_R_SIZE), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(kTopRadius, r, 1.0 * m); + + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE + kEpsilon, + 0.5), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + const Number mu_horizon = CosineOfHorizonZenithAngle(r); + ExpectNear(mu_horizon, mu, Number(kEpsilon)); + ExpectTrue(mu <= mu_horizon); + ExpectTrue(ray_r_mu_intersects_ground); + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 1.0 - 0.5 / SCATTERING_TEXTURE_MU_SIZE - kEpsilon, + 0.5), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(mu_horizon, mu, Number(5.0 * kEpsilon)); + ExpectTrue(mu >= mu_horizon); + ExpectFalse(ray_r_mu_intersects_ground); + + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + 0.5 / SCATTERING_TEXTURE_R_SIZE), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(-1.0, mu_s(), kEpsilon); + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 1.0 - 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + 0.5 / SCATTERING_TEXTURE_R_SIZE), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(1.0, mu_s(), kEpsilon); + + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(0.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + 0.5 / SCATTERING_TEXTURE_R_SIZE), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(-1.0, nu(), kEpsilon); + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + vec4(1.0, + 0.5 / SCATTERING_TEXTURE_MU_S_SIZE, + 0.5 / SCATTERING_TEXTURE_MU_SIZE, + 0.5 / SCATTERING_TEXTURE_R_SIZE), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(1.0, nu(), kEpsilon); + + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + GetScatteringTextureUvwzFromRMuMuSNu(atmosphere_parameters_, + kBottomRadius, -1.0, 1.0, -1.0, true), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(kBottomRadius, r, 1.0 * m); + ExpectNear(-1.0, mu(), kEpsilon); + ExpectNear(1.0, mu_s(), kEpsilon); + ExpectNear(-1.0, nu(), kEpsilon); + ExpectTrue(ray_r_mu_intersects_ground); + + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + GetScatteringTextureUvwzFromRMuMuSNu(atmosphere_parameters_, + kTopRadius, -1.0, 1.0, -1.0, true), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear(kTopRadius, r, 1.0 * m); + ExpectNear(-1.0, mu(), kEpsilon); + ExpectNear(1.0, mu_s(), kEpsilon); + ExpectNear(-1.0, nu(), kEpsilon); + ExpectTrue(ray_r_mu_intersects_ground); + + GetRMuMuSNuFromScatteringTextureUvwz(atmosphere_parameters_, + GetScatteringTextureUvwzFromRMuMuSNu(atmosphere_parameters_, + (kBottomRadius + kTopRadius) / 2.0, 0.2, 0.3, 0.4, false), + r, mu, mu_s, nu, ray_r_mu_intersects_ground); + ExpectNear((kBottomRadius + kTopRadius) / 2.0, r, 1.0 * m); + ExpectNear(0.2, mu(), kEpsilon); + ExpectNear(0.3, mu_s(), kEpsilon); + ExpectNear(0.4, nu(), kEpsilon); + ExpectFalse(ray_r_mu_intersects_ground); + } + +/* +

Single scattering texture: check that we get the same single +scattering value (more or less $\epsilon$) whether we compute it directly +with ComputeSingleScattering, or via a +quadrilinearly interpolated lookup in the precomputed single scattering texture. +*/ + + void TestComputeAndGetSingleScattering() { + LazyTransmittanceTexture transmittance_texture(atmosphere_parameters_); + LazySingleScatteringTexture single_rayleigh_scattering_texture( + atmosphere_parameters_, transmittance_texture, true); + LazySingleScatteringTexture single_mie_scattering_texture( + atmosphere_parameters_, transmittance_texture, false); + + // Vertical ray, from bottom atmosphere boundary, scattering angle 0. + IrradianceSpectrum rayleigh = GetScattering( + atmosphere_parameters_, single_rayleigh_scattering_texture, + kBottomRadius, 1.0, 1.0, 1.0, false); + IrradianceSpectrum mie = GetScattering( + atmosphere_parameters_, single_mie_scattering_texture, + kBottomRadius, 1.0, 1.0, 1.0, false); + IrradianceSpectrum expected_rayleigh; + IrradianceSpectrum expected_mie; + ComputeSingleScattering( + atmosphere_parameters_, transmittance_texture, + kBottomRadius, 1.0, 1.0, 1.0, false, expected_rayleigh, expected_mie); + ExpectNear(1.0, (rayleigh / expected_rayleigh)[0](), kEpsilon); + ExpectNear(1.0, (mie / expected_mie)[0](), kEpsilon); + + // Vertical ray, from top atmosphere boundary, scattering angle 180 degrees. + rayleigh = GetScattering( + atmosphere_parameters_, single_rayleigh_scattering_texture, + kTopRadius, -1.0, 1.0, -1.0, true); + mie = GetScattering( + atmosphere_parameters_, single_mie_scattering_texture, + kTopRadius, -1.0, 1.0, -1.0, true); + ComputeSingleScattering( + atmosphere_parameters_, transmittance_texture, + kTopRadius, -1.0, 1.0, -1.0, true, expected_rayleigh, expected_mie); + ExpectNear(1.0, (rayleigh / expected_rayleigh)[0](), kEpsilon); + ExpectNear(1.0, (mie / expected_mie)[0](), kEpsilon); + + // Horizontal ray, from bottom of atmosphere, scattering angle 90 degrees. + rayleigh = GetScattering( + atmosphere_parameters_, single_rayleigh_scattering_texture, + kBottomRadius, 0.0, 0.0, 0.0, false); + mie = GetScattering( + atmosphere_parameters_, single_mie_scattering_texture, + kBottomRadius, 0.0, 0.0, 0.0, false); + ComputeSingleScattering( + atmosphere_parameters_, transmittance_texture, + kBottomRadius, 0.0, 0.0, 0.0, false, expected_rayleigh, expected_mie); + // The relative error is quite large in this case, i.e. between 6 to 8%. + ExpectNear(1.0, (rayleigh / expected_rayleigh)[0](), 1e-1); + ExpectNear(1.0, (mie / expected_mie)[0](), 1e-1); + + // Ray just above the horizon, sun at the zenith. + Number mu = CosineOfHorizonZenithAngle(kTopRadius); + rayleigh = GetScattering( + atmosphere_parameters_, single_rayleigh_scattering_texture, + kTopRadius, mu, 1.0, mu, false); + mie = GetScattering( + atmosphere_parameters_, single_mie_scattering_texture, + kTopRadius, mu, 1.0, mu, false); + ComputeSingleScattering( + atmosphere_parameters_, transmittance_texture, + kTopRadius, mu, 1.0, mu, false, expected_rayleigh, expected_mie); + ExpectNear(1.0, (rayleigh / expected_rayleigh)[0](), kEpsilon); + ExpectNear(1.0, (mie / expected_mie)[0](), kEpsilon); + } + +/* +

Multiple scattering, step 1: check that the numerical integration in +ComputeScatteringDensity gives the expected result in two cases +where the integral can be computed anaytically: +

+*/ + + void TestComputeScatteringDensity() { + RadianceSpectrum kRadiance(13.0 * watt_per_square_meter_per_sr_per_nm); + TransmittanceTexture full_transmittance(DimensionlessSpectrum(1.0)); + ReducedScatteringTexture no_single_scattering( + IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm)); + ScatteringTexture uniform_multiple_scattering(kRadiance); + IrradianceTexture no_irradiance( + IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm)); + + RadianceDensitySpectrum scattering_density = ComputeScatteringDensity( + atmosphere_parameters_, full_transmittance, no_single_scattering, + no_single_scattering, uniform_multiple_scattering, no_irradiance, + kBottomRadius, 0.0, 0.0, 1.0, 3); + SpectralRadianceDensity kExpectedScatteringDensity = + (kRayleighScattering + kMieScattering) * kRadiance[0]; + ExpectNear( + 1.0, + (scattering_density[0] / kExpectedScatteringDensity)(), + 2.0 * kEpsilon); + + IrradianceSpectrum kIrradiance(13.0 * watt_per_square_meter_per_nm); + IrradianceTexture uniform_irradiance(kIrradiance); + ScatteringTexture no_multiple_scattering( + RadianceSpectrum(0.0 * watt_per_square_meter_per_sr_per_nm)); + scattering_density = ComputeScatteringDensity( + atmosphere_parameters_, full_transmittance, no_single_scattering, + no_single_scattering, no_multiple_scattering, uniform_irradiance, + kBottomRadius, 0.0, 0.0, 1.0, 3); + kExpectedScatteringDensity = (kRayleighScattering + kMieScattering) * + kGroundAlbedo / (2.0 * PI * sr) * kIrradiance[0]; + ExpectNear( + 1.0, + (scattering_density[0] / kExpectedScatteringDensity)(), + 2.0 * kEpsilon); + } + +/* +

Multiple scattering, step 2: check that the numerical integration in +ComputeMultipleScattering gives the expected result in some cases +where the integral can be computed anaytically. If the radiance computed from +step 1 is the same everywhere and in all directions, and if the transmittance +is 1, then the numerical integration in ComputeMultipleScattering +should simply be equal to the radiance times the distance to the nearest +atmosphere boundary. We check this below for a vertical ray looking bottom, and +for a ray looking at the horizon. +*/ + + void TestComputeMultipleScattering() { + RadianceDensitySpectrum kRadianceDensity( + 0.17 * watt_per_cubic_meter_per_sr_per_nm); + TransmittanceTexture full_transmittance(DimensionlessSpectrum(1.0)); + ScatteringDensityTexture uniform_scattering_density(kRadianceDensity); + + // Vertical ray, looking bottom. + Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + Length distance_to_ground = r - kBottomRadius; + ExpectNear( + kRadianceDensity[0] * distance_to_ground, + ComputeMultipleScattering(atmosphere_parameters_, full_transmittance, + uniform_scattering_density, r, -1.0, 1.0, -1.0, true)[0], + kRadianceDensity[0] * distance_to_ground * kEpsilon); + + // Ray just below the horizon. + Number mu = CosineOfHorizonZenithAngle(kTopRadius); + Length distance_to_horizon = + sqrt(kTopRadius * kTopRadius - kBottomRadius * kBottomRadius); + ExpectNear( + kRadianceDensity[0] * distance_to_horizon, + ComputeMultipleScattering(atmosphere_parameters_, full_transmittance, + uniform_scattering_density, kTopRadius, mu, 1.0, mu, true)[0], + kRadianceDensity[0] * distance_to_horizon * kEpsilon); + } + +/* +

Multiple scattering texture, step 1: check that we get the same result +for the first step of the multiple scattering computation, whether we compute +it directly, or via a quadrilinearly interpolated lookup in a precomputed +texture. For this, we use the same test cases as in +TestComputeScatteringDensity, where the end result can be computed +analytically. +*/ + + void TestComputeAndGetScatteringDensity() { + RadianceSpectrum kRadiance(13.0 * watt_per_square_meter_per_sr_per_nm); + TransmittanceTexture full_transmittance(DimensionlessSpectrum(1.0)); + ReducedScatteringTexture no_single_scattering( + IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm)); + ScatteringTexture uniform_multiple_scattering(kRadiance); + IrradianceTexture no_irradiance( + IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm)); + LazyScatteringDensityTexture multiple_scattering1(atmosphere_parameters_, + full_transmittance, no_single_scattering, no_single_scattering, + uniform_multiple_scattering, no_irradiance, 3); + + RadianceDensitySpectrum scattering_density = GetScattering( + atmosphere_parameters_, multiple_scattering1, + kBottomRadius, 0.0, 0.0, 1.0, false); + SpectralRadianceDensity kExpectedScatteringDensity = + (kRayleighScattering + kMieScattering) * kRadiance[0]; + ExpectNear( + 1.0, + (scattering_density[0] / kExpectedScatteringDensity)(), + 2.0 * kEpsilon); + + IrradianceSpectrum kIrradiance(13.0 * watt_per_square_meter_per_nm); + IrradianceTexture uniform_irradiance(kIrradiance); + ScatteringTexture no_multiple_scattering( + RadianceSpectrum(0.0 * watt_per_square_meter_per_sr_per_nm)); + + LazyScatteringDensityTexture multiple_scattering2(atmosphere_parameters_, + full_transmittance, no_single_scattering, no_single_scattering, + no_multiple_scattering, uniform_irradiance, 3); + scattering_density = GetScattering( + atmosphere_parameters_, multiple_scattering2, + kBottomRadius, 0.0, 0.0, 1.0, false); + kExpectedScatteringDensity = (kRayleighScattering + kMieScattering) * + kGroundAlbedo / (2.0 * PI * sr) * kIrradiance[0]; + ExpectNear( + 1.0, + (scattering_density[0] / kExpectedScatteringDensity)(), + 2.0 * kEpsilon); + } + +/* +

Multiple scattering texture, step 2: check that we get the same result +for the second step of the multiple scattering computation, whether we compute +it directly, or via a quadrilinearly interpolated lookup in a precomputed +texture. For this, we use the same test cases as in +TestComputeMultipleScattering, where the end result can be computed +analytically. +*/ + + void TestComputeAndGetMultipleScattering() { + RadianceDensitySpectrum kRadianceDensity( + 0.17 * watt_per_cubic_meter_per_sr_per_nm); + TransmittanceTexture full_transmittance(DimensionlessSpectrum(1.0)); + ScatteringDensityTexture uniform_scattering_density(kRadianceDensity); + LazyMultipleScatteringTexture multiple_scattering(atmosphere_parameters_, + full_transmittance, uniform_scattering_density); + + // Vertical ray, looking bottom. + Length r = kBottomRadius * 0.2 + kTopRadius * 0.8; + Length distance_to_ground = r - kBottomRadius; + ExpectNear( + kRadianceDensity[0] * distance_to_ground, + GetScattering(atmosphere_parameters_, multiple_scattering, + r, -1.0, 1.0, -1.0, true)[0], + kRadianceDensity[0] * distance_to_ground * kEpsilon); + + // Ray just below the horizon. + Number mu = CosineOfHorizonZenithAngle(kTopRadius); + Length distance_to_horizon = + sqrt(kTopRadius * kTopRadius - kBottomRadius * kBottomRadius); + ExpectNear( + kRadianceDensity[0] * distance_to_horizon, + GetScattering(atmosphere_parameters_, multiple_scattering, + kTopRadius, mu, 1.0, mu, true)[0], + kRadianceDensity[0] * distance_to_horizon * kEpsilon); + } + +/* +

Ground irradiance, indirect case: check that the numerical integration +in ComputeIndirectIrradiance gives the expected result in a case +where this result can be computed analytically. More precisely, if the sky +radiance is the same everywhere and in all directions, then the ground +irradiance should be equal to $\pi$ times the sky radiance (because +$\int_0^{2\pi}\int_0^{\pi/2}\cos\theta\sin\theta\mathrm{d}\theta\mathrm{d}\phi= +\pi$, which is also why the Lambertian BRDF is $1/\pi$). +*/ + + void TestComputeIndirectIrradiance() { + ReducedScatteringTexture no_single_scattering; + ScatteringTexture uniform_multiple_scattering( + RadianceSpectrum(1.0 * watt_per_square_meter_per_sr_per_nm)); + IrradianceSpectrum irradiance = ComputeIndirectIrradiance( + atmosphere_parameters_, no_single_scattering, no_single_scattering, + uniform_multiple_scattering, kBottomRadius, 1.0, 2); + // The relative error is about 1% here. + ExpectNear( + PI, + irradiance[0].to(watt_per_square_meter_per_nm), + 10.0 * kEpsilon); + } + +/* +

Mapping to ground irradiance texture coordinates: check that the +boundary values of $r$ ($r_\mathrm{bottom}$ and $r_\mathrm{top}$) and $\mu$ +($-1$ and $1$) are mapped to the centers of the boundary texels of the ground +irradiance texture. +*/ + + void TestGetIrradianceTextureUvFromRMuS() { + ExpectNear( + 0.5 / IRRADIANCE_TEXTURE_HEIGHT, + GetIrradianceTextureUvFromRMuS( + atmosphere_parameters_, kBottomRadius, 0.0).y(), + kEpsilon); + ExpectNear( + 1.0 - 0.5 / IRRADIANCE_TEXTURE_HEIGHT, + GetIrradianceTextureUvFromRMuS( + atmosphere_parameters_, kTopRadius, 0.0).y(), + kEpsilon); + ExpectNear( + 0.5 / IRRADIANCE_TEXTURE_WIDTH, + GetIrradianceTextureUvFromRMuS( + atmosphere_parameters_, kBottomRadius, -1.0).x(), + kEpsilon); + ExpectNear( + 1.0 - 0.5 / IRRADIANCE_TEXTURE_WIDTH, + GetIrradianceTextureUvFromRMuS( + atmosphere_parameters_, kBottomRadius, 1.0).x(), + kEpsilon); + } + +/* +

Mapping from ground irradiance texture coordinates: check that the +centers of the boundary texels of the ground irradiance texture are mapped to +the boundary values of $r$ ($r_\mathrm{bottom}$ and $r_\mathrm{top}$) and $\mu$ +($-1$ and $1$). +*/ + + void TestGetRMuSFromIrradianceTextureUv() { + Length r; + Number mu_s; + GetRMuSFromIrradianceTextureUv(atmosphere_parameters_, + vec2(0.5 / IRRADIANCE_TEXTURE_WIDTH, + 0.5 / IRRADIANCE_TEXTURE_HEIGHT), + r, mu_s); + ExpectNear(kBottomRadius, r, 1.0 * m); + GetRMuSFromIrradianceTextureUv(atmosphere_parameters_, + vec2(0.5 / IRRADIANCE_TEXTURE_WIDTH, + 1.0 - 0.5 / IRRADIANCE_TEXTURE_HEIGHT), + r, mu_s); + ExpectNear(kTopRadius, r, 1.0 * m); + GetRMuSFromIrradianceTextureUv(atmosphere_parameters_, + vec2(0.5 / IRRADIANCE_TEXTURE_WIDTH, + 0.5 / IRRADIANCE_TEXTURE_HEIGHT), + r, mu_s); + ExpectNear(-1.0, mu_s(), kEpsilon); + GetRMuSFromIrradianceTextureUv(atmosphere_parameters_, + vec2(1.0 - 0.5 / IRRADIANCE_TEXTURE_WIDTH, + 0.5 / IRRADIANCE_TEXTURE_HEIGHT), + r, mu_s); + ExpectNear(1.0, mu_s(), kEpsilon); + } + +/* +

Ground irradiance texture: check that we get the same ground +irradiance whether we compute it directly with +ComputeIndirectIrradiance, or via a bilinearly interpolated lookup +in the precomputed ground irradiance texture. +*/ + + void TestComputeAndGetIrradiance() { + ReducedScatteringTexture no_single_scattering( + IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm)); + ScatteringTexture fake_multiple_scattering; + for (unsigned int x = 0; x < fake_multiple_scattering.size_x(); ++x) { + for (unsigned int y = 0; y < fake_multiple_scattering.size_y(); ++y) { + for (unsigned int z = 0; z < fake_multiple_scattering.size_z(); ++z) { + double v = z + fake_multiple_scattering.size_z() * + (y + fake_multiple_scattering.size_y() * x); + fake_multiple_scattering.Set(x, y, z, + RadianceSpectrum(v * watt_per_square_meter_per_sr_per_nm)); + } + } + } + + Length r = kBottomRadius * 0.8 + kTopRadius * 0.2; + Number mu_s = 0.25; + int scattering_order = 2; + LazyIndirectIrradianceTexture irradiance_texture(atmosphere_parameters_, + no_single_scattering, no_single_scattering, fake_multiple_scattering, + scattering_order); + ExpectNear( + 1.0, + (GetIrradiance(atmosphere_parameters_, irradiance_texture, r, mu_s) / + ComputeIndirectIrradiance(atmosphere_parameters_, + no_single_scattering, no_single_scattering, + fake_multiple_scattering, r, mu_s, scattering_order))[0](), + kEpsilon); + ExpectNotNear( + 1.0, + (GetIrradiance(atmosphere_parameters_, irradiance_texture, r, mu_s) / + ComputeIndirectIrradiance(atmosphere_parameters_, + no_single_scattering, no_single_scattering, + fake_multiple_scattering, r, 0.5, scattering_order))[0](), + kEpsilon); + } + +/* +

And that's it for the unit tests! We just need to implement the two methods +that we used above to set a uniform density of air molecules and aerosols, and +to remove aerosols: +*/ + + private: + void SetUniformAtmosphere() { + atmosphere_parameters_.rayleigh_scale_height = + std::numeric_limits::infinity() * km; + atmosphere_parameters_.mie_scale_height = + std::numeric_limits::infinity() * km; + } + + void RemoveAerosols() { + atmosphere_parameters_.mie_scattering[0] = 0.0 / km; + atmosphere_parameters_.mie_extinction[0] = 0.0 / km; + } + + AtmosphereParameters atmosphere_parameters_; +}; + +/* +

Finally, we need to create an instance of each of the above test cases (which +has the side effect of registering these instances in the TestCase +class, which can then run all the tests in its RunAllTests static +method). +*/ + +namespace { + +FunctionsTest distance_to_top_atmosphere_boundary( + "DistanceToTopAtmosphereBoundary", + &FunctionsTest::TestDistanceToTopAtmosphereBoundary); +FunctionsTest ray_intersects_ground( + "RayIntersectsGround", + &FunctionsTest::TestRayIntersectsGround); +FunctionsTest compute_optical_length_to_top_atmosphere_boundary( + "ComputeOpticalLengthToTopAtmosphereBoundary", + &FunctionsTest::TestComputeOpticalLengthToTopAtmosphereBoundary); +FunctionsTest compute_transmittance_to_top_atmosphere_boundary( + "ComputeTransmittanceToTopAtmosphereBoundary", + &FunctionsTest::TestComputeTransmittanceToTopAtmosphereBoundary); +FunctionsTest get_texture_coord_from_unit_range( + "GetTextureCoordFromUnitRange", + &FunctionsTest::TestGetTextureCoordFromUnitRange); +FunctionsTest get_transmittance_texture_uv_from_rmu( + "GetTransmittanceTextureUvFromRMu", + &FunctionsTest::TestGetTransmittanceTextureUvFromRMu); +FunctionsTest get_rmu_from_transmittance_texture_uv( + "GetRMuFromTransmittanceTextureUv", + &FunctionsTest::TestGetRMuFromTransmittanceTextureUv); +FunctionsTest get_transmittance_to_top_atmosphere_boundary( + "GetTransmittanceToTopAtmosphereBoundary", + &FunctionsTest::TestGetTransmittanceToTopAtmosphereBoundary); +FunctionsTest compute_and_get_transmittance( + "ComputeAndGetTransmittance", + &FunctionsTest::TestComputeAndGetTransmittance); + +FunctionsTest compute_single_scattering_integrand( + "ComputeSingleScatteringIntegrand", + &FunctionsTest::TestComputeSingleScatteringIntegrand); +FunctionsTest distance_to_nearest_atmosphere_boundary( + "DistanceToNearestAtmosphereBoundary", + &FunctionsTest::TestDistanceToNearestAtmosphereBoundary); +FunctionsTest compute_single_scattering( + "ComputeSingleScattering", + &FunctionsTest::TestComputeSingleScattering); +FunctionsTest phase_functions( + "PhaseFunctions", + &FunctionsTest::TestPhaseFunctions); +FunctionsTest get_scattering_texture_uvwz_from_rmumusnu( + "GetScatteringTextureUvwzFromRMuMuSNu", + &FunctionsTest::TestGetScatteringTextureUvwzFromRMuMuSNu); +FunctionsTest get_rmumusnu_from_scattering_texture_uvwz( + "GetRMuMuSNuFromScatteringTextureUvwz", + &FunctionsTest::TestGetRMuMuSNuFromScatteringTextureUvwz); +FunctionsTest compute_and_get_scattering( + "ComputeAndGetSingleScattering", + &FunctionsTest::TestComputeAndGetSingleScattering); + +FunctionsTest compute_scattering_density( + "ComputeScatteringDensity", + &FunctionsTest::TestComputeScatteringDensity); +FunctionsTest compute_multiple_scattering( + "ComputeMultipleScattering", + &FunctionsTest::TestComputeMultipleScattering); +FunctionsTest compute_and_get_scattering_density( + "ComputeAndGetScatteringDensity", + &FunctionsTest::TestComputeAndGetScatteringDensity); +FunctionsTest compute_and_get_multiple_scattering( + "ComputeAndGetMultipleScattering", + &FunctionsTest::TestComputeAndGetMultipleScattering); + +FunctionsTest compute_indirect_irradiance( + "ComputeIndirectIrradiance", + &FunctionsTest::TestComputeIndirectIrradiance); +FunctionsTest get_irradiance_texture_uv_from_rmus( + "GetIrradianceTextureUvFromRMuS", + &FunctionsTest::TestGetIrradianceTextureUvFromRMuS); +FunctionsTest get_rmus_from_irradiance_texture_uv( + "GetRMuSFromIrradianceTextureUv", + &FunctionsTest::TestGetRMuSFromIrradianceTextureUv); +FunctionsTest get_irradiance( + "GetComputeAndGetIrradiance", + &FunctionsTest::TestComputeAndGetIrradiance); + +} // anonymous namespace + +} // namespace reference +} // namespace atmosphere diff --git a/atmosphere/reference/model.cc b/atmosphere/reference/model.cc new file mode 100644 index 0000000..3f5c6e0 --- /dev/null +++ b/atmosphere/reference/model.cc @@ -0,0 +1,278 @@ +/** + * 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. + */ + +/*

atmosphere/reference/model.cc

+ +

This file implements our atmosphere model on CPU. Its main role is to +precompute the transmittance, scattering and irradiance textures. The C++ +functions to precompute them are provided in +functions.h, but they are not sufficient. +They must be call on each texel of the precomputed textures, and these textures +must be computed in the correct order, with the correct input and output +textures, to precompute each scattering order in sequence, as described in +Algorithm 4.1 of our paper. +This is the role of the following code. + +

We start by including the files we need: +*/ + +#include "atmosphere/reference/model.h" + +#include "atmosphere/reference/functions.h" +#include "util/progress_bar.h" + +/* +

The constructor of the Model class allocates the precomputed +textures, but does not initialize them. +*/ + +namespace atmosphere { +namespace reference { + +Model::Model(const AtmosphereParameters& atmosphere, + const std::string& cache_directory) + : atmosphere_(atmosphere), + cache_directory_(cache_directory) { + transmittance_texture_.reset(new TransmittanceTexture()); + scattering_texture_.reset(new ReducedScatteringTexture()); + single_mie_scattering_texture_.reset(new ReducedScatteringTexture()); + irradiance_texture_.reset(new IrradianceTexture()); +} + +/* +

The initialization is done in the following method, which first tries to load +the textures from disk, if they have already been precomputed. +*/ + +void Model::Init(unsigned int num_scattering_orders) { + std::ifstream file; + file.open(cache_directory_ + "transmittance.dat"); + if (file.good()) { + file.close(); + transmittance_texture_->Load(cache_directory_ + "transmittance.dat"); + scattering_texture_->Load(cache_directory_ + "scattering.dat"); + single_mie_scattering_texture_->Load( + cache_directory_ + "single_mie_scattering.dat"); + irradiance_texture_->Load(cache_directory_ + "irradiance.dat"); + return; + } + +/* +

If they have not already been precomputed, we must compute them here. This +computation requires some temporary textures, in particular to store the +contribution of one scattering order, which is needed to compute the next order +of scattering (the final precomputed textures store the sum of all the +scattering orders). We allocate these textures here (they are automatically +destroyed at the end of this method). +*/ + + std::unique_ptr + delta_irradiance_texture(new IrradianceTexture()); + std::unique_ptr + delta_rayleigh_scattering_texture(new ReducedScatteringTexture()); + ReducedScatteringTexture* delta_mie_scattering_texture = + single_mie_scattering_texture_.get(); + std::unique_ptr + delta_scattering_density_texture(new ScatteringDensityTexture()); + std::unique_ptr + delta_multiple_scattering_texture(new ScatteringTexture()); + +/* +

Since the computation phase takes several minutes, we show a progress bar to +provide feedback to the user. The following constants roughly represent the +relative duration of each computation phase, and are used to display a progress +value which is roughly proportional to the elapsed time. +*/ + + constexpr unsigned int kTransmittanceProgress = 1; + constexpr unsigned int kDirectIrradianceProgress = 1; + constexpr unsigned int kSingleScatteringProgress = 10; + constexpr unsigned int kScatteringDensityProgress = 100; + constexpr unsigned int kIndirectIrradianceProgress = 10; + constexpr unsigned int kMultipleScatteringProgress = 10; + const unsigned int kTotalProgress = + TRANSMITTANCE_TEXTURE_WIDTH * TRANSMITTANCE_TEXTURE_HEIGHT * + kTransmittanceProgress + + IRRADIANCE_TEXTURE_WIDTH * IRRADIANCE_TEXTURE_HEIGHT * ( + kDirectIrradianceProgress + + kIndirectIrradianceProgress * (num_scattering_orders - 1)) + + SCATTERING_TEXTURE_WIDTH * SCATTERING_TEXTURE_HEIGHT * + SCATTERING_TEXTURE_DEPTH * ( + kSingleScatteringProgress + + (kScatteringDensityProgress + kMultipleScatteringProgress) * + (num_scattering_orders - 1)); + + ProgressBar progress_bar(kTotalProgress); + +/* +

The remaining code of this method implements Algorithm 4.1 of our paper, +using several threads to speed up computations (by computing several texels of +a texture in parallel). +*/ + + // Compute the transmittance, and store it in transmittance_texture_. + RunJobs([&](unsigned int j) { + for (unsigned int i = 0; i < TRANSMITTANCE_TEXTURE_WIDTH; ++i) { + transmittance_texture_->Set(i, j, + ComputeTransmittanceToTopAtmosphereBoundaryTexture( + atmosphere_, vec2(i + 0.5, j + 0.5))); + progress_bar.Increment(kTransmittanceProgress); + } + }, TRANSMITTANCE_TEXTURE_HEIGHT); + + // Compute the direct irradiance, store it in delta_irradiance_texture, and + // initialize irradiance_texture_ with zeros (we don't want the direct + // irradiance in irradiance_texture_, but only the irradiance from the sky). + RunJobs([&](unsigned int j) { + for (unsigned int i = 0; i < IRRADIANCE_TEXTURE_WIDTH; ++i) { + delta_irradiance_texture->Set(i, j, + ComputeDirectIrradianceTexture( + atmosphere_, *transmittance_texture_, vec2(i + 0.5, j + 0.5))); + irradiance_texture_->Set( + i, j, IrradianceSpectrum(0.0 * watt_per_square_meter_per_nm)); + progress_bar.Increment(kDirectIrradianceProgress); + } + }, IRRADIANCE_TEXTURE_HEIGHT); + + // Compute the rayleigh and mie single scattering, and store them in + // delta_rayleigh_scattering_texture and delta_mie_scattering_texture, as well + // as in scattering_texture. + RunJobs([&](unsigned int k) { + for (unsigned int j = 0; j < SCATTERING_TEXTURE_HEIGHT; ++j) { + for (unsigned int i = 0; i < SCATTERING_TEXTURE_WIDTH; ++i) { + IrradianceSpectrum rayleigh; + IrradianceSpectrum mie; + ComputeSingleScatteringTexture(atmosphere_, *transmittance_texture_, + vec3(i + 0.5, j + 0.5, k + 0.5), rayleigh, mie); + delta_rayleigh_scattering_texture->Set(i, j, k, rayleigh); + delta_mie_scattering_texture->Set(i, j, k, mie); + scattering_texture_->Set(i, j, k, rayleigh); + progress_bar.Increment(kSingleScatteringProgress); + } + } + }, SCATTERING_TEXTURE_DEPTH); + + // Compute the 2nd, 3rd and 4th order of scattering, in sequence. + for (unsigned int scattering_order = 2; + scattering_order <= num_scattering_orders; + ++scattering_order) { + // Compute the scattering density, and store it in + // delta_scattering_density_texture. + RunJobs([&](unsigned int k) { + for (unsigned int j = 0; j < SCATTERING_TEXTURE_HEIGHT; ++j) { + for (unsigned int i = 0; i < SCATTERING_TEXTURE_WIDTH; ++i) { + RadianceDensitySpectrum scattering_density; + scattering_density = ComputeScatteringDensityTexture(atmosphere_, + *transmittance_texture_, *delta_rayleigh_scattering_texture, + *delta_mie_scattering_texture, + *delta_multiple_scattering_texture, *delta_irradiance_texture, + vec3(i + 0.5, j + 0.5, k + 0.5), scattering_order); + delta_scattering_density_texture->Set(i, j, k, scattering_density); + progress_bar.Increment(kScatteringDensityProgress); + } + } + }, SCATTERING_TEXTURE_DEPTH); + + // Compute the indirect irradiance, store it in delta_irradiance_texture and + // accumulate it in irradiance_texture_. + RunJobs([&](unsigned int j) { + for (unsigned int i = 0; i < IRRADIANCE_TEXTURE_WIDTH; ++i) { + IrradianceSpectrum delta_irradiance; + delta_irradiance = ComputeIndirectIrradianceTexture( + atmosphere_, *delta_rayleigh_scattering_texture, + *delta_mie_scattering_texture, *delta_multiple_scattering_texture, + vec2(i + 0.5, j + 0.5), scattering_order - 1); + delta_irradiance_texture->Set(i, j, delta_irradiance); + progress_bar.Increment(kIndirectIrradianceProgress); + } + }, IRRADIANCE_TEXTURE_HEIGHT); + (*irradiance_texture_) += *delta_irradiance_texture; + + // Compute the multiple scattering, store it in + // delta_multiple_scattering_texture, and accumulate it in + // scattering_texture_. + RunJobs([&](unsigned int k) { + for (unsigned int j = 0; j < SCATTERING_TEXTURE_HEIGHT; ++j) { + for (unsigned int i = 0; i < SCATTERING_TEXTURE_WIDTH; ++i) { + RadianceSpectrum delta_multiple_scattering; + Number nu; + delta_multiple_scattering = ComputeMultipleScatteringTexture( + atmosphere_, *transmittance_texture_, + *delta_scattering_density_texture, + vec3(i + 0.5, j + 0.5, k + 0.5), nu); + delta_multiple_scattering_texture->Set( + i, j, k, delta_multiple_scattering); + scattering_texture_->Set(i, j, k, + scattering_texture_->Get(i, j, k) + + delta_multiple_scattering * (1.0 / RayleighPhaseFunction(nu))); + progress_bar.Increment(kMultipleScatteringProgress); + } + } + }, SCATTERING_TEXTURE_DEPTH); + } + + transmittance_texture_->Save(cache_directory_ + "transmittance.dat"); + scattering_texture_->Save(cache_directory_ + "scattering.dat"); + single_mie_scattering_texture_->Save( + cache_directory_ + "single_mie_scattering.dat"); + irradiance_texture_->Save(cache_directory_ + "irradiance.dat"); +} + +/* +

Once the textures have been computed or loaded from the cache, they can be +used to compute the sky radiance and the sun and sky irradiance. The functions +for doing that are provided in functions.h and we +just need here to wrap them in their corresponding methods: +*/ + +RadianceSpectrum Model::GetSkyRadiance(Position camera, Direction view_ray, + Length shadow_length, Direction sun_direction, + DimensionlessSpectrum* transmittance) const { + return reference::GetSkyRadiance(atmosphere_, *transmittance_texture_, + *scattering_texture_, *single_mie_scattering_texture_, + camera, view_ray, shadow_length, sun_direction, *transmittance); +} + +RadianceSpectrum Model::GetSkyRadianceToPoint(Position camera, Position point, + Length shadow_length, Direction sun_direction, + DimensionlessSpectrum* transmittance) const { + return reference::GetSkyRadianceToPoint(atmosphere_, *transmittance_texture_, + *scattering_texture_, *single_mie_scattering_texture_, + camera, point, shadow_length, sun_direction, *transmittance); +} + +IrradianceSpectrum Model::GetSunAndSkyIrradiance(Position point, + Direction normal, Direction sun_direction, + IrradianceSpectrum* sky_irradiance) const { + return reference::GetSunAndSkyIrradiance(atmosphere_, *transmittance_texture_, + *irradiance_texture_, point, normal, sun_direction, *sky_irradiance); +} + +} // namespace reference +} // namespace atmosphere diff --git a/atmosphere/reference/model.h b/atmosphere/reference/model.h new file mode 100644 index 0000000..81976c1 --- /dev/null +++ b/atmosphere/reference/model.h @@ -0,0 +1,88 @@ +/** + * 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. + */ + +/*

atmosphere/reference/model.h

+ +

This file defines the API to use our atmosphere model on CPU. +To use it: +

+*/ + +#ifndef ATMOSPHERE_REFERENCE_MODEL_H_ +#define ATMOSPHERE_REFERENCE_MODEL_H_ + +#include +#include +#include + +#include "atmosphere/reference/definitions.h" + +namespace atmosphere { +namespace reference { + +class Model { + public: + Model(const AtmosphereParameters& atmosphere, + const std::string& cache_directory); + + void Init(unsigned int num_scattering_orders = 4); + + RadianceSpectrum GetSkyRadiance(Position camera, Direction view_ray, + Length shadow_length, Direction sun_direction, + DimensionlessSpectrum* transmittance) const; + + RadianceSpectrum GetSkyRadianceToPoint(Position camera, Position point, + Length shadow_length, Direction sun_direction, + DimensionlessSpectrum* transmittance) const; + + IrradianceSpectrum GetSunAndSkyIrradiance(Position p, Direction normal, + Direction sun_direction, IrradianceSpectrum* sky_irradiance) const; + + private: + const AtmosphereParameters atmosphere_; + const std::string cache_directory_; + std::unique_ptr transmittance_texture_; + std::unique_ptr scattering_texture_; + std::unique_ptr single_mie_scattering_texture_; + std::unique_ptr irradiance_texture_; +}; + +} // namespace reference +} // namespace atmosphere + +#endif // ATMOSPHERE_REFERENCE_MODEL_H_ diff --git a/atmosphere/reference/model_test.cc b/atmosphere/reference/model_test.cc new file mode 100644 index 0000000..d5842ed --- /dev/null +++ b/atmosphere/reference/model_test.cc @@ -0,0 +1,947 @@ +/** + * 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. + */ + +/*

atmosphere/reference/model_test.cc

+ +

This file provides tests that compare the results of our GPU atmosphere model +with reference images computed with the same code, but executed on CPU in full +spectral mode and with double precision floats. The goal is to make sure that +the approximations made in the GPU version (half precision floats, single Mie +scattering extrapolated from a single wavelength, and luminance values computed +from 3 wavelengths instead of 47 wavelengths on CPU) do not significantly reduce +the image quality. + +

The code is organized as follows: +

+ +

The test results can be seen here. + +

Constants

+ +

We start by including the files we need. Since we want to render images with +the GPU and CPU versions of our algorithm, we need to include the GPU and CPU +model definitions: +*/ + +#include "atmosphere/reference/model.h" + +#include +#include + +#include +#include +#include + +#include "atmosphere/model.h" +#include "atmosphere/reference/definitions.h" +#include "minpng/minpng.h" +#include "test/test_case.h" +#include "util/progress_bar.h" + +/* +

Our test scene is a sphere on a purely spherical planet. Its position and +size are specified by the following constants (note that we use a large sphere +so that it can produce visible light shafts, in order to test them): +*/ + +namespace atmosphere { +namespace reference { + +namespace { + +constexpr Length kSphereRadius = 1.0 * km; +constexpr Position kSphereCenter = Position(0.0 * km, 0.0 * km, kSphereRadius); + +/* +

Our tests use length values expressed in kilometers and, for the tests based +on radiance values (as opposed to luminance values), make use of the following +3 wavelengths: +*/ + +constexpr Length kLengthUnit = 1.0 * km; +constexpr Wavelength kLambdaR = atmosphere::Model::kLambdaR * nm; +constexpr Wavelength kLambdaG = atmosphere::Model::kLambdaG * nm; +constexpr Wavelength kLambdaB = atmosphere::Model::kLambdaB * nm; + +/* +

The test scene is rendered on GPU by the following shaders. The vertex shader +simply renders a full screen quad, and outputs the view ray direction in model +space: +*/ + +const char* kVertexShader = R"( + #version 330 + uniform mat3 model_from_clip; + layout(location = 0) in vec4 vertex; + out vec3 view_ray; + void main() { + view_ray = model_from_clip * vertex.xyw; + gl_Position = vertex; + })"; + +/* +

The fragment shader computes the radiance (or luminance, if the USE_LUMINANCE +preprocessor macro is defined) corresponding to this view ray and uses a simple +tone mapping function to convert it to a final color. This shader takes as input +some uniforms describing the camera and the scene: +*/ + +const char* kFragmentShader = R"( + #define OUT(x) out x + uniform vec3 camera_; + uniform float exposure_; + uniform vec3 earth_center_; + uniform vec3 sun_direction_; + uniform vec3 sun_radiance_; + uniform vec2 sun_size_; + uniform vec3 ground_albedo_; + uniform vec3 sphere_albedo_; + in vec3 view_ray; + layout(location = 0) out vec3 color; + + #ifdef USE_LUMINANCE + #define GetSkyRadiance GetSkyLuminance + #define GetSkyRadianceToPoint GetSkyLuminanceToPoint + #define GetSunAndSkyIrradiance GetSunAndSkyIlluminance + #endif + + vec3 GetSkyRadiance(vec3 camera, vec3 view_ray, float shadow_length, + vec3 sun_direction, out vec3 transmittance); + vec3 GetSkyRadianceToPoint(vec3 camera, vec3 point, float shadow_length, + vec3 sun_direction, out vec3 transmittance); + vec3 GetSunAndSkyIrradiance( + vec3 p, vec3 normal, vec3 sun_direction, out vec3 sky_irradiance); + vec3 GetViewRayRadiance(vec3 view_ray, vec3 view_ray_diff); + + void main() { + color = GetViewRayRadiance(view_ray, dFdx(view_ray) + dFdy(view_ray)); + color = pow(vec3(1.0) - exp(-color * exposure_), vec3(1.0 / 2.2)); + })"; + +/* +

The main function GetViewRayRadiance is defined in +model_test.glsl, which must be appended to +the above code (together with +definitions.glsl) to get a complete +shader. These GLSL files are provided as C++ string literals by the generated +files included below: +*/ + +#include "atmosphere/definitions.glsl.inc" +#include "atmosphere/reference/model_test.glsl.inc" + +/* +

Each test case produces two images, using two different methods, and checks +that the difference between the two is small enough. These images are stored on +disk with the following function, which is a simple wrapper around the +minpng library: +*/ + +typedef std::unique_ptr Image; + +const char* kOutputDir = "output/Doc/atmosphere/reference/"; +constexpr unsigned int kWidth = 640; +constexpr unsigned int kHeight = 360; + +void WritePngArgb(const std::string& name, void* pixels) { + write_png((std::string(kOutputDir) + name).c_str(), pixels, kWidth, kHeight); +} + +} // anonymous namespace + +/* +

Test fixture

+ +

The test fixture provides a shared class and shared methods for all the +test cases. It extends the TestCase class provided by the dimensional_types library: +*/ + +using std::max; +using std::min; + +class ModelTest : public dimensional::TestCase { + public: + template + ModelTest(const std::string& name, T test) + : TestCase("ModelTest " + name, static_cast(test)), name_(name) {} + +/* +

Setup methods

+ +

The SetUp method is called before each test case. We put here +the initialization code which must be executed before any test case, i.e. the +initialization of the atmosphere parameters (all our tests use the same +atmosphere parameters) and of the constant scene parameters (earth center, sun +size and radiance, surface albedos): +*/ + + void SetUp() override { + // Values from "Reference Solar Spectral Irradiance: ASTM G-173", ETR column + // (see http://rredc.nrel.gov/solar/spectra/am1.5/ASTMG173/ASTMG173.html), + // summed and averaged in each bin (e.g. the value for 360nm is the average + // of the ASTM G-173 values for all wavelengths between 360 and 370nm). + constexpr int kLambdaMin = 360; + constexpr int kLambdaMax = 830; + constexpr double kSolarIrradiance[48] = { + 1.11776, 1.14259, 1.01249, 1.14716, 1.72765, 1.73054, 1.6887, 1.61253, + 1.91198, 2.03474, 2.02042, 2.02212, 1.93377, 1.95809, 1.91686, 1.8298, + 1.8685, 1.8931, 1.85149, 1.8504, 1.8341, 1.8345, 1.8147, 1.78158, 1.7533, + 1.6965, 1.68194, 1.64654, 1.6048, 1.52143, 1.55622, 1.5113, 1.474, 1.4482, + 1.41018, 1.36775, 1.34188, 1.31429, 1.28303, 1.26758, 1.2367, 1.2082, + 1.18737, 1.14683, 1.12362, 1.1058, 1.07124, 1.04992 + }; + constexpr ScatteringCoefficient kRayleigh = 1.24062e-6 / m; + constexpr Length kRayleighScaleHeight = 8000.0 * m; + constexpr Length kMieScaleHeight = 1200.0 * m; + constexpr double kMieAngstromAlpha = 0.0; + constexpr double kMieAngstromBeta = 5.328e-3; + constexpr double kMieSingleScatteringAlbedo = 0.9; + constexpr double kMiePhaseFunctionG = 0.8; + + std::vector solar_irradiance; + std::vector rayleigh_scattering; + std::vector mie_scattering; + std::vector mie_extinction; + for (int l = kLambdaMin; l <= kLambdaMax; l += 10) { + double lambda = static_cast(l) * 1e-3; // micro-meters + SpectralIrradiance solar = kSolarIrradiance[(l - kLambdaMin) / 10] * + watt_per_square_meter_per_nm; + ScatteringCoefficient rayleigh = kRayleigh * pow(lambda, -4); + ScatteringCoefficient mie = kMieAngstromBeta / kMieScaleHeight * + pow(lambda, -kMieAngstromAlpha); + solar_irradiance.push_back(solar); + rayleigh_scattering.push_back(rayleigh); + mie_scattering.push_back(mie * kMieSingleScatteringAlbedo); + mie_extinction.push_back(mie); + } + + atmosphere_parameters_.solar_irradiance = IrradianceSpectrum( + kLambdaMin * nm, kLambdaMax * nm, solar_irradiance); + atmosphere_parameters_.sun_angular_radius = 0.2678 * deg; + atmosphere_parameters_.bottom_radius = 6360.0 * km; + atmosphere_parameters_.top_radius = 6420.0 * km; + atmosphere_parameters_.rayleigh_scale_height = kRayleighScaleHeight; + atmosphere_parameters_.rayleigh_scattering = ScatteringSpectrum( + kLambdaMin * nm, kLambdaMax * nm, rayleigh_scattering); + atmosphere_parameters_.mie_scale_height = kMieScaleHeight; + atmosphere_parameters_.mie_scattering = ScatteringSpectrum( + kLambdaMin * nm, kLambdaMax * nm, mie_scattering); + atmosphere_parameters_.mie_extinction = ScatteringSpectrum( + kLambdaMin * nm, kLambdaMax * nm, mie_extinction); + atmosphere_parameters_.mie_phase_function_g = kMiePhaseFunctionG; + atmosphere_parameters_.ground_albedo = DimensionlessSpectrum(0.1); + atmosphere_parameters_.mu_s_min = cos(102.0 * deg); + + earth_center_ = + Position(0.0 * m, 0.0 * m, -atmosphere_parameters_.bottom_radius); + + SolidAngle sun_solid_angle = 2.0 * PI * + (1.0 - cos(atmosphere_parameters_.sun_angular_radius)) * sr; + sun_radiance_ = + atmosphere_parameters_.solar_irradiance * (1.0 / sun_solid_angle); + sun_size_ = dimensional::vec2( + tan(atmosphere_parameters_.sun_angular_radius), + cos(atmosphere_parameters_.sun_angular_radius)); + + ground_albedo_ = GetGrassAlbedo(); + sphere_albedo_ = GetSnowAlbedo(); + program_ = 0; + } + +/* +

where the ground and sphere albedos are provided by the following methods: +*/ + + DimensionlessSpectrum GetGrassAlbedo() { + // Grass spectral albedo from Uwe Feister and Rolf Grewe, "Spectral albedo + // measurements in the UV and visible region over different types of + // surfaces", Photochemistry and Photobiology, 62, 736-744, 1995. + constexpr double kGrassAlbedo[45] = { + 0.018, 0.019, 0.019, 0.020, 0.022, 0.024, 0.027, 0.029, 0.030, 0.031, + 0.032, 0.032, 0.032, 0.033, 0.035, 0.040, 0.055, 0.073, 0.084, 0.089, + 0.089, 0.079, 0.069, 0.063, 0.061, 0.057, 0.052, 0.051, 0.048, 0.042, + 0.039, 0.035, 0.035, 0.043, 0.087, 0.156, 0.234, 0.334, 0.437, 0.513, + 0.553, 0.571, 0.579, 0.581, 0.587 + }; + std::vector grass_albedo_samples; + for (int i = 0; i < 45; ++i) { + grass_albedo_samples.push_back(kGrassAlbedo[i]); + } + return DimensionlessSpectrum(360.0 * nm, 800.0 * nm, grass_albedo_samples); + } + + DimensionlessSpectrum GetSnowAlbedo() { + // Snow 5cm spectral albedo from Uwe Feister and Rolf Grewe, "Spectral + // albedo measurements in the UV and visible region over different types of + // surfaces", Photochemistry and Photobiology, 62, 736-744, 1995. + constexpr double kSnowAlbedo[7] = { + 0.796, 0.802, 0.807, 0.810, 0.818, 0.825, 0.826 + }; + std::vector snow_albedo_samples; + for (int i = 0; i < 7; ++i) { + snow_albedo_samples.push_back(kSnowAlbedo[i]); + } + return DimensionlessSpectrum(360.0 * nm, 420.0 * nm, snow_albedo_samples); + } + +/* +

The GPU model is initialized differently depending on the test case, so we +provide a separate method to initialize it: +*/ + + void InitGpuModel(bool combine_textures) { + if (!glutGet(GLUT_INIT_STATE)) { + int argc = 0; + char** argv = nullptr; + glutInit(&argc, argv); + glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); + glutInitWindowSize(kWidth, kHeight); + glutCreateWindow("ModelTest"); + glutHideWindow(); + glewInit(); + } + + std::vector wavelengths; + const auto& spectrum = atmosphere_parameters_.solar_irradiance; + for (unsigned int i = 0; i < spectrum.size(); ++i) { + wavelengths.push_back(spectrum.GetSample(i).to(nm)); + } + model_.reset(new atmosphere::Model( + wavelengths, + atmosphere_parameters_.solar_irradiance.to( + watt_per_square_meter_per_nm), + atmosphere_parameters_.sun_angular_radius.to(rad), + atmosphere_parameters_.bottom_radius.to(m), + atmosphere_parameters_.top_radius.to(m), + atmosphere_parameters_.rayleigh_scale_height.to(m), + atmosphere_parameters_.rayleigh_scattering.to(1.0 / m), + atmosphere_parameters_.mie_scale_height.to(m), + atmosphere_parameters_.mie_scattering.to(1.0 / m), + atmosphere_parameters_.mie_extinction.to(1.0 / m), + atmosphere_parameters_.mie_phase_function_g(), + atmosphere_parameters_.ground_albedo.to(Number::Unit()), + acos(atmosphere_parameters_.mu_s_min()), + kLengthUnit.to(m), + combine_textures)); + model_->Init(); + glutSwapBuffers(); + } + +/* +

Likewise, the CPU model might not needed by all test cases, so we provide a +separate method to initialize it: +*/ + + void InitCpuModel() { + reference_model_.reset( + new reference::Model(atmosphere_parameters_, "output/")); + reference_model_->Init(); + } + +/* +

Finally, before rendering an image with the GPU or CPU model, we must +initialize the camera (position, transform matrix, exposure) and the sun +direction and choose the rendering output (radiance or luminance). For this we +provide the following method: +*/ + + void SetViewParameters(Angle sun_theta, Angle sun_phi, bool use_luminance) { + // Transform matrix from camera frame to world space. + const float kCameraPos[3] = { 2000.0, -8000.0, 500.0 }; + constexpr float kPitch = M_PI / 30.0; + const float model_from_view[16] = { + 1.0, 0.0, 0.0, kCameraPos[0], + 0.0, -sinf(kPitch), -cosf(kPitch), kCameraPos[1], + 0.0, cosf(kPitch), -sinf(kPitch), kCameraPos[2], + 0.0, 0.0, 0.0, 1.0 + }; + + // Transform matrix from clip space to camera space. + constexpr float kFovY = 50.0 / 180.0 * M_PI; + constexpr float kTanFovY = std::tan(kFovY / 2.0); + const float view_from_clip[16] = { + kTanFovY * static_cast(kWidth) / kHeight, 0.0, 0.0, 0.0, + 0.0, kTanFovY, 0.0, 0.0, + 0.0, 0.0, 0.0, -1.0, + 0.0, 0.0, 1.0, 1.0 + }; + + // Transform matrix from clip space to world space. + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 3; ++col) { + int col2 = col < 2 ? col : 3; + model_from_clip_[col + 3 * row] = + model_from_view[0 + 4 * row] * view_from_clip[col2 + 0] + + model_from_view[1 + 4 * row] * view_from_clip[col2 + 4] + + model_from_view[2 + 4 * row] * view_from_clip[col2 + 8]; + } + } + + camera_ = Position(kCameraPos[0] * m, kCameraPos[1] * m, kCameraPos[2] * m); + exposure_ = use_luminance ? 1e-4 : 10.0; + use_luminance_ = use_luminance; + sun_direction_ = Direction( + cos(sun_phi) * sin(sun_theta), + sin(sun_phi) * sin(sun_theta), + cos(sun_theta)); + } + +/* +

The last "setup" method is the TearDown method, which is called +after each test case. It releases all the resources that might have been +allocated during the test: +*/ + + void TearDown() override { + model_ = nullptr; + reference_model_ = nullptr; + if (program_) { + glDeleteProgram(program_); + } + } + +/* +

Rendering methods

+ +

Once the test fixture is initialized with the above methods, we can render +one or more images, either with the GPU or with the CPU model. For the GPU model +we must first initialize the GPU shader, which can be done with the following +method: +*/ + + void InitShader() { + GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex_shader, 1, &kVertexShader, NULL); + glCompileShader(vertex_shader); + + const std::string fragment_shader_str = + "#version 330\n" + + std::string(use_luminance_ ? "#define USE_LUMINANCE\n" : "") + + std::string(kFragmentShader) + definitions_glsl + + "const vec3 kSphereCenter = vec3(0.0, 0.0, " + + std::to_string(kSphereRadius.to(kLengthUnit)) + ");\n" + + "const float kSphereRadius = " + + std::to_string(kSphereRadius.to(kLengthUnit)) + ";\n" + + model_test_glsl; + const char* fragment_shader_source = fragment_shader_str.c_str(); + GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL); + glCompileShader(fragment_shader); + + if (program_) { + glDeleteProgram(program_); + } + program_ = glCreateProgram(); + glAttachShader(program_, vertex_shader); + glAttachShader(program_, fragment_shader); + glAttachShader(program_, model_->GetShader()); + glLinkProgram(program_); + glDetachShader(program_, vertex_shader); + glDetachShader(program_, fragment_shader); + glDetachShader(program_, model_->GetShader()); + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + glUseProgram(program_); + model_->SetProgramUniforms(program_, 0, 1, 2, 3); + glUniformMatrix3fv(glGetUniformLocation(program_, "model_from_clip"), + 1, true, model_from_clip_.data()); + glUniform3f(glGetUniformLocation(program_, "camera_"), + camera_.x.to(kLengthUnit), + camera_.y.to(kLengthUnit), + camera_.z.to(kLengthUnit)); + glUniform1f(glGetUniformLocation(program_, "exposure_"), + exposure_()); + glUniform3f(glGetUniformLocation(program_, "earth_center_"), + earth_center_.x.to(kLengthUnit), + earth_center_.y.to(kLengthUnit), + earth_center_.z.to(kLengthUnit)); + glUniform3f(glGetUniformLocation(program_, "sun_direction_"), + sun_direction_.x(), + sun_direction_.y(), + sun_direction_.z()); + glUniform3f(glGetUniformLocation(program_, "sun_radiance_"), + sun_radiance_(kLambdaR).to(watt_per_square_meter_per_sr_per_nm), + sun_radiance_(kLambdaG).to(watt_per_square_meter_per_sr_per_nm), + sun_radiance_(kLambdaB).to(watt_per_square_meter_per_sr_per_nm)); + glUniform2f(glGetUniformLocation(program_, "sun_size_"), + sun_size_.x(), sun_size_.y()); + glUniform3f(glGetUniformLocation(program_, "ground_albedo_"), + ground_albedo_(kLambdaR)(), + ground_albedo_(kLambdaG)(), + ground_albedo_(kLambdaB)()); + glUniform3f(glGetUniformLocation(program_, "sphere_albedo_"), + sphere_albedo_(kLambdaR)(), + sphere_albedo_(kLambdaG)(), + sphere_albedo_(kLambdaB)()); + } + +/* +

With the help of this method, we can now implement a method to render an +image with the GPU model. For this we just need to render a full screen quad +with the GPU program, and then read back the framebuffer pixels. +*/ + + Image RenderGpuImage() { + InitShader(); + + glViewport(0, 0, kWidth, kHeight); + glBegin(GL_TRIANGLE_STRIP); + glVertex4f(-1.0, -1.0, 0.0, 1.0); + glVertex4f(+1.0, -1.0, 0.0, 1.0); + glVertex4f(-1.0, +1.0, 0.0, 1.0); + glVertex4f(+1.0, +1.0, 0.0, 1.0); + glEnd(); + glutSwapBuffers(); + + std::unique_ptr gl_pixels( + new unsigned char[4 * kWidth * kHeight]); + glReadPixels( + 0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, gl_pixels.get()); + + Image pixels(new unsigned int[kWidth * kHeight]); + for (unsigned int j = 0; j < kHeight; ++j) { + for (unsigned int i = 0; i < kWidth; ++i) { + int gl_offset = 4 * (i + (kHeight - 1 - j) * kWidth); + int offset = i + j * kWidth; + pixels[offset] = (255 << 24) | + (gl_pixels[gl_offset] << 16) | + (gl_pixels[gl_offset + 1] << 8) | + gl_pixels[gl_offset + 2]; + } + } + return pixels; + } + +/* +

In order to render an image with the CPU model, we must first provide its +CPU implementation. For this, as for the unit tests of the GPU model, we simply +view the GLSL shader model_test.glsl as C++ +code (see the Introduction), that we include here +directly (after the definitions of the functions and macros it requires - the +"uniforms" are provided by the fields of the test fixture class, defined at the +end of this file): +*/ + + RadianceSpectrum GetSkyRadiance(Position camera, Direction view_ray, + Length shadow_length, Direction sun_direction, + DimensionlessSpectrum& transmittance) { + return reference_model_->GetSkyRadiance( + camera, view_ray, shadow_length, sun_direction, &transmittance); + } + + RadianceSpectrum GetSkyRadianceToPoint(Position camera, Position point, + Length shadow_length, Direction sun_direction, + DimensionlessSpectrum& transmittance) { + return reference_model_->GetSkyRadianceToPoint( + camera, point, shadow_length, sun_direction, &transmittance); + } + + IrradianceSpectrum GetSunAndSkyIrradiance(Position point, Direction normal, + Direction sun_direction, IrradianceSpectrum& sky_irradiance) { + return reference_model_->GetSunAndSkyIrradiance( + point, normal, sun_direction, &sky_irradiance); + } + +#define OUT(x) x& +#include "atmosphere/reference/model_test.glsl" + +/* +

With this CPU implementation, we can render an image with a simple loop over +all the pixels, calling GetViewRayRadiance for each pixel, and +using the same tone mapping function as in the GPU version to convert the result +to a final color. The main difference with the GPU model is the conversion from +a radiance spectrum to an sRGB value, which must be done explicitely if a +luminance output is desired (otherwise, for radiance outputs, we simply need to +sample the radiance spectrum at the 3 predefined wavelengths): +*/ + + Image RenderCpuImage() { + constexpr auto kMaxLuminousEfficacy = MAX_LUMINOUS_EFFICACY * lm / watt; + std::vector wavelengths; + std::vector x_values; + std::vector y_values; + std::vector z_values; + for (unsigned int i = 0; i < 95 * 4; i += 4) { + wavelengths.push_back(CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[i] * nm); + x_values.push_back(CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[i + 1]); + y_values.push_back(CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[i + 2]); + z_values.push_back(CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[i + 3]); + } + const auto cie_x_bar = DimensionlessSpectrum(wavelengths, x_values); + const auto cie_y_bar = DimensionlessSpectrum(wavelengths, y_values); + const auto cie_z_bar = DimensionlessSpectrum(wavelengths, z_values); + + Image pixels(new unsigned int[kWidth * kHeight]); + ProgressBar progress_bar(kWidth * kHeight); + RunJobs([&](unsigned int j) { + double y = 1.0 - 2.0 * (j + 0.5) / kHeight; + double dy = -2.0 / kHeight; + for (unsigned int i = 0; i < kWidth; ++i) { + double x = 2.0 * (i + 0.5) / kWidth - 1.0; + double dx = 2.0 / kWidth; + + Direction view_ray( + model_from_clip_[0] * x + model_from_clip_[1] * y + + model_from_clip_[2], + model_from_clip_[3] * x + model_from_clip_[4] * y + + model_from_clip_[5], + model_from_clip_[6] * x + model_from_clip_[7] * y + + model_from_clip_[8]); + + Direction view_ray_diff( + model_from_clip_[0] * dx + model_from_clip_[1] * dy, + model_from_clip_[3] * dx + model_from_clip_[4] * dy, + model_from_clip_[6] * dx + model_from_clip_[7] * dy); + + RadianceSpectrum radiance = GetViewRayRadiance(view_ray, view_ray_diff); + + double r, g, b; + if (use_luminance_) { + Luminance x = kMaxLuminousEfficacy * Integral(radiance * cie_x_bar); + Luminance y = kMaxLuminousEfficacy * Integral(radiance * cie_y_bar); + Luminance z = kMaxLuminousEfficacy * Integral(radiance * cie_z_bar); + r = (XYZ_TO_SRGB[0] * x + XYZ_TO_SRGB[1] * y + XYZ_TO_SRGB[2] * z).to( + cd_per_square_meter); + g = (XYZ_TO_SRGB[3] * x + XYZ_TO_SRGB[4] * y + XYZ_TO_SRGB[5] * z).to( + cd_per_square_meter); + b = (XYZ_TO_SRGB[6] * x + XYZ_TO_SRGB[7] * y + XYZ_TO_SRGB[8] * z).to( + cd_per_square_meter); + } else { + r = radiance(kLambdaR).to(watt_per_square_meter_per_sr_per_nm); + g = radiance(kLambdaG).to(watt_per_square_meter_per_sr_per_nm); + b = radiance(kLambdaB).to(watt_per_square_meter_per_sr_per_nm); + } + + r = std::pow(1.0 - std::exp(-r * exposure_()), 1.0 / 2.2); + g = std::pow(1.0 - std::exp(-g * exposure_()), 1.0 / 2.2); + b = std::pow(1.0 - std::exp(-b * exposure_()), 1.0 / 2.2); + unsigned int red = static_cast(r * 255.0); + unsigned int green = static_cast(g * 255.0); + unsigned int blue = static_cast(b * 255.0); + pixels[i + j * kWidth] = + (255 << 24) | (red << 16) | (green << 8) | blue; + progress_bar.Increment(1); + } + }, kHeight); + return pixels; + } + +/* +

Comparison methods

+ +

After some images have been rendered, we want to compare them in order to +check whether two images of the same scene, rendered with different methods, are +close enough or not. This is the goal of the following method, which uses the +Peak Signal +to Noise Ratio as the image difference measure. +*/ + + double ComputePSNR(const unsigned int* image1, const unsigned int* image2) { + double square_error_sum = 0.0; + for (unsigned int j = 0; j < kHeight; ++j) { + for (unsigned int i = 0; i < kWidth; ++i) { + int argb1 = image1[i + j * kWidth]; + int argb2 = image2[i + j * kWidth]; + dimensional::Vector3 rgb1( + (argb1 >> 16) & 0xFF, (argb1 >> 8) & 0xFF, argb1 & 0xFF); + dimensional::Vector3 rgb2( + (argb2 >> 16) & 0xFF, (argb2 >> 8) & 0xFF, argb2 & 0xFF); + square_error_sum += dot(rgb1 - rgb2, rgb1 - rgb2)(); + } + } + double mean_square_error = sqrt(square_error_sum / (kWidth * kHeight)); + return 10.0 * log(255 * 255 / mean_square_error) / log(10.0); + } + +/* +

Also, in order to visually compare the images, it is useful to have an HTML +test report, showing for each test case its two images and their PSNR score +difference. For this, the following method compares two images, writes them to +disk, creates or appends a test report entry in a test report file, and finally +returns the computed PSNR. +*/ + + double Compare(Image image1, Image image2, const std::string& caption, + bool append) { + double psnr = ComputePSNR(image1.get(), image2.get()); + WritePngArgb(name_ + "1.png", image1.get()); + WritePngArgb(name_ + "2.png", image2.get()); + std::ofstream file(std::string(kOutputDir) + "test_report.html", + append ? std::ios_base::app : std::ios_base::trunc); + file << "

" << name_ << " (PSNR = " << psnr << "dB)

" << std::endl + << "

" << caption << std::endl + << "

" << std::endl + << "" << std::endl; + file.close(); + return psnr; + } + +/* +

Test cases

+ +

Thanks to the above helper methods we can finally implement some test cases +to make sure that the GPU model, despite its approximations, is almost as +accurate as the full spectral, but much slower CPU model. + +

The first test case compares the radiance computations, done on GPU vs CPU. +It uses a separate RGB texture for the single Mie scattering, which means that +the computations done on GPU and CPU, for the 3 wavelengths of the GPU model, +are exactly the same (except for the floating point precision: single precision +on GPU - with half precision in the precomputed textures, vs double precision on +CPU). As a consequence, we expect the two images to be nearly identical: +*/ + + void TestRadianceSeparateTextures() { + const std::string kCaption = "Left: GPU model, combine_textures = false. " + "Right: CPU model. Both images show the spectral radiance at 3 " + "predefined wavelengths (i.e. no conversion to sRGB via CIE XYZ)."; + InitGpuModel(false /* combine_textures */); + InitCpuModel(); + SetViewParameters(65.0 * deg, 90.0 * deg, false /* use_luminance */); + ExpectLess( + 47.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, false)); + } + +/* +

The following test case is almost the same as the previous one, except that +we use the the combine_textures option in the GPU model. This leads to some +approximations on the GPU side, not present in the CPU model. As a consequence, +we expect a slightly larger difference between the two images, compared to the +previous test case: +*/ + + void TestRadianceCombineTextures() { + const std::string kCaption = "Left: GPU model, combine_textures = true. " + "Right: CPU model. Both images show the spectral radiance at 3 " + "predefined wavelengths (i.e. no conversion to sRGB via CIE XYZ)."; + InitGpuModel(true /* combine_textures */); + InitCpuModel(); + SetViewParameters(65.0 * deg, 90.0 * deg, false /* use_luminance */); + ExpectLess( + 46.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The following test case is the same as the previous one, for a sunset scene. +In this case the single Mie scattering contribution is larger than in the +previous test, so we expect a larger difference between the GPU and CPU results +(due to the GPU approximations for the single Mie scattering term): +*/ + + void TestRadianceCombineTexturesSunSet() { + const std::string kCaption = "Left: GPU model, combine_textures = true. " + "Right: CPU model. Both images show the spectral radiance at 3 " + "predefined wavelengths (i.e. no conversion to sRGB via CIE XYZ)."; + InitGpuModel(true /* combine_textures */); + InitCpuModel(); + SetViewParameters(88.0 * deg, 90.0 * deg, false /* use_luminance */); + ExpectLess( + 40.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The following test case compares the sRGB luminance computations, done on GPU +vs CPU. The GPU computations use approximations to obtain the sRGB values from +the value of the spectral radiance at only 3 wavelengths, while the CPU +computations use a "full spectral" rendering method (using the value of the +spectral radiance at 47 wavelengths between 360 and 830 nm). To evaluate the +effect of these approximations alone, we don't use the combine_textures option +on GPU (which introduces additional approximations). Similarly, we use constant +albedos instead of wavelength dependent albedos to avoid additional +approximations: +*/ + + void TestLuminanceSeparateTexturesConstantAlbedo() { + const std::string kCaption = "Left: GPU model, combine_textures = false. " + "Right: CPU model. Both images show the sRGB luminance (radiance " + "converted to CIE XYZ and then to sRGB - with some approximations on " + "GPU)."; + sphere_albedo_ = DimensionlessSpectrum(0.8); + ground_albedo_ = DimensionlessSpectrum(0.1); + InitGpuModel(false /* combine_textures */); + InitCpuModel(); + SetViewParameters(65.0 * deg, 90.0 * deg, true /* use_luminance */); + ExpectLess( + 43.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The following test case is almost the same as the previous one, except that +we use the the combine_textures option in the GPU model. This leads to some +additional approximations on the GPU side, not present in the CPU model. As a +consequence, we expect a slightly larger difference between the two images, +compared to the previous test case: +*/ + + void TestLuminanceCombineTexturesConstantAlbedo() { + const std::string kCaption = "Left: GPU model, combine_textures = true. " + "Right: CPU model. Both images show the sRGB luminance (radiance " + "converted to CIE XYZ and then to sRGB - with some approximations on " + "GPU)."; + sphere_albedo_ = DimensionlessSpectrum(0.8); + ground_albedo_ = DimensionlessSpectrum(0.1); + InitGpuModel(true /* combine_textures */); + InitCpuModel(); + SetViewParameters(65.0 * deg, 90.0 * deg, true /* use_luminance */); + ExpectLess( + 43.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The following test case is the same as the previous one, for a sunset scene. +In this case the single Mie scattering contribution is larger than in the +previous test, so we expect a larger difference between the GPU and CPU results +(due to the GPU approximations for the single Mie scattering term): +*/ + + void TestLuminanceCombineTexturesConstantAlbedoSunSet() { + const std::string kCaption = "Left: GPU model, combine_textures = true. " + "Right: CPU model. Both images show the sRGB luminance (radiance " + "converted to CIE XYZ and then to sRGB - with some approximations on " + "GPU)."; + sphere_albedo_ = DimensionlessSpectrum(0.8); + ground_albedo_ = DimensionlessSpectrum(0.1); + InitGpuModel(true /* combine_textures */); + InitCpuModel(); + SetViewParameters(88.0 * deg, 90.0 * deg, true /* use_luminance */); + ExpectLess( + 37.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The following test case compares the sRGB luminance computations, done on GPU +vs CPU, with wavelength dependent albedo values. This leads, on the GPU side, to +new approximations compared to the CPU reference model (indeed, on GPU we +convert the sky and sun radiance to sRGB, and then multiply these sRGB values by +the albedo, sampled at 3 wavelengths - while the correct method, used on CPU, is +to perform all the computations, including the albedo multiplication, in a full +spectral way, and to convert to XYZ and then to sRGB only at the very end). +Because of this additional approximation, we expect a larger difference between +the GPU and CPU model than in the previous test cases: +*/ + + void TestLuminanceCombineTexturesSpectralAlbedo() { + const std::string kCaption = "Left: GPU model, combine_textures = true. " + "Right: CPU model. Both images show the sRGB luminance (radiance " + "converted to CIE XYZ and then to sRGB - with some approximations on " + "GPU)."; + InitGpuModel(true /* combine_textures */); + InitCpuModel(); + SetViewParameters(65.0 * deg, 90.0 * deg, true /* use_luminance */); + ExpectLess( + 38.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The last test case compares the sRGB luminance computations, done on GPU +vs CPU, in a "worst case" situation: combined textures on GPU and a sunset scene +(leading to large differences in the single Mie component), and wavelength +dependent albedo values (see the previous test case): +*/ + + void TestLuminanceCombineTexturesSpectralAlbedoSunSet() { + const std::string kCaption = "Left: GPU model, combine_textures = true. " + "Right: CPU model. Both images show the sRGB luminance (radiance " + "converted to CIE XYZ and then to sRGB - with some approximations on " + "GPU)."; + InitGpuModel(true /* combine_textures */); + InitCpuModel(); + SetViewParameters(88.0 * deg, 90.0 * deg, true /* use_luminance */); + ExpectLess( + 37.0, Compare(RenderGpuImage(), RenderCpuImage(), kCaption, true)); + } + +/* +

The rest of the code simply declares the fields of our test fixture class, +and registers the test cases in the test framework: +*/ + + private: + std::string name_; + AtmosphereParameters atmosphere_parameters_; + DimensionlessSpectrum ground_albedo_; + DimensionlessSpectrum sphere_albedo_; + Position earth_center_; + RadianceSpectrum sun_radiance_; + dimensional::vec2 sun_size_; + + std::unique_ptr model_; + std::unique_ptr reference_model_; + GLuint program_; + + std::array model_from_clip_; + Position camera_; + Number exposure_; + bool use_luminance_; + Direction sun_direction_; +}; + +namespace { + +ModelTest radiance1( + "RadianceSeparateTextures", + &ModelTest::TestRadianceSeparateTextures); +ModelTest radiance2( + "RadianceCombineTextures", + &ModelTest::TestRadianceCombineTextures); +ModelTest radiance3( + "RadianceCombineTexturesSunSet", + &ModelTest::TestRadianceCombineTexturesSunSet); +ModelTest luminance1( + "LuminanceSeparateTexturesConstantAlbedo", + &ModelTest::TestLuminanceSeparateTexturesConstantAlbedo); +ModelTest luminance2( + "LuminanceCombineTexturesConstantAlbedo", + &ModelTest::TestLuminanceCombineTexturesConstantAlbedo); +ModelTest luminance3( + "LuminanceCombineTexturesConstantAlbedoSunSet", + &ModelTest::TestLuminanceCombineTexturesConstantAlbedoSunSet); +ModelTest luminance4( + "LuminanceCombineTexturesSpectralAlbedo", + &ModelTest::TestLuminanceCombineTexturesSpectralAlbedo); +ModelTest luminance5( + "LuminanceCombineTexturesSpectralAlbedoSunSet", + &ModelTest::TestLuminanceCombineTexturesSpectralAlbedoSunSet); + +} // anonymous namespace + +} // namespace reference +} // namespace atmosphere diff --git a/atmosphere/reference/model_test.glsl b/atmosphere/reference/model_test.glsl new file mode 100644 index 0000000..0b4e21b --- /dev/null +++ b/atmosphere/reference/model_test.glsl @@ -0,0 +1,348 @@ +/** + * 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. + */ + +/*

atmosphere/reference/model_test.glsl

+ +

This GLSL file is used to render the test scene in +model_test.cc, on GPU and CPU, in order to +evaluate the approximations made in the GPU atmosphere model. For this reason, +as for the GPU model shaders, it is written in such a way that it can be +compiled either with a GLSL compiler, or with a C++ compiler. + +

+ +

The test scene, shown above, is a sphere S on a purely spherical planet P. It +is rendered by "ray tracing", i.e. the vertex shader outputs the view ray +direction, and the fragment shader computes the intersection of this ray with +the spheres S and P to produce the final pixels. The fragment shader also +computes the intersection of the light rays with the sphere S, to compute +shadows, as well as the intersections of the view ray with the shadow volume of +S, in order to compute light shafts. + +

Shadows and light shafts

+ +

The functions to compute shadows and light shafts must be defined before we +can use them in the main shader function, so we define them first. Testing if +a point is in the shadow of the sphere S is equivalent to test if the +corresponding light ray intersects the sphere, which is very simple to do. +However, this is only valid for a punctual light source, which is not the case +of the Sun. In the following function we compute an approximate (and biased) +soft shadow by taking the angular size of the Sun into account: +*/ + +Number GetSunVisibility(Position point, Direction sun_direction) { + Position p = point - kSphereCenter; + Length p_dot_v = dot(p, sun_direction); + Area p_dot_p = dot(p, p); + Area ray_sphere_center_squared_distance = p_dot_p - p_dot_v * p_dot_v; + Length distance_to_intersection = -p_dot_v - sqrt( + kSphereRadius * kSphereRadius - ray_sphere_center_squared_distance); + if (distance_to_intersection > 0.0 * m) { + // Compute the distance between the view ray and the sphere, and the + // corresponding (tangent of the) subtended angle. Finally, use this to + // compute an approximate sun visibility. + Length ray_sphere_distance = + kSphereRadius - sqrt(ray_sphere_center_squared_distance); + Number ray_sphere_angular_distance = -ray_sphere_distance / p_dot_v; + return smoothstep( + Number(1.0), Number(0.0), ray_sphere_angular_distance / sun_size_.x); + } + return 1.0; +} + +/* +

The sphere also partially occludes the sky light, and we approximate this +effect with an ambient occlusion factor. The ambient occlusion factor due to a +sphere is given in Radiation View Factors (Isidoro Martinez, 1995). In the simple case where +the sphere is fully visible, it is given by the following function: +*/ + +Number GetSkyVisibility(Position point) { + Position p = point - kSphereCenter; + Area p_dot_p = dot(p, p); + return + 1.0 + p.z / sqrt(p_dot_p) * kSphereRadius * kSphereRadius / p_dot_p; +} + +/* +

To compute light shafts we need the intersections of the view ray with the +shadow volume of the sphere S. Since the Sun is not a punctual light source this +shadow volume is not a cylinder but a cone (for the umbra, plus another cone for +the penumbra, but we ignore it here): + + + + + + + + + + + + + + + + + + + + + + p + q + s + v + R + r + ρ + d + δ + α + + +

Noting, as in the above figure, $\bp$ the camera position, $\bv$ and $\bs$ +the unit view ray and sun direction vectors and $R$ the sphere radius (supposed +to be centered on the origin), the point at distance $d$ from the camera is +$\bq=\bp+d\bv$. This point is at a distance $\delta=-\bq\cdot\bs$ from the +sphere center along the umbra cone axis, and at a distance $r$ from this axis +given by $r^2=\bq\cdot\bq-\delta^2$. Finally, at distance $\delta$ along the +axis the umbra cone has radius $\rho=R-\delta\tan\alpha$, where $\alpha$ is +the Sun's angular radius. The point at distance $d$ from the camera is on the +shadow cone only if $r^2=\rho^2$, i.e. only if +\begin{equation} +(\bp+d\bv)\cdot(\bp+d\bv)-((\bp+d\bv)\cdot\bs)^2= +(R+((\bp+d\bv)\cdot\bs)\tan\alpha)^2 +\end{equation} +Developping this gives a quadratic equation for $d$: +\begin{equation} +ad^2+2bd+c=0 +\end{equation} +where +

    +
  • $a=1-l(\bv\cdot\bs)^2$,
  • +
  • $b=\bp\cdot\bv-l(\bp\cdot\bs)(\bv\cdot\bs)-\tan(\alpha)R(\bv\cdot\bs)$,
  • +
  • $c=\bp\cdot\bp-l(\bp\cdot\bs)^2-2\tan(\alpha)R(\bp\cdot\bs)-R^2$,
  • +
  • $l=1+\tan^2\alpha$
  • +
+From this we deduce the two possible solutions for $d$, which must be clamped to +the actual shadow part of the mathematical cone (i.e. the slab between the +sphere center and the cone apex or, in other words, the points for which +$\delta$ is between $0$ and $R/\tan\alpha$). The following function implements +these equations: +*/ + +void GetSphereShadowInOut(Direction view_direction, Direction sun_direction, + OUT(Length) d_in, OUT(Length) d_out) { + Position pos = camera_ - kSphereCenter; + Length pos_dot_sun = dot(pos, sun_direction_); + Number view_dot_sun = dot(view_direction, sun_direction_); + Number k = sun_size_.x; + Number l = 1.0 + k * k; + Number a = 1.0 - l * view_dot_sun * view_dot_sun; + Length b = dot(pos, view_direction) - l * pos_dot_sun * view_dot_sun - + k * kSphereRadius * view_dot_sun; + Area c = dot(pos, pos) - l * pos_dot_sun * pos_dot_sun - + 2.0 * k * kSphereRadius * pos_dot_sun - kSphereRadius * kSphereRadius; + Area discriminant = b * b - a * c; + if (discriminant > 0.0 * m2) { + d_in = max(0.0 * m, (-b - sqrt(discriminant)) / a); + d_out = (-b + sqrt(discriminant)) / a; + // The values of d for which delta is equal to 0 and kSphereRadius / k. + Length d_base = -pos_dot_sun / view_dot_sun; + Length d_apex = -(pos_dot_sun + kSphereRadius / k) / view_dot_sun; + if (view_dot_sun > 0.0) { + d_in = max(d_in, d_apex); + d_out = a > 0.0 ? min(d_out, d_base) : d_base; + } else { + d_in = a > 0.0 ? max(d_in, d_base) : d_base; + d_out = min(d_out, d_apex); + } + } else { + d_in = 0.0 * m; + d_out = 0.0 * m; + } +} + +/*

Main shading function

+ +

Using these functions we can now implement the main shader function, which +computes the radiance from the scene for a given view ray. This function first +tests if the view ray intersects the sphere S. If so it computes the sun and +sky light received by the sphere at the intersection point, combines this with +the sphere BRDF and the aerial perspective between the camera and the sphere. +It then does the same with the ground, i.e. with the planet sphere P, and then +computes the sky radiance and transmittance. Finally, all these terms are +composited together (an opacity is also computed for each object, using an +approximate view cone - sphere intersection factor) to get the final radiance. + +

We start with the computation of the intersections of the view ray with the +shadow volume of the sphere, because they are needed to get the aerial +perspective for the sphere and the planet: +*/ + +RadianceSpectrum GetViewRayRadiance(Direction view_ray, + Direction view_ray_diff) { + // Normalized view direction vector. + Direction view_direction = normalize(view_ray); + // Tangent of the angle subtended by this fragment. + Number fragment_angular_size = length(view_ray_diff) / length(view_ray); + + Length shadow_in; + Length shadow_out; + GetSphereShadowInOut(view_direction, sun_direction_, shadow_in, shadow_out); + +/* +

We then test whether the view ray intersects the sphere S or not. If it does, +we compute an approximate (and biased) opacity value, using the same +approximation as in GetSunVisibility: +*/ + + // Compute the distance between the view ray line and the sphere center, + // and the distance between the camera and the intersection of the view + // ray with the sphere (or NaN if there is no intersection). + Position p = camera_ - kSphereCenter; + Length p_dot_v = dot(p, view_direction); + Area p_dot_p = dot(p, p); + Area ray_sphere_center_squared_distance = p_dot_p - p_dot_v * p_dot_v; + Length distance_to_intersection = -p_dot_v - sqrt( + kSphereRadius * kSphereRadius - ray_sphere_center_squared_distance); + + // Compute the radiance reflected by the sphere, if the ray intersects it. + Number sphere_alpha = 0.0; + RadianceSpectrum sphere_radiance = + RadianceSpectrum(0.0 * watt_per_square_meter_per_sr_per_nm); + if (distance_to_intersection > 0.0 * m) { + // Compute the distance between the view ray and the sphere, and the + // corresponding (tangent of the) subtended angle. Finally, use this to + // compute the approximate analytic antialiasing factor sphere_alpha. + Length ray_sphere_distance = + kSphereRadius - sqrt(ray_sphere_center_squared_distance); + Number ray_sphere_angular_distance = -ray_sphere_distance / p_dot_v; + sphere_alpha = + min(ray_sphere_angular_distance / fragment_angular_size, 1.0); + +/* +

We can then compute the intersection point and its normal, and use them to +get the sun and sky irradiance received at this point. The reflected radiance +follows, by multiplying the irradiance with the sphere BRDF: +*/ + Position point = camera_ + view_direction * distance_to_intersection; + Direction normal = normalize(point - kSphereCenter); + + // Compute the radiance reflected by the sphere. + IrradianceSpectrum sky_irradiance; + IrradianceSpectrum sun_irradiance = GetSunAndSkyIrradiance( + point - earth_center_, normal, sun_direction_, sky_irradiance); + sphere_radiance = + sphere_albedo_ * (1.0 / (PI * sr)) * (sun_irradiance + sky_irradiance); + +/* +

Finally, we take into account the aerial perspective between the camera and +the sphere, which depends on the length of this segment which is in shadow: +*/ + Length shadow_length = + max(0.0 * m, min(shadow_out, distance_to_intersection) - shadow_in); + DimensionlessSpectrum transmittance; + RadianceSpectrum in_scatter = GetSkyRadianceToPoint(camera_ - earth_center_, + point - earth_center_, shadow_length, sun_direction_, transmittance); + sphere_radiance = sphere_radiance * transmittance + in_scatter; + } + +/* +

In the following we repeat the same steps as above, but for the planet sphere +P instead of the sphere S (a smooth opacity is not really needed here, so we +don't compute it. Note also how we modulate the sun and sky irradiance received +on the ground by the sun and sky visibility factors): +*/ + + // Compute the distance between the view ray line and the Earth center, + // and the distance between the camera and the intersection of the view + // ray with the ground (or NaN if there is no intersection). + p = camera_ - earth_center_; + p_dot_v = dot(p, view_direction); + p_dot_p = dot(p, p); + Area ray_earth_center_squared_distance = p_dot_p - p_dot_v * p_dot_v; + distance_to_intersection = -p_dot_v - sqrt( + earth_center_.z * earth_center_.z - ray_earth_center_squared_distance); + + // Compute the radiance reflected by the ground, if the ray intersects it. + Number ground_alpha = 0.0; + RadianceSpectrum ground_radiance = + RadianceSpectrum(0.0 * watt_per_square_meter_per_sr_per_nm); + if (distance_to_intersection > 0.0 * m) { + Position point = camera_ + view_direction * distance_to_intersection; + Direction normal = normalize(point - earth_center_); + + // Compute the radiance reflected by the ground. + IrradianceSpectrum sky_irradiance; + IrradianceSpectrum sun_irradiance = GetSunAndSkyIrradiance( + point - earth_center_, normal, sun_direction_, sky_irradiance); + ground_radiance = ground_albedo_ * (1.0 / (PI * sr)) * ( + sun_irradiance * GetSunVisibility(point, sun_direction_) + + sky_irradiance * GetSkyVisibility(point)); + + Length shadow_length = + max(0.0 * m, min(shadow_out, distance_to_intersection) - shadow_in); + DimensionlessSpectrum transmittance; + RadianceSpectrum in_scatter = GetSkyRadianceToPoint(camera_ - earth_center_, + point - earth_center_, shadow_length, sun_direction_, transmittance); + ground_radiance = ground_radiance * transmittance + in_scatter; + ground_alpha = 1.0; + } + +/* +

Finally, we compute the radiance and transmittance of the sky, and composite +together, from back to front, the radiance and opacities of all the ojects of +the scene: +*/ + + // Compute the radiance of the sky. + Length shadow_length = max(0.0 * m, shadow_out - shadow_in); + DimensionlessSpectrum transmittance; + RadianceSpectrum radiance = GetSkyRadiance( + camera_ - earth_center_, view_direction, shadow_length, sun_direction_, + transmittance); + + // If the view ray intersects the Sun, add the Sun radiance. + if (dot(view_direction, sun_direction_) > sun_size_.y) { + radiance = radiance + transmittance * sun_radiance_; + } + radiance = radiance * (1.0 - ground_alpha) + ground_radiance * ground_alpha; + radiance = radiance * (1.0 - sphere_alpha) + sphere_radiance * sphere_alpha; + return radiance; +} diff --git a/external/dimensional_types b/external/dimensional_types new file mode 160000 index 0000000..84e1e53 --- /dev/null +++ b/external/dimensional_types @@ -0,0 +1 @@ +Subproject commit 84e1e53320cd89119d31ec87cfdc2b51825b0c4b diff --git a/external/minpng b/external/minpng new file mode 160000 index 0000000..cce0215 --- /dev/null +++ b/external/minpng @@ -0,0 +1 @@ +Subproject commit cce0215eb227dac8379068aa3e242756321947ba diff --git a/external/progress_bar b/external/progress_bar new file mode 160000 index 0000000..b34cdce --- /dev/null +++ b/external/progress_bar @@ -0,0 +1 @@ +Subproject commit b34cdce09e505e36df90bd7e3b621949f5a52c24 diff --git a/index b/index new file mode 100644 index 0000000..721dfa1 --- /dev/null +++ b/index @@ -0,0 +1,216 @@ +

Precomputed Atmospheric Scattering:
a New Implementation

+ +

Eric Bruneton, 2017

+ +
+ +
+ +

Introduction

+ +

This document presents a new implementation of our +Precomputed Atmospheric +Scattering paper. This new implementation is motivated by the fact that the +original implementation: +

    +
  • has almost no comments and no documentation, and as a result is difficult to understand and to reuse, +
  • +
  • has absolutely no tests, despite the high risk of implementation errors + due to the complexity of the atmospheric scattering equations, +
  • +
  • contains ad-hoc constants in its texture coordinates mapping functions + which are adapted to the Earth case, but cannot be reused for other planets, +
  • +
  • provides only one of the two options presented in the paper to store the + single Mie scattering components (i.e. store the 3 components, or store only + one and reconstruct the others with an approximation), +
  • +
  • does not implement the light shaft algorithm presented in the paper, +
  • +
  • uses an extra-terrestrial solar spectrum independent of the wavelength + (with an arbitrary and completely unphysical value "100") and displays the + radiance values directly instead of converting them first to luminance + values (via the CIE color matching functions). +
  • +
+To address these concerns, our new implementation: +
    +
  • uses more descriptive function and variable names, and adds extensive + comments and documentation. +
  • +
  • uses static type checking to verify the dimensional homogeneity of all the expressions, and uses unit tests to + check more complex constraints, +
  • +
  • uses slightly improved texture coordinates mapping functions which, in + particular, no longer use ad-hoc constants, +
  • +
  • provides the two options presented in the paper to store the single Mie + scattering components (which are then compared in our tests), +
  • +
  • partially implement the light shaft algorithm presented in the paper (it + implements Eqs. 17 and 18, but not the shadow volume algorithm), +
  • +
  • uses a configurable extra-terrestrial solar spectrum, and converts the + radiance values to RGB luminance values as described in A Qualitative and Quantitative + Evaluation of 8 Clear Sky Models. This gives almost the same results as + with a full spectral rendering method, at a fraction of the cost (we check + this by comparing the GPU results against full spectral CPU renderings). +
  • +
+ +

The sections below explain how this new implementation can be used, present +its structure and its documentation and give more details about its tests. + +

Usage

+ +

Our new implementation can be used in C++ / OpenGL applications as explained +in model.h, and as demonstrated in the +demo in atmosphere/demo. To run this demo, simply type make +demo in the main directory. + +

The default settings of this demo use the real solar spectrum, and display +luminance values computed as described in Section 14.3 of +A Qualitative and Quantitative +Evaluation of 8 Clear Sky Models. To simulate the settings of the original +implementation, set the solar spectrum to "constant", and turn off the +"use luminance" setting. + +

Structure

+ +

The source code is organized as follows: + +

    +
  • atmosphere/
      +
    • demo/
      • ...
    • +
    • reference/
      • ...
    • +
    • constants.h
    • +
    • definitions.glsl
    • +
    • functions.glsl
    • +
    • model.h
    • +
    • model.cc
    • +
  • +
+ +

The most important files are the 5 files in the atmosphere +directory. They contain the GLSL shaders that implement our atmosphere model, +and provide a C++ API to precompute the atmosphere textures and to use them in +an OpenGL application. This code does not depend on the content of the other +directories, and is the only piece which is needed in order to use our +atmosphere model on GPU. + +

The other directories provide examples and tests: +

    +
  • The atmosphere/demo directory shows how the API provided in + atmosphere can be used in practice, using a small C++/OpenGL + demo application. +
  • +
  • The atmosphere/reference directory provides a way to execute + our GLSL code on CPU. Its main purpose is to provide unit tests for the GLSL + shaders, and to statically check the dimensional homogeneity of all the expressions. This process is + explained in more details in the Tests section. + This code is also used to compute reference images on CPU using full + spectral rendering, in order to evaluate the accuracy of the approximate + "radiance to RGB luminance" conversion performed by the GPU shaders. It + depends on external libraries such as dimensional_types + (to check the dimensional homogeneity) and + minpng. +
  • +
+ +

Documentation

+ +

The documentation consists of a set of web pages, generated from the +extensive comments in each source code file: +

+ +

Tests

+ +

To reduce the risk of implementation errors, two kinds of verifications are +performed: +

    +
  • the dimensional homogeneity is checked at compile time, via static type + checking, +
  • +
  • the behavior of each function is checked at runtime, via unit tests. +
  • +
+ +

The main issue to implement this is that a GLSL compiler cannot check the +dimensional homogeneity, unlike a C++ compiler (see for instance +Boost.Units). Our solution to this problem is to write our GLSL code in such +a way that it can be compiled both by a GLSL compiler and by a C++ compiler. +For this: +

    +
  • we use macros to hide the few syntactic differences between GLSL and C++. + For instance, we define OUT(x) as out x in GLSL, + and as x& in C++, and declare output variables as + OUT(SomeType) someName in our shaders. +
  • +
  • we define the physical types, such as length or power, in a separate file, + which we provide in two versions: +
      +
    • the GLSL version + defines the physical types as aliases of predefined types, such as + float, +
    • +
    • the C++ version + defines the physical types based on dimensional_types + abstractions, which are designed to produce compile errors when + attempting to add, subtract or compare expressions with different + physical dimensions. +
    • +
    +
  • +
  • we use the predefined GLSL variables such as gl_FragCoord + only in the main functions, which we reduce to the minimum + (e.g. main() { gl_FragColor = Main(gl_FragCoord); }) and + exclude from the C++ compilation. +
  • +
+ +

Thanks to this double GLSL and C++ compilation, the unit tests for the GLSL +code can then be implemented either in GLSL or in C++. We chose C++ because it +is much more practical. Indeed, a C++ unit test does not need to send data to +the GPU and to read back the test result, unlike a GLSL unit test. diff --git a/precomputed_atmopheric_scattering.cbp b/precomputed_atmopheric_scattering.cbp new file mode 100644 index 0000000..33b60cb --- /dev/null +++ b/precomputed_atmopheric_scattering.cbp @@ -0,0 +1,206 @@ + + + + + + diff --git a/tools/docgen_main.cc b/tools/docgen_main.cc new file mode 100644 index 0000000..325fda5 --- /dev/null +++ b/tools/docgen_main.cc @@ -0,0 +1,117 @@ +/** + * 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. + */ + +#include +#include +#include +#include + +namespace { + +std::string GenerateHtml(const std::string& source, + const std::string& html_template) { + + std::string src = source; + // Put the copyright notice in a comment. + src = std::regex_replace(src, std::regex("\\/\\*\\*"), "", + std::regex_constants::format_first_only); + + // Put the code parts in

 blocks.
+  const std::string kPreBegin = "
";
+  const std::string kPreEnd = "
"; + src = std::regex_replace(src, std::regex("\\n\\/\\*"), kPreEnd); + src = std::regex_replace(src, std::regex("\\*\\/\\n"), kPreBegin); + if (src.find(kPreBegin) == std::string::npos) { + src = std::regex_replace(src, std::regex("-->\\n"), "-->\n" + kPreBegin, + std::regex_constants::format_first_only); + } + src += kPreEnd; + + // Escape the < and > characters in
 blocks.
+  std::stringstream body;
+  const std::string kBodyPlaceHolder = "BODY";
+  size_t start_pos = html_template.find(kBodyPlaceHolder);
+  body << html_template.substr(0, start_pos);
+  body << "\n";
+  bool in_pre_block = false;
+  for (unsigned int i = 0; i < src.length(); ++i) {
+    if (src[i] == '>') {
+      if (in_pre_block) {
+        body << ">";
+      } else {
+        if (i + 1 >= kPreBegin.length() &&
+            src.substr(i + 1 - kPreBegin.length(), kPreBegin.length()) ==
+                kPreBegin) {
+          in_pre_block = true;
+        }
+        body << ">";
+      }
+    } else if (src[i] == '<') {
+      if (in_pre_block &&
+          i + kPreEnd.length() <= src.length() &&
+          src.substr(i, kPreEnd.length()) == kPreEnd) {
+        in_pre_block = false;
+      }
+      body << (in_pre_block ? "<" : "<");
+    } else {
+      body << src[i];
+    }
+  }
+  body << "\n";
+  body << html_template.substr(start_pos + kBodyPlaceHolder.length());
+  return body.str();
+}
+
+}  // anonymous namespace
+
+int main(int argc, char** argv) {
+  if (argc != 4) {
+    std::cout << "Usage: " << argv[0]
+              << "   " << std::endl;
+    return -1;
+  }
+
+  std::ifstream source_stream(argv[1]);
+  std::string source(
+      (std::istreambuf_iterator(source_stream)),
+      std::istreambuf_iterator());
+
+  std::ifstream html_template_stream(argv[2]);
+  std::string html_template(
+      (std::istreambuf_iterator(html_template_stream)),
+      std::istreambuf_iterator());
+
+  std::ofstream output_file(argv[3]);
+  output_file << GenerateHtml(source, html_template);
+  output_file.close();
+
+  return 0;
+}
diff --git a/tools/docgen_template.html b/tools/docgen_template.html
new file mode 100644
index 0000000..00cc7eb
--- /dev/null
+++ b/tools/docgen_template.html
@@ -0,0 +1,108 @@
+
+  
+    
+      
+      
+      
+      
+      
+    
+  
+BODY
+