├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── cmake ├── FindEigen3.cmake └── OptimizeForArchitecture.cmake ├── include ├── AxialMoments.hpp ├── DirectionsSampling.hpp ├── SHRotation.hpp ├── SphericalHarmonics.hpp ├── SphericalIntegration.hpp └── Utils.hpp ├── scripts ├── PlotMaterial.py └── ProjectMaterials.py ├── tests ├── ArvoMoments.hpp ├── MomentsEquals.cpp ├── MomentsSign.cpp ├── MomentsVsArvo.cpp ├── MomentsVsMC.cpp ├── SH.hpp ├── SphericalH.cpp ├── SphericalInt.cpp ├── TestProduct.cpp ├── TestRotations.cpp ├── TestUtils.cpp ├── Tests.hpp ├── Timings.cpp └── UnitIntegral.cpp └── utils ├── Alta2Sh.cpp ├── IntegralConvergence.cpp ├── Merl.hpp └── Merl2Sh.cpp /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "externals/glm"] 2 | path = externals/glm 3 | url = https://github.com/g-truc/glm.git 4 | [submodule "externals/eigen"] 5 | path = externals/eigen 6 | url = https://github.com/RLovelett/eigen.git 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.2) 2 | project (AxialMoments CXX) 3 | 4 | # Add platform dependent flags 5 | set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") 6 | #include (OptimizeForArchitecture) 7 | 8 | # For debug build 9 | #set(CMAKE_BUILD_TYPE RELEASE) 10 | set(CMAKE_BUILD_TYPE DEBUG) 11 | 12 | if ( CMAKE_COMPILER_IS_GNUCC ) 13 | add_definitions ("-Wall -pedantic") 14 | endif ( CMAKE_COMPILER_IS_GNUCC ) 15 | 16 | include_directories ("include" "externals/eigen" "externals/glm" ".") 17 | 18 | # Add dependencies 19 | find_package(Threads REQUIRED) 20 | 21 | # Add main test suite 22 | add_executable (TestUtils tests/TestUtils.cpp include/Utils.hpp) 23 | add_executable (TestMnVsMC tests/MomentsVsMC.cpp include/AxialMoments.hpp) 24 | add_executable (TestMnVsArvo tests/MomentsVsArvo.cpp include/AxialMoments.hpp) 25 | add_executable (TestMnSign tests/MomentsSign.cpp include/AxialMoments.hpp) 26 | add_executable (TestMnEqual tests/MomentsEquals.cpp include/AxialMoments.hpp) 27 | add_executable (TestSpheInt tests/SphericalInt.cpp include/AxialMoments.hpp include/SphericalIntegration.hpp tests/SH.hpp) 28 | add_executable (TestSpheHar tests/SphericalH.cpp include/AxialMoments.hpp include/SphericalIntegration.hpp tests/SH.hpp) 29 | add_executable (TestRotation tests/TestRotations.cpp) 30 | add_executable (TestProduct tests/TestProduct.cpp) 31 | add_executable (UnitIntegral tests/UnitIntegral.cpp) 32 | add_executable (TimingsTests tests/Timings.cpp) 33 | target_link_libraries(TestRotation ${CMAKE_THREAD_LIBS_INIT}) 34 | target_compile_features(TestUtils PRIVATE cxx_range_for) 35 | target_compile_features(TestMnVsArvo PRIVATE cxx_range_for) 36 | target_compile_features(TestMnVsMC PRIVATE cxx_range_for) 37 | target_compile_features(TestMnSign PRIVATE cxx_range_for) 38 | target_compile_features(TestMnEqual PRIVATE cxx_range_for) 39 | target_compile_features(TestSpheInt PRIVATE cxx_range_for) 40 | target_compile_features(TestSpheHar PRIVATE cxx_range_for) 41 | target_compile_features(TestRotation PRIVATE cxx_range_for) 42 | target_compile_features(TestProduct PRIVATE cxx_range_for) 43 | target_compile_features(UnitIntegral PRIVATE cxx_range_for) 44 | target_compile_features(TimingsTests PRIVATE cxx_range_for) 45 | 46 | enable_testing() 47 | add_test(TestUtils TestUtils) 48 | add_test(TestSpheHar TestSpheHar) 49 | add_test(TestMnVsMC TestMnVsMC) 50 | add_test(TestMnVsArvo TestMnVsArvo) 51 | add_test(TestMnSign TestMnSign) 52 | add_test(TestMnEqual TestMnEqual) 53 | add_test(TestSpheInt TestSpheInt) 54 | add_test(TestRotation TestRotation) 55 | add_test(TestProduct TestProduct) 56 | add_test(UnitIntegral UnitIntegral) 57 | 58 | # Add SH examples 59 | add_executable (Merl2Sh utils/Merl2Sh.cpp) 60 | target_compile_features(Merl2Sh PRIVATE cxx_range_for) 61 | target_compile_options(Merl2Sh INTERFACE -O3 -msse3 -mfpmath=sse) 62 | target_link_libraries(Merl2Sh ${CMAKE_THREAD_LIBS_INIT}) 63 | 64 | set(ALTA_INCLUDE_DIR " " CACHE "ALTA include directory" PATH) 65 | set(ALTA_LIBRARY " " CACHE "ALTA core library file" FILEPATH) 66 | 67 | if( (EXISTS "${ALTA_INCLUDE_DIR}/core/plugins_manager.h") AND 68 | (EXISTS ${ALTA_LIBRARY}) ) 69 | message("Using ALTA library") 70 | include_directories(${ALTA_INCLUDE_DIR}) 71 | add_executable (Alta2Sh utils/Alta2Sh.cpp) 72 | target_compile_features(Alta2Sh PRIVATE cxx_range_for) 73 | target_compile_options(Alta2Sh INTERFACE -O3 -msse3 -mfpmath=sse -m64 -march=nocona) 74 | target_link_libraries(Alta2Sh ${CMAKE_THREAD_LIBS_INIT} ${ALTA_LIBRARY}) 75 | else() 76 | message("Could not find plugins_manager.h in ${ALTA_INCLUDE_DIR} or ${ALTA_LIBRARY}") 77 | endif() 78 | 79 | add_executable (IntegralConvergence utils/IntegralConvergence.cpp) 80 | target_compile_features(IntegralConvergence PRIVATE cxx_range_for) 81 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Analytical Integration of Spherical Harmonics ## 2 | 3 | This code implements the analytical integration method from *Integrating Clipped Spherical Harmonics Expansions*. It permits to efficiently integrates *Spherical Harmonics* (SH) over polygonal geometries. 4 | 5 | 6 | ### Use this code ### 7 | 8 | The integration code is header only and template based. It requires a C++11 compiler and uses [Eigen](http://eigen.tuxfamily.org) to perform linear algebra operations. Eigen is provided as a submodule of this git repository. 9 | 10 | A CMake script is provided to perform the sanity check of the code. Examples of use of the spherical integration code can be found in `test`. To compile the test suite, please init the different submodules using `git submodule update --init --recursive` and use the CMake script to generate the different binaries. 11 | 12 | #### Templates #### 13 | 14 | This code relies on templates to enable a simpler integration into other codebases. We provide examples of our wrappers in `tests/Test.hpp`. To use our code, you will have to define the following class wrappers: 15 | 16 | + `class SH` that provides `FastBasis`, a method to compute the vector of Spherical Harmonics basis elements for a given input vector and other SH related functions. See our implementation in `tests/SphericalInt.cpp`. 17 | + `class Vector` that represent 3D vectors. This class needs to provide static functions such as `Dot`, `Normalize` and `Cross`. 18 | + `class Triangle` and `class Edge` that represent a spherical triangle which is a simple iterator over a set of edges. 19 | 20 | #### Using Spherical Integration Code #### 21 | 22 | The spherical integration code is located in the `SphericalIntegration.hpp` header. Given an input triangle `triangle` and a *Spherical Harmonics* decomposition a function to integrate `clm`, the method works as follows: 23 | 24 | First, convert the SH expansion to a Zonal Harmonics expansion `zlm` with a set of basis vectors `basis`. You can use our `SamplingBlueNoise` method located in `DirectionsSampling.hpp` to generate a basis vectors: 25 | 26 | // Generate a basis vector for an order `order` SH 27 | const auto basis = SamplingBlueNoise(2*order+1); 28 | 29 | 30 | Then use this basis vectors to compute the conversion SH -> ZH -> Power of cosines: 31 | 32 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 33 | // and compute the product of the two: `Prod = A x Zw`. 34 | const auto ZW = ZonalWeights(basis); 35 | const auto Y = ZonalExpansion(basis); 36 | const auto A = computeInverse(Y); 37 | const auto cpw = clm.transpose() * (A*ZW); 38 | 39 | Note that the matrix `A*ZW` can be cached to improve performances. 40 | 41 | Then the `AxialMoments` method will return the vector of cosine power integrals for the triangle `triangle`: 42 | 43 | // Analytical evaluation of the integral of power of cosines for 44 | // the different basis elements up to the order defined by the 45 | // number of elements in the basis 46 | const auto moments = AxialMoments(triangle, basis); 47 | return cpw.dot(moments); 48 | 49 | -------------------------------------------------------------------------------- /cmake/FindEigen3.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find Eigen3 lib 2 | # 3 | # This module supports requiring a minimum version, e.g. you can do 4 | # find_package(Eigen3 3.1.2) 5 | # to require version 3.1.2 or newer of Eigen3. 6 | # 7 | # Once done this will define 8 | # 9 | # EIGEN3_FOUND - system has eigen lib with correct version 10 | # EIGEN3_INCLUDE_DIR - the eigen include directory 11 | # EIGEN3_VERSION - eigen version 12 | # 13 | # This module reads hints about search locations from 14 | # the following enviroment variables: 15 | # 16 | # EIGEN3_ROOT 17 | # EIGEN3_ROOT_DIR 18 | 19 | # Copyright (c) 2006, 2007 Montel Laurent, 20 | # Copyright (c) 2008, 2009 Gael Guennebaud, 21 | # Copyright (c) 2009 Benoit Jacob 22 | # Redistribution and use is allowed according to the terms of the 2-clause BSD license. 23 | 24 | if(NOT Eigen3_FIND_VERSION) 25 | if(NOT Eigen3_FIND_VERSION_MAJOR) 26 | set(Eigen3_FIND_VERSION_MAJOR 2) 27 | endif(NOT Eigen3_FIND_VERSION_MAJOR) 28 | if(NOT Eigen3_FIND_VERSION_MINOR) 29 | set(Eigen3_FIND_VERSION_MINOR 91) 30 | endif(NOT Eigen3_FIND_VERSION_MINOR) 31 | if(NOT Eigen3_FIND_VERSION_PATCH) 32 | set(Eigen3_FIND_VERSION_PATCH 0) 33 | endif(NOT Eigen3_FIND_VERSION_PATCH) 34 | 35 | set(Eigen3_FIND_VERSION "${Eigen3_FIND_VERSION_MAJOR}.${Eigen3_FIND_VERSION_MINOR}.${Eigen3_FIND_VERSION_PATCH}") 36 | endif(NOT Eigen3_FIND_VERSION) 37 | 38 | macro(_eigen3_check_version) 39 | file(READ "${EIGEN3_INCLUDE_DIR}/Eigen/src/Core/util/Macros.h" _eigen3_version_header) 40 | 41 | string(REGEX MATCH "define[ \t]+EIGEN_WORLD_VERSION[ \t]+([0-9]+)" _eigen3_world_version_match "${_eigen3_version_header}") 42 | set(EIGEN3_WORLD_VERSION "${CMAKE_MATCH_1}") 43 | string(REGEX MATCH "define[ \t]+EIGEN_MAJOR_VERSION[ \t]+([0-9]+)" _eigen3_major_version_match "${_eigen3_version_header}") 44 | set(EIGEN3_MAJOR_VERSION "${CMAKE_MATCH_1}") 45 | string(REGEX MATCH "define[ \t]+EIGEN_MINOR_VERSION[ \t]+([0-9]+)" _eigen3_minor_version_match "${_eigen3_version_header}") 46 | set(EIGEN3_MINOR_VERSION "${CMAKE_MATCH_1}") 47 | 48 | set(EIGEN3_VERSION ${EIGEN3_WORLD_VERSION}.${EIGEN3_MAJOR_VERSION}.${EIGEN3_MINOR_VERSION}) 49 | if(${EIGEN3_VERSION} VERSION_LESS ${Eigen3_FIND_VERSION}) 50 | set(EIGEN3_VERSION_OK FALSE) 51 | else(${EIGEN3_VERSION} VERSION_LESS ${Eigen3_FIND_VERSION}) 52 | set(EIGEN3_VERSION_OK TRUE) 53 | endif(${EIGEN3_VERSION} VERSION_LESS ${Eigen3_FIND_VERSION}) 54 | 55 | if(NOT EIGEN3_VERSION_OK) 56 | 57 | message(STATUS "Eigen3 version ${EIGEN3_VERSION} found in ${EIGEN3_INCLUDE_DIR}, " 58 | "but at least version ${Eigen3_FIND_VERSION} is required") 59 | endif(NOT EIGEN3_VERSION_OK) 60 | endmacro(_eigen3_check_version) 61 | 62 | if (EIGEN3_INCLUDE_DIR) 63 | 64 | # in cache already 65 | _eigen3_check_version() 66 | set(EIGEN3_FOUND ${EIGEN3_VERSION_OK}) 67 | 68 | else (EIGEN3_INCLUDE_DIR) 69 | 70 | find_path(EIGEN3_INCLUDE_DIR NAMES signature_of_eigen3_matrix_library 71 | HINTS 72 | ENV EIGEN3_ROOT 73 | ENV EIGEN3_ROOT_DIR 74 | PATHS 75 | ${CMAKE_INSTALL_PREFIX}/include 76 | ${KDE4_INCLUDE_DIR} 77 | PATH_SUFFIXES eigen3 eigen 78 | ) 79 | 80 | if(EIGEN3_INCLUDE_DIR) 81 | _eigen3_check_version() 82 | endif(EIGEN3_INCLUDE_DIR) 83 | 84 | include(FindPackageHandleStandardArgs) 85 | find_package_handle_standard_args(Eigen3 DEFAULT_MSG EIGEN3_INCLUDE_DIR EIGEN3_VERSION_OK) 86 | 87 | mark_as_advanced(EIGEN3_INCLUDE_DIR) 88 | 89 | endif(EIGEN3_INCLUDE_DIR) 90 | 91 | -------------------------------------------------------------------------------- /cmake/OptimizeForArchitecture.cmake: -------------------------------------------------------------------------------- 1 | #============================================================================= 2 | # Copyright 2010-2013 Matthias Kretz 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright notice, 9 | # this list of conditions and the following disclaimer. 10 | # 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | # 15 | # * The names of Kitware, Inc., the Insight Consortium, or the names of 16 | # any consortium members, or of any contributors, may not be used to 17 | # endorse or promote products derived from this software without 18 | # specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS'' 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR 24 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | #============================================================================= 31 | 32 | get_filename_component(_currentDir "${CMAKE_CURRENT_LIST_FILE}" PATH) 33 | include("${_currentDir}/AddCompilerFlag.cmake") 34 | include(CheckIncludeFile) 35 | 36 | macro(_my_find _list _value _ret) 37 | list(FIND ${_list} "${_value}" _found) 38 | if(_found EQUAL -1) 39 | set(${_ret} FALSE) 40 | else(_found EQUAL -1) 41 | set(${_ret} TRUE) 42 | endif(_found EQUAL -1) 43 | endmacro(_my_find) 44 | 45 | macro(AutodetectHostArchitecture) 46 | set(TARGET_ARCHITECTURE "generic") 47 | set(Vc_ARCHITECTURE_FLAGS) 48 | set(_vendor_id) 49 | set(_cpu_family) 50 | set(_cpu_model) 51 | if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 52 | file(READ "/proc/cpuinfo" _cpuinfo) 53 | string(REGEX REPLACE ".*vendor_id[ \t]*:[ \t]+([a-zA-Z0-9_-]+).*" "\\1" _vendor_id "${_cpuinfo}") 54 | string(REGEX REPLACE ".*cpu family[ \t]*:[ \t]+([a-zA-Z0-9_-]+).*" "\\1" _cpu_family "${_cpuinfo}") 55 | string(REGEX REPLACE ".*model[ \t]*:[ \t]+([a-zA-Z0-9_-]+).*" "\\1" _cpu_model "${_cpuinfo}") 56 | string(REGEX REPLACE ".*flags[ \t]*:[ \t]+([^\n]+).*" "\\1" _cpu_flags "${_cpuinfo}") 57 | elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") 58 | exec_program("/usr/sbin/sysctl -n machdep.cpu.vendor" OUTPUT_VARIABLE _vendor_id) 59 | exec_program("/usr/sbin/sysctl -n machdep.cpu.model" OUTPUT_VARIABLE _cpu_model) 60 | exec_program("/usr/sbin/sysctl -n machdep.cpu.family" OUTPUT_VARIABLE _cpu_family) 61 | exec_program("/usr/sbin/sysctl -n machdep.cpu.features" OUTPUT_VARIABLE _cpu_flags) 62 | string(TOLOWER "${_cpu_flags}" _cpu_flags) 63 | string(REPLACE "." "_" _cpu_flags "${_cpu_flags}") 64 | elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") 65 | get_filename_component(_vendor_id "[HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0;VendorIdentifier]" NAME CACHE) 66 | get_filename_component(_cpu_id "[HKEY_LOCAL_MACHINE\\Hardware\\Description\\System\\CentralProcessor\\0;Identifier]" NAME CACHE) 67 | mark_as_advanced(_vendor_id _cpu_id) 68 | string(REGEX REPLACE ".* Family ([0-9]+) .*" "\\1" _cpu_family "${_cpu_id}") 69 | string(REGEX REPLACE ".* Model ([0-9]+) .*" "\\1" _cpu_model "${_cpu_id}") 70 | endif(CMAKE_SYSTEM_NAME STREQUAL "Linux") 71 | if(_vendor_id STREQUAL "GenuineIntel") 72 | if(_cpu_family EQUAL 6) 73 | # Any recent Intel CPU except NetBurst 74 | if(_cpu_model EQUAL 62) 75 | set(TARGET_ARCHITECTURE "ivy-bridge") 76 | elseif(_cpu_model EQUAL 60) 77 | set(TARGET_ARCHITECTURE "haswell") 78 | elseif(_cpu_model EQUAL 58) 79 | set(TARGET_ARCHITECTURE "ivy-bridge") 80 | elseif(_cpu_model EQUAL 47) # Xeon E7 4860 81 | set(TARGET_ARCHITECTURE "westmere") 82 | elseif(_cpu_model EQUAL 46) # Xeon 7500 series 83 | set(TARGET_ARCHITECTURE "westmere") 84 | elseif(_cpu_model EQUAL 45) # Xeon TNG 85 | set(TARGET_ARCHITECTURE "sandy-bridge") 86 | elseif(_cpu_model EQUAL 44) # Xeon 5600 series 87 | set(TARGET_ARCHITECTURE "westmere") 88 | elseif(_cpu_model EQUAL 42) # Core TNG 89 | set(TARGET_ARCHITECTURE "sandy-bridge") 90 | elseif(_cpu_model EQUAL 37) # Core i7/i5/i3 91 | set(TARGET_ARCHITECTURE "westmere") 92 | elseif(_cpu_model EQUAL 31) # Core i7/i5 93 | set(TARGET_ARCHITECTURE "westmere") 94 | elseif(_cpu_model EQUAL 30) # Core i7/i5 95 | set(TARGET_ARCHITECTURE "westmere") 96 | elseif(_cpu_model EQUAL 29) 97 | set(TARGET_ARCHITECTURE "penryn") 98 | elseif(_cpu_model EQUAL 28) 99 | set(TARGET_ARCHITECTURE "atom") 100 | elseif(_cpu_model EQUAL 26) 101 | set(TARGET_ARCHITECTURE "nehalem") 102 | elseif(_cpu_model EQUAL 23) 103 | set(TARGET_ARCHITECTURE "penryn") 104 | elseif(_cpu_model EQUAL 15) 105 | set(TARGET_ARCHITECTURE "merom") 106 | elseif(_cpu_model EQUAL 14) 107 | set(TARGET_ARCHITECTURE "core") 108 | elseif(_cpu_model LESS 14) 109 | message(WARNING "Your CPU (family ${_cpu_family}, model ${_cpu_model}) is not known. Auto-detection of optimization flags failed and will use the generic CPU settings with SSE2.") 110 | set(TARGET_ARCHITECTURE "generic") 111 | else() 112 | message(WARNING "Your CPU (family ${_cpu_family}, model ${_cpu_model}) is not known. Auto-detection of optimization flags failed and will use the 65nm Core 2 CPU settings.") 113 | set(TARGET_ARCHITECTURE "merom") 114 | endif() 115 | elseif(_cpu_family EQUAL 7) # Itanium (not supported) 116 | message(WARNING "Your CPU (Itanium: family ${_cpu_family}, model ${_cpu_model}) is not supported by OptimizeForArchitecture.cmake.") 117 | elseif(_cpu_family EQUAL 15) # NetBurst 118 | list(APPEND _available_vector_units_list "sse" "sse2") 119 | if(_cpu_model GREATER 2) # Not sure whether this must be 3 or even 4 instead 120 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3") 121 | endif(_cpu_model GREATER 2) 122 | endif(_cpu_family EQUAL 6) 123 | elseif(_vendor_id STREQUAL "AuthenticAMD") 124 | if(_cpu_family EQUAL 22) # 16h 125 | set(TARGET_ARCHITECTURE "AMD 16h") 126 | elseif(_cpu_family EQUAL 21) # 15h 127 | if(_cpu_model LESS 2) 128 | set(TARGET_ARCHITECTURE "bulldozer") 129 | else() 130 | set(TARGET_ARCHITECTURE "piledriver") 131 | endif() 132 | elseif(_cpu_family EQUAL 20) # 14h 133 | set(TARGET_ARCHITECTURE "AMD 14h") 134 | elseif(_cpu_family EQUAL 18) # 12h 135 | elseif(_cpu_family EQUAL 16) # 10h 136 | set(TARGET_ARCHITECTURE "barcelona") 137 | elseif(_cpu_family EQUAL 15) 138 | set(TARGET_ARCHITECTURE "k8") 139 | if(_cpu_model GREATER 64) # I don't know the right number to put here. This is just a guess from the hardware I have access to 140 | set(TARGET_ARCHITECTURE "k8-sse3") 141 | endif(_cpu_model GREATER 64) 142 | endif() 143 | endif(_vendor_id STREQUAL "GenuineIntel") 144 | endmacro() 145 | 146 | macro(OptimizeForArchitecture) 147 | set(TARGET_ARCHITECTURE "auto" CACHE STRING "CPU architecture to optimize for. Using an incorrect setting here can result in crashes of the resulting binary because of invalid instructions used.\nSetting the value to \"auto\" will try to optimize for the architecture where cmake is called.\nOther supported values are: \"none\", \"generic\", \"core\", \"merom\" (65nm Core2), \"penryn\" (45nm Core2), \"nehalem\", \"westmere\", \"sandy-bridge\", \"ivy-bridge\", \"haswell\", \"atom\", \"k8\", \"k8-sse3\", \"barcelona\", \"istanbul\", \"magny-cours\", \"bulldozer\", \"interlagos\", \"piledriver\", \"AMD 14h\", \"AMD 16h\".") 148 | set(_force) 149 | if(NOT _last_target_arch STREQUAL "${TARGET_ARCHITECTURE}") 150 | message(STATUS "target changed from \"${_last_target_arch}\" to \"${TARGET_ARCHITECTURE}\"") 151 | set(_force FORCE) 152 | endif() 153 | set(_last_target_arch "${TARGET_ARCHITECTURE}" CACHE STRING "" FORCE) 154 | mark_as_advanced(_last_target_arch) 155 | string(TOLOWER "${TARGET_ARCHITECTURE}" TARGET_ARCHITECTURE) 156 | 157 | set(_march_flag_list) 158 | set(_available_vector_units_list) 159 | 160 | if(TARGET_ARCHITECTURE STREQUAL "auto") 161 | AutodetectHostArchitecture() 162 | message(STATUS "Detected CPU: ${TARGET_ARCHITECTURE}") 163 | endif(TARGET_ARCHITECTURE STREQUAL "auto") 164 | 165 | if(TARGET_ARCHITECTURE STREQUAL "core") 166 | list(APPEND _march_flag_list "core2") 167 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3") 168 | elseif(TARGET_ARCHITECTURE STREQUAL "merom") 169 | list(APPEND _march_flag_list "merom") 170 | list(APPEND _march_flag_list "core2") 171 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3") 172 | elseif(TARGET_ARCHITECTURE STREQUAL "penryn") 173 | list(APPEND _march_flag_list "penryn") 174 | list(APPEND _march_flag_list "core2") 175 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3") 176 | message(STATUS "Sadly the Penryn architecture exists in variants with SSE4.1 and without SSE4.1.") 177 | if(_cpu_flags MATCHES "sse4_1") 178 | message(STATUS "SSE4.1: enabled (auto-detected from this computer's CPU flags)") 179 | list(APPEND _available_vector_units_list "sse4.1") 180 | else() 181 | message(STATUS "SSE4.1: disabled (auto-detected from this computer's CPU flags)") 182 | endif() 183 | elseif(TARGET_ARCHITECTURE STREQUAL "nehalem") 184 | list(APPEND _march_flag_list "nehalem") 185 | list(APPEND _march_flag_list "corei7") 186 | list(APPEND _march_flag_list "core2") 187 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4.1" "sse4.2") 188 | elseif(TARGET_ARCHITECTURE STREQUAL "westmere") 189 | list(APPEND _march_flag_list "westmere") 190 | list(APPEND _march_flag_list "corei7") 191 | list(APPEND _march_flag_list "core2") 192 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4.1" "sse4.2") 193 | elseif(TARGET_ARCHITECTURE STREQUAL "haswell") 194 | list(APPEND _march_flag_list "core-avx2") 195 | list(APPEND _march_flag_list "core-avx-i") 196 | list(APPEND _march_flag_list "corei7-avx") 197 | list(APPEND _march_flag_list "core2") 198 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4.1" "sse4.2" "avx" "avx2" "rdrnd" "f16c" "fma") 199 | elseif(TARGET_ARCHITECTURE STREQUAL "ivy-bridge") 200 | list(APPEND _march_flag_list "core-avx-i") 201 | list(APPEND _march_flag_list "corei7-avx") 202 | list(APPEND _march_flag_list "core2") 203 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4.1" "sse4.2" "avx" "rdrnd" "f16c") 204 | elseif(TARGET_ARCHITECTURE STREQUAL "sandy-bridge") 205 | list(APPEND _march_flag_list "sandybridge") 206 | list(APPEND _march_flag_list "corei7-avx") 207 | list(APPEND _march_flag_list "core2") 208 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4.1" "sse4.2" "avx") 209 | elseif(TARGET_ARCHITECTURE STREQUAL "atom") 210 | list(APPEND _march_flag_list "atom") 211 | list(APPEND _march_flag_list "core2") 212 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3") 213 | elseif(TARGET_ARCHITECTURE STREQUAL "k8") 214 | list(APPEND _march_flag_list "k8") 215 | list(APPEND _available_vector_units_list "sse" "sse2") 216 | elseif(TARGET_ARCHITECTURE STREQUAL "k8-sse3") 217 | list(APPEND _march_flag_list "k8-sse3") 218 | list(APPEND _march_flag_list "k8") 219 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3") 220 | elseif(TARGET_ARCHITECTURE STREQUAL "AMD 16h") 221 | list(APPEND _march_flag_list "btver2") 222 | list(APPEND _march_flag_list "btver1") 223 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4a" "sse4.1" "sse4.2" "avx" "f16c") 224 | elseif(TARGET_ARCHITECTURE STREQUAL "AMD 14h") 225 | list(APPEND _march_flag_list "btver1") 226 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4a") 227 | elseif(TARGET_ARCHITECTURE STREQUAL "piledriver") 228 | list(APPEND _march_flag_list "bdver2") 229 | list(APPEND _march_flag_list "bdver1") 230 | list(APPEND _march_flag_list "bulldozer") 231 | list(APPEND _march_flag_list "barcelona") 232 | list(APPEND _march_flag_list "core2") 233 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4a" "sse4.1" "sse4.2" "avx" "xop" "fma4" "fma" "f16c") 234 | elseif(TARGET_ARCHITECTURE STREQUAL "interlagos") 235 | list(APPEND _march_flag_list "bdver1") 236 | list(APPEND _march_flag_list "bulldozer") 237 | list(APPEND _march_flag_list "barcelona") 238 | list(APPEND _march_flag_list "core2") 239 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4a" "sse4.1" "sse4.2" "avx" "xop" "fma4") 240 | elseif(TARGET_ARCHITECTURE STREQUAL "bulldozer") 241 | list(APPEND _march_flag_list "bdver1") 242 | list(APPEND _march_flag_list "bulldozer") 243 | list(APPEND _march_flag_list "barcelona") 244 | list(APPEND _march_flag_list "core2") 245 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "ssse3" "sse4a" "sse4.1" "sse4.2" "avx" "xop" "fma4") 246 | elseif(TARGET_ARCHITECTURE STREQUAL "barcelona") 247 | list(APPEND _march_flag_list "barcelona") 248 | list(APPEND _march_flag_list "core2") 249 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "sse4a") 250 | elseif(TARGET_ARCHITECTURE STREQUAL "istanbul") 251 | list(APPEND _march_flag_list "barcelona") 252 | list(APPEND _march_flag_list "core2") 253 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "sse4a") 254 | elseif(TARGET_ARCHITECTURE STREQUAL "magny-cours") 255 | list(APPEND _march_flag_list "barcelona") 256 | list(APPEND _march_flag_list "core2") 257 | list(APPEND _available_vector_units_list "sse" "sse2" "sse3" "sse4a") 258 | elseif(TARGET_ARCHITECTURE STREQUAL "generic") 259 | list(APPEND _march_flag_list "generic") 260 | elseif(TARGET_ARCHITECTURE STREQUAL "none") 261 | # add this clause to remove it from the else clause 262 | else(TARGET_ARCHITECTURE STREQUAL "core") 263 | message(FATAL_ERROR "Unknown target architecture: \"${TARGET_ARCHITECTURE}\". Please set TARGET_ARCHITECTURE to a supported value.") 264 | endif(TARGET_ARCHITECTURE STREQUAL "core") 265 | 266 | if(NOT TARGET_ARCHITECTURE STREQUAL "none") 267 | set(_disable_vector_unit_list) 268 | set(_enable_vector_unit_list) 269 | _my_find(_available_vector_units_list "sse2" SSE2_FOUND) 270 | _my_find(_available_vector_units_list "sse3" SSE3_FOUND) 271 | _my_find(_available_vector_units_list "ssse3" SSSE3_FOUND) 272 | _my_find(_available_vector_units_list "sse4.1" SSE4_1_FOUND) 273 | _my_find(_available_vector_units_list "sse4.2" SSE4_2_FOUND) 274 | _my_find(_available_vector_units_list "sse4a" SSE4a_FOUND) 275 | if(DEFINED Vc_AVX_INTRINSICS_BROKEN AND Vc_AVX_INTRINSICS_BROKEN) 276 | UserWarning("AVX disabled per default because of old/broken compiler") 277 | set(AVX_FOUND false) 278 | set(XOP_FOUND false) 279 | set(FMA4_FOUND false) 280 | set(AVX2_FOUND false) 281 | else() 282 | _my_find(_available_vector_units_list "avx" AVX_FOUND) 283 | if(DEFINED Vc_FMA4_INTRINSICS_BROKEN AND Vc_FMA4_INTRINSICS_BROKEN) 284 | UserWarning("FMA4 disabled per default because of old/broken compiler") 285 | set(FMA4_FOUND false) 286 | else() 287 | _my_find(_available_vector_units_list "fma4" FMA4_FOUND) 288 | endif() 289 | if(DEFINED Vc_XOP_INTRINSICS_BROKEN AND Vc_XOP_INTRINSICS_BROKEN) 290 | UserWarning("XOP disabled per default because of old/broken compiler") 291 | set(XOP_FOUND false) 292 | else() 293 | _my_find(_available_vector_units_list "xop" XOP_FOUND) 294 | endif() 295 | if(DEFINED Vc_AVX2_INTRINSICS_BROKEN AND Vc_AVX2_INTRINSICS_BROKEN) 296 | UserWarning("AVX2 disabled per default because of old/broken compiler") 297 | set(AVX2_FOUND false) 298 | else() 299 | _my_find(_available_vector_units_list "avx2" AVX2_FOUND) 300 | endif() 301 | endif() 302 | set(USE_SSE2 ${SSE2_FOUND} CACHE BOOL "Use SSE2. If SSE2 instructions are not enabled the SSE implementation will be disabled." ${_force}) 303 | set(USE_SSE3 ${SSE3_FOUND} CACHE BOOL "Use SSE3. If SSE3 instructions are not enabled they will be emulated." ${_force}) 304 | set(USE_SSSE3 ${SSSE3_FOUND} CACHE BOOL "Use SSSE3. If SSSE3 instructions are not enabled they will be emulated." ${_force}) 305 | set(USE_SSE4_1 ${SSE4_1_FOUND} CACHE BOOL "Use SSE4.1. If SSE4.1 instructions are not enabled they will be emulated." ${_force}) 306 | set(USE_SSE4_2 ${SSE4_2_FOUND} CACHE BOOL "Use SSE4.2. If SSE4.2 instructions are not enabled they will be emulated." ${_force}) 307 | set(USE_SSE4a ${SSE4a_FOUND} CACHE BOOL "Use SSE4a. If SSE4a instructions are not enabled they will be emulated." ${_force}) 308 | set(USE_AVX ${AVX_FOUND} CACHE BOOL "Use AVX. This will double some of the vector sizes relative to SSE." ${_force}) 309 | set(USE_AVX2 ${AVX2_FOUND} CACHE BOOL "Use AVX2. This will double all of the vector sizes relative to SSE." ${_force}) 310 | set(USE_XOP ${XOP_FOUND} CACHE BOOL "Use XOP." ${_force}) 311 | set(USE_FMA4 ${FMA4_FOUND} CACHE BOOL "Use FMA4." ${_force}) 312 | mark_as_advanced(USE_SSE2 USE_SSE3 USE_SSSE3 USE_SSE4_1 USE_SSE4_2 USE_SSE4a USE_AVX USE_AVX2 USE_XOP USE_FMA4) 313 | if(USE_SSE2) 314 | list(APPEND _enable_vector_unit_list "sse2") 315 | else(USE_SSE2) 316 | list(APPEND _disable_vector_unit_list "sse2") 317 | endif(USE_SSE2) 318 | if(USE_SSE3) 319 | list(APPEND _enable_vector_unit_list "sse3") 320 | else(USE_SSE3) 321 | list(APPEND _disable_vector_unit_list "sse3") 322 | endif(USE_SSE3) 323 | if(USE_SSSE3) 324 | list(APPEND _enable_vector_unit_list "ssse3") 325 | else(USE_SSSE3) 326 | list(APPEND _disable_vector_unit_list "ssse3") 327 | endif(USE_SSSE3) 328 | if(USE_SSE4_1) 329 | list(APPEND _enable_vector_unit_list "sse4.1") 330 | else(USE_SSE4_1) 331 | list(APPEND _disable_vector_unit_list "sse4.1") 332 | endif(USE_SSE4_1) 333 | if(USE_SSE4_2) 334 | list(APPEND _enable_vector_unit_list "sse4.2") 335 | else(USE_SSE4_2) 336 | list(APPEND _disable_vector_unit_list "sse4.2") 337 | endif(USE_SSE4_2) 338 | if(USE_SSE4a) 339 | list(APPEND _enable_vector_unit_list "sse4a") 340 | else(USE_SSE4a) 341 | list(APPEND _disable_vector_unit_list "sse4a") 342 | endif(USE_SSE4a) 343 | if(USE_AVX) 344 | list(APPEND _enable_vector_unit_list "avx") 345 | # we want SSE intrinsics to result in instructions using the VEX prefix. 346 | # Otherwise integer ops (which require the older SSE intrinsics) would 347 | # always have a large penalty. 348 | list(APPEND _enable_vector_unit_list "sse2avx") 349 | else(USE_AVX) 350 | list(APPEND _disable_vector_unit_list "avx") 351 | endif(USE_AVX) 352 | if(USE_XOP) 353 | list(APPEND _enable_vector_unit_list "xop") 354 | else() 355 | list(APPEND _disable_vector_unit_list "xop") 356 | endif() 357 | if(USE_FMA4) 358 | list(APPEND _enable_vector_unit_list "fma4") 359 | else() 360 | list(APPEND _disable_vector_unit_list "fma4") 361 | endif() 362 | if(USE_AVX2) 363 | list(APPEND _enable_vector_unit_list "avx2") 364 | else() 365 | list(APPEND _disable_vector_unit_list "avx2") 366 | endif() 367 | if(MSVC) 368 | # MSVC on 32 bit can select /arch:SSE2 (since 2010 also /arch:AVX) 369 | # MSVC on 64 bit cannot select anything (should have changed with MSVC 2010) 370 | _my_find(_enable_vector_unit_list "avx" _avx) 371 | set(_avx_flag FALSE) 372 | if(_avx) 373 | AddCompilerFlag("/arch:AVX" CXX_FLAGS Vc_ARCHITECTURE_FLAGS CXX_RESULT _avx_flag) 374 | endif() 375 | if(NOT _avx_flag) 376 | _my_find(_enable_vector_unit_list "sse2" _found) 377 | if(_found) 378 | AddCompilerFlag("/arch:SSE2" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 379 | endif() 380 | endif() 381 | foreach(_flag ${_enable_vector_unit_list}) 382 | string(TOUPPER "${_flag}" _flag) 383 | string(REPLACE "." "_" _flag "__${_flag}__") 384 | add_definitions("-D${_flag}") 385 | endforeach(_flag) 386 | elseif(CMAKE_CXX_COMPILER MATCHES "/(icpc|icc)$") # ICC (on Linux) 387 | _my_find(_available_vector_units_list "avx2" _found) 388 | if(_found) 389 | AddCompilerFlag("-xCORE-AVX2" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 390 | else(_found) 391 | _my_find(_available_vector_units_list "f16c" _found) 392 | if(_found) 393 | AddCompilerFlag("-xCORE-AVX-I" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 394 | else(_found) 395 | _my_find(_available_vector_units_list "avx" _found) 396 | if(_found) 397 | AddCompilerFlag("-xAVX" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 398 | else(_found) 399 | _my_find(_available_vector_units_list "sse4.2" _found) 400 | if(_found) 401 | AddCompilerFlag("-xSSE4.2" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 402 | else(_found) 403 | _my_find(_available_vector_units_list "sse4.1" _found) 404 | if(_found) 405 | AddCompilerFlag("-xSSE4.1" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 406 | else(_found) 407 | _my_find(_available_vector_units_list "ssse3" _found) 408 | if(_found) 409 | AddCompilerFlag("-xSSSE3" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 410 | else(_found) 411 | _my_find(_available_vector_units_list "sse3" _found) 412 | if(_found) 413 | # If the target host is an AMD machine then we still want to use -xSSE2 because the binary would refuse to run at all otherwise 414 | _my_find(_march_flag_list "barcelona" _found) 415 | if(NOT _found) 416 | _my_find(_march_flag_list "k8-sse3" _found) 417 | endif(NOT _found) 418 | if(_found) 419 | AddCompilerFlag("-xSSE2" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 420 | else(_found) 421 | AddCompilerFlag("-xSSE3" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 422 | endif(_found) 423 | else(_found) 424 | _my_find(_available_vector_units_list "sse2" _found) 425 | if(_found) 426 | AddCompilerFlag("-xSSE2" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 427 | endif(_found) 428 | endif(_found) 429 | endif(_found) 430 | endif(_found) 431 | endif(_found) 432 | endif(_found) 433 | endif(_found) 434 | endif(_found) 435 | else() # not MSVC and not ICC => GCC, Clang, Open64 436 | foreach(_flag ${_march_flag_list}) 437 | AddCompilerFlag("-march=${_flag}" CXX_RESULT _good CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 438 | if(_good) 439 | break() 440 | endif(_good) 441 | endforeach(_flag) 442 | foreach(_flag ${_enable_vector_unit_list}) 443 | AddCompilerFlag("-m${_flag}" CXX_RESULT _result CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 444 | if(_result) 445 | set(_header FALSE) 446 | if(_flag STREQUAL "sse3") 447 | set(_header "pmmintrin.h") 448 | elseif(_flag STREQUAL "ssse3") 449 | set(_header "tmmintrin.h") 450 | elseif(_flag STREQUAL "sse4.1") 451 | set(_header "smmintrin.h") 452 | elseif(_flag STREQUAL "sse4.2") 453 | set(_header "smmintrin.h") 454 | elseif(_flag STREQUAL "sse4a") 455 | set(_header "ammintrin.h") 456 | elseif(_flag STREQUAL "avx") 457 | set(_header "immintrin.h") 458 | elseif(_flag STREQUAL "avx2") 459 | set(_header "immintrin.h") 460 | elseif(_flag STREQUAL "fma4") 461 | set(_header "x86intrin.h") 462 | elseif(_flag STREQUAL "xop") 463 | set(_header "x86intrin.h") 464 | endif() 465 | set(_resultVar "HAVE_${_header}") 466 | string(REPLACE "." "_" _resultVar "${_resultVar}") 467 | if(_header) 468 | CHECK_INCLUDE_FILE("${_header}" ${_resultVar} "-m${_flag}") 469 | if(NOT ${_resultVar}) 470 | set(_useVar "USE_${_flag}") 471 | string(TOUPPER "${_useVar}" _useVar) 472 | string(REPLACE "." "_" _useVar "${_useVar}") 473 | message(STATUS "disabling ${_useVar} because ${_header} is missing") 474 | set(${_useVar} FALSE) 475 | list(APPEND _disable_vector_unit_list "${_flag}") 476 | endif() 477 | endif() 478 | if(NOT _header OR ${_resultVar}) 479 | set(Vc_ARCHITECTURE_FLAGS "${Vc_ARCHITECTURE_FLAGS} -m${_flag}") 480 | endif() 481 | endif() 482 | endforeach(_flag) 483 | foreach(_flag ${_disable_vector_unit_list}) 484 | AddCompilerFlag("-mno-${_flag}" CXX_FLAGS Vc_ARCHITECTURE_FLAGS) 485 | endforeach(_flag) 486 | endif() 487 | endif() 488 | endmacro(OptimizeForArchitecture) 489 | -------------------------------------------------------------------------------- /include/AxialMoments.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Eigen includes 4 | #include 5 | 6 | // STL includes 7 | #include 8 | #include 9 | 10 | #define AXIAL_EPSILON 1.0E-6 11 | 12 | /* _Cosine Sum Integral_ 13 | */ 14 | inline Eigen::VectorXf CosSumIntegral(float x, float y, float c, int n) { 15 | 16 | const float siny = sin(y); 17 | const float sinx = sin(x); 18 | const float cosy = cos(y); 19 | const float cosx = cos(x); 20 | const float cosy2 = cosy*cosy; 21 | const float cosx2 = cosx*cosx; 22 | 23 | static const Eigen::Vector2i i1 = {1, 1}; 24 | static const Eigen::Vector2i i2 = {2, 2}; 25 | Eigen::Vector2i i = {0, 1}; 26 | Eigen::Vector2f F = {y-x, siny-sinx}; 27 | Eigen::Vector2f S = {0.0f, 0.0f}; 28 | 29 | Eigen::VectorXf R = Eigen::VectorXf::Zero(n+2); 30 | 31 | Eigen::Vector2f pow_c = {1.0, c}; 32 | Eigen::Vector2f pow_cosx = {cosx, cosx2}; 33 | Eigen::Vector2f pow_cosy = {cosy, cosy2}; 34 | 35 | while(i[1] <= n) { 36 | // S <= S + c^{i} F 37 | S += pow_c.cwiseProduct(F); 38 | 39 | // The resulting vector `R` is shifted of one to the right. This is due to 40 | // the fact that order `n` moment requires only up to order `n-1` power of 41 | // cosine integrals. 42 | R.segment(i[1], 2) = S; 43 | 44 | // T <= cos(y)^{i+1} sin(y) - cos(x)^{i+1} sin(x) 45 | // F <= (T + i+1) F / (i+2) 46 | auto T = pow_cosy*siny - pow_cosx*sinx; 47 | F = (T + (i+i1).cast().cwiseProduct(F)).cwiseQuotient((i+i2).cast()); 48 | 49 | // Update temp variable 50 | i += i2; 51 | pow_c *= c*c; 52 | pow_cosx *= cosx2; 53 | pow_cosy *= cosy2; 54 | } 55 | 56 | return R; 57 | } 58 | 59 | /* _sign_ 60 | * 61 | * Sign function template 62 | */ 63 | template 64 | inline int sign(T val) { 65 | return (T(0) <= val) - (val < T(0)); 66 | } 67 | 68 | /* _clamp_ 69 | * 70 | * Clamp function template to restrict a given function to be in between 71 | * boundaries. 72 | */ 73 | template 74 | inline T clamp(T val, T a, T b) { 75 | return std::max(a, std::min(b, val)); 76 | } 77 | 78 | 79 | /* _Line Integral_ 80 | */ 81 | template 82 | inline Eigen::VectorXf LineIntegral(const Vector& A, const Vector& B, 83 | const Vector& w, int n) { 84 | #ifdef ZERO_ORTHOGONAL 85 | auto wDotA = Vector::Dot(A, w); 86 | auto wDotB = Vector::Dot(B, w); 87 | // Zeroth moment and orthogonal directions 'w' to the while edge do not 88 | // require this part since it will always return '0'; 89 | if(std::abs(wDotA) < AXIAL_EPSILON && std::abs(wDotB) < AXIAL_EPSILON) { 90 | return Eigen::VectorXf::Zero(n+2); 91 | } 92 | #endif 93 | 94 | // Note: expanding the (I-ssT)B expression from Arvo's LineIntegral pseudo 95 | // code to the projection of B on plane with A as normal. 96 | const auto s = Vector::Normalize(A); 97 | const auto t = Vector::Normalize(B - Vector::Dot(s, B)*s); 98 | 99 | const auto a = Vector::Dot(w, s); 100 | const auto b = Vector::Dot(w, t); 101 | const auto c = sqrt(a*a + b*b); 102 | 103 | // Compute the arc-length on which to compute the integral of the moment 104 | // function and the shift 'p' that change the integral to the integral of 105 | // a shifted cosine. 106 | const auto r = Vector::Dot(s, B) / Vector::Dot(B,B); 107 | const auto l = acos(clamp(r, -1.0f, 1.0f)); 108 | const auto p = atan2(b, a); 109 | 110 | return CosSumIntegral(-p, l-p, c, n); 111 | } 112 | 113 | /* _Boundary Integral_ 114 | * 115 | * Compute the integral along P egdes of the up to order 'n' axial moment 116 | * around w. By using 'n' = 'w' you can compute the single axis moment. Double 117 | * axis moment with second axis begin the normal must use 'v' == 'n' ('n' being 118 | * the normal). 119 | */ 120 | template 121 | inline Eigen::VectorXf BoundaryIntegral(const Polygon& P, const Vector& w, 122 | const Vector& v, int n) { 123 | // Init to zero 124 | Eigen::VectorXf b = Eigen::VectorXf::Zero(n+2); 125 | 126 | for(auto& edge : P) { 127 | // Compute the edge normal 128 | const auto normal = Vector::Normalize(Vector::Cross(edge.A, edge.B)); 129 | 130 | // Add the egde integral to the total integral 131 | const auto dotNV = Vector::Dot(normal, v); 132 | const auto lineInt = LineIntegral(edge.A, edge.B, w, n); 133 | b += dotNV * lineInt; 134 | } 135 | 136 | return b; 137 | } 138 | 139 | /* _Solid Angle_ 140 | * 141 | * Compute the solid angle sustained by a `Polygon P`. 142 | */ 143 | template 144 | inline float SolidAngle(const Polygon& P) { 145 | if(P.size() == 3) { 146 | // Using the method of Oosterom and Strackee [1983] 147 | const Vector& A = P[0].A; 148 | const Vector& B = P[1].A; 149 | const Vector& C = P[2].A; 150 | 151 | const Vector bc = Vector::Cross(B,C); 152 | const float num = std::abs(Vector::Dot(bc, A)); 153 | const float al = Vector::Length(A); 154 | const float bl = Vector::Length(B); 155 | const float cl = Vector::Length(C); 156 | const float den = al*bl*cl 157 | + Vector::Dot(A, B)*cl 158 | + Vector::Dot(A, C)*bl 159 | + Vector::Dot(B, C)*al; 160 | 161 | float phi = atan2(num, den); 162 | if(phi < 0) { 163 | phi += M_PI; 164 | } 165 | return 2.0f * phi; 166 | 167 | } else { 168 | // Using the algorithm for computing solid angle of polyhedral cones by 169 | // Mazonka found in http://arxiv.org/pdf/1205.1396v2.pdf 170 | std::complex z(1, 0); 171 | for(unsigned int k=0; k 0) ? k-1 : P.size()-1].A; 173 | const Vector& B = P[k].A; 174 | const Vector& C = P[k].B; 175 | 176 | const float ak = Vector::Dot(A, C); 177 | const float bk = Vector::Dot(A, B); 178 | const float ck = Vector::Dot(B, C); 179 | const float dk = Vector::Dot(A, Vector::Cross(B, C)); 180 | const std::complex zk(bk*ck-ak, dk); 181 | z *= zk; 182 | } 183 | const float arg = std::arg(z); 184 | return arg; 185 | } 186 | } 187 | 188 | /* _Check Polygon_ 189 | * 190 | * Check if the Poylgon P is well oriented. For a triangle, the centroid of the 191 | * triangle `D` is computed as `A + B + C / 3` and compared to the normal of 192 | * the triangle using the orientation. The normal and the centroid must match 193 | * orientation for the normal of edges to be outwards. 194 | */ 195 | template 196 | inline bool CheckPolygon(const Polygon& P) { 197 | 198 | // A closed Polygon cannot be smaller than 3 Edges. 199 | if(P.size() < 3) { 200 | return false; 201 | 202 | // Special case for triangles. 203 | } else if(P.size() == 3) { 204 | // Check with respect to centroid 205 | const auto D = (P[0].A + P[1].A + P[2].A) / 3.0f; 206 | const auto N = Vector::Cross(P[1].A-P[0].A, P[2].A-P[0].A); 207 | return Vector::Dot(D, N) <= 0.0f; 208 | 209 | // General computation 210 | } else { 211 | // This is a heuristic to select a point on the bounding box of the 212 | // polygon. The orientation test is then computed on this particular 213 | // corner. 214 | unsigned int K = 0; 215 | const Vector* minX = &P[0].B; 216 | for(unsigned int k=1; kx < minX->x || (X->x <= minX->x && X->y < minX->y)) { 219 | minX = X; 220 | K = k; 221 | } 222 | } 223 | 224 | // Perform the test as defined on the Wikipedia page: 225 | // https://en.wikipedia.org/wiki/Curve_orientation 226 | const int K2 = (K < P.size()-1) ? K+1 : 0; 227 | const auto D = (P[K].A + P[K].B + P[K2].B) / 3.0f; 228 | const auto N = Vector::Cross(P[K].B-P[K].A, P[K2].B-P[K].A); 229 | const bool r = Vector::Dot(D, N) <= 0.0f; 230 | return r; 231 | } 232 | } 233 | 234 | /* _Axial Moments_ 235 | * 236 | * input: 237 | * + Polygon P: A set of egdes that can be enumerated using iterators. 238 | Each edge must enable to access two Vector A and B. 239 | * + Vector w: A 3D vector with elements accessible as x,y,z this 240 | vector defines the axis on which to compute moments. 241 | * + int n: The maximum moment order to be computed. 242 | * 243 | * output: 244 | * + VectorX r: A vector containing all moments up to order 'n' 245 | */ 246 | template 247 | inline Eigen::VectorXf AxialMoment(const Polygon& P, const Vector& w, int n) { 248 | 249 | // Check if the polygon is well oriented 250 | const bool check = CheckPolygon(P); 251 | if(!check) { 252 | return Eigen::VectorXf::Zero(n+2); 253 | } 254 | 255 | // Arvo's boundary integral for single vector moment. 256 | Eigen::VectorXf a = - BoundaryIntegral(P, w, w, n); 257 | 258 | // Generate the 'b' vector which equals to the Polygon solid angle for 259 | // even moments and zero for odd moments. 260 | const int n2 = (n+2)/2; 261 | auto b = Eigen::Map>(a.data(), n2); 262 | b += Eigen::VectorXf::Constant(n2, SolidAngle(P)); 263 | 264 | // 'c' is the vector of linear elements, storing 'i+1' for index 'i' 265 | auto c = Eigen::VectorXf::LinSpaced(n+2, 1, n+2); 266 | 267 | return a.cwiseQuotient(c); 268 | } 269 | 270 | /* _Axial Moments_ 271 | * 272 | * Compute the axial moments for given set of directions used for lobe sharing 273 | * the maximum cosine order to compute the integral is a function of the size 274 | * of the directions list. 275 | */ 276 | template 277 | inline Eigen::VectorXf AxialMoments(const Polygon& P, 278 | const std::vector& directions) { 279 | 280 | const int dsize = directions.size(); 281 | const int order = (dsize-1) / 2 + 1; 282 | 283 | Eigen::VectorXf result(dsize*order); 284 | 285 | for(int i=0; i(P, w, order-1); 292 | 293 | const int shift = i*order; 294 | result.segment(shift, order) = In.segment(0, order); 295 | } 296 | return result; 297 | } 298 | -------------------------------------------------------------------------------- /include/DirectionsSampling.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // include STL 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* Generate a uniform distribution of points on the sphere using random 11 | * sampling with the STL uniform random number generator. 12 | */ 13 | template 14 | inline std::vector SamplingRandom(int nb) { 15 | 16 | std::mt19937 gen(std::chrono::system_clock::now().time_since_epoch().count()); 17 | std::uniform_real_distribution dist(0.0,1.0); 18 | 19 | std::vector res; 20 | res.reserve(nb); 21 | 22 | for(int i=0; i 42 | inline float MinDotDistance(const std::vector& dirs, const Vector& w) { 43 | 44 | // The set of testing direction is empty. 45 | if(dirs.size() == 0) { 46 | return 0.0f; 47 | } 48 | 49 | // Got a least one direction in `dirs` 50 | float dist = std::numeric_limits::max(); 51 | for(auto& d : dirs) { 52 | const float dot = Vector::Dot(d, w); 53 | dist = std::min(dist, dot); 54 | } 55 | return dist; 56 | } 57 | 58 | /* _Sampling Blue Noise Directions_ 59 | */ 60 | template 61 | inline std::vector SamplingBlueNoise(int nb, int MAX_TRY = 1000) { 62 | 63 | std::mt19937 gen(std::chrono::system_clock::now().time_since_epoch().count()); 64 | std::uniform_real_distribution dist(0.0,1.0); 65 | 66 | std::vector res; 67 | res.reserve(nb); 68 | 69 | float max_dist = 0.5; 70 | int n=0; 71 | while(n 108 | inline std::vector SamplingFibonacci(int nb) { 109 | 110 | // Golden ratio 111 | const float gratio = (sqrt(5.0f)+1.0f)/2.0f; 112 | 113 | std::vector res; 114 | res.reserve(nb); 115 | 116 | for(int i=0; i 17 | #include 18 | 19 | // Include STL 20 | #include 21 | #include 22 | 23 | template 24 | using VectorX = Eigen::Matrix; 25 | 26 | // Get the total number of coefficients for a function represented by 27 | // all spherical harmonic basis of degree <= @order (it is a point of 28 | // confusion that the order of an SH refers to its degree and not the order). 29 | constexpr int GetCoefficientCount(int order) { 30 | return (order + 1) * (order + 1); 31 | } 32 | 33 | // Get the one dimensional index associated with a particular degree @l 34 | // and order @m. This is the index that can be used to access the Coeffs 35 | // returned by SHSolver. 36 | constexpr int GetIndex(int l, int m) { 37 | return l * (l + 1) + m; 38 | } 39 | 40 | // Usage: CHECK(bool, string message); 41 | // Note that it must end a semi-colon, making it look like a 42 | // valid C++ statement (hence the awkward do() while(false)). 43 | #ifndef NDEBUG 44 | # define CHECK(condition, message) \ 45 | do { \ 46 | if (!(condition)) { \ 47 | std::cerr << "Check failed (" #condition ") in " << __FILE__ \ 48 | << ":" << __LINE__ << ", message: " << message << std::endl; \ 49 | std::exit(EXIT_FAILURE); \ 50 | } \ 51 | } while(false) 52 | #else 53 | # define ASSERT(condition, message) do {} while(false) 54 | #endif 55 | 56 | // Return true if the first value is within epsilon of the second value. 57 | bool NearByMargin(double actual, double expected) { 58 | double diff = actual - expected; 59 | if (diff < 0.0) { 60 | diff = -diff; 61 | } 62 | // 5 bits of error in mantissa (source of '32 *') 63 | return diff < 32 * std::numeric_limits::epsilon(); 64 | } 65 | 66 | // ---- The following functions are used to implement SH rotation computations 67 | // based on the recursive approach described in [1, 4]. The names of the 68 | // functions correspond with the notation used in [1, 4]. 69 | 70 | // See http://en.wikipedia.org/wiki/Kronecker_delta 71 | double KroneckerDelta(int i, int j) { 72 | if (i == j) { 73 | return 1.0; 74 | } else { 75 | return 0.0; 76 | } 77 | } 78 | 79 | // [4] uses an odd convention of referring to the rows and columns using 80 | // centered indices, so the middle row and column are (0, 0) and the upper 81 | // left would have negative coordinates. 82 | // 83 | // This is a convenience function to allow us to access an Eigen::MatrixXf 84 | // in the same manner, assuming r is a (2l+1)x(2l+1) matrix. 85 | double GetCenteredElement(const Eigen::MatrixXf& r, int i, int j) { 86 | // The shift to go from [-l, l] to [0, 2l] is (rows - 1) / 2 = l, 87 | // (since the matrix is assumed to be square, rows == cols). 88 | int offset = (r.rows() - 1) / 2; 89 | return r(i + offset, j + offset); 90 | } 91 | 92 | // P is a helper function defined in [4] that is used by the functions U, V, W. 93 | // This should not be called on its own, as U, V, and W (and their coefficients) 94 | // select the appropriate matrix elements to access (arguments @a and @b). 95 | double P(int i, int a, int b, int l, const std::vector& r) { 96 | if (b == l) { 97 | return GetCenteredElement(r[1], i, 1) * 98 | GetCenteredElement(r[l - 1], a, l - 1) - 99 | GetCenteredElement(r[1], i, -1) * 100 | GetCenteredElement(r[l - 1], a, -l + 1); 101 | } else if (b == -l) { 102 | return GetCenteredElement(r[1], i, 1) * 103 | GetCenteredElement(r[l - 1], a, -l + 1) + 104 | GetCenteredElement(r[1], i, -1) * 105 | GetCenteredElement(r[l - 1], a, l - 1); 106 | } else { 107 | return GetCenteredElement(r[1], i, 0) * GetCenteredElement(r[l - 1], a, b); 108 | } 109 | } 110 | 111 | // The functions U, V, and W should only be called if the correspondingly 112 | // named coefficient u, v, w from the function ComputeUVWCoeff() is non-zero. 113 | // When the coefficient is 0, these would attempt to access matrix elements that 114 | // are out of bounds. The list of rotations, @r, must have the @l - 1 115 | // previously completed band rotations. These functions are valid for l >= 2. 116 | 117 | double U(int m, int n, int l, const std::vector& r) { 118 | // Although [1, 4] split U into three cases for m == 0, m < 0, m > 0 119 | // the actual values are the same for all three cases 120 | return P(0, m, n, l, r); 121 | } 122 | 123 | double V(int m, int n, int l, const std::vector& r) { 124 | if (m == 0) { 125 | return P(1, 1, n, l, r) + P(-1, -1, n, l, r); 126 | } else if (m > 0) { 127 | return P(1, m - 1, n, l, r) * sqrt(1 + KroneckerDelta(m, 1)) - 128 | P(-1, -m + 1, n, l, r) * (1 - KroneckerDelta(m, 1)); 129 | } else { 130 | // Note there is apparent errata in [1,4,4b] dealing with this particular 131 | // case. [4b] writes it should be P*(1-d)+P*(1-d)^0.5 132 | // [1] writes it as P*(1+d)+P*(1-d)^0.5, but going through the math by hand, 133 | // you must have it as P*(1-d)+P*(1+d)^0.5 to form a 2^.5 term, which 134 | // parallels the case where m > 0. 135 | return P(1, m + 1, n, l, r) * (1 - KroneckerDelta(m, -1)) + 136 | P(-1, -m - 1, n, l, r) * sqrt(1 + KroneckerDelta(m, -1)); 137 | } 138 | } 139 | 140 | double W(int m, int n, int l, const std::vector& r) { 141 | if (m == 0) { 142 | // whenever this happens, w is also 0 so W can be anything 143 | return 0.0; 144 | } else if (m > 0) { 145 | return P(1, m + 1, n, l, r) + P(-1, -m - 1, n, l, r); 146 | } else { 147 | return P(1, m - 1, n, l, r) - P(-1, -m + 1, n, l, r); 148 | } 149 | } 150 | 151 | // Calculate the coefficients applied to the U, V, and W functions. Because 152 | // their equations share many common terms they are computed simultaneously. 153 | void ComputeUVWCoeff(int m, int n, int l, double* u, double* v, double* w) { 154 | double d = KroneckerDelta(m, 0); 155 | double denom = (abs(n) == l ? 2.0 * l * (2.0 * l - 1) : (l + n) * (l - n)); 156 | 157 | *u = sqrt((l + m) * (l - m) / denom); 158 | *v = 0.5 * sqrt((1 + d) * (l + abs(m) - 1.0) * (l + abs(m)) / denom) 159 | * (1 - 2 * d); 160 | *w = -0.5 * sqrt((l - abs(m) - 1) * (l - abs(m)) / denom) * (1 - d); 161 | } 162 | 163 | // Calculate the (2l+1)x(2l+1) rotation matrix for the band @l. 164 | // This uses the matrices computed for band 1 and band l-1 to compute the 165 | // matrix for band l. @rotations must contain the previously computed l-1 166 | // rotation matrices, and the new matrix for band l will be appended to it. 167 | // 168 | // This implementation comes from p. 5 (6346), Table 1 and 2 in [4] taking 169 | // into account the corrections from [4b]. 170 | void ComputeBandRotation(int l, std::vector* rotations) { 171 | // The band's rotation matrix has rows and columns equal to the number of 172 | // coefficients within that band (-l <= m <= l implies 2l + 1 coefficients). 173 | Eigen::MatrixXf rotation(2 * l + 1, 2 * l + 1); 174 | for (int m = -l; m <= l; m++) { 175 | for (int n = -l; n <= l; n++) { 176 | double u, v, w; 177 | ComputeUVWCoeff(m, n, l, &u, &v, &w); 178 | 179 | // The functions U, V, W are only safe to call if the coefficients 180 | // u, v, w are not zero 181 | if (!NearByMargin(u, 0.0)) 182 | u *= U(m, n, l, *rotations); 183 | if (!NearByMargin(v, 0.0)) 184 | v *= V(m, n, l, *rotations); 185 | if (!NearByMargin(w, 0.0)) 186 | w *= W(m, n, l, *rotations); 187 | 188 | rotation(m + l, n + l) = (u + v + w); 189 | } 190 | } 191 | 192 | rotations->push_back(rotation); 193 | } 194 | 195 | class Rotation { 196 | public: 197 | /* 198 | // Create a new Rotation that can applies @rotation to sets of coefficients 199 | // for the given @order. @order must be at least 0. 200 | static std::unique_ptr Create(int order, 201 | const Eigen::Quaternionf& rotation); 202 | 203 | // Create a new Rotation that applies the same rotation as @rotation. This 204 | // can be used to efficiently calculate the matrices for the same 3x3 205 | // transform when a new order is necessary. 206 | static std::unique_ptr Create(int order, const Rotation& rotation); 207 | */ 208 | // Transform the SH basis coefficients in @coeff by this rotation and store 209 | // them into @result. These may be the same vector. The @result vector will 210 | // be resized if necessary, but @coeffs must have its size equal to 211 | // GetCoefficientCount(order()). 212 | // 213 | // This rotation transformation produces a set of coefficients that are equal 214 | // to the coefficients found by projecting the original function rotated by 215 | // the same rotation matrix. 216 | // 217 | // There are explicit instantiations for double, float, and Array3f. 218 | template 219 | void Apply(const std::vector& coeffs, std::vector* result) const; 220 | void Apply(const Eigen::MatrixXf& coeffs, Eigen::MatrixXf& result) const; 221 | void Apply(const Eigen::VectorXf& coeffs, Eigen::VectorXf& result) const; 222 | 223 | // The order (0-based) that the rotation was constructed with. It can only 224 | // transform coefficient vectors that were fit using the same order. 225 | int order() const; 226 | 227 | // Return the rotation that is effectively applied to the inputs of the 228 | // original function. 229 | Eigen::Quaternionf rotation() const; 230 | 231 | // Return the (2l+1)x(2l+1) matrix for transforming the coefficients within 232 | // band @l by the rotation. @l must be at least 0 and less than or equal to 233 | // the order this rotation was initially constructed with. 234 | const Eigen::MatrixXf& band_rotation(int l) const; 235 | 236 | Rotation(int order, const Eigen::Quaternionf& rotation); 237 | 238 | private: 239 | const int order_; 240 | const Eigen::Quaternionf rotation_; 241 | 242 | std::vector band_rotations_; 243 | }; 244 | 245 | 246 | Rotation::Rotation(int order, const Eigen::Quaternionf& rotation) 247 | : order_(order), rotation_(rotation) { 248 | band_rotations_.reserve(GetCoefficientCount(order)); 249 | 250 | // Order 0 (first band) is simply the 1x1 identity since the SH basis 251 | // function is a simple sphere. 252 | Eigen::MatrixXf r(1, 1); 253 | r(0, 0) = 1.0; 254 | band_rotations_.push_back(r); 255 | 256 | r.resize(3, 3); 257 | // The second band's transformation is simply a permutation of the 258 | // rotation matrix's elements, provided in Appendix 1 of [1], updated to 259 | // include the Condon-Shortely phase. The recursive method in 260 | // ComputeBandRotation preserves the proper phases as high bands are computed. 261 | Eigen::Matrix3f rotation_mat = rotation.toRotationMatrix(); 262 | r(0, 0) = rotation_mat(1, 1); 263 | r(0, 1) = -rotation_mat(1, 2); 264 | r(0, 2) = rotation_mat(1, 0); 265 | r(1, 0) = -rotation_mat(2, 1); 266 | r(1, 1) = rotation_mat(2, 2); 267 | r(1, 2) = -rotation_mat(2, 0); 268 | r(2, 0) = rotation_mat(0, 1); 269 | r(2, 1) = -rotation_mat(0, 2); 270 | r(2, 2) = rotation_mat(0, 0); 271 | band_rotations_.push_back(r); 272 | 273 | // Recursively build the remaining band rotations, using the equations 274 | // provided in [4, 4b]. 275 | for (int l = 2; l <= order; l++) { 276 | ComputeBandRotation(l, &band_rotations_); 277 | } 278 | } 279 | 280 | /* 281 | std::unique_ptr Rotation::Create( 282 | int order, const Eigen::Quaternionf& rotation) { 283 | #ifndef NDEBUG 284 | CHECK(order >= 0, "Order must be at least 0."); 285 | CHECK(NearByMargin(rotation.squaredNorm(), 1.0), 286 | "Rotation must be normalized."); 287 | #endif 288 | 289 | std::unique_ptr sh_rot(new Rotation(order, rotation)); 290 | 291 | // Order 0 (first band) is simply the 1x1 identity since the SH basis 292 | // function is a simple sphere. 293 | Eigen::MatrixXf r(1, 1); 294 | r(0, 0) = 1.0; 295 | sh_rot->band_rotations_.push_back(r); 296 | 297 | r.resize(3, 3); 298 | // The second band's transformation is simply a permutation of the 299 | // rotation matrix's elements, provided in Appendix 1 of [1], updated to 300 | // include the Condon-Shortely phase. The recursive method in 301 | // ComputeBandRotation preserves the proper phases as high bands are computed. 302 | Eigen::Matrix3f rotation_mat = rotation.toRotationMatrix(); 303 | r(0, 0) = rotation_mat(1, 1); 304 | r(0, 1) = -rotation_mat(1, 2); 305 | r(0, 2) = rotation_mat(1, 0); 306 | r(1, 0) = -rotation_mat(2, 1); 307 | r(1, 1) = rotation_mat(2, 2); 308 | r(1, 2) = -rotation_mat(2, 0); 309 | r(2, 0) = rotation_mat(0, 1); 310 | r(2, 1) = -rotation_mat(0, 2); 311 | r(2, 2) = rotation_mat(0, 0); 312 | sh_rot->band_rotations_.push_back(r); 313 | 314 | // Recursively build the remaining band rotations, using the equations 315 | // provided in [4, 4b]. 316 | for (int l = 2; l <= order; l++) { 317 | ComputeBandRotation(l, &(sh_rot->band_rotations_)); 318 | } 319 | 320 | return sh_rot; 321 | } 322 | 323 | std::unique_ptr Rotation::Create(int order, 324 | const Rotation& rotation) { 325 | #ifndef NDEBUG 326 | CHECK(order >= 0, "Order must be at least 0."); 327 | #endif 328 | 329 | std::unique_ptr sh_rot(new Rotation(order, rotation.rotation_)); 330 | 331 | // Copy up to min(order, rotation.order_) band rotations into the new 332 | // SHRotation. For shared orders, they are the same. If the new order is 333 | // higher than already calculated then the remainder will be computed next. 334 | for (int l = 0; l <= std::min(order, rotation.order_); l++) { 335 | sh_rot->band_rotations_.push_back(rotation.band_rotations_[l]); 336 | } 337 | 338 | // Calculate remaining bands (automatically skipped if there are no more). 339 | for (int l = rotation.order_ + 1; l <= order; l++) { 340 | ComputeBandRotation(l, &(sh_rot->band_rotations_)); 341 | } 342 | 343 | return sh_rot; 344 | } 345 | */ 346 | 347 | int Rotation::order() const { return order_; } 348 | 349 | Eigen::Quaternionf Rotation::rotation() const { return rotation_; } 350 | 351 | const Eigen::MatrixXf& Rotation::band_rotation(int l) const { 352 | return band_rotations_[l]; 353 | } 354 | 355 | template 356 | void Rotation::Apply(const std::vector& coeff, 357 | std::vector* result) const { 358 | #ifndef NDEBUG 359 | CHECK(coeff.size() == GetCoefficientCount(order_), 360 | "Incorrect number of coefficients provided."); 361 | #endif 362 | 363 | // Resize to the required number of coefficients. 364 | // If result is already the same size as coeff, there's no need to zero out 365 | // its values since each index will be written explicitly later. 366 | if (result->size() != coeff.size()) { 367 | result->assign(coeff.size(), T()); 368 | } 369 | 370 | // Because of orthogonality, the coefficients outside of each band do not 371 | // interact with one another. By separating them into band-specific matrices, 372 | // we take advantage of that sparsity. 373 | 374 | for (int l = 0; l <= order_; l++) { 375 | VectorX band_coeff(2 * l + 1); 376 | 377 | // Fill band_coeff from the subset of @coeff that's relevant. 378 | for (int m = -l; m <= l; m++) { 379 | // Offset by l to get the appropiate vector component (0-based instead 380 | // of starting at -l). 381 | band_coeff(m + l) = coeff[GetIndex(l, m)]; 382 | } 383 | 384 | band_coeff = band_rotations_[l].cast() * band_coeff; 385 | 386 | // Copy rotated coefficients back into the appropriate subset into @result. 387 | for (int m = -l; m <= l; m++) { 388 | (*result)[GetIndex(l, m)] = band_coeff(m + l); 389 | } 390 | } 391 | } 392 | 393 | void Rotation::Apply(const Eigen::MatrixXf& coeffs, 394 | Eigen::MatrixXf& result) const { 395 | const int rows = coeffs.cols(); 396 | for(int l=0; l<=order_; ++l) { 397 | const int i = l*l; 398 | const int n = 2*l+1; 399 | result.block(i, 0, n, rows) = band_rotations_[l] * coeffs.block(i, 0, n, rows); 400 | } 401 | } 402 | 403 | void Rotation::Apply(const Eigen::VectorXf& coeffs, 404 | Eigen::VectorXf& result) const { 405 | for(int l=0; l<=order_; ++l) { 406 | const int i = l*l; 407 | const int n = 2*l+1; 408 | result.segment(i, n) = band_rotations_[l] * coeffs.segment(i, n); 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /include/SphericalHarmonics.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Eigen includes 4 | #include 5 | #include 6 | 7 | // STL includes 8 | #include 9 | #include 10 | #ifdef USE_THREADS 11 | #include 12 | #endif 13 | 14 | // Local include 15 | #include "DirectionsSampling.hpp" 16 | 17 | /* This header provide multiple ways to project spherical function to Spherical 18 | * Harmonics vectors. 19 | * 20 | * + 'ProjectToSH' return the SH coefficient vector when the Functor to be 21 | * projected is bandlimited to the max order of SH. Warning: do not use with 22 | * highly varying functions! This projection will probably behave very 23 | * badly. In such case, it is advised to use the 'ProjectToShMC' method. 24 | * 25 | * + 'ProjectToShMC' return the SH coefficient vector by integrating 26 | * using Monte Carlo integration. It is possible to choose the type of 27 | * random or quasi- random sequence for the integration. 28 | * 29 | * + 'TripleTensorProduct' return the premultiplied triple tensor product: 30 | * the integral of multiplied by the SH coefficients 'clm' 31 | * of 'f'. The integral is computed using Monte Carlo, as 'ProjectToShMC' 32 | * does. 33 | * 34 | * TODO: Template the direction sequence. 35 | */ 36 | 37 | #define USE_FIBONACCI_SEQ 38 | //#define USE_BLUENOISE_SEQ 39 | 40 | /* From a set of `basis` vector directions, and a spherical function `Functor`, 41 | * generate the Spherical Harmonics projection of the `Functor`. This algorithm 42 | * works as follows: 43 | * 44 | * 1. Evaluate the matrix Ylm(w_i) for each SH index and each direction 45 | * 2. Evaluate the vector of the functor [f(w_0), ..., f(w_n)] 46 | * 3. Return the product Ylm(w_i)^{-1} [f(w_0), ..., f(w_n)] 47 | * 48 | * Requierement: `basis` must have the dimension of the output SH vector. 49 | * `f` must be real valued. Higher dimension functor are not 50 | * handled yet. 51 | */ 52 | template 53 | inline Eigen::VectorXf ProjectToSH(const Functor& f, 54 | const std::vector& basis) { 55 | 56 | // Get the number of elements to compute 57 | const int dsize = basis.size(); 58 | const int order = sqrt(dsize)-1; 59 | 60 | Eigen::MatrixXf Ylm(dsize, dsize); 61 | Eigen::VectorXf flm(dsize); 62 | 63 | for(unsigned int i=0; i 80 | inline Eigen::VectorXf ProjectToShMC(const Functor& f, int order, int M=100000) { 81 | 82 | std::mt19937 gen(0); 83 | std::uniform_real_distribution dist(0.0,1.0); 84 | 85 | Eigen::VectorXf shCoeffs((order+1)*(order+1)); 86 | #if defined(USE_FIBONACCI_SEQ) 87 | const std::vector directions = SamplingFibonacci(M); 88 | #elif defined(USE_BLUENOISE_SEQ) 89 | const std::vector directions = SamplingBlueNoise(M); 90 | #else 91 | const std::vector directions = SamplingRandom(M); 92 | #endif 93 | for(auto& w : directions) { 94 | // Evaluate the function and the basis vector 95 | shCoeffs += f(w) * SH::FastBasis(w, order); 96 | } 97 | shCoeffs *= 4.0*M_PI / float(M); 98 | 99 | return shCoeffs; 100 | } 101 | 102 | /* Compute the triple tensor product \int Ylm * Ylm * Ylm 103 | * 104 | * ## Arguments: 105 | * 106 | * + 'ylm' is the input spherical function SH coeffcients. They will be 107 | * prefactored to the triple product tensor to build the matrix. 108 | * 109 | * + 'truncated' allows to export either the truncated matrix (up to order 110 | * 'n', where 'n' is the order the input SH coeffs 'ylm') or the full matrix 111 | * that is order '2n-1'. 112 | */ 113 | template 114 | inline Eigen::MatrixXf TripleTensorProduct(const Eigen::VectorXf& ylm, 115 | bool truncated=true, 116 | int nDirections=100000) { 117 | 118 | // Compute the max order 119 | const int vsize = ylm.size(); 120 | const int order = (truncated) ? sqrt(vsize)-1 : 2*sqrt(vsize)-2; 121 | const int msize = (truncated) ? vsize : SH::Terms(order); 122 | 123 | Eigen::MatrixXf res = Eigen::MatrixXf::Zero(msize, msize); 124 | Eigen::VectorXf clm(msize); 125 | 126 | // Take a uniformly distributed point sequence and integrate the triple tensor 127 | // for each SH band 128 | #if defined(USE_FIBONACCI_SEQ) 129 | const std::vector directions = SamplingFibonacci(nDirections); 130 | #elif defined(USE_BLUENOISE_SEQ) 131 | const std::vector directions = SamplingBlueNoise(nDirections); 132 | #else 133 | const std::vector directions = SamplingRandom(nDirections); 134 | #endif 135 | for(auto& w : directions) { 136 | SH::FastBasis(w, order, clm); 137 | res += clm.segment(0, vsize).dot(ylm) * clm * clm.transpose(); 138 | } 139 | 140 | res *= 4.0f * M_PI / float(nDirections); 141 | return res; 142 | } 143 | 144 | /* Compute the triple tensor product \int Ylm * Ylm * Ylm for a bunch of 145 | * function projected on Spherical Harmonics. This method is specially 146 | * interesting to construct product of (R,G,B) component where each component 147 | * is stored in a separate vector. 148 | * 149 | * ## Arguments: 150 | * 151 | * + 'ylm' is the input spherical function SH coeffcients. They will be 152 | * prefactored to the triple product tensor to build the matrix. 153 | * 154 | * + 'truncated' allows to export either the truncated matrix (up to order 155 | * 'n', where 'n' is the order the input SH coeffs 'ylm') or the full matrix 156 | * that is order '2n-1'. 157 | */ 158 | template 159 | inline std::vector TripleTensorProduct( 160 | const std::vector& ylms, 161 | bool truncated=true, 162 | int nDirections=100000) { 163 | 164 | #ifdef USE_THREADS 165 | struct TTPThread : public std::thread { 166 | 167 | std::vector res; 168 | 169 | TTPThread(const std::vector& dirs, unsigned int start, unsigned int end, 170 | const std::vector& ylms, int order) : 171 | std::thread(&TTPThread::run, this, dirs, start, end, ylms, order) {} 172 | 173 | void run(const std::vector& dirs, unsigned int start, unsigned int end, 174 | const std::vector& ylms, int order) { 175 | const int vsize = ylms[0].size(); 176 | const float fact = 4.0f * M_PI / float(dirs.size()); 177 | 178 | const int msize = SH::Terms(order); 179 | Eigen::MatrixXf mat(msize, msize); 180 | Eigen::VectorXf clm(msize); 181 | 182 | res.reserve(3); 183 | res.push_back(Eigen::MatrixXf::Zero(msize, msize)); 184 | res.push_back(Eigen::MatrixXf::Zero(msize, msize)); 185 | res.push_back(Eigen::MatrixXf::Zero(msize, msize)); 186 | 187 | for(unsigned int k=start; k res(ylms.size(), Eigen::MatrixXf::Zero(msize, msize)); 210 | 211 | // Take a uniformly distributed point sequence and integrate the triple tensor 212 | // for each SH band 213 | #if defined(USE_FIBONACCI_SEQ) 214 | const std::vector directions = SamplingFibonacci(nDirections); 215 | #elif defined(USE_BLUENOISE_SEQ) 216 | const std::vector directions = SamplingBlueNoise(nDirections); 217 | #else 218 | const std::vector directions = SamplingRandom(nDirections); 219 | #endif 220 | 221 | #ifdef USE_THREADS 222 | std::vector threads; 223 | const unsigned int nthreads = std::thread::hardware_concurrency(); 224 | for(unsigned int i=0; ijoin(); 233 | 234 | for(unsigned int i=0; i<3; ++i) { 235 | res[i] += thread->res[i]; 236 | } 237 | } 238 | #else 239 | Eigen::VectorXf clm(msize); 240 | const float fact = 4.0f * M_PI / float(nDirections); 241 | for(auto& w : directions) { 242 | // Construct the matrix 243 | SH::FastBasis(w, order, clm); 244 | const auto matrix = clm * clm.transpose(); 245 | 246 | // For each SH vector apply the weight to the matrix and sum it 247 | for(unsigned int i=0; i 256 | Eigen::MatrixXf TripleTensorProductCos(int order, 257 | int nDirections=100000) { 258 | 259 | // Compute the max order 260 | const int msize = SH::Terms(order); 261 | 262 | Eigen::MatrixXf res = Eigen::MatrixXf::Zero(msize, msize); 263 | 264 | // Take a uniformly distributed point sequence and integrate the triple tensor 265 | // for each SH band 266 | #if defined(USE_FIBONACCI_SEQ) 267 | const std::vector directions = SamplingFibonacci(nDirections); 268 | #elif defined(USE_BLUENOISE_SEQ) 269 | const std::vector directions = SamplingBlueNoise(nDirections); 270 | #else 271 | const std::vector directions = SamplingRandom(nDirections); 272 | #endif 273 | const float fact = 4.0f * M_PI / float(nDirections); 274 | for(auto& w : directions) { 275 | // Construct the matrix 276 | const Eigen::VectorXf clm = SH::FastBasis(w, order); 277 | const auto matrix = clm * clm.transpose(); 278 | 279 | // For each SH vector apply the weight to the matrix and sum it 280 | if(w[2] > 0.0) { 281 | res += fact * w[2] * matrix; 282 | } 283 | } 284 | return res; 285 | } 286 | -------------------------------------------------------------------------------- /include/SphericalIntegration.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Include Eigen 4 | #include 5 | #include 6 | #ifdef USE_SPARSE_EIGEN 7 | #include 8 | typedef Eigen::SparseMatrix MatrixType; 9 | #else 10 | typedef Eigen::MatrixXf MatrixType; 11 | #endif 12 | 13 | // Include STL 14 | #include 15 | #include 16 | 17 | // Local include 18 | #include "AxialMoments.hpp" 19 | 20 | /* _Zonal Weigths_ 21 | * 22 | * Compute the matrix of weights w_{l,i} that converts dot powers (wd, w)^i 23 | * to a Zonal Hamonics. More precisely: 24 | * 25 | * y_{l, 0}(w · wd) = ∑_i w_{l,i} (w · wd)^i, V l in [0 .. order] 26 | * 27 | */ 28 | inline Eigen::MatrixXf ZonalWeights(int order) { 29 | 30 | // W is the matrix of the weights. It is build recursively using the Legendre 31 | // polynomials. The trick here is that Legendre polynomials are simply the 32 | // shift in order of a previous pol. summed to another previous pol. 33 | Eigen::MatrixXf W = Eigen::MatrixXf::Zero(order, order); 34 | W(0,0) = 1.0f; 35 | W(1,1) = 1.0f; 36 | for(int n=2; n 58 | inline Eigen::MatrixXf ZonalWeights(const std::vector& directions) { 59 | 60 | const int dsize = directions.size(); 61 | const int order = (dsize-1) / 2 + 1; 62 | const int mrows = order*order; 63 | 64 | Eigen::MatrixXf result = Eigen::MatrixXf::Zero(mrows, dsize*order); 65 | 66 | const auto ZW = ZonalWeights(order); 67 | 68 | // Each vector fills a given set of column entries with decreasing 69 | // number to do the swapping. For example, the 0th vector will fill 70 | // entries from order 0 to max_order but the 1srt vector will fill 71 | // entries from 1 to max_order. 72 | for(int i=0; i= 2*j+1) { 75 | continue; 76 | } 77 | 78 | const int shift_rows = j*j + i; 79 | const int shift_cols = order*i; 80 | 81 | result.block(shift_rows, shift_cols, 1, order) = ZW.row(j); 82 | } 83 | } 84 | return result; 85 | } 86 | 87 | /* _Zonal Normalization_ 88 | * 89 | * This little code compute the zonal normalization for spherical harmonics 90 | * basis using the √(2l+1 / 4π) factor. It can later by applied to a zonal 91 | * vector using Eigen fast product: a.cwiseProduct(b) 92 | */ 93 | inline Eigen::VectorXf ZonalNormalization(int order) { 94 | Eigen::VectorXf res = Eigen::VectorXf(order); 95 | const float f = 1.0f/sqrt(4.0f*M_PI); 96 | for(int i=0; i 110 | inline Eigen::VectorXf RotatedZH(const Vector& d, const Vector& w, int order) { 111 | 112 | Eigen::VectorXf res = Eigen::VectorXf::Zero(order); 113 | const float z = Vector::Dot(d, w); 114 | 115 | res[0] = 1.0f; 116 | if(order == 1) { 117 | return res /sqrt(4.0f*M_PI); 118 | } 119 | 120 | res[1] = z; 121 | if(order == 2) { 122 | res[1] *= sqrt(3.0f); 123 | return res / sqrt(4.0f*M_PI); 124 | } 125 | 126 | // Using Bonnet recurrence formula 127 | for(int i=2; i 149 | inline Eigen::VectorXf ZHEvalFast(const std::vector& dirs, const Vector& w) { 150 | 151 | // Get the number of elements to compute 152 | const int dsize = dirs.size(); 153 | const int order = (dsize-1) / 2 + 1; 154 | const int vsize = order*order; 155 | 156 | Eigen::VectorXf v(vsize); 157 | 158 | // The loop is first over the order term then over the set of directions 159 | // starting from the 0 index that is reused for all bands. 160 | float ylm = 0.0; 161 | for(int i=0; i(n, w, order); 165 | 166 | for(int l=0; l= 2*l+1) { 168 | continue; 169 | } 170 | v[l*l + i] = ylm[l]; 171 | } 172 | } 173 | 174 | return v; 175 | } 176 | 177 | 178 | /* _Zonal Expansion_ 179 | * 180 | * Expands a Spherical Harmonics vector into a Zonal Harmonics matrix transform. 181 | * Each band of spherical harmonics is decomposed into a set of Zonal Hamonics 182 | * with specific directions `directions`. 183 | * 184 | * The conversion matrix A is computed as the inverse of the evaluation of the 185 | * SH basis a the particular directions. 186 | * 187 | * The vector of directions must be of size `2*order+1`, where `order` is the 188 | * max order of the SH expansion. 189 | * 190 | * The template class SH must permit to access its basis elements, the y_{l,m} 191 | * as the static function `SH::FastBasis(const Vector& w, int order)`. 192 | */ 193 | template 194 | inline MatrixType ZonalExpansion(const std::vector& directions) { 195 | 196 | // Get the current band. Here I use a shifted order number. The integer 197 | // order is actually `order+1` to compute the number of rows and iterate 198 | // over it. Later in the code I evaluate the correct order to get the 199 | // ylm from SH::FastBasis. 200 | const int dsize = directions.size(); 201 | const int order = (dsize-1) / 2 + 1; 202 | const int mrows = order*order; 203 | assert(order >= 0); 204 | 205 | const auto zhNorm = ZonalNormalization(order); 206 | 207 | #ifdef USE_SPARSE_EIGEN 208 | MatrixType Y(mrows, mrows); 209 | std::vector> triplets; 210 | #else 211 | MatrixType Y = Eigen::MatrixXf::Zero(mrows, mrows); 212 | #endif 213 | for(int i=0; i= 2*j+1) { 225 | continue; 226 | } 227 | 228 | const int shift = j*j; 229 | const int vsize = 2*j+1; 230 | #ifdef USE_SPARSE_EIGEN 231 | for(int k=0; k(shift+i, shift+k, v)); 234 | } 235 | Y.setFromTriplets(triplets.begin(), triplets.end()); 236 | #else 237 | Y.block(shift+i, shift, 1, vsize) = ylm.segment(shift, vsize).transpose() / zhNorm[j]; 238 | #endif 239 | } 240 | } 241 | 242 | return Y; 243 | } 244 | 245 | /* _Compute the Inverse of Matrix Y_ 246 | * 247 | * Since the matrix resulting from ZonalExpansion is block diagonal, its 248 | * inverse is trivial to compute. We must simply take the inverse of each 249 | * block, in place. 250 | * 251 | * TODO: Make the sparse version. 252 | */ 253 | inline MatrixType computeInverse(const MatrixType& Y) { 254 | const int nrows = Y.rows(); 255 | const int order = sqrt(nrows); 256 | 257 | #ifdef USE_SPARSE_EIGEN 258 | MatrixType A(mrows, nrows); 259 | std::vector> triplets; 260 | #else 261 | MatrixType A = Eigen::MatrixXf::Zero(nrows, nrows); 262 | #endif 263 | 264 | for(int j=0; j 293 | inline float computeSHIntegral(const Eigen::VectorXf& clm, 294 | const std::vector& basis, 295 | const Triangle& triangle) { 296 | 297 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 298 | // and compute the product of the two: `Prod = A x Zw`. 299 | const auto ZW = ZonalWeights(basis); 300 | const auto Y = ZonalExpansion(basis); 301 | const auto A = computeInverse(Y); 302 | 303 | const auto Prod = A*ZW; 304 | 305 | // Analytical evaluation of the integral of power of cosines for 306 | // the different basis elements up to the order defined by the 307 | // number of elements in the basis 308 | const auto moments = AxialMoments(triangle, basis); 309 | 310 | return clm.dot(Prod * moments); 311 | } 312 | 313 | 314 | #ifdef LATER 315 | /* _Compute the SH Integral over a Spherial Triangle_ 316 | * 317 | * This function is provided as an example of how to use the different 318 | * components of this package. It is probably much faster to precompute the 319 | * product `Prod` of the ZonalWeights and the Zonal to SH conversion matrix. 320 | */ 321 | template 322 | inline float computeSHIntegral(const Eigen::VectorXf& clm, 323 | const Triangle& triangle) { 324 | 325 | // Get the order of the provided SH vector and compute the directional 326 | // sampling of the sphere for rotated ZH/cosines. 327 | const auto order = sqrt(clm.size())-1; 328 | const auto basis = SamplingFibonacci(2*order+1); 329 | 330 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 331 | // and compute the product of the two: `Prod = A x Zw`. 332 | const auto ZW = ZonalWeights(basis); 333 | const auto Y = ZonalExpansion(basis); 334 | const auto A = computeInverse(Y); 335 | 336 | const auto Prod = A*ZW; 337 | 338 | // Analytical evaluation of the integral of power of cosines for 339 | // the different basis elements up to the order defined by the 340 | // number of elements in the basis 341 | const auto moments = AxialMoments(triangle, basis); 342 | 343 | return clm.dot(Prod * moments); 344 | } 345 | #endif 346 | -------------------------------------------------------------------------------- /include/Utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Include Eigen 4 | #include 5 | 6 | // Include STL 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct IOError : public std::exception { 13 | std::string _msg; 14 | 15 | IOError(const std::string& filename = "") { 16 | _msg = "Unable to open file descriptor \""; 17 | _msg.append(filename); 18 | _msg.append("\""); 19 | } 20 | 21 | virtual const char* what() const throw() { 22 | return _msg.c_str(); 23 | } 24 | }; 25 | 26 | /* Enable to load a list of Eigen Matrices to binary format 27 | */ 28 | inline std::vector LoadMatrices(const std::string& filename) { 29 | // Open file 30 | auto out = std::fopen(filename.c_str(), "r"); 31 | if(out == nullptr) { 32 | IOError ioException(filename); 33 | throw ioException; 34 | } 35 | 36 | // Load the number of matrices 37 | int size; 38 | std::fread(&size, sizeof(int), 1, out); 39 | std::vector matrices(size); 40 | 41 | for(int k=0; k& matrices) { 63 | 64 | // File descriptor 65 | auto out = std::fopen(filename.c_str(), "w"); 66 | 67 | // Output list of matrices 68 | const int size = matrices.size(); 69 | std::fwrite(&size, sizeof(int), 1, out); 70 | for(const auto& mat : matrices) { 71 | // Output the matrix size 72 | const int nrows = mat.rows(); 73 | const int ncols = mat.cols(); 74 | size_t s = std::fwrite(&nrows, sizeof(int), 1, out); 75 | assert(s == 1); 76 | s = std::fwrite(&ncols, sizeof(int), 1, out); 77 | assert(s == 1); 78 | s = std::fwrite(mat.data(), sizeof(float), nrows*ncols, out); 79 | assert(s == size_t(nrows*ncols)); 80 | } 81 | std::fclose(out); 82 | } 83 | 84 | /* Enable to load a list of Eigen Matrices to binary format 85 | */ 86 | inline Eigen::MatrixXf LoadMatrix(const std::string& filename) { 87 | // Open file 88 | auto out = std::fopen(filename.c_str(), "r"); 89 | if(out == nullptr) { 90 | IOError ioException(filename); 91 | throw ioException; 92 | } 93 | 94 | // Load the number of matrices 95 | Eigen::MatrixXf mat; 96 | int nrows; 97 | int ncols; 98 | size_t s = std::fread(&nrows, sizeof(int), 1, out); 99 | assert(s == 1); 100 | s = std::fread(&ncols, sizeof(int), 1, out); 101 | assert(s == 1); 102 | mat = Eigen::MatrixXf(nrows, ncols); 103 | s = std::fread(mat.data(), sizeof(float), nrows*ncols, out); 104 | assert(s == size_t(nrows*ncols)); 105 | 106 | std::fclose(out); 107 | return mat; 108 | } 109 | 110 | /* Enable to save a list of Eigen Matrices from a binary format 111 | */ 112 | inline void SaveMatrix(const std::string& filename, 113 | const Eigen::MatrixXf& mat) { 114 | 115 | // File descriptor 116 | auto out = std::fopen(filename.c_str(), "w"); 117 | 118 | // Output the matrix size 119 | const int nrows = mat.rows(); 120 | const int ncols = mat.cols(); 121 | size_t s = std::fwrite(&nrows, sizeof(int), 1, out); 122 | assert(s == 1); 123 | s = std::fwrite(&ncols, sizeof(int), 1, out); 124 | assert(s == 1); 125 | s = std::fwrite(mat.data(), sizeof(float), nrows*ncols, out); 126 | assert(s == size_t(nrows*ncols)); 127 | 128 | std::fclose(out); 129 | } -------------------------------------------------------------------------------- /scripts/PlotMaterial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | directory = sys.argv[1] 5 | 6 | from glob import glob 7 | from os import path 8 | files = glob(path.join(directory, '*.gnuplot')) 9 | 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | for f in files: 13 | print '>> Process data file ' + f 14 | matrix = np.loadtxt(f) 15 | 16 | X = matrix[:, [0]] 17 | Y = matrix[:, [2]] 18 | Z = matrix[:, [5]] 19 | plt.plot(X, Y) 20 | plt.plot(X, Z) 21 | outfile = f.replace('gnuplot', 'png') 22 | print '>> Saving to ' + outfile + '\n' 23 | plt.savefig(outfile) 24 | plt.clf() 25 | -------------------------------------------------------------------------------- /scripts/ProjectMaterials.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | import argparse 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('-r', default=False, help='Force recomputing solution if mats file already present') 8 | parser.add_argument('-o', default=15, help='Maximum band for the SH expansion') 9 | parser.add_argument('-n', default=500, help='Number of elements for the spherical quadrature') 10 | parser.add_argument('dir', metavar='dir', type=str, help='Ddirectory containing MERL database objects') 11 | args = parser.parse_args() 12 | order = str(args.o) 13 | N = str(args.n) 14 | Rcmp = args.r 15 | dir = args.dir 16 | 17 | from glob import glob 18 | from os import path 19 | files = glob(path.join(dir, '*.binary')) 20 | 21 | script = path.split(path.realpath(sys.argv[0]))[0] 22 | build = path.join(script, '../build/') 23 | 24 | bin = '' 25 | if path.isfile(path.join(build, 'Merl2Sh')): 26 | bin = path.join(build, 'Merl2Sh') 27 | else: 28 | print '>> Error: could not find \'Merl2Sh\'' 29 | exit(1) 30 | 31 | from subprocess import call 32 | for f in files: 33 | 34 | output = path.splitext(f)[0] + '.mats' 35 | if (not Rcmp) and path.exists(output): 36 | continue; 37 | 38 | print '>> Process data file ' + f 39 | args = [path.normpath(bin), '-o', order, '-n', N, f] 40 | call(args) 41 | -------------------------------------------------------------------------------- /tests/ArvoMoments.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //////////////////////////////////////////// 4 | // James Arvo's Axial Moment 5 | //////////////////////////////////////////// 6 | 7 | float dot(const float* a, const float* b) { 8 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; 9 | } 10 | 11 | void CrossProduct(const float* A, const float* B, float* out) { 12 | out[0] = -A[2] * B[1] + A[1] * B[2]; 13 | out[1] = A[2] * B[0] - A[0] * B[2]; 14 | out[2] = -A[1] * B[0] + A[0] * B[1]; 15 | } 16 | 17 | float ArvoCosSumIntegral( float x, float y, float c, int m, int n ) { 18 | int i = n % 2; 19 | float F = (i == 0) ? (y - x) : (sin(y) - sin(x)); 20 | float S = 0.0f; 21 | while( i <= n ) { 22 | if( i >= m ) { 23 | S += pow(c,i) * F; 24 | } 25 | float T = pow(cos(y),i+1.0f) * sin(y) - pow(cos(x),i+1.0f) * sin(x); 26 | F = (T + (i+1.0f)*F)/(i+2.0f); 27 | i += 2; 28 | } 29 | 30 | return S; 31 | } 32 | 33 | float ArvoLineIntegral(const float* A, const float* B, const float* w, int m, int n ) { 34 | const float epsilon = 1e-7; 35 | if( (n < 0) || ( (fabs(dot(w,A)) < epsilon) && (fabs(dot(w,B)) < epsilon) ) ) { 36 | return 0; 37 | } 38 | 39 | float lenA = sqrt(dot(A,A)); 40 | float lenB = sqrt(dot(B,B)); 41 | float s[3] = { A[0] / lenA, A[1] / lenA, A[2] / lenA }; 42 | float M[3][3] = { {1.0f - s[0]*s[0], -s[0]*s[1], -s[0]*s[2]}, {-s[0]*s[1], 1.0f - s[1]*s[1], -s[1]*s[2]}, {-s[0]*s[2], -s[1]*s[2], 1.0f - s[2]*s[2]} }; 43 | float t[3] = { M[0][0] * B[0] + M[0][1] * B[1] + M[0][2] * B[2], M[1][0] * B[0] + M[1][1] * B[1] + M[1][2] * B[2], M[2][0] * B[0] + M[2][1] * B[1] + M[2][2] * B[2] } ; 44 | float len_t = sqrt(dot(t,t)); 45 | t[0] /= len_t; 46 | t[1] /= len_t; 47 | t[2] /= len_t; 48 | float a = dot(w,s); 49 | float b = dot(w,t); 50 | float c = sqrt(a*a + b*b); 51 | //float l = acos( dot(A,B) / (lenA * lenB)); 52 | float acosVal = dot(A,B) / (lenA * lenB); 53 | float l; 54 | float eps = 1e-6; 55 | if(std::abs(acosVal-(-1)) < eps) 56 | l = M_PI; 57 | else if(std::abs(acosVal-1) < eps) 58 | l = 0; 59 | else 60 | l = acos( dot(A,B) / (lenA * lenB)); 61 | //printf("%f %f\n", dot(A, B), l); 62 | float signb = (b < 0) ? -1 : 1; 63 | float phi = signb * acos(a/c); 64 | return ArvoCosSumIntegral(-phi, l - phi, c, m, n); 65 | } 66 | 67 | float ArvoBoundaryIntegral(const float* tri, const float w[3], const float v[3], int m, int n ) { 68 | float b = 0; 69 | 70 | // For each edge (unrolled): 71 | // E1 72 | const float* A = &tri[3]; 73 | const float* B = &tri[0]; 74 | float n1[3]; 75 | CrossProduct(A,B,n1); 76 | float n1len = sqrt(dot(n1,n1)); 77 | n1[0] /= n1len; 78 | n1[1] /= n1len; 79 | n1[2] /= n1len; 80 | b += dot(n1,v) * ArvoLineIntegral(A, B, w, m, n); 81 | 82 | // E2 83 | A = &tri[6]; 84 | B = &tri[3]; 85 | float n2[3]; 86 | CrossProduct(A,B,n2); 87 | float n2len = sqrt(dot(n2,n2)); 88 | n2[0] /= n2len; 89 | n2[1] /= n2len; 90 | n2[2] /= n2len; 91 | b += dot(n2,v) * ArvoLineIntegral(A, B, w, m, n); 92 | 93 | // E3 94 | A = &tri[0]; 95 | B = &tri[6]; 96 | float n3[3]; 97 | CrossProduct(A,B,n3); 98 | float n3len = sqrt(dot(n3,n3)); 99 | n3[0] /= n3len; 100 | n3[1] /= n3len; 101 | n3[2] /= n3len; 102 | b += dot(n3,v) * ArvoLineIntegral(A, B, w, m, n); 103 | 104 | return b; 105 | } 106 | 107 | float TriangleSolidAngle(const float *v0, const float *v1, const float *v2 ) { 108 | //float V0[3] = { v0[0] / v0len, v0[1] / v0len, v0[2] / v0len }; 109 | //float V1[3] = { v1[0] / v1len, v1[1] / v1len, v1[2] / v1len }; 110 | //float V2[3] = { v2[0] / v2len, v2[1] / v2len, v2[2] / v2len }; 111 | float V0[3] = { v0[0], v0[1], v0[2] }; 112 | float V1[3] = { v1[0], v1[1], v1[2] }; 113 | float V2[3] = { v2[0], v2[1], v2[2] }; 114 | 115 | float temp[3]; 116 | CrossProduct(V1,V2,temp); 117 | float det = std::abs(dot(temp,V0)); 118 | float al = sqrt(dot(V0,V0)); 119 | float bl = sqrt(dot(V1,V1)); 120 | float cl = sqrt(dot(V2,V2)); 121 | float div = al*bl*cl + dot(V0,V1)*cl + dot(V0,V2)*bl + dot(V1,V2)*al; 122 | float at = atan2(det, div); 123 | if(at < 0) at += M_PI; // If det>0 && div<0 atan2 returns < 0, so add pi. 124 | float omega = 2.0f * at; 125 | return omega; 126 | } 127 | 128 | float ArvoAxialMoment(const float* tri, const float w[3], int n) { 129 | const float epsilon = 1e-10; 130 | float a = -ArvoBoundaryIntegral(tri,w,w,0,n-1); 131 | if( n % 2 == 0 ) { a += TriangleSolidAngle(&tri[0], &tri[3], &tri[6]); } //|| fabs(a) < epsilon 132 | 133 | return a / (n+1); 134 | } -------------------------------------------------------------------------------- /tests/MomentsEquals.cpp: -------------------------------------------------------------------------------- 1 | // Include Moment + test suite 2 | #include "AxialMoments.hpp" 3 | #include "Tests.hpp" 4 | 5 | // Include GLM 6 | #include 7 | 8 | // Include STL 9 | #include 10 | 11 | /* Check if all computed moments are equals between the two configurations. 12 | */ 13 | int CheckEquals(const glm::vec3& wA, const Triangle& triA, 14 | const glm::vec3& wB, const Triangle& triB, 15 | int nMin, int nMax, float Epsilon = 1.0E-3f) { 16 | 17 | // Track the number of failed tests 18 | int nb_fails = 0; 19 | 20 | auto momentsA = AxialMoment(triA, wA, nMax); 21 | auto momentsB = AxialMoment(triB, wB, nMax); 22 | 23 | // Test the difference between analytical code and MC 24 | if(!momentsA.isApprox(momentsB)) { 25 | ++nb_fails; 26 | for(unsigned int n=0; n 7 | 8 | // Include STL 9 | #include 10 | 11 | /* Check if all computed moments are positive. 12 | * This happens when the triangle covers a region with >= 0 for all v in 13 | * the triangle. 14 | */ 15 | int CheckPositive(const glm::vec3& w, const Triangle& tri, 16 | int nMin, int nMax) { 17 | 18 | std::cout << "Triangle set using:" << std::endl; 19 | std::cout << " + A = : " << tri[0].A << std::endl; 20 | std::cout << " + B = : " << tri[1].A << std::endl; 21 | std::cout << " + C = : " << tri[2].A << std::endl; 22 | std::cout << std::endl; 23 | 24 | std::cout << "Moment with respect to axis w = " << w << std::endl; 25 | std::cout << std::endl; 26 | 27 | // Track the number of failed tests 28 | int nb_fails = 0; 29 | 30 | auto moments = AxialMoment(tri, w, nMax); 31 | 32 | // Test the difference between analytical code and MC 33 | for(int n=nMin; n<=nMax; ++n) { 34 | if(moments[n] < 0.0f) { 35 | ++nb_fails; 36 | std::cout << "Error for n=" << n << " : " 37 | << moments[n] << " < 0" << std::endl; 38 | } else if(std::isnan(moments[n])) { 39 | ++nb_fails; 40 | std::cout << "Error for n=" << n << " : " 41 | << moments[n] << " is NaN" << std::endl; 42 | } 43 | } 44 | 45 | if(nb_fails == 0) { 46 | std::cout << "Test passed!" << std::endl; 47 | } 48 | std::cout << std::endl; 49 | return nb_fails; 50 | } 51 | 52 | /* Check if computed moments alternatve between positive and negative. 53 | * This happens when the triangle covers a region with <= 0 for all v in 54 | * the triangle. 55 | */ 56 | int CheckAlternate(const glm::vec3& w, const Triangle& tri, 57 | int nMin, int nMax) { 58 | 59 | std::cout << "Triangle set using:" << std::endl; 60 | std::cout << " + A = : " << tri[0].A << std::endl; 61 | std::cout << " + B = : " << tri[1].A << std::endl; 62 | std::cout << " + C = : " << tri[2].A << std::endl; 63 | std::cout << std::endl; 64 | 65 | std::cout << "Moment with respect to axis w = " << w << std::endl; 66 | std::cout << std::endl; 67 | 68 | // Track the number of failed tests 69 | int nb_fails = 0; 70 | 71 | auto moments = AxialMoment(tri, w, nMax); 72 | 73 | // Test the difference between analytical code and MC 74 | for(int n=nMin; n<=nMax; ++n) { 75 | if(moments[n] < 0.0f && n % 2 == 0) { 76 | ++nb_fails; 77 | std::cout << "Error for n=" << n << " : " 78 | << moments[n] << " < 0" << std::endl; 79 | } else if(moments[n] > 0.0f && n % 2 == 1) { 80 | ++nb_fails; 81 | std::cout << "Error for n=" << n << " : " 82 | << moments[n] << " > 0" << std::endl; 83 | } else if(std::isnan(moments[n])) { 84 | ++nb_fails; 85 | std::cout << "Error for n=" << n << " : " 86 | << moments[n] << " is NaN" << std::endl; 87 | } 88 | } 89 | 90 | if(nb_fails == 0) { 91 | std::cout << "Test passed!" << std::endl; 92 | } 93 | std::cout << std::endl; 94 | return nb_fails; 95 | } 96 | 97 | int main(int argc, char** argv) { 98 | 99 | const float Eps = 0.0f; 100 | 101 | glm::vec3 A, B, C; 102 | A = glm::vec3(Eps, Eps, 1.0); 103 | B = glm::vec3(Eps, 0.5, 1.0); 104 | C = glm::vec3(0.5, Eps, 1.0); 105 | Triangle tri(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 106 | glm::vec3 w; 107 | 108 | // Track the number of failed tests 109 | int nMin = 0, nMax = 10; 110 | int nb_fails = 0; 111 | 112 | #ifdef ONLY_CHECK 113 | // Change the moments' axis 114 | w = glm::normalize(glm::vec3(0, 0, 1)); 115 | nb_fails += CheckPositive(w, tri, nMin, nMax); 116 | 117 | w = glm::normalize(glm::vec3(0, 0, -1)); 118 | nb_fails += CheckAlternate(w, tri, nMin, nMax); 119 | #endif 120 | 121 | w = glm::normalize(glm::vec3(1, 0, 0)); 122 | nb_fails += CheckPositive(w, tri, nMin, nMax); 123 | 124 | #ifdef ONLY_CHECK 125 | w = glm::normalize(glm::vec3(-1, 0, 0)); 126 | nb_fails += CheckAlternate(w, tri, nMin, nMax); 127 | #endif 128 | 129 | // Alternate triangle configuration 130 | A = glm::vec3(0.0, 0.0, 1.0); 131 | B = glm::vec3(0.0, 1.0, 0.0); 132 | C = glm::vec3(1.0, 0.0, 0.0); 133 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 134 | w = glm::normalize(glm::vec3(1, 0, 0)); 135 | nb_fails += CheckPositive(w, tri, nMin, nMax); 136 | 137 | if(nb_fails == 0) 138 | return EXIT_SUCCESS; 139 | else 140 | return EXIT_FAILURE; 141 | } 142 | -------------------------------------------------------------------------------- /tests/MomentsVsArvo.cpp: -------------------------------------------------------------------------------- 1 | #include "AxialMoments.hpp" 2 | #include "ArvoMoments.hpp" 3 | #include "Tests.hpp" 4 | 5 | // Include GLM 6 | #include 7 | 8 | // Include STL 9 | #include 10 | #include 11 | 12 | int TestSolidAngle(const Triangle& tri, float Epsilon = 1.0E-5) { 13 | 14 | std::cout << "Triangle set using:" << std::endl; 15 | std::cout << " + A = : " << tri[0].A << std::endl; 16 | std::cout << " + B = : " << tri[1].A << std::endl; 17 | std::cout << " + C = : " << tri[2].A << std::endl; 18 | std::cout << std::endl; 19 | 20 | // Track the number of failed tests 21 | int nb_fails = 0; 22 | 23 | auto analytical = SolidAngle(tri); 24 | std::cout << "Analytical solid angle : " << analytical << std::endl; 25 | 26 | float tri_array[9] = {tri[0].A.x, tri[0].A.y, tri[0].A.z, 27 | tri[1].A.x, tri[1].A.y, tri[1].A.z, 28 | tri[2].A.x, tri[2].A.y, tri[2].A.z }; 29 | 30 | auto arvo = TriangleSolidAngle(&tri_array[0], &tri_array[3], &tri_array[6]); 31 | std::cout << "Arvo's solid angle : " << arvo << std::endl; 32 | 33 | if(std::abs(analytical - arvo) > Epsilon*std::abs(analytical) || 34 | std::isnan(analytical)) { 35 | std::cerr << "Error: solid angle differs from Arvo's!" << std::endl; 36 | std::cerr << " error is = " << std::abs(analytical - arvo) << std::endl; 37 | ++nb_fails; 38 | } 39 | 40 | std::cout << std::endl; 41 | 42 | return nb_fails; 43 | } 44 | 45 | int TestSolidAngle(const Quad& quad, float Epsilon = 1.0E-5) { 46 | 47 | std::cout << "Quad set using:" << std::endl; 48 | std::cout << " + A = : " << quad[0].A << std::endl; 49 | std::cout << " + B = : " << quad[1].A << std::endl; 50 | std::cout << " + C = : " << quad[2].A << std::endl; 51 | std::cout << " + D = : " << quad[3].A << std::endl; 52 | std::cout << std::endl; 53 | 54 | // Track the number of failed tests 55 | int nb_fails = 0; 56 | 57 | auto analytical = SolidAngle(quad); 58 | std::cout << "Analytical solid angle : " << analytical << std::endl; 59 | 60 | float quad_array[9] = {quad[0].A.x, quad[0].A.y, quad[0].A.z, 61 | quad[1].A.x, quad[1].A.y, quad[1].A.z, 62 | quad[2].A.x, quad[2].A.y, quad[2].A.z }; 63 | float arvo = TriangleSolidAngle(&quad_array[0], &quad_array[3], &quad_array[6]); 64 | float sec_array[9] = {quad[2].A.x, quad[2].A.y, quad[2].A.z, 65 | quad[3].A.x, quad[3].A.y, quad[3].A.z, 66 | quad[0].A.x, quad[0].A.y, quad[0].A.z }; 67 | arvo += TriangleSolidAngle(&sec_array[0], &sec_array[3], &sec_array[6]); 68 | std::cout << "Arvo's solid angle : " << arvo << std::endl; 69 | 70 | if(std::abs(analytical - arvo) > Epsilon*std::abs(analytical) || 71 | std::isnan(analytical)) { 72 | std::cerr << "Error: solid angle differs from Arvo's!" << std::endl; 73 | std::cerr << " error is = " << std::abs(analytical - arvo) << std::endl; 74 | ++nb_fails; 75 | } 76 | 77 | std::cout << std::endl; 78 | 79 | return nb_fails; 80 | } 81 | 82 | int TestMoments(const glm::vec3& w, const Triangle& tri, 83 | int nMin, int nMax, 84 | float Epsilon = 1.0E-5) { 85 | 86 | std::cout << "Triangle set using:" << std::endl; 87 | std::cout << " + A = : " << tri[0].A << std::endl; 88 | std::cout << " + B = : " << tri[1].A << std::endl; 89 | std::cout << " + C = : " << tri[2].A << std::endl; 90 | std::cout << std::endl; 91 | 92 | std::cout << "Moment with respect to axis w = " << w << std::endl; 93 | std::cout << std::endl; 94 | 95 | // Track the number of failed tests 96 | int nb_fails = 0; 97 | 98 | auto moments = AxialMoment(tri, w, nMax); 99 | 100 | // Test the difference between analytical code and MC 101 | for(int n=nMin; n<=nMax; ++n) { 102 | auto analytical = moments[n]; 103 | std::cout << "Analytical for n=" << n << " : " << analytical << std::endl; 104 | 105 | float tri_array[9] = {tri[0].A.x, tri[0].A.y, tri[0].A.z, 106 | tri[1].A.x, tri[1].A.y, tri[1].A.z, 107 | tri[2].A.x, tri[2].A.y, tri[2].A.z }; 108 | float w_array[3] = {w.x, w.y, w.z}; 109 | 110 | auto arvo = ArvoAxialMoment(tri_array, w_array, n); 111 | std::cout << "Arvo's for n=" << n << " : " << arvo << std::endl; 112 | 113 | if(std::abs(analytical - arvo) > Epsilon*std::abs(analytical) || 114 | std::isnan(analytical)) { 115 | std::cerr << "Error: moment " << n << " differs from Arvo's!" << std::endl; 116 | std::cerr << " error is = " << std::abs(analytical - arvo) << std::endl; 117 | ++nb_fails; 118 | } 119 | } 120 | std::cout << std::endl; 121 | 122 | return nb_fails; 123 | } 124 | 125 | int main(int argc, char** argv) { 126 | 127 | // Track the number of failed tests 128 | float Eps = 1.0E-5, Epsilon = 1.0E-2; 129 | int nMin = 0, nMax = 10; 130 | int nb_fails = 0; 131 | 132 | 133 | // Generate a triangle + lobe direction configuration 134 | glm::vec3 A, B, C, D, w; 135 | Triangle tri; 136 | 137 | 138 | /* Check the computation of the solid angle */ 139 | A = glm::vec3( 0.5,-0.5, 1.0); 140 | B = glm::vec3(-0.5,-0.5, 1.0); 141 | C = glm::vec3( 0.0, 0.5, 1.0); 142 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 143 | nb_fails += TestSolidAngle(tri, Epsilon); 144 | 145 | // Shfited triangle on the right upper quadrant 146 | A = glm::vec3(Eps, Eps, 1.0); 147 | B = glm::vec3(Eps, 0.5, 1.0); 148 | C = glm::vec3(0.5, Eps, 1.0); 149 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 150 | nb_fails += TestSolidAngle(tri, Epsilon); 151 | 152 | A = glm::vec3( 0.5, 0.0, 1.0); 153 | B = glm::vec3( 0.0, 0.5, 1.0); 154 | C = glm::vec3(-0.5, 0.0, 1.0); 155 | D = glm::vec3( 0.0, -0.5, 1.0); 156 | Quad quad = Quad(glm::normalize(A), glm::normalize(B), 157 | glm::normalize(C), glm::normalize(D)); 158 | nb_fails += TestSolidAngle(quad, Epsilon); 159 | 160 | /* Moment computation comparison */ 161 | 162 | // Change the moments' axis 163 | A = glm::vec3(Eps, Eps, 1.0); 164 | B = glm::vec3(Eps, 0.5, 1.0); 165 | C = glm::vec3(0.5, Eps, 1.0); 166 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 167 | w = glm::normalize(glm::vec3(0, 0, 1)); 168 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 169 | 170 | w = glm::normalize(glm::vec3(0, 0, -1)); 171 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 172 | 173 | w = glm::normalize(glm::vec3(1, 0, 1)); 174 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 175 | 176 | w = glm::normalize(glm::vec3(1, 0, 0)); 177 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 178 | 179 | // Change the triangle slightly but change the same axis. 180 | A = glm::vec3(0.0, 0.0, 1.0); 181 | B = glm::vec3(0.0, 0.5, 1.0); 182 | C = glm::vec3(0.5, 0.0, 1.0); 183 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 184 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 185 | 186 | // Change the triangle 187 | A = glm::vec3(0.00, 0.00, 1.0); 188 | B = glm::vec3(0.00, 0.1, 1.0); 189 | C = glm::vec3(0.01, 0.00, 1.0); 190 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 191 | w = glm::normalize(glm::vec3(0.05,0.05,1)); 192 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 193 | 194 | // Check the case where Nmax is odd 195 | nMax = 11; 196 | A = glm::vec3(Eps, Eps, 1.0); 197 | B = glm::vec3(Eps, 0.5, 1.0); 198 | C = glm::vec3(0.5, Eps, 1.0); 199 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 200 | 201 | // Change the moments' axis 202 | w = glm::normalize(glm::vec3(0, 0, 1)); 203 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 204 | 205 | w = glm::normalize(glm::vec3(0, 0, -1)); 206 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 207 | 208 | // Integrate a full quadrant 209 | nMax = 10; 210 | A = glm::vec3(0.0, 0.0, 1.0); 211 | B = glm::vec3(0.0, 1.0, 0.0); 212 | C = glm::vec3(1.0, 0.0, 0.0); 213 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 214 | 215 | w = glm::normalize(glm::vec3(0, 0, 1)); 216 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 217 | 218 | w = glm::normalize(glm::vec3(1, 1, 1)); 219 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 220 | 221 | if(nb_fails == 0) 222 | return EXIT_SUCCESS; 223 | else 224 | return EXIT_FAILURE; 225 | } 226 | -------------------------------------------------------------------------------- /tests/MomentsVsMC.cpp: -------------------------------------------------------------------------------- 1 | #include "AxialMoments.hpp" 2 | #include "DirectionsSampling.hpp" 3 | 4 | //#define USE_TRIANGLE_SAMPLING 5 | #include "Tests.hpp" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | std::mt19937 gen(0); 13 | std::uniform_real_distribution dist(0.0,1.0); 14 | 15 | typedef std::pair VectorXfPair; 16 | 17 | VectorXfPair MonteCarloMoments(const Triangle& triangle, 18 | const Vector& w, int n) { 19 | 20 | // Number of MC samples 21 | const int M = 10000000; 22 | Eigen::VectorXf mean = Eigen::VectorXf::Zero(n+1); 23 | Eigen::VectorXf var = Eigen::VectorXf::Zero(n+1); 24 | for(int k=0; k 48 | int TestMoments(const glm::vec3& w, const Polygon& polygon, 49 | int nMin, int nMax, 50 | float Epsilon = 1.0E-5) { 51 | 52 | std::cout << "Polygon set using:" << std::endl; 53 | for(unsigned int k=0; k " << moments.transpose() << std::endl; 79 | std::cout << " + Tri => " << temp.transpose() << std::endl; 80 | } 81 | } 82 | 83 | // Compute the moments using Monte-Carlo 84 | VectorXfPair mc; 85 | if(polygon.size() == 3) { 86 | Triangle tr = Triangle(polygon[0].A, polygon[1].A, polygon[2].A); 87 | mc = MonteCarloMoments(tr, w, nMax); 88 | } else if(polygon.size() == 4) { 89 | Triangle tr = Triangle(polygon[0].A, polygon[1].A, polygon[2].A); 90 | mc = MonteCarloMoments(tr, w, nMax); 91 | //std::cout << mc.first << std::endl << std::endl; 92 | tr = Triangle(polygon[1].A, polygon[2].A, polygon[3].A); 93 | auto temp = MonteCarloMoments(tr, w, nMax); 94 | //std::cout << temp.first << std::endl << std::endl; 95 | mc.first += temp.first; 96 | mc.second = (mc.second.cwiseAbs2() + temp.second.cwiseAbs2()).cwiseSqrt(); 97 | } 98 | 99 | // Test the difference between analytical code and MC 100 | for(int n=nMin; n<=nMax; ++n) { 101 | auto analytical = moments[n]; 102 | auto mcI = std::pair(mc.first[n], mc.second[n]); 103 | std::cout << "Analytical for n=" << n << " : " << analytical << std::endl; 104 | 105 | std::cout << "MonteCarlo for n=" << n << " : " << mcI.first 106 | << " ± " << mcI.second << std::endl; 107 | 108 | if(!closeTo(analytical, mcI) || std::isnan(analytical)) { 109 | std::cerr << "Error: moment " << n << " differs from MC!" << std::endl; 110 | std::cerr << " error is = " << std::abs(analytical - mcI.first) << std::endl; 111 | ++nb_fails; 112 | } 113 | } 114 | std::cout << std::endl; 115 | 116 | return nb_fails; 117 | } 118 | 119 | std::pair MonteCarloSolidAngle(const Triangle& triangle) { 120 | 121 | // Number of MC samples 122 | const int M = 10000000; 123 | float mean = 0.0f; 124 | float var = 0.0f; 125 | float fact = 4.0f*M_PI; 126 | for(int k=0; k(mean, 5.0f*sqrt(var/M)); 140 | } 141 | 142 | template 143 | int TestSolidAngle(const Polygon& polygon, float Epsilon = 1.0E-5) { 144 | 145 | std::cout << "Polygon set using:" << std::endl; 146 | for(unsigned int k=0; k(polygon); 154 | std::cout << "Analytical solid angle : " << analytical << std::endl; 155 | 156 | std::pair mc; 157 | if(polygon.size() == 3) { 158 | Triangle tr = Triangle(polygon[0].A, polygon[1].A, polygon[2].A); 159 | mc = MonteCarloSolidAngle(tr); 160 | } else if(polygon.size() == 4) { 161 | Triangle tr = Triangle(polygon[0].A, polygon[1].A, polygon[2].A); 162 | mc = MonteCarloSolidAngle(tr); 163 | tr = Triangle(polygon[2].A, polygon[3].A, polygon[0].A); 164 | auto temp = MonteCarloSolidAngle(tr); 165 | mc.first += temp.first; 166 | mc.second += temp.second; 167 | } 168 | std::cout << "MC solid angle : " << mc.first 169 | << " ± " << mc.second << std::endl; 170 | 171 | if(!closeTo(analytical, mc) || std::isnan(analytical)) { 172 | std::cerr << "Error: solid angle differs from MC!" << std::endl; 173 | std::cerr << " error is = " << std::abs(analytical - mc.first) << std::endl; 174 | ++nb_fails; 175 | } 176 | std::cout << std::endl; 177 | 178 | return nb_fails; 179 | } 180 | 181 | int main(int argc, char** argv) { 182 | 183 | // Track the number of failed tests 184 | float Eps = 1.0E-5, Epsilon = 1.0E-2; 185 | int nMin = 0, nMax = 10; 186 | int nb_fails = 0; 187 | 188 | 189 | // Generate a triangle + lobe direction configuration 190 | glm::vec3 A, B, C, D, w; 191 | Triangle tri; Quad quad; 192 | 193 | 194 | /* Check the solid angle */ 195 | 196 | // Check on Triangle 197 | A = glm::vec3( 0.5,-0.5, 1.0); 198 | B = glm::vec3(-0.5,-0.5, 1.0); 199 | C = glm::vec3( 0.0, 0.5, 1.0); 200 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 201 | nb_fails += TestSolidAngle(tri, Epsilon); 202 | 203 | // Check on Quad 204 | C = glm::vec3(-0.5, 0.5, 1.0); 205 | D = glm::vec3( 0.5, 0.5, 1.0); 206 | quad = Quad(glm::normalize(A), glm::normalize(B), glm::normalize(C), glm::normalize(D)); 207 | nb_fails += TestSolidAngle(quad, Epsilon); 208 | 209 | /* Check the moments */ 210 | 211 | // Shfited triangle on the right upper quadrant 212 | A = glm::vec3(Eps, Eps, 1.0); 213 | B = glm::vec3(Eps, 0.5, 1.0); 214 | C = glm::vec3(0.5, Eps, 1.0); 215 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 216 | 217 | // Change the moments' axis 218 | w = glm::normalize(glm::vec3(0, 0, 1)); 219 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 220 | 221 | w = glm::normalize(glm::vec3(0, 0, -1)); 222 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 223 | 224 | w = glm::normalize(glm::vec3(1, 0, 1)); 225 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 226 | 227 | w = glm::normalize(glm::vec3(1, 0, 0)); 228 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 229 | 230 | // Change the triangle slightly but change the same axis. 231 | A = glm::vec3(0.0, 0.0, 1.0); 232 | B = glm::vec3(0.0, 0.5, 1.0); 233 | C = glm::vec3(0.5, 0.0, 1.0); 234 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 235 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 236 | 237 | // Change the triangle 238 | A = glm::vec3(0.00, 0.00, 1.0); 239 | B = glm::vec3(0.00, 0.1, 1.0); 240 | C = glm::vec3(0.01, 0.00, 1.0); 241 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 242 | w = glm::normalize(glm::vec3(0.05,0.05,1)); 243 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 244 | 245 | // Check the case where Nmax is odd 246 | nMax = 11; 247 | A = glm::vec3(Eps, Eps, 1.0); 248 | B = glm::vec3(Eps, 0.5, 1.0); 249 | C = glm::vec3(0.5, Eps, 1.0); 250 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 251 | 252 | // Change the moments' axis 253 | w = glm::normalize(glm::vec3(0, 0, 1)); 254 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 255 | 256 | w = glm::normalize(glm::vec3(0, 0, -1)); 257 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 258 | 259 | // Integrate a full quadrant 260 | nMax = 10; 261 | A = glm::vec3(0.0, 0.0, 1.0); 262 | B = glm::vec3(0.0, 1.0, 0.0); 263 | C = glm::vec3(1.0, 0.0, 0.0); 264 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 265 | 266 | w = glm::normalize(glm::vec3(0, 0, 1)); 267 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 268 | 269 | w = glm::normalize(glm::vec3(1, 1, 1)); 270 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 271 | 272 | // Add random direction with a small triangle with centroid being 273 | // the z-vector. 274 | A = glm::vec3( 0.5,-0.5, 1.0); 275 | B = glm::vec3(-0.5,-0.5, 1.0); 276 | C = glm::vec3( 0.0, 0.5, 1.0); 277 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 278 | 279 | for(int nb_rand=0; nb_rand<10; ++nb_rand) { 280 | w = glm::normalize(glm::vec3(2.0f*(dist(gen) - 0.5f), 2.0f*(dist(gen) - 0.5f), 2.0f*(dist(gen) - 0.5f))); 281 | nb_fails += TestMoments(w, tri, nMin, nMax, Epsilon); 282 | } 283 | 284 | // Check on Quads with the unit upper direction and randomly choosen 285 | // directions. 286 | A = glm::vec3( 0.5,-0.5, 1.0); 287 | B = glm::vec3(-0.5,-0.5, 1.0); 288 | C = glm::vec3(-0.5, 0.5, 1.0); 289 | D = glm::vec3( 0.5, 0.5, 1.0); 290 | 291 | // Test for normal direction 292 | quad = Quad(glm::normalize(A), glm::normalize(B), glm::normalize(C), glm::normalize(D)); 293 | w = glm::normalize(glm::vec3(0, 0, 1)); 294 | 295 | // Test for a grazing direction. This should give zero moments for 296 | // odd orders. 297 | nb_fails += TestMoments(w, quad, nMin, nMax, Epsilon); 298 | w = glm::normalize(glm::vec3(1, 0, 0)); 299 | 300 | // Random direction testing. 301 | nb_fails += TestMoments(w, quad, nMin, nMax, Epsilon); 302 | for(int nb_rand=0; nb_rand<10; ++nb_rand) { 303 | w = glm::normalize(glm::vec3(2.0f*(dist(gen) - 0.5f), 2.0f*(dist(gen) - 0.5f), 2.0f*(dist(gen) - 0.5f))); 304 | nb_fails += TestMoments(w, quad, nMin, nMax, Epsilon); 305 | } 306 | 307 | if(nb_fails == 0) 308 | return EXIT_SUCCESS; 309 | else 310 | return EXIT_FAILURE; 311 | } 312 | -------------------------------------------------------------------------------- /tests/SphericalH.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Local includes 9 | #include "Tests.hpp" 10 | #include "SH.hpp" 11 | #include "SphericalHarmonics.hpp" 12 | #include "DirectionsSampling.hpp" 13 | #include "SphericalIntegration.hpp" 14 | 15 | // GLM include 16 | #include 17 | 18 | 19 | struct CosPowFunctor { 20 | // Constructor 21 | int _exp; 22 | inline CosPowFunctor(int exp=1) : _exp(exp) {} 23 | 24 | // Operator function 25 | inline float operator()(const Vector& w) const { 26 | return pow(glm::clamp(w.z, 0.0f, 1.0f), _exp); 27 | } 28 | }; 29 | 30 | /* Test the SH projection with respect to a diffuse functor: f(w) = (w·n)+ 31 | */ 32 | int TestPhongProjection(int order = 5, int exp = 1, float Epsilon = 1.0E-3f) { 33 | 34 | std::cout << "Test of the project of a cosine power to SH" << std::endl; 35 | std::cout << " + SH max order = " << order << std::endl; 36 | std::cout << " + Cosine power = " << exp << std::endl; 37 | 38 | const int msize = (order+1)*(order+1); 39 | int nb_fails = 0; 40 | 41 | const CosPowFunctor f(exp); 42 | const std::vector basis = SamplingBlueNoise(msize); 43 | const Eigen::VectorXf clm = ProjectToSH(f, basis); 44 | 45 | for(auto& w : basis) { 46 | 47 | const Eigen::VectorXf ylm = SH::FastBasis(w, order); 48 | 49 | const float pi = ylm.dot(clm); 50 | const float fi = f(w); 51 | 52 | if(!closeTo(fi, pi)) { 53 | std::cout << "SH(w) = " << pi << " ≠ " << fi << " f(w), " 54 | << "for w = "<< w << std::endl; 55 | ++nb_fails; 56 | } 57 | } 58 | 59 | // Regenerate another set of directions 60 | const std::vector query = SamplingBlueNoise(100); 61 | for(auto& w : basis) { 62 | 63 | const Eigen::VectorXf ylm = SH::FastBasis(w, order); 64 | 65 | const float pi = ylm.dot(clm); 66 | const float fi = f(w); 67 | 68 | if(!closeTo(fi, pi)) { 69 | std::cout << "SH(w) = " << pi << " ≠ " << fi << " f(w), " 70 | << "for w = "<< w << std::endl; 71 | ++nb_fails; 72 | } 73 | } 74 | 75 | if(nb_fails > 0) { 76 | std::cerr << "Test failed!" << std::endl; 77 | } else { 78 | std::cout << "Test success!" << std::endl; 79 | } 80 | std::cout << std::endl; 81 | 82 | return nb_fails; 83 | } 84 | 85 | /* Compute the projection of the HG phase function to Zonal Harmonics. 86 | */ 87 | Eigen::VectorXf HeyneyeProjection(float g, int order) { 88 | 89 | // Fill the Zonal Coefficients 90 | Eigen::VectorXf zhG = Eigen::VectorXf::Zero(SH::Terms(order)); 91 | zhG[0] = 1.0f/sqrt(4*M_PI); 92 | float powg = g; 93 | for(int k=1; k(100); 120 | for(const auto& w : directions) { 121 | //for(int nt=0; nt<360; ++nt) { 122 | 123 | // float theta = 2*M_PI * float(nt) / float(360); 124 | // const Vector w(sin(theta), 0.0, cos(theta)); 125 | 126 | const Eigen::VectorXf ylm = SH::FastBasis(w, order); 127 | 128 | const float vSH = ylm.dot(zhG); 129 | const float vHG = HeyneyeGreenstein(w, g); 130 | 131 | //std::cout << theta << "\t" << vHG << "\t" << vSH << std::endl; 132 | if(!closeTo(vSH, vHG)) { 133 | std::cout << "Error: with g=" << g << " and w="; 134 | std::cout << w << " => " << vSH << " ≠ " << vHG << std::endl; 135 | nb_fails++; 136 | } 137 | } 138 | 139 | return nb_fails; 140 | } 141 | 142 | int main(int argc, char** argv) { 143 | int nb_fails = 0; 144 | 145 | int order = 5; 146 | int exp = 1; 147 | 148 | 149 | // Test the HG decomposition for trivial case: 150 | nb_fails += TestHeyneyeProjection(0.0f, 5); 151 | nb_fails += TestHeyneyeProjection(0.1f, 10); 152 | nb_fails += TestHeyneyeProjection(0.5f, 18); 153 | 154 | // Test the diffuse project (exponent = 1) 155 | nb_fails += TestPhongProjection(order, exp); 156 | 157 | // Test for low exponnent phong 158 | exp = 3; 159 | nb_fails += TestPhongProjection(order, exp); 160 | 161 | // Test for mid exponnent phong 162 | order = 15; 163 | exp = 10; 164 | nb_fails += TestPhongProjection(order, exp); 165 | 166 | if(nb_fails > 0) { 167 | return EXIT_FAILURE; 168 | } else { 169 | return EXIT_SUCCESS; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/TestProduct.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Local includes 9 | #include "SH.hpp" 10 | #include "Tests.hpp" 11 | #include "SphericalHarmonics.hpp" 12 | #include "DirectionsSampling.hpp" 13 | #include "SphericalIntegration.hpp" 14 | 15 | // GLM include 16 | #include 17 | 18 | struct ProductSH { 19 | 20 | const Eigen::VectorXf _flm, _glm; 21 | const int _order; 22 | 23 | ProductSH(const Eigen::VectorXf& flm, const Eigen::VectorXf& glm) : 24 | _flm(flm), _glm(glm), _order(sqrt(glm.size())-1) { 25 | assert(glm.size() == flm.size()); 26 | } 27 | 28 | float operator()(const Vector& w) const { 29 | const auto ylm = SH::FastBasis(w, _order); 30 | return _flm.dot(ylm) * _glm.dot(ylm); 31 | } 32 | }; 33 | 34 | /* Compute the decomposition of a shading cosine on the SH basis up to 19 35 | * coefficients. 36 | */ 37 | Eigen::VectorXf DiffuseSHDecomposition(int order) { 38 | assert(order < 19); 39 | 40 | const float pisqrt = sqrt(M_PI); 41 | Eigen::VectorXf zhCoeffs(19); 42 | zhCoeffs[0] = pisqrt/2.0; 43 | zhCoeffs[1] = sqrt(M_PI/3.0); 44 | zhCoeffs[2] = sqrt(5.0*M_PI)/8.0; 45 | zhCoeffs[3] = 0.0; 46 | zhCoeffs[4] = -pisqrt/16.0; 47 | zhCoeffs[5] = 0.0; 48 | zhCoeffs[6] = sqrt(13.0*M_PI)/128.0; 49 | zhCoeffs[7] = 0.0; 50 | zhCoeffs[8] = -sqrt(17.0*M_PI)/256.0; 51 | zhCoeffs[9] = 0.0; 52 | zhCoeffs[10] = 7.0*sqrt(7.0*M_PI/3.0)/1024.0; 53 | zhCoeffs[11] = 0.0; 54 | zhCoeffs[12] = -15.0*pisqrt/2048; 55 | zhCoeffs[13] = 0.0; 56 | zhCoeffs[14] = 33.0*sqrt(29.0*M_PI)/32768.0; 57 | zhCoeffs[15] = 0.0; 58 | zhCoeffs[16] = -143.0*sqrt(11.0*M_PI/3.0)/65536.0; 59 | zhCoeffs[17] = 0.0; 60 | zhCoeffs[18] = 143.0*sqrt(37.0*M_PI)/262144.0; 61 | 62 | const int vsize = (order+1)*(order+1); 63 | Eigen::VectorXf shCoeffs = Eigen::VectorXf::Zero(vsize); 64 | for(int l=0; l(fYlm, trunc); 92 | const Eigen::VectorXf cosFYlm = fMat.block(0, 0, msize, nsize) * cosYlm; 93 | 94 | const int nDirections = 100; 95 | std::cout << "# Testing against " << nDirections 96 | << " [quasi]-random directions" << std::endl; 97 | const std::vector directions = SamplingFibonacci(nDirections); 98 | for(auto& w : directions) { 99 | const auto ylm = SH::FastBasis(w, morder); 100 | const auto rlm = ylm.segment(0, nsize); 101 | 102 | // Compute the product of f and cos using the individual values 103 | const float cosVal = cosYlm.dot(rlm); 104 | const float fVal = fYlm.dot(rlm); 105 | const float prodVal = cosVal*fVal; 106 | 107 | // Compute the product of f and cos using the precomputed product 108 | const float altVal = cosFYlm.dot(ylm); 109 | 110 | if(! closeTo(prodVal, altVal)) { 111 | ++nb_fails; 112 | std::cout << "# for direction " << w << ": " 113 | << prodVal << " ≠ " << altVal << std::endl; 114 | } 115 | } 116 | 117 | if(nb_fails == 0) { 118 | std::cout << "# Test passed!" << std::endl; 119 | } else { 120 | std::cout << "# Test failed!" << std::endl; 121 | } 122 | std::cout << std::endl; 123 | return nb_fails; 124 | } 125 | 126 | /* Take the product of a random function with a shading cosine and perform the 127 | * integral of this function over a spherical triangle. Compare this with the 128 | * Monte-Carlo integral. 129 | */ 130 | int IntegrateProducts() { 131 | int nb_fails = 0; 132 | 133 | const bool trunc = false; 134 | const int order = 1; 135 | const int morder = trunc ? order : 2*order-1; 136 | const int nsize = SH::Terms(order); 137 | const int msize = trunc ? nsize : SH::Terms(morder); 138 | const Eigen::VectorXf cosYlm = DiffuseSHDecomposition(order); 139 | std::vector fYlms; 140 | fYlms.push_back(0.1 * cosYlm);//Eigen::VectorXf::Random(SH::Terms(order)).cwiseAbs(); 141 | fYlms.push_back(1.0 * cosYlm);//Eigen::VectorXf::Random(SH::Terms(order)).cwiseAbs(); 142 | fYlms.push_back(2.0 * cosYlm);//Eigen::VectorXf::Random(SH::Terms(order)).cwiseAbs(); 143 | 144 | std::vector cosFYlms; 145 | 146 | // Precompute the matrix precomputed triple tensor product to evaluate the 147 | // product of (f · cos)(w) using SH. 148 | std::cout << "# Precomputing the TripleTensorProduct" << std::endl; 149 | const auto fMats = TripleTensorProduct(fYlms, trunc); 150 | 151 | for(const auto& fMat : fMats) { 152 | cosFYlms.push_back(fMat.block(0, 0, msize, nsize) * cosYlm); 153 | } 154 | 155 | const int nDirections = 100; 156 | std::cout << "# Testing against " << nDirections 157 | << " [quasi]-random directions" << std::endl; 158 | const std::vector directions = SamplingFibonacci(nDirections); 159 | for(auto& w : directions) { 160 | const auto ylm = SH::FastBasis(w, morder); 161 | const auto rlm = ylm.segment(0, nsize); 162 | 163 | // Compute the product of f and cos using the individual values 164 | const float cosVal = cosYlm.dot(rlm); 165 | std::vector prodVals; 166 | for(const auto& fYlm : fYlms) { 167 | const float fVal = fYlm.dot(rlm); 168 | prodVals.push_back(cosVal*fVal); 169 | } 170 | 171 | // Compute the product of f and cos using the precomputed product 172 | std::vector altVals; 173 | for(const auto& cosFYlm : cosFYlms) { 174 | altVals.push_back(cosFYlm.dot(ylm)); 175 | } 176 | 177 | assert(altVals.size() == prodVals.size()); 178 | for(auto i=0; i MonteCarloSH(const Eigen::VectorXf& alm, 200 | const Eigen::VectorXf& blm, 201 | const Triangle& triangle) { 202 | 203 | static std::mt19937 gen(0); 204 | static std::uniform_real_distribution dist(0.0,1.0); 205 | 206 | const int order = sqrt(alm.size())-1; 207 | Eigen::VectorXf ylm(alm.size()); 208 | 209 | // Number of MC samples 210 | const int M = 10000000; 211 | float mean = 0.0f; 212 | float var = 0.0f; 213 | for(int k=0; k(mean, 5.0f*sqrt(var/M)); 235 | } 236 | 237 | /* Check if the integration of the spherical function with SH coeffs `clm` 238 | * over the spherical triangle `triangle` is the same if done using ZH 239 | * expansion + Arvo's integral or MC method. 240 | */ 241 | int CheckSHIntegration(const Eigen::VectorXf& alm, 242 | const Eigen::VectorXf& blm, 243 | const Triangle& tri, 244 | bool clampSH = true, 245 | float Epsilon = 1.0E-3) { 246 | 247 | std::cout << "Testing the analytical integration with:" << std::endl; 248 | std::cout << " + Triangle ABC" << std::endl; 249 | std::cout << " + A = : " << tri[0].A << std::endl; 250 | std::cout << " + B = : " << tri[1].A << std::endl; 251 | std::cout << " + C = : " << tri[2].A << std::endl; 252 | std::cout << " + SH expansion of the integrand" << std::endl; 253 | std::cout << " + alm = [" << alm.transpose() << "]" << std::endl; 254 | std::cout << " + blm = [" << blm.transpose() << "]" << std::endl; 255 | 256 | assert(alm.size() == blm.size()); 257 | const int order = sqrt(alm.size())-1; 258 | int nb_fails = 0; 259 | 260 | /* Analytical solution */ 261 | 262 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 263 | // and compute the product of the two: `Prod = A x Zw`. 264 | const int nbVec = (clampSH) ? 2*order+1 : 4*order+1; 265 | const auto basis = SamplingBlueNoise(nbVec); 266 | const auto ZW = ZonalWeights(basis); 267 | const auto Y = ZonalExpansion(basis); 268 | const auto A = computeInverse(Y); 269 | const auto TPM = TripleTensorProduct(alm, clampSH); 270 | const auto prod = (A*ZW).transpose(); 271 | 272 | Eigen::VectorXf zlm; 273 | if(clampSH) { 274 | zlm = prod * (TPM*blm); 275 | } else { 276 | zlm = prod * (TPM.block(0,0,SH::Terms(2*order),SH::Terms(order))*blm); 277 | } 278 | const auto shI = zlm.dot(AxialMoments(tri, basis)); 279 | 280 | /* Monte-Carlo solution */ 281 | const auto mcI = MonteCarloSH(alm, blm, tri); 282 | 283 | // Check the difference between the two solutions 284 | if(!closeTo(shI, mcI)) { 285 | ++nb_fails; 286 | 287 | std::cout << "Error: Monte-Carlo = " << mcI.first 288 | << "(±" << mcI.second << ")" 289 | << " ≠ Analytical = " << shI << std::endl; 290 | } else { 291 | std::cout << "Test passed!" << std::endl; 292 | } 293 | 294 | std::cout << std::endl; 295 | return nb_fails; 296 | } 297 | 298 | int main(int argc, char** argv) { 299 | 300 | int nb_fails = 0; 301 | 302 | //* 303 | // Load an example 304 | //nb_fails += IntegrateProduct(); 305 | nb_fails += IntegrateProducts(); 306 | //*/ 307 | 308 | // Test spherical integration of products 309 | int order = 5; 310 | Vector A( 0.5,-0.5, 1.0), B(-0.5,-0.5, 1.0), C( 0.0, 0.5, 1.0); 311 | auto tri = Triangle(Vector::Normalize(A), Vector::Normalize(B), Vector::Normalize(C)); 312 | Eigen::VectorXf clm = DiffuseSHDecomposition(order); 313 | nb_fails += CheckSHIntegration(clm, clm, tri); 314 | 315 | // Test spherical integration of products with no clamping 316 | nb_fails += CheckSHIntegration(clm, clm, tri, false); 317 | 318 | // Test spherical integration of products with no clamping, 319 | // using a random SH expansion. 320 | Eigen::VectorXf dlm = Eigen::VectorXf::Random(SH::Terms(order)); 321 | nb_fails += CheckSHIntegration(clm, dlm, tri, false); 322 | 323 | if(nb_fails > 0) { 324 | return EXIT_FAILURE; 325 | } else { 326 | return EXIT_SUCCESS; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /tests/TestRotations.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Local includes 10 | #include "SH.hpp" 11 | #include "Tests.hpp" 12 | #include "SphericalHarmonics.hpp" 13 | #include "DirectionsSampling.hpp" 14 | #include "SphericalIntegration.hpp" 15 | #include "SHRotation.hpp" 16 | 17 | // GLM include 18 | #include 19 | 20 | int CheckQuaternion() { 21 | 22 | Eigen::Matrix3f rot; 23 | rot << 1.0, 0.0, 0.0, 24 | 0.0, 0.0,-1.0, 25 | 0.0, 1.0, 0.0; 26 | 27 | Eigen::Quaternionf quat(rot); 28 | 29 | std::cout << rot << std::endl << std::endl; 30 | std::cout << quat.toRotationMatrix() << std::endl << std::endl; 31 | std::cout << quat.norm() << std::endl << std::endl; 32 | 33 | return 0; 34 | } 35 | 36 | int CheckRotation(int order = 5, int nbTrials = 10) { 37 | int nb_fails = 0; 38 | 39 | auto axisRot = Eigen::AngleAxisf(0.25*M_PI, Eigen::Vector3f::UnitX()); 40 | auto quaternion = Eigen::Quaternionf(axisRot); 41 | if(!closeTo(quaternion.norm(), 1.0f)) { 42 | std::cerr << "Fail! The Quaternion is not normalized." << std::endl; 43 | return ++nb_fails; 44 | } 45 | auto rotation = Rotation(order, quaternion); 46 | 47 | Eigen::VectorXf clm = Eigen::VectorXf::Random(SHTerms(order)); 48 | Eigen::VectorXf rclm = Eigen::VectorXf::Zero(SHTerms(order)); 49 | rotation.Apply(clm, rclm); 50 | 51 | Eigen::VectorXf ylm(SH::Terms(order)); 52 | Eigen::VectorXf rylm(SH::Terms(order)); 53 | 54 | const auto dirs = SamplingFibonacci(nbTrials); 55 | for(auto& w : dirs) { 56 | SHEvalFast(w, order, ylm); 57 | SHEvalFast(quaternion._transformVector(w), order, ylm); 58 | 59 | const float v = ylm.dot(clm); 60 | const float rv = rylm.dot(rclm); 61 | if(! closeTo(v, rv)) { 62 | std::cerr << "for direction " << w.transpose() << ": " 63 | << v << " ≠ " << rv << std::endl; 64 | nb_fails++; 65 | } 66 | } 67 | 68 | return nb_fails; 69 | } 70 | 71 | int CheckRotationExplicitMatrix(int order = 5, int nbTrials = 10) { 72 | int nb_fails = 0; 73 | 74 | Eigen::Matrix3f rot; 75 | rot << 1.0, 0.0, 0.0, 76 | 0.0, 0.0,-1.0, 77 | 0.0, 1.0, 0.0; 78 | auto quaternion = Eigen::Quaternionf(rot); 79 | if(!closeTo(quaternion.norm(), 1.0f)) { 80 | std::cerr << "Fail! The Quaternion is not normalized." << std::endl; 81 | return ++nb_fails; 82 | } 83 | auto rotation = Rotation(order, quaternion); 84 | 85 | Eigen::VectorXf clm = Eigen::VectorXf::Random(SHTerms(order)); 86 | Eigen::VectorXf rclm = Eigen::VectorXf::Zero(SHTerms(order)); 87 | rotation.Apply(clm, rclm); 88 | 89 | Eigen::VectorXf ylm(SH::Terms(order)); 90 | Eigen::VectorXf rylm(SH::Terms(order)); 91 | 92 | const auto dirs = SamplingFibonacci(nbTrials); 93 | for(auto& w : dirs) { 94 | SHEvalFast(w, order, ylm); 95 | SHEvalFast(quaternion._transformVector(w), order, rylm); 96 | 97 | const float v = ylm.dot(clm); 98 | const float rv = rylm.dot(rclm); 99 | if(! closeTo(v, rv)) { 100 | std::cerr << "for direction " << w.transpose() << ": " 101 | << v << " ≠ " << rv << std::endl; 102 | nb_fails++; 103 | } 104 | } 105 | 106 | return nb_fails; 107 | } 108 | 109 | int main(int argc, char** argv) { 110 | 111 | int nb_fails = 0; 112 | 113 | CheckQuaternion(); 114 | 115 | // Check the rotation code with multiple threads 116 | std::vector threads; 117 | for(int k=0; k<10; ++k) { 118 | threads.push_back(std::thread(CheckRotation, k, 20)); 119 | } 120 | for(auto& thread : threads) { 121 | thread.join(); 122 | } 123 | 124 | // Check another method to init the quaternion 125 | CheckRotationExplicitMatrix(5, 20); 126 | 127 | if(nb_fails > 0) { 128 | return EXIT_FAILURE; 129 | } else { 130 | return EXIT_SUCCESS; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/TestUtils.cpp: -------------------------------------------------------------------------------- 1 | // Eigen includes 2 | #include 3 | 4 | // STL includes 5 | #include 6 | #include 7 | 8 | // Library include 9 | #include 10 | #include "Tests.hpp" 11 | 12 | 13 | /* Check if clamping a polyong works */ 14 | int CheckPolygonClamping() { 15 | 16 | int nb_fails = 0; 17 | PolygonConstructor pConstruct; 18 | Polygon polygon; 19 | Vector A, B, C; 20 | 21 | // Create a polygon above the horizon 22 | A = Vector(0,0,1); 23 | B = Vector(0,1,1); 24 | C = Vector(1,0,1); 25 | pConstruct = PolygonConstructor(Vector::Normalize(A), Vector::Normalize(B), Vector::Normalize(C)); 26 | polygon = pConstruct.ProjectToHemisphere(Vector(0,0,0), Vector(0,0,1)); 27 | if(polygon.size() != 3) { 28 | std::cerr << "Error: projecting canonical triangle doesn't provide the same triangle" << std::endl; 29 | std::cout << polygon << std::endl; 30 | ++nb_fails; 31 | } 32 | 33 | // Create a polygon above the horizon 34 | A = Vector(0,0,1); 35 | B = Vector(0,1,0); 36 | C = Vector(1,0,0); 37 | pConstruct = PolygonConstructor(A, B, C); 38 | polygon = pConstruct.ProjectToHemisphere(Vector(0,0,0), Vector(0,0,1)); 39 | if(polygon.size() != 3) { 40 | std::cerr << "Error: projecting canonical triangle doesn't provide the same triangle" << std::endl; 41 | std::cout << polygon << std::endl; 42 | ++nb_fails; 43 | } 44 | 45 | // Create a polygon below the horizon 46 | A = Vector(0,0,1); 47 | B = Vector(0,1,1); 48 | C = Vector(1,0,1); 49 | pConstruct = PolygonConstructor(Vector::Normalize(A), Vector::Normalize(B), Vector::Normalize(C)); 50 | polygon = pConstruct.ProjectToHemisphere(Vector(0,0,0), Vector(0,0,-1)); 51 | if(polygon.size() != 0) { 52 | std::cerr << "Error: projecting canonical triangle should return nothing" << std::endl; 53 | std::cout << polygon << std::endl; 54 | ++nb_fails; 55 | } 56 | 57 | // Create a polygon crossing the horizon 58 | A = Vector(0, 0, 1); 59 | B = Vector(0, 1, 1); 60 | C = Vector(1, 0,-1); 61 | pConstruct = PolygonConstructor(Vector::Normalize(A), Vector::Normalize(B), Vector::Normalize(C)); 62 | polygon = pConstruct.ProjectToHemisphere(Vector(0,0,0), Vector(0,0,1)); 63 | if(polygon.size() != 4) { 64 | std::cerr << "Error: projecting canonical crossing triangle should return a 4-polygon" << std::endl; 65 | std::cout << polygon << std::endl; 66 | ++nb_fails; 67 | } 68 | 69 | // Create a polygon crossing the horizon 70 | A = Vector(0, 0, 1); 71 | B = Vector(0, 1, 0); 72 | C = Vector(1, 0,-1); 73 | pConstruct = PolygonConstructor(Vector::Normalize(A), Vector::Normalize(B), Vector::Normalize(C)); 74 | polygon = pConstruct.ProjectToHemisphere(Vector(0,0,0), Vector(0,0,1)); 75 | if(polygon.size() != 3) { 76 | std::cerr << "Error: projecting canonical crossing triangle should return a 3-polygon" << std::endl; 77 | std::cout << polygon << std::endl; 78 | ++nb_fails; 79 | } 80 | 81 | 82 | // Create a polygon crossing the horizon 83 | A = Vector(0, 0, 1); 84 | B = Vector(0, 1, 0); 85 | C = Vector(1, 0,-1); 86 | pConstruct = PolygonConstructor(Vector::Normalize(A), Vector::Normalize(B), Vector::Normalize(C)); 87 | pConstruct.push_back(Vector::Normalize(Vector(0,0,-1))); 88 | pConstruct.push_back(Vector::Normalize(Vector(1,0,-1))); 89 | polygon = pConstruct.ProjectToHemisphere(Vector(0,0,0), Vector(0,0,1)); 90 | if(polygon.size() != 3) { 91 | std::cerr << "Error: projecting canonical crossing triangle should return a 3-polygon" << std::endl; 92 | std::cout << polygon << std::endl; 93 | ++nb_fails; 94 | } 95 | return nb_fails; 96 | } 97 | 98 | int main(int argc, char** argv) { 99 | 100 | int nmats = 5; 101 | int ncols = 10; 102 | int nrows = 15; 103 | 104 | // Create matrices 105 | std::vector matrices; 106 | for(int n=0; n read_mats = LoadMatrices("test.mats"); 116 | 117 | int nb_fails = 0; 118 | for(int i=0; i 0) { 137 | std::cerr << "Failure!" << std::endl; 138 | return EXIT_FAILURE; 139 | } else { 140 | std::cerr << "Success!" << std::endl; 141 | return EXIT_SUCCESS; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Tests.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Include GLM 4 | #include 5 | 6 | // Include STL 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // Eigen CORE 13 | #include 14 | 15 | // Local include 16 | #include "SH.hpp" 17 | 18 | /* _Is `a` close to `b` ?_ 19 | * 20 | * This template function check if a-b is smaller than a fraction of the max 21 | * between a and b. By default, it checks if it is smaller than 1 percent of 22 | * the maximum value. 23 | */ 24 | template 25 | inline bool closeTo(const T& a, const T&b, const T& Percentage = T(0.01)) { 26 | if(a == T(0.0) || b == T(0.0)) { 27 | return std::abs(a-b) < Percentage; 28 | } else { 29 | const T c = std::max(std::max(a, b), Percentage); 30 | return (std::abs(a-b) < Percentage * c); 31 | } 32 | } 33 | 34 | /* _Is `a` inside the confidence interval `[m-s, m+s]? 35 | * 36 | * `b` is supposed to be a confidence interval in the form of µ±ε where µ is 37 | * `b.first` and ε is `b.second`. 38 | */ 39 | template 40 | inline bool closeTo(const T& a, const std::pair& b) { 41 | assert(b.second >= T(0)); 42 | return (a < b.first + b.second) && (a > b.first - b.second); 43 | } 44 | 45 | struct Vector : public glm::vec3 { 46 | 47 | Vector() : glm::vec3() {} 48 | Vector(float x, float y, float z) : glm::vec3(x, y, z) {} 49 | Vector(const glm::vec3& w) : glm::vec3(w) {} 50 | 51 | static inline float Dot(const glm::vec3& a, const glm::vec3& b) { 52 | return glm::dot(a, b); 53 | } 54 | 55 | static inline glm::vec3 Cross(const glm::vec3& a, const glm::vec3& b) { 56 | return glm::cross(a, b); 57 | } 58 | 59 | static inline glm::vec3 Normalize(const glm::vec3& a) { 60 | return glm::normalize(a); 61 | } 62 | 63 | static inline float Length(const glm::vec3& a) { 64 | return glm::length(a); 65 | } 66 | }; 67 | 68 | std::ostream& operator<< (std::ostream& out, const glm::vec3& a) { 69 | out << "[" << a.x << ", " << a.y << ", " << a.z << "]"; 70 | return out; 71 | } 72 | 73 | 74 | struct Edge { 75 | Edge(const Vector& a, const Vector& b) : A(a), B(b) {} 76 | Vector A, B; 77 | }; 78 | 79 | /* 'The Triangle' structure represent a spherical triangle of three coordinates 80 | * A, B and C by storing its Edges in a vcetor. 81 | * 82 | * TODO: Extend to the polygon structure 83 | */ 84 | struct Triangle : public std::vector { 85 | Triangle() { 86 | } 87 | Triangle(const Vector& A, const Vector& B, const Vector& C) { 88 | this->push_back(Edge(A, B)); 89 | this->push_back(Edge(B, C)); 90 | this->push_back(Edge(C, A)); 91 | } 92 | }; 93 | 94 | struct Quad: public std::vector { 95 | Quad() { 96 | } 97 | Quad(const Vector& A, const Vector& B, 98 | const Vector& C, const Vector& D) { 99 | this->push_back(Edge(A, B)); 100 | this->push_back(Edge(B, C)); 101 | this->push_back(Edge(C, D)); 102 | this->push_back(Edge(D, A)); 103 | } 104 | }; 105 | 106 | struct Polygon : public std::vector { 107 | 108 | // Constructor 109 | Polygon() { 110 | } 111 | Polygon(const Vector& A, const Vector& B, const Vector& C) { 112 | this->push_back(Edge(A, B)); 113 | this->push_back(Edge(B, C)); 114 | this->push_back(Edge(C, A)); 115 | } 116 | }; 117 | 118 | std::ostream& operator<<(std::ostream& out, const Polygon& polygon) { 119 | for(auto& edge : polygon) { 120 | out << " + " << edge.A << std::endl; 121 | } 122 | return out; 123 | } 124 | 125 | struct PolygonConstructor : public std::vector { 126 | 127 | // Constructor 128 | PolygonConstructor() { 129 | } 130 | PolygonConstructor(const Vector& A, const Vector& B, const Vector& C) { 131 | this->push_back(A); 132 | this->push_back(B); 133 | this->push_back(C); 134 | } 135 | 136 | Polygon ProjectToHemisphere(const Vector& p) const { 137 | Polygon P; 138 | #ifdef REMOVE 139 | std::cout << "A = " << P[0].A << std::endl; 140 | std::cout << "B = " << P[1].A << std::endl; 141 | std::cout << "C = " << P[2].A << std::endl; 142 | #endif 143 | for(unsigned int k=0; ksize(); ++k) { 144 | const Vector& A = this->at(k); 145 | const Vector& B = (k == this->size()-1) ? this->at(0) : this->at(k+1); 146 | 147 | P.push_back(Edge(Vector::Normalize(A-p), Vector::Normalize(B-p))); 148 | } 149 | return P; 150 | } 151 | 152 | /* Clamp the Polygon with respect to the Shading normal. 153 | */ 154 | Polygon ProjectToHemisphere(const Vector& p, const Vector& n) const { 155 | Polygon P; 156 | #ifdef REMOVE 157 | std::cout << "A = " << P[0].A << std::endl; 158 | std::cout << "B = " << P[1].A << std::endl; 159 | std::cout << "C = " << P[2].A << std::endl; 160 | #endif 161 | // Constant 162 | const unsigned int size = this->size(); 163 | 164 | // Starting vector of the Edge. This vector can be clamped if necessary 165 | // to account for the shading horizon. 166 | unsigned int start = 0; 167 | Vector A, M; 168 | float dotAn; 169 | bool condition = true; 170 | do { 171 | // Get the current element 172 | A = Vector::Normalize(this->at(start) - p); 173 | 174 | // Initial condition: the vertex needs to be above the horizon 175 | dotAn = Vector::Dot(A, n); 176 | condition = dotAn < 0.0; 177 | ++start; 178 | 179 | } while(condition && startat(next) - p); 188 | 189 | const float dotBn = Vector::Dot(B, n); 190 | 191 | // First case: the beginning of the Edge was below the hemisphere. 192 | // Then we must create the intermediate point N and create an egde 193 | // using M and N and another with N and B.. 194 | if(dotAn < 0 && dotBn >= 0) { 195 | const float alpha = dotAn / (dotAn - dotBn); 196 | const Vector N = Vector::Normalize(A + alpha*(B-A)); 197 | 198 | if(Vector::Dot(M, N) < 1.0f) { 199 | P.push_back(Edge(M, N)); 200 | } 201 | if(alpha > 0 && Vector::Dot(N, B) < 1.0f) { 202 | P.push_back(Edge(N, B)); 203 | } 204 | 205 | } else if(dotAn >= 0 && dotBn < 0) { 206 | const float alpha = dotAn / (dotAn - dotBn); 207 | M = Vector::Normalize(A + alpha*(B-A)); 208 | if(alpha > 0) { 209 | P.push_back(Edge(A, M)); 210 | } 211 | 212 | // The next point is a valid one (above the horizon). Add the Edge 213 | // (A,B) 214 | } else if(dotAn >= 0 && dotBn >= 0) { 215 | if(Vector::Dot(A, B) < 1.0f) { 216 | P.push_back(Edge(A, B)); 217 | } 218 | } 219 | 220 | // Update the loop variables. 221 | A = B; 222 | dotAn = dotBn; 223 | } 224 | return P; 225 | } 226 | }; 227 | 228 | std::mt19937 _test_gen(0); 229 | std::uniform_real_distribution _test_dist(0.0,1.0); 230 | 231 | /* 'Sample' generate a random direction on the unit sphere with uniform 232 | * distribution using _test_gen and _test_dist random number generators.. 233 | */ 234 | glm::vec3 Sample() { 235 | 236 | glm::vec3 out; 237 | 238 | // Sample the cosine of the elevation 239 | const double z = _test_dist(_test_gen); 240 | out.z = 2.0*z - 1.0; 241 | const double z2 = out.z*out.z; 242 | 243 | // Sample the azimuth 244 | const double phi = 2.0*M_PI*_test_dist(_test_gen); 245 | out.x = sqrt(1.0-z2) * cos(phi); 246 | out.y = sqrt(1.0-z2) * sin(phi); 247 | return out; 248 | } 249 | 250 | // Sample a spherical triangle using Arvo's stratified method. 251 | // Note: This code is not reliable right now. An error in the computation of the 252 | // solid angle bias the distribution of points. Use the uniform sampling method 253 | // instead. 254 | glm::vec3 SampleSphericalTriangle(const Triangle& triangle, float& pdf) { 255 | const glm::vec3& A = triangle[0].A; 256 | const glm::vec3& B = triangle[1].A; 257 | const glm::vec3& C = triangle[2].A; 258 | 259 | const glm::vec3 ab = glm::normalize(glm::cross(A, B)); 260 | const glm::vec3 ac = glm::normalize(glm::cross(A, C)); 261 | const glm::vec3 ba = glm::normalize(glm::cross(B, A)); 262 | const glm::vec3 bc = glm::normalize(glm::cross(B, C)); 263 | const glm::vec3 cb = glm::normalize(glm::cross(C, B)); 264 | 265 | const float alpha = acos(glm::dot(ba, ac)); 266 | const float beta = acos(glm::dot(cb, ab)); 267 | const float gamma = acos(glm::dot(ac, bc)); 268 | 269 | const float area = alpha + beta + gamma - M_PI; 270 | pdf = 1.0f / area; 271 | 272 | const float phi = _test_dist(_test_gen)*area - alpha; 273 | const float t = cos(phi); 274 | const float s = sin(phi); 275 | const float u = t - cos(alpha); 276 | const float v = s + sin(alpha)*glm::dot(A, B); 277 | 278 | const float q = (v*t - u*s)*cos(alpha) - v / ((v*s + u*t)*sin(alpha)); 279 | 280 | glm::vec3 hC = q*A + float(sqrt(1.0f-q*q))*glm::normalize(C-glm::dot(C, A)*A); 281 | 282 | // Select the cos(theta) 283 | const float z = 1.0f - _test_dist(_test_gen)*(1.0f - glm::dot(hC, B)); 284 | 285 | return z*B + float(sqrt(1.0f-z*z))*glm::normalize(hC - glm::dot(hC, B)*B); 286 | } 287 | 288 | bool HitTriangle(const Triangle& triangle, const Vector& w) { 289 | 290 | const float Epsilon = 1.0E-6; 291 | 292 | auto& p1 = triangle[0].A; 293 | auto& p2 = triangle[0].B; 294 | auto& p3 = triangle[1].B; 295 | 296 | //Find vectors for two edges sharing vertex/point p1 297 | auto e1 = p2 - p1; 298 | auto e2 = p3 - p1; 299 | 300 | // calculating determinant 301 | auto p = Vector::Cross(w, e2); 302 | auto det = Vector::Dot(e1, p); 303 | 304 | //if determinant is near zero, ray lies in plane of triangle otherwise not 305 | if (det > -Epsilon && det < Epsilon) { return false; } 306 | auto invDet = 1.0f / det; 307 | 308 | //calculate distance from p1 to ray origin 309 | auto t = - p1; 310 | 311 | //Calculate u parameter 312 | auto u = Vector::Dot(t, p) * invDet; 313 | 314 | //Check for ray hit 315 | if (u < 0 || u > 1) { return false; } 316 | 317 | //Prepare to test v parameter 318 | auto q = glm::cross(t, e1); 319 | 320 | //Calculate v parameter 321 | auto v = Vector::Dot(w, q) * invDet; 322 | 323 | //Check for ray hit 324 | if (v < 0 || u + v > 1) { return false; } 325 | 326 | if ((Vector::Dot(e2, q) * invDet) > Epsilon) { 327 | //ray does intersect 328 | return true; 329 | } 330 | 331 | // No hit at all 332 | return false; 333 | } 334 | 335 | /* Spherical Harmonics wrapper for the code in 'SphericalHarmonics.hpp' 336 | */ 337 | struct SH { 338 | 339 | // Inline for FastBasis 340 | static inline Eigen::VectorXf FastBasis(const Vector& w, int lmax) { 341 | const auto size = Terms(lmax); 342 | Eigen::VectorXf res(size); 343 | SHEvalFast(w, lmax, res); 344 | return res; 345 | } 346 | static inline void FastBasis(const Vector& w, int lmax, Eigen::VectorXf& clm) { 347 | assert(clm.size() == Terms(lmax)); 348 | SHEvalFast(w, lmax, clm); 349 | } 350 | 351 | // Inline for Terms 352 | static inline int Terms(int band) { 353 | return SHTerms(band); 354 | } 355 | 356 | // Inline for Index 357 | static inline int Index(int l, int m) { 358 | return SHIndex(l, m); 359 | } 360 | }; 361 | -------------------------------------------------------------------------------- /tests/Timings.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | typedef std::chrono::high_resolution_clock Clock; 9 | 10 | // Local includes 11 | #include "Tests.hpp" 12 | #include "SH.hpp" 13 | #include "SphericalHarmonics.hpp" 14 | #include "SphericalIntegration.hpp" 15 | #include "DirectionsSampling.hpp" 16 | 17 | // GLM include 18 | #include 19 | 20 | int main(int argc, char** argv) { 21 | 22 | auto A = glm::vec3( 0.0, 0.5, 0.5); 23 | auto B = glm::vec3(-0.5, 0.5, 0.0); 24 | auto C = glm::vec3( 0.5, 0.5, 0.0); 25 | auto tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 26 | 27 | std::cout <<"#\to\tn\tms\tms" << std::endl; 28 | 29 | int max_order = 18; 30 | int max_trials = 1000; 31 | for(int order=1; order(2*order+1); 39 | 40 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 41 | // and compute the product of the two: `Prod = A x Zw`. 42 | const auto ZW = ZonalWeights(basis); 43 | const auto Y = ZonalExpansion(basis); 44 | const auto A = computeInverse(Y); 45 | 46 | const auto Prod = (A*ZW).eval(); 47 | const auto Fact = (Prod.transpose() * clm).eval(); 48 | 49 | // Analytical evaluation of the integral of power of cosines for 50 | // the different basis elements up to the order defined by the 51 | // number of elements in the basis 52 | auto start = Clock::now(); 53 | float shI = 0.0; 54 | for(int trial=0; trial(tri, basis); 56 | shI += moments[0]; 57 | } 58 | shI /= max_trials; 59 | auto end = Clock::now(); 60 | double timing_no_mult = std::chrono::duration_cast(end - start).count() / (double)max_trials; 61 | 62 | start = Clock::now(); 63 | shI = 0.0; 64 | for(int trial=0; trial(tri, basis); 66 | //shI += Fact.dot(moments); 67 | shI += clm.dot(Prod * moments); 68 | } 69 | shI /= max_trials; 70 | end = Clock::now(); 71 | double timing_mult = std::chrono::duration_cast(end - start).count() / (double)max_trials; 72 | 73 | 74 | std::cout << "\t" << order << "\t" << (order+1)*(order+1) << "\t" << timing_no_mult << "\t" << timing_mult << std::endl; 75 | } 76 | 77 | return EXIT_SUCCESS; 78 | } 79 | -------------------------------------------------------------------------------- /tests/UnitIntegral.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Local includes 9 | //#define USE_SPARSE_EIGEN 10 | #include "Tests.hpp" 11 | #include "SH.hpp" 12 | #include "SphericalHarmonics.hpp" 13 | #include "SphericalIntegration.hpp" 14 | #include "DirectionsSampling.hpp" 15 | 16 | // GLM include 17 | #include 18 | 19 | 20 | bool CheckNormalization(const Eigen::VectorXf& clm, const std::vector& basis) { 21 | 22 | Triangle tri; 23 | 24 | float shI = 0.0f; 25 | 26 | // Upper hemisphere 27 | tri = Triangle(Vector(0,0,1), Vector(0,1,0), Vector(1,0,0)); 28 | shI += clm.dot(AxialMoments(tri, basis)); 29 | 30 | tri = Triangle(Vector(0,0,1), Vector(1,0,0), Vector(0,-1,0)); 31 | shI += clm.dot(AxialMoments(tri, basis)); 32 | 33 | tri = Triangle(Vector(0,0,1), Vector(0,-1,0), Vector(-1,0,0)); 34 | shI += clm.dot(AxialMoments(tri, basis)); 35 | 36 | tri = Triangle(Vector(0,0,1), Vector(-1,0,0), Vector(0,1,0)); 37 | shI += clm.dot(AxialMoments(tri, basis)); 38 | 39 | // Lower hemisphere 40 | tri = Triangle(Vector(0,0,-1), Vector(1,0,0), Vector(0,1,0)); 41 | shI += clm.dot(AxialMoments(tri, basis)); 42 | 43 | tri = Triangle(Vector(0,0,-1), Vector(0,1,0), Vector(-1,0,0)); 44 | shI += clm.dot(AxialMoments(tri, basis)); 45 | 46 | tri = Triangle(Vector(0,0,-1), Vector(-1,0,0), Vector(0,-1,0)); 47 | shI += clm.dot(AxialMoments(tri, basis)); 48 | 49 | tri = Triangle(Vector(0,0,-1), Vector(0,-1,0), Vector(1,0,0)); 50 | shI += clm.dot(AxialMoments(tri, basis)); 51 | 52 | bool check = closeTo(shI, 0.0f); 53 | if(!check) { std::cout << "Error, lost in precision: I=" << shI; } 54 | 55 | return check; 56 | } 57 | 58 | int main(int argc, char** argv) { 59 | 60 | int nb_fails = 0; 61 | int maxorder = 8; 62 | int maxsize = (maxorder+1)*(maxorder+1); 63 | // Precompute the set of ZH directions 64 | const auto basis = SamplingFibonacci(2*maxorder+1); 65 | //const auto basis = SamplingBlueNoise(2*maxorder+1); 66 | std::cout << "Done sampling enough directions" << std::endl; 67 | 68 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 69 | // and compute the product of the two: `Prod = A x Zw`. 70 | const auto ZW = ZonalWeights(basis); 71 | const auto Y = ZonalExpansion(basis); 72 | const auto A = computeInverse(Y); 73 | const auto Prod = A*ZW; 74 | std::cout << "Done with precomputing the matrix" << std::endl; 75 | 76 | for(int order=1; order<=maxorder; ++order) { 77 | 78 | // Loop for each SH coeff on this band 79 | int size = (order+1)*(order+1); 80 | for(int i=order*order; i(alta::plugins_manager::get_data(plugin)); 41 | _d->load(filename); 42 | } else { 43 | _f = alta::ptr(alta::plugins_manager::load_function(filename)); 44 | } 45 | 46 | // Exception when a ALTA material cannot be loaded. 47 | if(!_f && !_d) { 48 | throw 1; 49 | } 50 | } 51 | 52 | AltaBRDF(const alta::ptr& d, 53 | const alta::ptr& f) : _d(d), _f(f) {} 54 | 55 | template 56 | RGB eval(const Vector &wo, const Vector &wi) const { 57 | if (wi.z <= 0 || wo.z <= 0) { 58 | return RGB(0.0f, 0.0f, 0.0f); 59 | } 60 | 61 | double cart[6]; 62 | cart[0] = wi[0]; 63 | cart[1] = wi[1]; 64 | cart[2] = wi[2]; 65 | cart[3] = wo[0]; 66 | cart[4] = wo[1]; 67 | cart[5] = wo[2]; 68 | 69 | 70 | /* Return the value of the BRDF from the function object */ 71 | if(!_d) { 72 | vec x(_f->dimX()); 73 | alta::params::convert(&cart[0], alta::params::CARTESIAN, _f->input_parametrization(), &x[0]); 74 | vec y = _f->value(x); 75 | RGB res; 76 | if(_f->dimY() == 3) { 77 | res = RGB(std::max(y[0], 0.0), std::max(y[1], 0.0), std::max(y[2], 0.0)); 78 | } else { 79 | const auto ym = std::max(y[0], 0.0); 80 | res = RGB(ym, ym, ym); 81 | } 82 | return res; 83 | 84 | /* Treat the case of a BRDF from interpolated data */ 85 | } else { 86 | vec x(_d->dimX()); 87 | alta::params::convert(&cart[0], alta::params::CARTESIAN, _d->input_parametrization(), &x[0]); 88 | 89 | vec y = _d->value(x); 90 | RGB res; 91 | if(_d->dimY() == 3) { 92 | res = RGB(std::max(y[0], 0.0), std::max(y[1], 0.0), std::max(y[2], 0.0)); 93 | } else { 94 | const auto ym = std::max(y[0], 0.0); 95 | res = RGB(ym, ym, ym); 96 | } 97 | return res; 98 | } 99 | } 100 | 101 | template 102 | RGB value(const Vector &wo, const Vector &wi) const { 103 | #ifdef FORCE_BILATERAL_SYMMETRY 104 | return 0.5*(eval(wo, wi) + eval(wi, wo)); 105 | #else 106 | return eval(wo, wi); 107 | #endif 108 | } 109 | }; 110 | 111 | struct AltaProjectionThread : public std::thread { 112 | 113 | std::vector cijs; 114 | 115 | AltaProjectionThread(const AltaBRDF* brdf, 116 | const std::vector* ws, 117 | int _order, int _skip, int _nthread) : 118 | std::thread(&AltaProjectionThread::run, this, brdf, ws, _skip, _order, _nthread) {} 119 | 120 | void run(const AltaBRDF* brdf, 121 | const std::vector* dirs, 122 | int skip, int order, int nthread) { 123 | 124 | // Allocate memory 125 | cijs = std::vector(6, Eigen::MatrixXf::Zero(SH::Terms(order), SH::Terms(order))); 126 | 127 | const int size = SH::Terms(order); 128 | Eigen::VectorXf ylmo(size); 129 | Eigen::VectorXf ylmi(size); 130 | for(unsigned int i=skip; isize(); i+=nthread) { 131 | const Vector& wo = (*dirs)[i]; 132 | 133 | if(skip == 0) { 134 | std::cout << "Progress: " << i << " / " << dirs->size() << " \r"; 135 | std::cout.flush(); 136 | } 137 | 138 | // Skip below the horizon configuration 139 | if(wo.z < 0.0) continue; 140 | SH::FastBasis(wo, order, ylmo); 141 | 142 | for(unsigned int j=0; jsize(); ++j) { 143 | const Vector& wi = (*dirs)[j]; 144 | // Skip below the horizon configuration 145 | if(wi.z < 0.0) continue; 146 | 147 | // Evaluate the BRDF value 148 | const auto rgb = brdf->value(wi, wo); 149 | SH::FastBasis(wi, order, ylmi); 150 | 151 | Eigen::MatrixXf mat = ylmo * ylmi.transpose(); 152 | #ifndef SYMMETRIZE 153 | mat = 0.5f*(mat + mat.transpose()); 154 | #endif 155 | cijs[0] += rgb[0] * mat; 156 | cijs[1] += rgb[1] * mat; 157 | cijs[2] += rgb[2] * mat; 158 | // Note: Here the correct weighting should be with respect to wi.z 159 | // but I use wo.z since it allows to reduce the ringing drastically. 160 | #ifdef LOOKS_BETTER 161 | cijs[3] += rgb[0] * wo.z * mat; 162 | cijs[4] += rgb[1] * wo.z * mat; 163 | cijs[5] += rgb[2] * wo.z * mat; 164 | #else // CORRECT 165 | cijs[3] += rgb[0] * wi.z * mat; 166 | cijs[4] += rgb[1] * wi.z * mat; 167 | cijs[5] += rgb[2] * wi.z * mat; 168 | #endif 169 | } 170 | } 171 | } 172 | }; 173 | 174 | int AltaProjectionMatrix(const std::string& filename, 175 | const std::string& plugin, 176 | const std::string& type, 177 | int order = 15, int N = 100000) { 178 | 179 | // Constants 180 | const int size = SH::Terms(order); 181 | 182 | // Load the BRDF 183 | AltaBRDF brdf(filename, plugin, type); 184 | 185 | const auto k = filename.rfind('.'); 186 | std::string ofilename = filename; 187 | ofilename.replace(k, std::string::npos, ".mats"); 188 | std::cout << "Will output to \"" << ofilename << "\"" << std::endl; 189 | 190 | // Values 191 | std::vector cijs(6, Eigen::MatrixXf::Zero(size, size)); 192 | const auto dirs = SamplingFibonacci(N); 193 | 194 | const int nbthreads = std::thread::hardware_concurrency(); 195 | std::vector threads; 196 | for(int k=0; kjoin(); 203 | cijs[0] += th->cijs[0]; 204 | cijs[1] += th->cijs[1]; 205 | cijs[2] += th->cijs[2]; 206 | cijs[3] += th->cijs[3]; 207 | cijs[4] += th->cijs[4]; 208 | cijs[5] += th->cijs[5]; 209 | delete th; 210 | } 211 | const float factor = 16.0*M_PI*M_PI / float(N*N); 212 | cijs[0] *= factor; 213 | cijs[1] *= factor; 214 | cijs[2] *= factor; 215 | cijs[3] *= factor; 216 | cijs[4] *= factor; 217 | cijs[5] *= factor; 218 | 219 | SaveMatrices(ofilename, cijs); 220 | 221 | // Print values 222 | std::string gfilename = filename; 223 | gfilename.replace(k, std::string::npos, ".gnuplot"); 224 | std::ofstream file(gfilename.c_str(), std::ios_base::trunc); 225 | const float thetai = -0.5f*M_PI * 30.f/90.f; 226 | const Vector wi(sin(thetai), 0, cos(thetai)); 227 | const auto ylmi = SH::FastBasis(wi, order); 228 | Vector wo; 229 | for(int i=0; i<90; ++i) { 230 | const float theta = 0.5*M_PI * i / float(90); 231 | wo.x = sin(theta); 232 | wo.y = 0; 233 | wo.z = cos(theta); 234 | 235 | // Ref 236 | const auto RGB = brdf.value(wi, wo); 237 | const float R = RGB[0]; 238 | const float G = RGB[1]; 239 | const float B = RGB[2]; 240 | 241 | // SH expansion 242 | const auto ylmo = SH::FastBasis(wo, order); 243 | const Eigen::VectorXf rlm = cijs[0] * ylmo; 244 | const Eigen::VectorXf glm = cijs[1] * ylmo; 245 | const Eigen::VectorXf blm = cijs[2] * ylmo; 246 | const float r = ylmi.dot(rlm); 247 | const float g = ylmi.dot(glm); 248 | const float b = ylmi.dot(blm); 249 | 250 | file << theta << "\t" << r << "\t" << g << "\t" << b 251 | << "\t" << R << "\t" << G << "\t" << B 252 | << std::endl; 253 | } 254 | 255 | int nb_fails = 0; 256 | return nb_fails; 257 | } 258 | 259 | bool parseArguments(int argc, char** argv, std::string& filename, 260 | std::string& plugin, std::string& type, int& order, int& nb) { 261 | 262 | // Loop over all the different elements of the command line and search for 263 | // some patterns 264 | for(int k=0; k 1) { 287 | filename = argv[argc-1]; 288 | return true; 289 | } else { 290 | return false; 291 | } 292 | } 293 | 294 | int main(int argc, char** argv) { 295 | 296 | int nb_fails = 0; 297 | std::string filename; 298 | std::string plugin, type; 299 | int order = 3; 300 | int nb = 10000; 301 | if(! parseArguments(argc, argv, filename, plugin, type, order, nb)) { 302 | return EXIT_SUCCESS; 303 | } 304 | 305 | // Load an example 306 | nb_fails += AltaProjectionMatrix(filename, plugin, type, order, nb); 307 | 308 | if(nb_fails > 0) { 309 | return EXIT_FAILURE; 310 | } else { 311 | return EXIT_SUCCESS; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /utils/IntegralConvergence.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | 5 | // System includes 6 | #include 7 | 8 | // Local includes 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // Wrappers and utils from tests 15 | #include 16 | 17 | #define NB_TRIALS 1000 18 | 19 | void ConvergenceSH(const Eigen::VectorXf& clm, 20 | const Triangle& triangle, 21 | unsigned int oMin, 22 | unsigned int oMax) { 23 | 24 | // Get the order of the provided SH vector and compute the directional 25 | // sampling of the sphere for rotated ZH/cosines. 26 | const auto basis = SamplingFibonacci(2*oMax+1); 27 | 28 | std::vector Prods; 29 | for(unsigned int o=oMin; o(&basis[0], &basis[2*o+1]); 32 | 33 | // Get the Zonal weights matrix and the Zlm -> Ylm conversion matrix 34 | // and compute the product of the two: `Prod = A x Zw`. 35 | const auto ZW = ZonalWeights(cbasis); 36 | const auto Y = ZonalExpansion(cbasis); 37 | const auto A = computeInverse(Y); 38 | const auto Prod = A*ZW; 39 | Prods.push_back(Prod); 40 | } 41 | 42 | 43 | for(unsigned int o=oMin; o(&basis[0], &basis[2*o+1]); 55 | const auto ylm = clm.segment(0, SH::Terms(o)); 56 | 57 | // Analytical evaluation of the integral of power of cosines for 58 | // the different basis elements up to the order defined by the 59 | // number of elements in the basis 60 | const auto moments = AxialMoments(triangle, cbasis); 61 | 62 | // Compute the integral using a restricted matrix 63 | I = ylm.dot(Prods[o-oMin] * moments); 64 | } 65 | 66 | gettimeofday(&time, NULL); 67 | const auto end = (time.tv_sec * 1000.0) + (time.tv_usec / 1000.0); 68 | 69 | auto avg_time = (end-start)/NB_TRIALS; 70 | 71 | std::cout << o << "\t" << avg_time << "\t" << I << std::endl; 72 | } 73 | } 74 | 75 | 76 | int main(int argc, char** argv) { 77 | 78 | const int oMin = 1; 79 | const int oMax = 18; 80 | Eigen::VectorXf clm = Eigen::VectorXf(SH::Terms(oMax)); 81 | clm[0] = 1.0; 82 | clm[9] = 1.0; 83 | 84 | glm::vec3 A, B, C; 85 | Triangle tri; 86 | A = glm::vec3(0.0, 0.0, 1.0); 87 | B = glm::vec3(0.0, 0.5, 1.0); 88 | C = glm::vec3(0.5, 0.0, 1.0); 89 | tri = Triangle(glm::normalize(A), glm::normalize(B), glm::normalize(C)); 90 | 91 | ConvergenceSH(clm, tri, oMin, oMax); 92 | 93 | return EXIT_SUCCESS; 94 | } 95 | -------------------------------------------------------------------------------- /utils/Merl.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2005 Mitsubishi Electric Research Laboratories All Rights 2 | // Reserved. 3 | 4 | // Permission to use, copy and modify this software and its documentation 5 | // without fee for educational, research and non-profit purposes, is hereby 6 | // granted, provided that the above copyright notice and the following three 7 | // paragraphs appear in all copies. 8 | 9 | // To request permission to incorporate this software into commercial products 10 | // contact: Vice President of Marketing and Business Development; Mitsubishi 11 | // Electric Research Laboratories (MERL), 201 Broadway, Cambridge, MA 02139 or 12 | // . 13 | 14 | // IN NO EVENT SHALL MERL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, 15 | // INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF 16 | // THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF MERL HAS BEEN 17 | // ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 18 | 19 | // MERL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | // PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND MERL 22 | // HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS OR 23 | // MODIFICATIONS. 24 | 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | // Include Eigen 31 | #include 32 | 33 | #define BRDF_SAMPLING_RES_THETA_H 90 34 | #define BRDF_SAMPLING_RES_THETA_D 90 35 | #define BRDF_SAMPLING_RES_PHI_D 360 36 | 37 | #define RED_SCALE (1.0/1500.0) 38 | #define GREEN_SCALE (1.15/1500.0) 39 | #define BLUE_SCALE (1.66/1500.0) 40 | 41 | struct MerlBRDF { 42 | 43 | /* Storage of the BRDF data */ 44 | double* brdf; 45 | 46 | /* Constructor and destructor */ 47 | MerlBRDF() : brdf(nullptr) { 48 | } 49 | 50 | ~MerlBRDF() { 51 | if(brdf != nullptr) { 52 | delete[] brdf; 53 | } 54 | } 55 | 56 | /* Evaluate the BRDF for a given couple of vectors */ 57 | template 58 | RGB const value(const Vector& wi, const Vector& wo) const { 59 | double ti = acos(wi.z); 60 | double pi = atan2(wi.y, wi.x); 61 | double to = acos(wo.z); 62 | double po = atan2(wo.y, wo.x); 63 | 64 | double r, g, b; 65 | lookup_brdf_val(ti, pi, to, po, r, g, b); 66 | return RGB(r, g, b); 67 | } 68 | 69 | /* Read BRDF data */ 70 | bool read_brdf(const std::string& filename) 71 | { 72 | FILE *f = fopen(filename.c_str(), "rb"); 73 | if (!f) { 74 | return false; 75 | } 76 | 77 | int dims[3]; 78 | fread(dims, sizeof(int), 3, f); 79 | int n = dims[0] * dims[1] * dims[2]; 80 | if (n != BRDF_SAMPLING_RES_THETA_H * 81 | BRDF_SAMPLING_RES_THETA_D * 82 | BRDF_SAMPLING_RES_PHI_D / 2) 83 | { 84 | fprintf(stderr, "Dimensions don't match\n"); 85 | fclose(f); 86 | return false; 87 | } 88 | 89 | brdf = (double*) malloc (sizeof(double)*3*n); 90 | fread(brdf, sizeof(double), 3*n, f); 91 | 92 | fclose(f); 93 | return true; 94 | } 95 | 96 | /* Project this BRDF to SH 97 | * 98 | * Return a std::vector containing an Eigen::MatrixXf for each input 99 | * elevation. This matrix size is SH::Terms(order) x RGB. 100 | */ 101 | template 102 | std::vector projectToSH(int nbElev, int order, int M=10000) const { 103 | 104 | // Random sampler 105 | std::mt19937 gen(0); 106 | std::uniform_real_distribution dist(0.0,1.0); 107 | 108 | // Return vector 109 | std::vector res; 110 | res.reserve(nbElev); 111 | 112 | // Sample each incident direction 113 | for(int e=0; e(wi, wo); 134 | const auto ylm = SH::FastBasis(wo, order); 135 | shCoeffs.col(0) += rgb[0]*ylm; 136 | shCoeffs.col(1) += rgb[1]*ylm; 137 | shCoeffs.col(2) += rgb[2]*ylm; 138 | } 139 | shCoeffs *= 4.0*M_PI / float(M); 140 | res.push_back(shCoeffs); 141 | } 142 | 143 | return res; 144 | } 145 | 146 | // cross product of two vectors 147 | void cross_product (double* v1, double* v2, double* out) const 148 | { 149 | out[0] = v1[1]*v2[2] - v1[2]*v2[1]; 150 | out[1] = v1[2]*v2[0] - v1[0]*v2[2]; 151 | out[2] = v1[0]*v2[1] - v1[1]*v2[0]; 152 | } 153 | 154 | // normalize vector 155 | void normalize(double* v) const 156 | { 157 | // normalize 158 | double len = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]); 159 | v[0] = v[0] / len; 160 | v[1] = v[1] / len; 161 | v[2] = v[2] / len; 162 | } 163 | 164 | // rotate vector along one axis 165 | void rotate_vector(double* vector, double* axis, double angle, double* out) const 166 | { 167 | double temp; 168 | double cross[3]; 169 | double cos_ang = cos(angle); 170 | double sin_ang = sin(angle); 171 | 172 | out[0] = vector[0] * cos_ang; 173 | out[1] = vector[1] * cos_ang; 174 | out[2] = vector[2] * cos_ang; 175 | 176 | temp = axis[0]*vector[0]+axis[1]*vector[1]+axis[2]*vector[2]; 177 | temp = temp*(1.0-cos_ang); 178 | 179 | out[0] += axis[0] * temp; 180 | out[1] += axis[1] * temp; 181 | out[2] += axis[2] * temp; 182 | 183 | cross_product (axis,vector,cross); 184 | 185 | out[0] += cross[0] * sin_ang; 186 | out[1] += cross[1] * sin_ang; 187 | out[2] += cross[2] * sin_ang; 188 | } 189 | 190 | 191 | // convert standard coordinates to half vector/difference vector coordinates 192 | void std_coords_to_half_diff_coords(double theta_in, double fi_in, 193 | double theta_out, double fi_out, 194 | double& theta_half,double& fi_half, 195 | double& theta_diff,double& fi_diff ) const 196 | { 197 | 198 | // compute in vector 199 | double in_vec_z = cos(theta_in); 200 | double proj_in_vec = sin(theta_in); 201 | double in_vec_x = proj_in_vec*cos(fi_in); 202 | double in_vec_y = proj_in_vec*sin(fi_in); 203 | double in[3]= {in_vec_x,in_vec_y,in_vec_z}; 204 | normalize(in); 205 | 206 | 207 | // compute out vector 208 | double out_vec_z = cos(theta_out); 209 | double proj_out_vec = sin(theta_out); 210 | double out_vec_x = proj_out_vec*cos(fi_out); 211 | double out_vec_y = proj_out_vec*sin(fi_out); 212 | double out[3]= {out_vec_x,out_vec_y,out_vec_z}; 213 | normalize(out); 214 | 215 | 216 | // compute halfway vector 217 | double half_x = (in_vec_x + out_vec_x)/2.0f; 218 | double half_y = (in_vec_y + out_vec_y)/2.0f; 219 | double half_z = (in_vec_z + out_vec_z)/2.0f; 220 | double half[3] = {half_x,half_y,half_z}; 221 | normalize(half); 222 | 223 | // compute theta_half, fi_half 224 | theta_half = acos(half[2]); 225 | fi_half = atan2(half[1], half[0]); 226 | 227 | 228 | double bi_normal[3] = {0.0, 1.0, 0.0}; 229 | double normal[3] = { 0.0, 0.0, 1.0 }; 230 | double temp[3]; 231 | double diff[3]; 232 | 233 | // compute diff vector 234 | rotate_vector(in, normal , -fi_half, temp); 235 | rotate_vector(temp, bi_normal, -theta_half, diff); 236 | 237 | // compute theta_diff, fi_diff 238 | theta_diff = acos(diff[2]); 239 | fi_diff = atan2(diff[1], diff[0]); 240 | 241 | } 242 | 243 | 244 | // Lookup theta_half index 245 | // This is a non-linear mapping! 246 | // In: [0 .. pi/2] 247 | // Out: [0 .. 89] 248 | inline int theta_half_index(double theta_half) const 249 | { 250 | if (theta_half <= 0.0) 251 | return 0; 252 | double theta_half_deg = ((theta_half / (M_PI/2.0))*BRDF_SAMPLING_RES_THETA_H); 253 | double temp = theta_half_deg*BRDF_SAMPLING_RES_THETA_H; 254 | temp = sqrt(temp); 255 | int ret_val = (int)temp; 256 | if (ret_val < 0) ret_val = 0; 257 | if (ret_val >= BRDF_SAMPLING_RES_THETA_H) 258 | ret_val = BRDF_SAMPLING_RES_THETA_H-1; 259 | return ret_val; 260 | } 261 | 262 | 263 | // Lookup theta_diff index 264 | // In: [0 .. pi/2] 265 | // Out: [0 .. 89] 266 | inline int theta_diff_index(double theta_diff) const 267 | { 268 | int tmp = int(theta_diff / (M_PI * 0.5) * BRDF_SAMPLING_RES_THETA_D); 269 | if (tmp < 0) 270 | return 0; 271 | else if (tmp < BRDF_SAMPLING_RES_THETA_D - 1) 272 | return tmp; 273 | else 274 | return BRDF_SAMPLING_RES_THETA_D - 1; 275 | } 276 | 277 | 278 | // Lookup phi_diff index 279 | inline int phi_diff_index(double phi_diff) const 280 | { 281 | // Because of reciprocity, the BRDF is unchanged under 282 | // phi_diff -> phi_diff + M_PI 283 | if (phi_diff < 0.0) 284 | phi_diff += M_PI; 285 | 286 | // In: phi_diff in [0 .. pi] 287 | // Out: tmp in [0 .. 179] 288 | int tmp = int(phi_diff / M_PI * BRDF_SAMPLING_RES_PHI_D / 2); 289 | if (tmp < 0) 290 | return 0; 291 | else if (tmp < BRDF_SAMPLING_RES_PHI_D / 2 - 1) 292 | return tmp; 293 | else 294 | return BRDF_SAMPLING_RES_PHI_D / 2 - 1; 295 | } 296 | 297 | // Given a pair of incoming/outgoing angles, look up the BRDF. 298 | void lookup_brdf_val(double theta_in, double fi_in, 299 | double theta_out, double fi_out, 300 | double& red_val, double& green_val, 301 | double& blue_val) const 302 | { 303 | // Convert to halfangle / difference angle coordinates 304 | double theta_half, fi_half, theta_diff, fi_diff; 305 | 306 | std_coords_to_half_diff_coords(theta_in, fi_in, theta_out, fi_out, 307 | theta_half, fi_half, theta_diff, fi_diff); 308 | 309 | 310 | // Find index. 311 | // Note that phi_half is ignored, since isotropic BRDFs are assumed 312 | int ind = phi_diff_index(fi_diff) + 313 | theta_diff_index(theta_diff) * BRDF_SAMPLING_RES_PHI_D / 2 + 314 | theta_half_index(theta_half) * BRDF_SAMPLING_RES_PHI_D / 2 * 315 | BRDF_SAMPLING_RES_THETA_D; 316 | 317 | red_val = brdf[ind] * RED_SCALE; 318 | green_val = brdf[ind + BRDF_SAMPLING_RES_THETA_H*BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D/2] * GREEN_SCALE; 319 | blue_val = brdf[ind + BRDF_SAMPLING_RES_THETA_H*BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D] * BLUE_SCALE; 320 | 321 | 322 | if (red_val < 0.0 || green_val < 0.0 || blue_val < 0.0) 323 | fprintf(stderr, "Below horizon.\n"); 324 | 325 | } 326 | 327 | 328 | 329 | //#include "EXR_IO.h" 330 | // 331 | //int main(int argc, char *argv[]) 332 | //{ 333 | // if(argc != 3) { 334 | // fprintf(stderr, "Uncorrect number of arguments to the command line"); 335 | // fprintf(stderr, "Usage: brdf_read [input].binary [output].exr"); 336 | // return 1; 337 | // } 338 | // 339 | // 340 | // const char *filename = argv[1]; 341 | // double* brdf; 342 | // 343 | // // read brdf 344 | // if (!read_brdf(filename, brdf)) 345 | // { 346 | // fprintf(stderr, "Error reading %s\n", filename); 347 | // exit(1); 348 | // } 349 | // 350 | // const auto phi = 90; 351 | // 352 | // const auto SKIP_PHI = BRDF_SAMPLING_RES_PHI_D / 2; 353 | // 354 | // const auto SKIP = SKIP_PHI 355 | // * BRDF_SAMPLING_RES_THETA_D 356 | // * BRDF_SAMPLING_RES_THETA_H; 357 | // 358 | // const auto RES = BRDF_SAMPLING_RES_THETA_H 359 | // * BRDF_SAMPLING_RES_THETA_D; 360 | // 361 | // 362 | // double* img = new double[RES*3]; 363 | // 364 | // for(auto i=0; i::SaveEXR(argv[2], BRDF_SAMPLING_RES_THETA_H, BRDF_SAMPLING_RES_THETA_D, img); 383 | // 384 | // return 0; 385 | //} 386 | 387 | }; 388 | -------------------------------------------------------------------------------- /utils/Merl2Sh.cpp: -------------------------------------------------------------------------------- 1 | // STL includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Local includes 11 | #include "Merl.hpp" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // GLM include 20 | #include 21 | 22 | // Include Eigen 23 | #include 24 | 25 | struct MerlProjectionThread : public std::thread { 26 | 27 | int order; 28 | std::vector cijs; 29 | 30 | MerlProjectionThread(const MerlBRDF* brdf, 31 | const std::vector* ws, 32 | int order, int skip, int nthread) : 33 | std::thread(&MerlProjectionThread::run, this, brdf, ws, skip, order, nthread) {} 34 | 35 | /* Pre-convolved the Zonal basis to perform smooth evaluation. 36 | */ 37 | static void ApplyZonalFilter(Eigen::VectorXf& clm) { 38 | const auto bmax = floor(sqrt(clm.size())-1); 39 | for(unsigned int i=0; i* dirs, 48 | int skip, int order, int nthread) { 49 | 50 | cijs = std::vector(6, Eigen::MatrixXf::Zero(SH::Terms(order), SH::Terms(order))); 51 | 52 | const int size = SH::Terms(order); 53 | Eigen::VectorXf ylmo(size); 54 | Eigen::VectorXf ylmi(size); 55 | for(unsigned int i=skip; isize(); i+=nthread) { 56 | const Vector& wo = (*dirs)[i]; 57 | 58 | if(skip == 0) { 59 | std::cout << "Progress: " << i << " / " << dirs->size() << " \r"; 60 | std::cout.flush(); 61 | } 62 | 63 | // Skip below the horizon configuration 64 | if(wo.z < 0.0) continue; 65 | SH::FastBasis(wo, order, ylmo); 66 | 67 | for(unsigned int j=0; jsize(); ++j) { 68 | const Vector& wi = (*dirs)[j]; 69 | // Skip below the horizon configuration 70 | if(wi.z < 0.0) continue; 71 | 72 | // Evaluate the BRDF value 73 | const auto rgb = brdf->value(wi, wo); 74 | SH::FastBasis(wi, order, ylmi); 75 | 76 | // Apply filtering 77 | //ApplyZonalFilter(ylmo); 78 | //ApplyZonalFilter(ylmi); 79 | 80 | Eigen::MatrixXf mat = ylmo * ylmi.transpose(); 81 | #ifndef SYMMETRIZE 82 | mat = 0.5f*(mat + mat.transpose()); 83 | #endif 84 | cijs[0] += rgb[0] * mat; 85 | cijs[1] += rgb[1] * mat; 86 | cijs[2] += rgb[2] * mat; 87 | // Note: Here the correct weighting should be with respect to wi.z 88 | // but I use wo.z since it allows to reduce the ringing drastically. 89 | #ifdef LOOKS_BETTER 90 | cijs[3] += rgb[0] * wo.z * mat; 91 | cijs[4] += rgb[1] * wo.z * mat; 92 | cijs[5] += rgb[2] * wo.z * mat; 93 | #else // CORRECT 94 | cijs[3] += rgb[0] * wi.z * mat; 95 | cijs[4] += rgb[1] * wi.z * mat; 96 | cijs[5] += rgb[2] * wi.z * mat; 97 | #endif 98 | } 99 | } 100 | } 101 | }; 102 | 103 | int MerlProjectionMatrix(const std::string& filename, 104 | int order = 15, int N = 1000) { 105 | 106 | // Constants 107 | const int size = SH::Terms(order); 108 | 109 | // Load the BRDF 110 | MerlBRDF brdf; 111 | if(! brdf.read_brdf(filename)) { 112 | std::cerr << "Failed: unable to load the MERL brdf" << std::endl; 113 | return 1; 114 | } 115 | 116 | const auto k = filename.rfind('.'); 117 | std::string ofilename = filename; 118 | ofilename.replace(k, std::string::npos, ".mats"); 119 | std::cout << "Will output to \"" << ofilename << "\"" << std::endl; 120 | 121 | // Values 122 | std::vector cijs(6, Eigen::MatrixXf::Zero(size, size)); 123 | const auto dirs = SamplingFibonacci(N); 124 | 125 | const int nbthreads = std::thread::hardware_concurrency(); 126 | std::vector threads; 127 | for(int k=0; kjoin(); 134 | cijs[0] += th->cijs[0]; 135 | cijs[1] += th->cijs[1]; 136 | cijs[2] += th->cijs[2]; 137 | cijs[3] += th->cijs[3]; 138 | cijs[4] += th->cijs[4]; 139 | cijs[5] += th->cijs[5]; 140 | delete th; 141 | } 142 | const float factor = 16.0*M_PI*M_PI / float(N*N); 143 | cijs[0] *= factor; 144 | cijs[1] *= factor; 145 | cijs[2] *= factor; 146 | cijs[3] *= factor; 147 | cijs[4] *= factor; 148 | cijs[5] *= factor; 149 | 150 | SaveMatrices(ofilename, cijs); 151 | 152 | // Print values 153 | std::string gfilename = filename; 154 | gfilename.replace(k, std::string::npos, ".gnuplot"); 155 | std::ofstream file(gfilename.c_str(), std::ios_base::trunc); 156 | const float thetai = -0.5f*M_PI * 30.f/90.f; 157 | const Vector wi(sin(thetai), 0, cos(thetai)); 158 | const auto ylmi = SH::FastBasis(wi, order); 159 | Vector wo; 160 | for(int i=0; i<90; ++i) { 161 | const float theta = 0.5*M_PI * i / float(90); 162 | wo.x = sin(theta); 163 | wo.y = 0; 164 | wo.z = cos(theta); 165 | 166 | // Ref 167 | const auto RGB = brdf.value(wi, wo); 168 | const float R = RGB[0]; 169 | const float G = RGB[1]; 170 | const float B = RGB[2]; 171 | 172 | // SH expansion 173 | const auto ylmo = SH::FastBasis(wo, order); 174 | const Eigen::VectorXf rlm = cijs[0] * ylmo; 175 | const Eigen::VectorXf glm = cijs[1] * ylmo; 176 | const Eigen::VectorXf blm = cijs[2] * ylmo; 177 | const float r = ylmi.dot(rlm); 178 | const float g = ylmi.dot(glm); 179 | const float b = ylmi.dot(blm); 180 | 181 | file << theta << "\t" << r << "\t" << g << "\t" << b 182 | << "\t" << R << "\t" << G << "\t" << B 183 | << std::endl; 184 | } 185 | 186 | int nb_fails = 0; 187 | return nb_fails; 188 | } 189 | 190 | bool parseArguments(int argc, char** argv, std::string& filename, 191 | int& order, int& nb) { 192 | 193 | // Loop over all the different elements of the command line and search for 194 | // some patterns 195 | for(int k=0; k 1) { 210 | filename = argv[argc-1]; 211 | return true; 212 | } else { 213 | return false; 214 | } 215 | } 216 | 217 | int main(int argc, char** argv) { 218 | 219 | int nb_fails = 0; 220 | std::string filename; 221 | int order = 3; 222 | int nb = 1000; 223 | if(! parseArguments(argc, argv, filename, order, nb)) { 224 | return EXIT_SUCCESS; 225 | } 226 | 227 | // Load an example 228 | nb_fails += MerlProjectionMatrix(argv[argc-1], order, nb); 229 | 230 | if(nb_fails > 0) { 231 | return EXIT_FAILURE; 232 | } else { 233 | return EXIT_SUCCESS; 234 | } 235 | } 236 | --------------------------------------------------------------------------------