Repository: google/spherical-harmonics
Branch: master
Commit: ccb6c7fec875
Files: 18
Total size: 112.4 KB
Directory structure:
gitextract_mqbhqjx2/
├── .gitignore
├── AUTHORS
├── CONTRIBUTING
├── CONTRIBUTORS
├── LICENSE
├── LICENSE_HEADER
├── README.md
├── WORKSPACE
├── sh/
│ ├── BUILD
│ ├── default_image.cc
│ ├── default_image.h
│ ├── image.h
│ ├── spherical_harmonics.cc
│ ├── spherical_harmonics.h
│ └── spherical_harmonics_test.cc
└── third_party/
├── BUILD
├── eigen3.BUILD
└── gtest.BUILD
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
#ignore editor backups
*~
#ignore bazel build directories
bazel-*
================================================
FILE: AUTHORS
================================================
# This is the official list of [Project Name] authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as:
# Name or Organization <email address>
# The email address is not required for organizations.
Google Inc.
================================================
FILE: CONTRIBUTING
================================================
Want to contribute? Great! First, read this page (including the small print at the end).
### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.
### Code reviews
All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
================================================
FILE: CONTRIBUTORS
================================================
# People who have agreed to one of the CLAs and can contribute patches.
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, Google employees are listed here
# but not in AUTHORS, because Google holds the copyright.
#
# https://developers.google.com/open-source/cla/individual
# https://developers.google.com/open-source/cla/corporate
#
# Names should be added to this file as:
# Name <email address>
Michael Ludwig <michaelludwig@google.com>
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: LICENSE_HEADER
================================================
Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
3D Graphics-oriented Spherical Harmonics Library
================================================
Spherical harmonics can be a tricky thing to wrap your head around.
Even once the basic theories are understood, there's some surprisingly
finicky implementation work to get the functions coded properly.
This is especially true when it comes to rotations of spherical harmonics
(much of the literature is math-dense and contains errata).
Additionally, different literature sources use slightly different
conventions when defining the basis functions.
This library is a collection of useful functions for working with
spherical harmonics. It is not restricted to a maximum order of basis
function, using recursive definitions for both the SH basis functions
and SH rotation matrices. This library uses the convention of
including the Condon-Shortely phase function ((-1)^m) in the definition of
the basis function.
This is not an official Google project.
**Dependencies**
This library depends on [Eigen3](http://eigen.tuxfamily.org) to for its
underlying linear algebra primitives. Colors are represented as
`Eigen::Array3f`, where the components are ordered red, green, and blue.
[Google Test](https://code.google.com/p/googletest) is used for unit
testing.
The [Bazel](http://bazel.io) build tool is used to build the library.
This is responsible for downloading and configuring Eigen3 and the
testing framework. You may build the library by executing in the root directory:
bazel build //sh:spherical_harmonics
**General Functions**
See documentation in `sh/spherical_harmonics.h` for details on specific
functions. `sh/image.h` provides a very generic and simple image interface
that can be used to adapt this library with any actual imaging toolkit
already in use.
*Core SH Functions*
`EvalSH` - Evaluate the SH basis function of the given degree and order
at the provided position on a unit sphere. The position is described as
either a unit vector or as spherical coordinates.
`EvalSHSum` - Evaluate the approximation of a spherical function that
has already been converted to a vector of basis function coefficients.
*Projection Functions*
Used to estimate coefficients applied to basis functions to approximate
complex spherical functions as a weighted sum of the spherical harmonic
basis functions. Once projected, the returned coefficients can be
passed into `EvalSHSum`.
`ProjectFunction` - Project an analytic spherical function into every
basis function up to the specified order. This uses Monte Carlo
integration to estimate the coefficient for each basis function.
`ProjectEnvironment` - Project an environment map image arranged in
a latitude-longitude projection into the basis functions up to the
specified order. This is a specialization of `ProjectFunction` that
is more efficient when the spherical function is described as an
image containing an environment.
`ProjectSparseSamples` - Project a spherical function that has only
been sparsely evaluated (i.e. 10-50 times). Unlike the analytic
function, this uses a least-squares fitting to best estimate the
coefficients for each basis function. This works well when fitting
to photographic data where there can only be so many photos captured.
*Diffuse Irradiance Functions*
Diffuse irradiance can be efficiently represented in low-order
spherical harmonics. It can be computed quickly by estimating
the standard diffuse cosine-lobe as a vector of coefficients,
and the environment as spherical harmonics. Diffuse irradiance
is simply the dot product of the two coefficient vectors.
`RenderDiffuseIrradiance` - Compute diffuse irradiance for a given
unit normal vector and SH coefficients that describe the environment
illumination (i.e. from `ProjectEnvironment`).
`RenderDiffuseIrradianceMap` - Compute diffuse irradiance for every
normal vector described by the texels of the provided latitude-longitude
image. This can be useful for computing a texture map of diffuse
irradiance and then transferring it to the GPU for shader-based rendering.
*Spherical Harmonic Rotations*
If a complex spherical function is rotated, and a set of spherical
harmonic coefficients is needed for this new function, it's possible
to rotate the spherical harmonic coefficients of the original approximation
rather than re-projecting the rotated function. This is often much more
efficient and is used in `RenderDiffuseIrradiance` to transform the cosine
lobe function for the unit z-axis to any other normal vector.
`Rotation` - Object type that computes the transformation matrices that
suitably transform spherical harmonic coefficients given a quaternion
rotation.
*Utility Functions*
`GetCoefficientCount` - Return the total number of coefficients needed to
represent all basis functions up to a given order.
`GetIndex` - Return a 1-dimensional index (suitable for accessing the returned
vectors from all the project functions) given a degree and order.
`ToVector` - Transform spherical coordinates into a unit vector.
`ToSphericalCoords` - Transform a unit vector into spherical coordinates.
`ImageXToPhi` - Transform a pixel's x coordinate in an image of a specific width
to the phi spherical coordinate.
`ImageYToTheta` - Transform a pixel's y coordinate in an image of a specific height
to the theta spherical coordinate.
`ToImageCoords` - Transform spherical coordinates into floating-point image coordinates
given particular image dimensions. The coordinates can be used to bilinearly
interpolate an environment map, or cast to integers to access direct pixels.
**Literature**
The general spherical harmonic functions and fitting methods are from [1], the
environment map related functions are based on methods in [2] and [3], and
spherical harmonic rotations are from [4] and [5]:
1. R. Green, "Spherical Harmonic Lighting: The Gritty Details", GDC 2003,
http://www.research.scea.com/gdc2003/spherical-harmonic-lighting.pdf
2. R. Ramamoorthi and P. Hanrahan, "An Efficient Representation for
Irradiance Environment Maps",. , P., SIGGRAPH 2001, 497-500
3. R. Ramamoorthi and P. Hanrahan, “On the Relationship between Radiance and
Irradiance: Determining the Illumination from Images of a Convex
Lambertian Object,” J. Optical Soc. Am. A, vol. 18, no. 10, pp. 2448-2459,
2001.
4. J. Ivanic and K. Ruedenberg, "Rotation Matrices for Real Spherical
Harmonics. Direct Determination by Recursion", J. Phys. Chem., vol. 100,
no. 15, pp. 6342-6347, 1996. http://pubs.acs.org/doi/pdf/10.1021/jp953350u
5. Corrections to [4]: http://pubs.acs.org/doi/pdf/10.1021/jp9833350
================================================
FILE: WORKSPACE
================================================
workspace(name = "spherical_harmonics")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "eigen3",
url = "https://gitlab.com/libeigen/eigen/-/archive/3.3.5/eigen-3.3.5.zip",
strip_prefix = "eigen-3.3.5",
sha256 = "0e7aeece6c8874146c2a4addc437eebdf1ec4026680270f00e76705c8186f0b5",
build_file = "@//third_party:eigen3.BUILD",
)
http_archive(
name = "gtest",
url = "https://github.com/google/googletest/archive/release-1.8.0.zip",
strip_prefix = "googletest-release-1.8.0",
sha256 = "f3ed3b58511efd272eb074a3a6d6fb79d7c2e6a0e374323d1e6bcbcc1ef141bf",
build_file = "@//third_party:gtest.BUILD",
)
================================================
FILE: sh/BUILD
================================================
config_setting(
name = "windows",
constraint_values = [
"@bazel_tools//platforms:windows",
],
)
cc_library(
name = "image",
hdrs = ["image.h"],
deps = [
"@eigen3//:eigen3",
],
visibility = ["//visibility:public"],
alwayslink = 1
)
cc_library(
name = "spherical_harmonics",
srcs = ["spherical_harmonics.cc"],
hdrs = ["spherical_harmonics.h"],
deps = [
":image",
"@eigen3//:eigen3",
],
defines = select({
":windows": ["_USE_MATH_DEFINES",],
"//conditions:default": [],
}),
visibility = ["//visibility:public"],
)
cc_library(
name = "default_image",
srcs = ["default_image.cc"],
hdrs = ["default_image.h"],
deps = [
":image",
]
)
cc_test(
name = "spherical_harmonics_test",
size = "small",
srcs = [
"spherical_harmonics_test.cc",
],
deps = [
":default_image",
":spherical_harmonics",
"@gtest//:main",
],
linkopts = ["-lm"],
)
================================================
FILE: sh/default_image.cc
================================================
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "sh/default_image.h"
namespace sh {
DefaultImage::DefaultImage(int width, int height) : width_(width), height_(height) {
pixels_.reset(new Eigen::Array3f[width * height]);
}
int DefaultImage::width() const { return width_; }
int DefaultImage::height() const { return height_; }
Eigen::Array3f DefaultImage::GetPixel(int x, int y) const {
int index = x + y * width_;
return pixels_[index];
}
void DefaultImage::SetPixel(int x, int y, const Eigen::Array3f& v) {
int index = x + y * width_;
pixels_[index] = v;
}
} // namespace sh
================================================
FILE: sh/default_image.h
================================================
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Simple in-memory image implementation.
#ifndef SH_DEFAULT_IMAGE_H
#define SH_DEFAULT_IMAGE_H
#include "sh/image.h"
namespace sh {
class DefaultImage : public Image {
public:
DefaultImage(int width, int height);
int width() const override;
int height() const override;
Eigen::Array3f GetPixel(int x, int y) const override;
void SetPixel(int x, int y, const Eigen::Array3f& v) override;
private:
const int width_;
const int height_;
std::unique_ptr<Eigen::Array3f[]> pixels_;
};
} // namespace sh
#endif // SH_DEFAULT_IMAGE_H
================================================
FILE: sh/image.h
================================================
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A very simple image interface that reports image dimensions and allows
// for setting and getting individual pixels. Implement the interface by
// wrapping the image library of your choice.
#ifndef SH_IMAGE_H
#define SH_IMAGE_H
#include <memory>
#include "Eigen/Dense"
namespace sh {
class Image {
public:
Image() {}
virtual ~Image() {}
virtual int width() const = 0;
virtual int height() const = 0;
virtual Eigen::Array3f GetPixel(int x, int y) const = 0;
virtual void SetPixel(int x, int y, const Eigen::Array3f& v) = 0;
};
} // namespace sh
#endif // IMAGE_H
================================================
FILE: sh/spherical_harmonics.cc
================================================
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "sh/spherical_harmonics.h"
#include <iostream>
#include <limits>
#include <random>
namespace sh {
namespace {
// Number of precomputed factorials and double-factorials that can be
// returned in constant time.
const int kCacheSize = 16;
const int kHardCodedOrderLimit = 4;
const int kIrradianceOrder = 2;
const int kIrradianceCoeffCount = GetCoefficientCount(kIrradianceOrder);
// For efficiency, the cosine lobe for normal = (0, 0, 1) as the first 9
// spherical harmonic coefficients are hardcoded below. This was computed by
// evaluating:
// ProjectFunction(kIrradianceOrder, [] (double phi, double theta) {
// return Clamp(Eigen::Vector3d::UnitZ().dot(ToVector(phi, theta)),
// 0.0, 1.0);
// }, 10000000);
const std::vector<double> cosine_lobe = { 0.886227, 0.0, 1.02333, 0.0, 0.0, 0.0,
0.495416, 0.0, 0.0 };
// A zero template is required for EvalSHSum to handle its template
// instantiations and a type's default constructor does not necessarily
// initialize to zero.
template<typename T> T Zero();
template<> double Zero() { return 0.0; }
template<> float Zero() { return 0.0; }
template<> Eigen::Array3f Zero() { return Eigen::Array3f::Zero(); }
template <class T>
using VectorX = Eigen::Matrix<T, Eigen::Dynamic, 1>;
// Usage: CHECK(bool, string message);
// Note that it must end a semi-colon, making it look like a
// valid C++ statement (hence the awkward do() while(false)).
#ifndef NDEBUG
# define CHECK(condition, message) \
do { \
if (!(condition)) { \
std::cerr << "Check failed (" #condition ") in " << __FILE__ \
<< ":" << __LINE__ << ", message: " << message << std::endl; \
std::exit(EXIT_FAILURE); \
} \
} while(false)
#else
# define CHECK(condition, message) do {} while(false)
#endif
// Clamp the first argument to be greater than or equal to the second
// and less than or equal to the third.
double Clamp(double val, double min, double max) {
if (val < min) {
val = min;
}
if (val > max) {
val = max;
}
return val;
}
// Return true if the first value is within epsilon of the second value.
bool NearByMargin(double actual, double expected) {
double diff = actual - expected;
if (diff < 0.0) {
diff = -diff;
}
// 5 bits of error in mantissa (source of '32 *')
return diff < 32 * std::numeric_limits<double>::epsilon();
}
// Return floating mod x % m.
double FastFMod(double x, double m) {
return x - (m * floor(x / m));
}
// Hardcoded spherical harmonic functions for low orders (l is first number
// and m is second number (sign encoded as preceeding 'p' or 'n')).
//
// As polynomials they are evaluated more efficiently in cartesian coordinates,
// assuming that @d is unit. This is not verified for efficiency.
double HardcodedSH00(const Eigen::Vector3d& d) {
// 0.5 * sqrt(1/pi)
return 0.282095;
}
double HardcodedSH1n1(const Eigen::Vector3d& d) {
// -sqrt(3/(4pi)) * y
return -0.488603 * d.y();
}
double HardcodedSH10(const Eigen::Vector3d& d) {
// sqrt(3/(4pi)) * z
return 0.488603 * d.z();
}
double HardcodedSH1p1(const Eigen::Vector3d& d) {
// -sqrt(3/(4pi)) * x
return -0.488603 * d.x();
}
double HardcodedSH2n2(const Eigen::Vector3d& d) {
// 0.5 * sqrt(15/pi) * x * y
return 1.092548 * d.x() * d.y();
}
double HardcodedSH2n1(const Eigen::Vector3d& d) {
// -0.5 * sqrt(15/pi) * y * z
return -1.092548 * d.y() * d.z();
}
double HardcodedSH20(const Eigen::Vector3d& d) {
// 0.25 * sqrt(5/pi) * (-x^2-y^2+2z^2)
return 0.315392 * (-d.x() * d.x() - d.y() * d.y() + 2.0 * d.z() * d.z());
}
double HardcodedSH2p1(const Eigen::Vector3d& d) {
// -0.5 * sqrt(15/pi) * x * z
return -1.092548 * d.x() * d.z();
}
double HardcodedSH2p2(const Eigen::Vector3d& d) {
// 0.25 * sqrt(15/pi) * (x^2 - y^2)
return 0.546274 * (d.x() * d.x() - d.y() * d.y());
}
double HardcodedSH3n3(const Eigen::Vector3d& d) {
// -0.25 * sqrt(35/(2pi)) * y * (3x^2 - y^2)
return -0.590044 * d.y() * (3.0 * d.x() * d.x() - d.y() * d.y());
}
double HardcodedSH3n2(const Eigen::Vector3d& d) {
// 0.5 * sqrt(105/pi) * x * y * z
return 2.890611 * d.x() * d.y() * d.z();
}
double HardcodedSH3n1(const Eigen::Vector3d& d) {
// -0.25 * sqrt(21/(2pi)) * y * (4z^2-x^2-y^2)
return -0.457046 * d.y() * (4.0 * d.z() * d.z() - d.x() * d.x()
- d.y() * d.y());
}
double HardcodedSH30(const Eigen::Vector3d& d) {
// 0.25 * sqrt(7/pi) * z * (2z^2 - 3x^2 - 3y^2)
return 0.373176 * d.z() * (2.0 * d.z() * d.z() - 3.0 * d.x() * d.x()
- 3.0 * d.y() * d.y());
}
double HardcodedSH3p1(const Eigen::Vector3d& d) {
// -0.25 * sqrt(21/(2pi)) * x * (4z^2-x^2-y^2)
return -0.457046 * d.x() * (4.0 * d.z() * d.z() - d.x() * d.x()
- d.y() * d.y());
}
double HardcodedSH3p2(const Eigen::Vector3d& d) {
// 0.25 * sqrt(105/pi) * z * (x^2 - y^2)
return 1.445306 * d.z() * (d.x() * d.x() - d.y() * d.y());
}
double HardcodedSH3p3(const Eigen::Vector3d& d) {
// -0.25 * sqrt(35/(2pi)) * x * (x^2-3y^2)
return -0.590044 * d.x() * (d.x() * d.x() - 3.0 * d.y() * d.y());
}
double HardcodedSH4n4(const Eigen::Vector3d& d) {
// 0.75 * sqrt(35/pi) * x * y * (x^2-y^2)
return 2.503343 * d.x() * d.y() * (d.x() * d.x() - d.y() * d.y());
}
double HardcodedSH4n3(const Eigen::Vector3d& d) {
// -0.75 * sqrt(35/(2pi)) * y * z * (3x^2-y^2)
return -1.770131 * d.y() * d.z() * (3.0 * d.x() * d.x() - d.y() * d.y());
}
double HardcodedSH4n2(const Eigen::Vector3d& d) {
// 0.75 * sqrt(5/pi) * x * y * (7z^2-1)
return 0.946175 * d.x() * d.y() * (7.0 * d.z() * d.z() - 1.0);
}
double HardcodedSH4n1(const Eigen::Vector3d& d) {
// -0.75 * sqrt(5/(2pi)) * y * z * (7z^2-3)
return -0.669047 * d.y() * d.z() * (7.0 * d.z() * d.z() - 3.0);
}
double HardcodedSH40(const Eigen::Vector3d& d) {
// 3/16 * sqrt(1/pi) * (35z^4-30z^2+3)
double z2 = d.z() * d.z();
return 0.105786 * (35.0 * z2 * z2 - 30.0 * z2 + 3.0);
}
double HardcodedSH4p1(const Eigen::Vector3d& d) {
// -0.75 * sqrt(5/(2pi)) * x * z * (7z^2-3)
return -0.669047 * d.x() * d.z() * (7.0 * d.z() * d.z() - 3.0);
}
double HardcodedSH4p2(const Eigen::Vector3d& d) {
// 3/8 * sqrt(5/pi) * (x^2 - y^2) * (7z^2 - 1)
return 0.473087 * (d.x() * d.x() - d.y() * d.y())
* (7.0 * d.z() * d.z() - 1.0);
}
double HardcodedSH4p3(const Eigen::Vector3d& d) {
// -0.75 * sqrt(35/(2pi)) * x * z * (x^2 - 3y^2)
return -1.770131 * d.x() * d.z() * (d.x() * d.x() - 3.0 * d.y() * d.y());
}
double HardcodedSH4p4(const Eigen::Vector3d& d) {
// 3/16*sqrt(35/pi) * (x^2 * (x^2 - 3y^2) - y^2 * (3x^2 - y^2))
double x2 = d.x() * d.x();
double y2 = d.y() * d.y();
return 0.625836 * (x2 * (x2 - 3.0 * y2) - y2 * (3.0 * x2 - y2));
}
// Compute the factorial for an integer @x. It is assumed x is at least 0.
// This implementation precomputes the results for low values of x, in which
// case this is a constant time lookup.
//
// The vast majority of SH evaluations will hit these precomputed values.
double Factorial(int x) {
static const double factorial_cache[kCacheSize] = {1, 1, 2, 6, 24, 120, 720, 5040,
40320, 362880, 3628800, 39916800,
479001600, 6227020800,
87178291200, 1307674368000};
if (x < kCacheSize) {
return factorial_cache[x];
} else {
double s = factorial_cache[kCacheSize - 1];
for (int n = kCacheSize; n <= x; n++) {
s *= n;
}
return s;
}
}
// Compute the double factorial for an integer @x. This assumes x is at least
// 0. This implementation precomputes the results for low values of x, in
// which case this is a constant time lookup.
//
// The vast majority of SH evaluations will hit these precomputed values.
// See http://mathworld.wolfram.com/DoubleFactorial.html
double DoubleFactorial(int x) {
static const double dbl_factorial_cache[kCacheSize] = {1, 1, 2, 3, 8, 15, 48, 105,
384, 945, 3840, 10395, 46080,
135135, 645120, 2027025};
if (x < kCacheSize) {
return dbl_factorial_cache[x];
} else {
double s = dbl_factorial_cache[kCacheSize - (x % 2 == 0 ? 2 : 1)];
double n = x;
while (n >= kCacheSize) {
s *= n;
n -= 2.0;
}
return s;
}
}
// Evaluate the associated Legendre polynomial of degree @l and order @m at
// coordinate @x. The inputs must satisfy:
// 1. l >= 0
// 2. 0 <= m <= l
// 3. -1 <= x <= 1
// See http://en.wikipedia.org/wiki/Associated_Legendre_polynomials
//
// This implementation is based off the approach described in [1],
// instead of computing Pml(x) directly, Pmm(x) is computed. Pmm can be
// lifted to Pmm+1 recursively until Pml is found
double EvalLegendrePolynomial(int l, int m, double x) {
// Compute Pmm(x) = (-1)^m(2m - 1)!!(1 - x^2)^(m/2), where !! is the double
// factorial.
double pmm = 1.0;
// P00 is defined as 1.0, do don't evaluate Pmm unless we know m > 0
if (m > 0) {
double sign = (m % 2 == 0 ? 1 : -1);
pmm = sign * DoubleFactorial(2 * m - 1) * pow(1 - x * x, m / 2.0);
}
if (l == m) {
// Pml is the same as Pmm so there's no lifting to higher bands needed
return pmm;
}
// Compute Pmm+1(x) = x(2m + 1)Pmm(x)
double pmm1 = x * (2 * m + 1) * pmm;
if (l == m + 1) {
// Pml is the same as Pmm+1 so we are done as well
return pmm1;
}
// Use the last two computed bands to lift up to the next band until l is
// reached, using the recurrence relationship:
// Pml(x) = (x(2l - 1)Pml-1 - (l + m - 1)Pml-2) / (l - m)
for (int n = m + 2; n <= l; n++) {
double pmn = (x * (2 * n - 1) * pmm1 - (n + m - 1) * pmm) / (n - m);
pmm = pmm1;
pmm1 = pmn;
}
// Pmm1 at the end of the above loop is equal to Pml
return pmm1;
}
// ---- The following functions are used to implement SH rotation computations
// based on the recursive approach described in [1, 4]. The names of the
// functions correspond with the notation used in [1, 4].
// See http://en.wikipedia.org/wiki/Kronecker_delta
double KroneckerDelta(int i, int j) {
if (i == j) {
return 1.0;
} else {
return 0.0;
}
}
// [4] uses an odd convention of referring to the rows and columns using
// centered indices, so the middle row and column are (0, 0) and the upper
// left would have negative coordinates.
//
// This is a convenience function to allow us to access an Eigen::MatrixXd
// in the same manner, assuming r is a (2l+1)x(2l+1) matrix.
double GetCenteredElement(const Eigen::MatrixXd& r, int i, int j) {
// The shift to go from [-l, l] to [0, 2l] is (rows - 1) / 2 = l,
// (since the matrix is assumed to be square, rows == cols).
int offset = (r.rows() - 1) / 2;
return r(i + offset, j + offset);
}
// P is a helper function defined in [4] that is used by the functions U, V, W.
// This should not be called on its own, as U, V, and W (and their coefficients)
// select the appropriate matrix elements to access (arguments @a and @b).
double P(int i, int a, int b, int l, const std::vector<Eigen::MatrixXd>& r) {
if (b == l) {
return GetCenteredElement(r[1], i, 1) *
GetCenteredElement(r[l - 1], a, l - 1) -
GetCenteredElement(r[1], i, -1) *
GetCenteredElement(r[l - 1], a, -l + 1);
} else if (b == -l) {
return GetCenteredElement(r[1], i, 1) *
GetCenteredElement(r[l - 1], a, -l + 1) +
GetCenteredElement(r[1], i, -1) *
GetCenteredElement(r[l - 1], a, l - 1);
} else {
return GetCenteredElement(r[1], i, 0) * GetCenteredElement(r[l - 1], a, b);
}
}
// The functions U, V, and W should only be called if the correspondingly
// named coefficient u, v, w from the function ComputeUVWCoeff() is non-zero.
// When the coefficient is 0, these would attempt to access matrix elements that
// are out of bounds. The list of rotations, @r, must have the @l - 1
// previously completed band rotations. These functions are valid for l >= 2.
double U(int m, int n, int l, const std::vector<Eigen::MatrixXd>& r) {
// Although [1, 4] split U into three cases for m == 0, m < 0, m > 0
// the actual values are the same for all three cases
return P(0, m, n, l, r);
}
double V(int m, int n, int l, const std::vector<Eigen::MatrixXd>& r) {
if (m == 0) {
return P(1, 1, n, l, r) + P(-1, -1, n, l, r);
} else if (m > 0) {
return P(1, m - 1, n, l, r) * sqrt(1 + KroneckerDelta(m, 1)) -
P(-1, -m + 1, n, l, r) * (1 - KroneckerDelta(m, 1));
} else {
// Note there is apparent errata in [1,4,4b] dealing with this particular
// case. [4b] writes it should be P*(1-d)+P*(1-d)^0.5
// [1] writes it as P*(1+d)+P*(1-d)^0.5, but going through the math by hand,
// you must have it as P*(1-d)+P*(1+d)^0.5 to form a 2^.5 term, which
// parallels the case where m > 0.
return P(1, m + 1, n, l, r) * (1 - KroneckerDelta(m, -1)) +
P(-1, -m - 1, n, l, r) * sqrt(1 + KroneckerDelta(m, -1));
}
}
double W(int m, int n, int l, const std::vector<Eigen::MatrixXd>& r) {
if (m == 0) {
// whenever this happens, w is also 0 so W can be anything
return 0.0;
} else if (m > 0) {
return P(1, m + 1, n, l, r) + P(-1, -m - 1, n, l, r);
} else {
return P(1, m - 1, n, l, r) - P(-1, -m + 1, n, l, r);
}
}
// Calculate the coefficients applied to the U, V, and W functions. Because
// their equations share many common terms they are computed simultaneously.
void ComputeUVWCoeff(int m, int n, int l, double* u, double* v, double* w) {
double d = KroneckerDelta(m, 0);
double denom = (abs(n) == l ? 2.0 * l * (2.0 * l - 1) : (l + n) * (l - n));
*u = sqrt((l + m) * (l - m) / denom);
*v = 0.5 * sqrt((1 + d) * (l + abs(m) - 1.0) * (l + abs(m)) / denom)
* (1 - 2 * d);
*w = -0.5 * sqrt((l - abs(m) - 1) * (l - abs(m)) / denom) * (1 - d);
}
// Calculate the (2l+1)x(2l+1) rotation matrix for the band @l.
// This uses the matrices computed for band 1 and band l-1 to compute the
// matrix for band l. @rotations must contain the previously computed l-1
// rotation matrices, and the new matrix for band l will be appended to it.
//
// This implementation comes from p. 5 (6346), Table 1 and 2 in [4] taking
// into account the corrections from [4b].
void ComputeBandRotation(int l, std::vector<Eigen::MatrixXd>* rotations) {
// The band's rotation matrix has rows and columns equal to the number of
// coefficients within that band (-l <= m <= l implies 2l + 1 coefficients).
Eigen::MatrixXd rotation(2 * l + 1, 2 * l + 1);
for (int m = -l; m <= l; m++) {
for (int n = -l; n <= l; n++) {
double u, v, w;
ComputeUVWCoeff(m, n, l, &u, &v, &w);
// The functions U, V, W are only safe to call if the coefficients
// u, v, w are not zero
if (!NearByMargin(u, 0.0))
u *= U(m, n, l, *rotations);
if (!NearByMargin(v, 0.0))
v *= V(m, n, l, *rotations);
if (!NearByMargin(w, 0.0))
w *= W(m, n, l, *rotations);
rotation(m + l, n + l) = (u + v + w);
}
}
rotations->push_back(rotation);
}
} // namespace
Eigen::Vector3d ToVector(double phi, double theta) {
double r = sin(theta);
return Eigen::Vector3d(r * cos(phi), r * sin(phi), cos(theta));
}
void ToSphericalCoords(const Eigen::Vector3d& dir, double* phi, double* theta) {
CHECK(NearByMargin(dir.squaredNorm(), 1.0), "dir is not unit");
// Explicitly clamp the z coordinate so that numeric errors don't cause it
// to fall just outside of acos' domain.
*theta = acos(Clamp(dir.z(), -1.0, 1.0));
// We don't need to divide dir.y() or dir.x() by sin(theta) since they are
// both scaled by it and atan2 will handle it appropriately.
*phi = atan2(dir.y(), dir.x());
}
double ImageXToPhi(int x, int width) {
// The directions are measured from the center of the pixel, so add 0.5
// to convert from integer pixel indices to float pixel coordinates.
return 2.0 * M_PI * (x + 0.5) / width;
}
double ImageYToTheta(int y, int height) {
return M_PI * (y + 0.5) / height;
}
Eigen::Vector2d ToImageCoords(double phi, double theta, int width, int height) {
// Allow theta to repeat and map to 0 to pi. However, to account for cases
// where y goes beyond the normal 0 to pi range, phi may need to be adjusted.
theta = Clamp(FastFMod(theta, 2.0 * M_PI), 0.0, 2.0 * M_PI);
if (theta > M_PI) {
// theta is out of bounds. Effectively, theta has rotated past the pole
// so after adjusting theta to be in range, rotating phi by pi forms an
// equivalent direction.
theta = 2.0 * M_PI - theta; // now theta is between 0 and pi
phi += M_PI;
}
// Allow phi to repeat and map to the normal 0 to 2pi range.
// Clamp and map after adjusting theta in case theta was forced to update phi.
phi = Clamp(FastFMod(phi, 2.0 * M_PI), 0.0, 2.0 * M_PI);
// Now phi is in [0, 2pi] and theta is in [0, pi] so it's simple to inverse
// the linear equations in ImageCoordsToSphericalCoords, although there's no
// -0.5 because we're returning floating point coordinates and so don't need
// to center the pixel.
return Eigen::Vector2d(width * phi / (2.0 * M_PI), height * theta / M_PI);
}
double EvalSHSlow(int l, int m, double phi, double theta) {
CHECK(l >= 0, "l must be at least 0.");
CHECK(-l <= m && m <= l, "m must be between -l and l.");
double kml = sqrt((2.0 * l + 1) * Factorial(l - abs(m)) /
(4.0 * M_PI * Factorial(l + abs(m))));
if (m > 0) {
return sqrt(2.0) * kml * cos(m * phi) *
EvalLegendrePolynomial(l, m, cos(theta));
} else if (m < 0) {
return sqrt(2.0) * kml * sin(-m * phi) *
EvalLegendrePolynomial(l, -m, cos(theta));
} else {
return kml * EvalLegendrePolynomial(l, 0, cos(theta));
}
}
double EvalSHSlow(int l, int m, const Eigen::Vector3d& dir) {
double phi, theta;
ToSphericalCoords(dir, &phi, &theta);
return EvalSH(l, m, phi, theta);
}
double EvalSH(int l, int m, double phi, double theta) {
// If using the hardcoded functions, switch to cartesian
if (l <= kHardCodedOrderLimit) {
return EvalSH(l, m, ToVector(phi, theta));
} else {
// Stay in spherical coordinates since that's what the recurrence
// version is implemented in
return EvalSHSlow(l, m, phi, theta);
}
}
double EvalSH(int l, int m, const Eigen::Vector3d& dir) {
if (l <= kHardCodedOrderLimit) {
// Validate l and m here (don't do it generally since EvalSHSlow also
// checks it if we delegate to that function).
CHECK(l >= 0, "l must be at least 0.");
CHECK(-l <= m && m <= l, "m must be between -l and l.");
CHECK(NearByMargin(dir.squaredNorm(), 1.0), "dir is not unit.");
switch (l) {
case 0:
return HardcodedSH00(dir);
case 1:
switch (m) {
case -1:
return HardcodedSH1n1(dir);
case 0:
return HardcodedSH10(dir);
case 1:
return HardcodedSH1p1(dir);
}
case 2:
switch (m) {
case -2:
return HardcodedSH2n2(dir);
case -1:
return HardcodedSH2n1(dir);
case 0:
return HardcodedSH20(dir);
case 1:
return HardcodedSH2p1(dir);
case 2:
return HardcodedSH2p2(dir);
}
case 3:
switch (m) {
case -3:
return HardcodedSH3n3(dir);
case -2:
return HardcodedSH3n2(dir);
case -1:
return HardcodedSH3n1(dir);
case 0:
return HardcodedSH30(dir);
case 1:
return HardcodedSH3p1(dir);
case 2:
return HardcodedSH3p2(dir);
case 3:
return HardcodedSH3p3(dir);
}
case 4:
switch (m) {
case -4:
return HardcodedSH4n4(dir);
case -3:
return HardcodedSH4n3(dir);
case -2:
return HardcodedSH4n2(dir);
case -1:
return HardcodedSH4n1(dir);
case 0:
return HardcodedSH40(dir);
case 1:
return HardcodedSH4p1(dir);
case 2:
return HardcodedSH4p2(dir);
case 3:
return HardcodedSH4p3(dir);
case 4:
return HardcodedSH4p4(dir);
}
}
// This is unreachable given the CHECK's above but the compiler can't tell.
return 0.0;
} else {
// Not hard-coded so use the recurrence relation (which will convert this
// to spherical coordinates).
return EvalSHSlow(l, m, dir);
}
}
std::unique_ptr<std::vector<double>> ProjectFunction(
int order, const SphericalFunction& func, int sample_count) {
CHECK(order >= 0, "Order must be at least zero.");
CHECK(sample_count > 0, "Sample count must be at least one.");
// This is the approach demonstrated in [1] and is useful for arbitrary
// functions on the sphere that are represented analytically.
const int sample_side = static_cast<int>(floor(sqrt(sample_count)));
std::unique_ptr<std::vector<double>> coeffs(new std::vector<double>());
coeffs->assign(GetCoefficientCount(order), 0.0);
// generate sample_side^2 uniformly and stratified samples over the sphere
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> rng(0.0, 1.0);
for (int t = 0; t < sample_side; t++) {
for (int p = 0; p < sample_side; p++) {
double alpha = (t + rng(gen)) / sample_side;
double beta = (p + rng(gen)) / sample_side;
// See http://www.bogotobogo.com/Algorithms/uniform_distribution_sphere.php
double phi = 2.0 * M_PI * beta;
double theta = acos(2.0 * alpha - 1.0);
// evaluate the analytic function for the current spherical coords
double func_value = func(phi, theta);
// evaluate the SH basis functions up to band O, scale them by the
// function's value and accumulate them over all generated samples
for (int l = 0; l <= order; l++) {
for (int m = -l; m <= l; m++) {
double sh = EvalSH(l, m, phi, theta);
(*coeffs)[GetIndex(l, m)] += func_value * sh;
}
}
}
}
// scale by the probability of a particular sample, which is
// 4pi/sample_side^2. 4pi for the surface area of a unit sphere, and
// 1/sample_side^2 for the number of samples drawn uniformly.
double weight = 4.0 * M_PI / (sample_side * sample_side);
for (unsigned int i = 0; i < coeffs->size(); i++) {
(*coeffs)[i] *= weight;
}
return coeffs;
}
std::unique_ptr<std::vector<Eigen::Array3f>> ProjectEnvironment(
int order, const Image& env) {
CHECK(order >= 0, "Order must be at least zero.");
// An environment map projection is three different spherical functions, one
// for each color channel. The projection integrals are estimated by
// iterating over every pixel within the image.
double pixel_area = (2.0 * M_PI / env.width()) * (M_PI / env.height());
std::unique_ptr<std::vector<Eigen::Array3f>> coeffs(
new std::vector<Eigen::Array3f>());
coeffs->assign(GetCoefficientCount(order), Eigen::Array3f(0.0, 0.0, 0.0));
Eigen::Array3f color;
for (int t = 0; t < env.height(); t++) {
double theta = ImageYToTheta(t, env.height());
// The differential area of each pixel in the map is constant across a
// row. Must scale the pixel_area by sin(theta) to account for the
// stretching that occurs at the poles with this parameterization.
double weight = pixel_area * sin(theta);
for (int p = 0; p < env.width(); p++) {
double phi = ImageXToPhi(p, env.width());
color = env.GetPixel(p, t);
for (int l = 0; l <= order; l++) {
for (int m = -l; m <= l; m++) {
int i = GetIndex(l, m);
double sh = EvalSH(l, m, phi, theta);
(*coeffs)[i] += sh * weight * color.array();
}
}
}
}
return coeffs;
}
std::unique_ptr<std::vector<double>> ProjectSparseSamples(
int order, const std::vector<Eigen::Vector3d>& dirs,
const std::vector<double>& values) {
CHECK(order >= 0, "Order must be at least zero.");
CHECK(dirs.size() == values.size(),
"Directions and values must have the same size.");
// Solve a linear least squares system Ax = b for the coefficients, x.
// Each row in the matrix A are the values of the spherical harmonic basis
// functions evaluated at that sample's direction (from @dirs). The
// corresponding row in b is the value in @values.
std::unique_ptr<std::vector<double>> coeffs(new std::vector<double>());
coeffs->assign(GetCoefficientCount(order), 0.0);
Eigen::MatrixXd basis_values(dirs.size(), coeffs->size());
Eigen::VectorXd func_values(dirs.size());
double phi, theta;
for (unsigned int i = 0; i < dirs.size(); i++) {
func_values(i) = values[i];
ToSphericalCoords(dirs[i], &phi, &theta);
for (int l = 0; l <= order; l++) {
for (int m = -l; m <= l; m++) {
basis_values(i, GetIndex(l, m)) = EvalSH(l, m, phi, theta);
}
}
}
// Use SVD to find the least squares fit for the coefficients of the basis
// functions that best match the data
Eigen::VectorXd soln = basis_values.jacobiSvd(
Eigen::ComputeThinU | Eigen::ComputeThinV).solve(func_values);
// Copy everything over to our coeffs array
for (unsigned int i = 0; i < coeffs->size(); i++) {
(*coeffs)[i] = soln(i);
}
return coeffs;
}
template <typename T>
T EvalSHSum(int order, const std::vector<T>& coeffs, double phi, double theta) {
if (order <= kHardCodedOrderLimit) {
// It is faster to compute the cartesian coordinates once
return EvalSHSum(order, coeffs, ToVector(phi, theta));
}
CHECK(GetCoefficientCount(order) == coeffs.size(),
"Incorrect number of coefficients provided.");
T sum = Zero<T>();
for (int l = 0; l <= order; l++) {
for (int m = -l; m <= l; m++) {
sum += EvalSH(l, m, phi, theta) * coeffs[GetIndex(l, m)];
}
}
return sum;
}
template <typename T>
T EvalSHSum(int order, const std::vector<T>& coeffs,
const Eigen::Vector3d& dir) {
if (order > kHardCodedOrderLimit) {
// It is faster to switch to spherical coordinates
double phi, theta;
ToSphericalCoords(dir, &phi, &theta);
return EvalSHSum(order, coeffs, phi, theta);
}
CHECK(GetCoefficientCount(order) == coeffs.size(),
"Incorrect number of coefficients provided.");
CHECK(NearByMargin(dir.squaredNorm(), 1.0), "dir is not unit.");
T sum = Zero<T>();
for (int l = 0; l <= order; l++) {
for (int m = -l; m <= l; m++) {
sum += EvalSH(l, m, dir) * coeffs[GetIndex(l, m)];
}
}
return sum;
}
Rotation::Rotation(int order, const Eigen::Quaterniond& rotation)
: order_(order), rotation_(rotation) {
band_rotations_.reserve(GetCoefficientCount(order));
}
std::unique_ptr<Rotation> Rotation::Create(
int order, const Eigen::Quaterniond& rotation) {
CHECK(order >= 0, "Order must be at least 0.");
CHECK(NearByMargin(rotation.squaredNorm(), 1.0),
"Rotation must be normalized.");
std::unique_ptr<Rotation> sh_rot(new Rotation(order, rotation));
// Order 0 (first band) is simply the 1x1 identity since the SH basis
// function is a simple sphere.
Eigen::MatrixXd r(1, 1);
r(0, 0) = 1.0;
sh_rot->band_rotations_.push_back(r);
r.resize(3, 3);
// The second band's transformation is simply a permutation of the
// rotation matrix's elements, provided in Appendix 1 of [1], updated to
// include the Condon-Shortely phase. The recursive method in
// ComputeBandRotation preserves the proper phases as high bands are computed.
Eigen::Matrix3d rotation_mat = rotation.toRotationMatrix();
r(0, 0) = rotation_mat(1, 1);
r(0, 1) = -rotation_mat(1, 2);
r(0, 2) = rotation_mat(1, 0);
r(1, 0) = -rotation_mat(2, 1);
r(1, 1) = rotation_mat(2, 2);
r(1, 2) = -rotation_mat(2, 0);
r(2, 0) = rotation_mat(0, 1);
r(2, 1) = -rotation_mat(0, 2);
r(2, 2) = rotation_mat(0, 0);
sh_rot->band_rotations_.push_back(r);
// Recursively build the remaining band rotations, using the equations
// provided in [4, 4b].
for (int l = 2; l <= order; l++) {
ComputeBandRotation(l, &(sh_rot->band_rotations_));
}
return sh_rot;
}
std::unique_ptr<Rotation> Rotation::Create(int order,
const Rotation& rotation) {
CHECK(order >= 0, "Order must be at least 0.");
std::unique_ptr<Rotation> sh_rot(new Rotation(order, rotation.rotation_));
// Copy up to min(order, rotation.order_) band rotations into the new
// SHRotation. For shared orders, they are the same. If the new order is
// higher than already calculated then the remainder will be computed next.
for (int l = 0; l <= std::min(order, rotation.order_); l++) {
sh_rot->band_rotations_.push_back(rotation.band_rotations_[l]);
}
// Calculate remaining bands (automatically skipped if there are no more).
for (int l = rotation.order_ + 1; l <= order; l++) {
ComputeBandRotation(l, &(sh_rot->band_rotations_));
}
return sh_rot;
}
int Rotation::order() const { return order_; }
Eigen::Quaterniond Rotation::rotation() const { return rotation_; }
const Eigen::MatrixXd& Rotation::band_rotation(int l) const {
return band_rotations_[l];
}
template <typename T>
void Rotation::Apply(const std::vector<T>& coeff,
std::vector<T>* result) const {
CHECK(coeff.size() == GetCoefficientCount(order_),
"Incorrect number of coefficients provided.");
// Resize to the required number of coefficients.
// If result is already the same size as coeff, there's no need to zero out
// its values since each index will be written explicitly later.
if (result->size() != coeff.size()) {
result->assign(coeff.size(), T());
}
// Because of orthogonality, the coefficients outside of each band do not
// interact with one another. By separating them into band-specific matrices,
// we take advantage of that sparsity.
for (int l = 0; l <= order_; l++) {
VectorX<T> band_coeff(2 * l + 1);
// Fill band_coeff from the subset of @coeff that's relevant.
for (int m = -l; m <= l; m++) {
// Offset by l to get the appropiate vector component (0-based instead
// of starting at -l).
band_coeff(m + l) = coeff[GetIndex(l, m)];
}
band_coeff = band_rotations_[l].cast<T>() * band_coeff;
// Copy rotated coefficients back into the appropriate subset into @result.
for (int m = -l; m <= l; m++) {
(*result)[GetIndex(l, m)] = band_coeff(m + l);
}
}
}
void RenderDiffuseIrradianceMap(const Image& env_map, Image* diffuse_out) {
std::unique_ptr<std::vector<Eigen::Array3f>> coeffs =
ProjectEnvironment(kIrradianceOrder, env_map);
RenderDiffuseIrradianceMap(*coeffs, diffuse_out);
}
void RenderDiffuseIrradianceMap(const std::vector<Eigen::Array3f>& sh_coeffs,
Image* diffuse_out) {
for (int y = 0; y < diffuse_out->height(); y++) {
double theta = ImageYToTheta(y, diffuse_out->height());
for (int x = 0; x < diffuse_out->width(); x++) {
double phi = ImageXToPhi(x, diffuse_out->width());
Eigen::Vector3d normal = ToVector(phi, theta);
Eigen::Array3f irradiance = RenderDiffuseIrradiance(sh_coeffs, normal);
diffuse_out->SetPixel(x, y, irradiance);
}
}
}
Eigen::Array3f RenderDiffuseIrradiance(
const std::vector<Eigen::Array3f>& sh_coeffs,
const Eigen::Vector3d& normal) {
// Optimization for if sh_coeffs is empty, then there is no environmental
// illumination so irradiance is 0.0 regardless of the normal.
if (sh_coeffs.empty()) {
return Eigen::Array3f(0.0, 0.0, 0.0);
}
// Compute diffuse irradiance
Eigen::Quaterniond rotation;
rotation.setFromTwoVectors(Eigen::Vector3d::UnitZ(), normal).normalize();
std::vector<double> rotated_cos(kIrradianceCoeffCount);
std::unique_ptr<sh::Rotation> sh_rot(Rotation::Create(
kIrradianceOrder, rotation));
sh_rot->Apply(cosine_lobe, &rotated_cos);
Eigen::Array3f sum(0.0, 0.0, 0.0);
// The cosine lobe is 9 coefficients and after that all bands are assumed to
// be 0. If sh_coeffs provides more than 9, they are irrelevant then. If it
// provides fewer than 9, this assumes that the remaining coefficients would
// have been 0 and can safely ignore the rest of the cosine lobe.
unsigned int coeff_count = kIrradianceCoeffCount;
if (coeff_count > sh_coeffs.size()) {
coeff_count = sh_coeffs.size();
}
for (unsigned int i = 0; i < coeff_count; i++) {
sum += rotated_cos[i] * sh_coeffs[i];
}
return sum;
}
// ---- Template specializations -----------------------------------------------
template double EvalSHSum<double>(int order, const std::vector<double>& coeffs,
double phi, double theta);
template double EvalSHSum<double>(int order, const std::vector<double>& coeffs,
const Eigen::Vector3d& dir);
template float EvalSHSum<float>(int order, const std::vector<float>& coeffs,
double phi, double theta);
template float EvalSHSum<float>(int order, const std::vector<float>& coeffs,
const Eigen::Vector3d& dir);
template Eigen::Array3f EvalSHSum<Eigen::Array3f>(
int order, const std::vector<Eigen::Array3f>& coeffs,
double phi, double theta);
template Eigen::Array3f EvalSHSum<Eigen::Array3f>(
int order, const std::vector<Eigen::Array3f>& coeffs,
const Eigen::Vector3d& dir);
template void Rotation::Apply<double>(const std::vector<double>& coeff,
std::vector<double>* result) const;
template void Rotation::Apply<float>(const std::vector<float>& coeff,
std::vector<float>* result) const;
// The generic implementation for Rotate doesn't handle aggregate types
// like Array3f so split it apart, use the generic version and then recombine
// them into the final result.
template <> void Rotation::Apply<Eigen::Array3f>(
const std::vector<Eigen::Array3f>& coeff,
std::vector<Eigen::Array3f>* result) const {
// Separate the Array3f coefficients into three vectors.
std::vector<float> c1, c2, c3;
for (unsigned int i = 0; i < coeff.size(); i++) {
const Eigen::Array3f& c = coeff[i];
c1.push_back(c(0));
c2.push_back(c(1));
c3.push_back(c(2));
}
// Compute the rotation in place
Apply(c1, &c1);
Apply(c2, &c2);
Apply(c3, &c3);
// Coellesce back into Array3f
result->assign(GetCoefficientCount(order_), Eigen::Array3f::Zero());
for (unsigned int i = 0; i < result->size(); i++) {
(*result)[i] = Eigen::Array3f(c1[i], c2[i], c3[i]);
}
}
} // namespace sh
================================================
FILE: sh/spherical_harmonics.h
================================================
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The general spherical harmonic functions and fitting methods are from:
// 1. R. Green, "Spherical Harmonic Lighting: The Gritty Details", GDC 2003,
// http://www.research.scea.com/gdc2003/spherical-harmonic-lighting.pdf
//
// The environment map related functions are based on the methods in:
// 2. R. Ramamoorthi and P. Hanrahan, "An Efficient Representation for
// Irradiance Environment Maps",. , P., SIGGRAPH 2001, 497-500
// 3. R. Ramamoorthi and P. Hanrahan, “On the Relationship between Radiance and
// Irradiance: Determining the Illumination from Images of a Convex
// Lambertian Object,” J. Optical Soc. Am. A, vol. 18, no. 10, pp. 2448-2459,
// 2001.
//
// Spherical harmonic rotations are implemented using the recurrence relations
// described by:
// 4. J. Ivanic and K. Ruedenberg, "Rotation Matrices for Real Spherical
// Harmonics. Direct Determination by Recursion", J. Phys. Chem., vol. 100,
// no. 15, pp. 6342-6347, 1996.
// http://pubs.acs.org/doi/pdf/10.1021/jp953350u
// 4b. Corrections to initial publication:
// http://pubs.acs.org/doi/pdf/10.1021/jp9833350
#ifndef SH_SPHERICAL_HARMONICS_H_
#define SH_SPHERICAL_HARMONICS_H_
#include <array>
#include <vector>
#include <functional>
#include <memory>
#include "sh/image.h"
namespace sh {
// A spherical function, the first argument is phi, the second is theta.
// See EvalSH(int, int, double, double) for a description of these terms.
typedef std::function<double(double, double)> SphericalFunction;
const int kDefaultSampleCount = 10000;
// Get the total number of coefficients for a function represented by
// all spherical harmonic basis of degree <= @order (it is a point of
// confusion that the order of an SH refers to its degree and not the order).
constexpr int GetCoefficientCount(int order) {
return (order + 1) * (order + 1);
}
// Get the one dimensional index associated with a particular degree @l
// and order @m. This is the index that can be used to access the Coeffs
// returned by SHSolver.
constexpr int GetIndex(int l, int m) {
return l * (l + 1) + m;
}
// Convert from spherical coordinates to a direction vector. @phi represents
// the rotation about the Z axis and is from [0, 2pi]. @theta represents the
// angle down from the Z axis, from [0, pi].
Eigen::Vector3d ToVector(double phi, double theta);
// Convert from a direction vector to its spherical coordinates. The
// coordinates are written out to @phi and @theta. This is the inverse of
// ToVector.
// Check will fail if @dir is not unit.
void ToSphericalCoords(const Eigen::Vector3d& dir, double* phi, double* theta);
// Convert the (x, y) pixel coordinates into spherical coordinates (phi, theta)
// suitable for use with spherical harmonic evaluation. The x image axis maps
// to phi (0 to 2pi) and the y image axis maps to theta (0 to pi). A pixel index
// maps to the center of the pixel, so phi = 2pi (x + 0.5) / width and
// theta = pi (y + 0.5) / height. This is consistent with ProjectEnvironmentMap.
//
// x and y are not bounds checked against the image, but given the repeated
// nature of trigonometry functions, out-of-bounds x/y values produce reasonable
// phi and theta values (e.g. extending linearly beyond 0, pi, or 2pi).
// Results are undefined if the image dimensions are less than or equal to 0.
//
// The x and y functions are separated because they can be computed
// independently, unlike ToImageCoords.
double ImageXToPhi(int x, int width);
double ImageYToTheta(int y, int height);
// Convert the (phi, theta) spherical coordinates (using the convention
// defined spherical_harmonics.h) to pixel coordinates (x, y). The pixel
// coordinates are floating point to allow for later subsampling within the
// image. This is the inverse of ImageCoordsToSphericalCoords. It properly
// supports angles outside of the standard (0, 2pi) or (0, pi) range by mapping
// them back into it.
Eigen::Vector2d ToImageCoords(double phi, double theta, int width, int height);
// Evaluate the spherical harmonic basis function of degree @l and order @m
// for the given spherical coordinates, @phi and @theta.
// For low values of @l this will use a hard-coded function, otherwise it
// will fallback to EvalSHSlow that uses a recurrence relation to support all l.
double EvalSH(int l, int m, double phi, double theta);
// Evaluate the spherical harmonic basis function of degree @l and order @m
// for the given direction vector, @dir.
// Check will fail if @dir is not unit.
// For low values of @l this will use a hard-coded function, otherwise it
// will fallback to EvalSHSlow that uses a recurrence relation to support all l.
double EvalSH(int l, int m, const Eigen::Vector3d& dir);
// As EvalSH, but always uses the recurrence relationship. This is exposed
// primarily for testing purposes to ensure the hard-coded functions equal the
// recurrence relation version.
double EvalSHSlow(int l, int m, double phi, double theta);
// As EvalSH, but always uses the recurrence relationship. This is exposed
// primarily for testing purposes to ensure the hard-coded functions equal the
// recurrence relation version.
// Check will fail if @dir is not unit.
double EvalSHSlow(int l, int m, const Eigen::Vector3d& dir);
// Fit the given analytical spherical function to the SH basis functions
// up to @order. This uses Monte Carlo sampling to estimate the underlying
// integral. @sample_count determines the number of function evaluations
// performed. @sample_count is rounded to the greatest perfect square that
// is less than or equal to it.
//
// The samples are distributed uniformly over the surface of a sphere. The
// number of samples required to get a reasonable sampling of @func depends on
// the frequencies within that function. Lower frequency will not require as
// many samples. The recommended default kDefaultSampleCount should be
// sufficiently high for most functions, but is also likely overly conservative
// for many applications.
std::unique_ptr<std::vector<double>> ProjectFunction(
int order, const SphericalFunction& func, int sample_count);
// Fit the given environment map to the SH basis functions up to @order.
// It is assumed that the environment map is parameterized by theta along
// the x-axis (ranging from 0 to 2pi after normalizing out the resolution),
// and phi along the y-axis (ranging from 0 to pi after normalization).
//
// This fits three different functions, one for each color channel. The
// coefficients for these functions are stored in the respective indices
// of the Array3f values of the returned vector.
std::unique_ptr<std::vector<Eigen::Array3f>> ProjectEnvironment(
int order, const Image& env);
// Fit the given samples of a spherical function to the SH basis functions
// up to @order. This variant is used when there are relatively sparse
// evaluations or samples of the spherical function that must be fit and a
// regression is performed.
// @dirs and @values must have the same size. The directions in @dirs are
// assumed to be unit.
std::unique_ptr<std::vector<double>> ProjectSparseSamples(
int order, const std::vector<Eigen::Vector3d>& dirs,
const std::vector<double>& values);
// Evaluate the already computed coefficients for the SH basis functions up
// to @order, at the coordinates @phi and @theta. The length of the @coeffs
// vector must be equal to GetCoefficientCount(order).
// There are explicit instantiations for double, float, and Eigen::Array3f.
template <typename T>
T EvalSHSum(int order, const std::vector<T>& coeffs, double phi, double theta);
// As EvalSHSum, but inputting a direction vector instead of spherical coords.
// Check will fail if @dir is not unit.
template <typename T>
T EvalSHSum(int order, const std::vector<T>& coeffs,
const Eigen::Vector3d& dir);
// Render into @diffuse_out the diffuse irradiance for every normal vector
// representable in @diffuse_out, given the luminance stored in @env_map.
// Both @env_map and @diffuse_out use the latitude-longitude projection defined
// specified in ImageX/YToPhi/Theta. They may be of different
// resolutions. The resolution of @diffuse_out must be set before invoking this
// function.
void RenderDiffuseIrradianceMap(const Image& env_map,
Image* diffuse_out);
// Render into @diffuse_out diffuse irradiance for every normal vector
// representable in @diffuse_out, for the environment represented as the given
// spherical harmonic coefficients, @sh_coeffs. The resolution of
// @diffuse_out must be set before calling this function. Note that a high
// resolution is not necessary (64 x 32 is often quite sufficient).
// See RenderDiffuseIrradiance for how @sh_coeffs is interpreted.
void RenderDiffuseIrradianceMap(const std::vector<Eigen::Array3f>& sh_coeffs,
Image* diffuse_out);
// Compute the diffuse irradiance for @normal given the environment represented
// as the provided spherical harmonic coefficients, @sh_coeffs. Check will
// fail if @normal is not unit length. @sh_coeffs can be of any length. Any
// coefficient provided beyond what's used internally to represent the diffuse
// lobe (9) will be ignored. If @sh_coeffs is less than 9, the remaining
// coefficients are assumed to be 0. This naturally means that providing an
// empty coefficient array (e.g. when the environment is assumed to be black and
// not provided in calibration) will evaluate to 0 irradiance.
Eigen::Array3f RenderDiffuseIrradiance(
const std::vector<Eigen::Array3f>& sh_coeffs,
const Eigen::Vector3d& normal);
class Rotation {
public:
// Create a new Rotation that can applies @rotation to sets of coefficients
// for the given @order. @order must be at least 0.
static std::unique_ptr<Rotation> Create(int order,
const Eigen::Quaterniond& rotation);
// Create a new Rotation that applies the same rotation as @rotation. This
// can be used to efficiently calculate the matrices for the same 3x3
// transform when a new order is necessary.
static std::unique_ptr<Rotation> Create(int order, const Rotation& rotation);
// Transform the SH basis coefficients in @coeff by this rotation and store
// them into @result. These may be the same vector. The @result vector will
// be resized if necessary, but @coeffs must have its size equal to
// GetCoefficientCount(order()).
//
// This rotation transformation produces a set of coefficients that are equal
// to the coefficients found by projecting the original function rotated by
// the same rotation matrix.
//
// There are explicit instantiations for double, float, and Array3f.
template <typename T>
void Apply(const std::vector<T>& coeffs, std::vector<T>* result) const;
// The order (0-based) that the rotation was constructed with. It can only
// transform coefficient vectors that were fit using the same order.
int order() const;
// Return the rotation that is effectively applied to the inputs of the
// original function.
Eigen::Quaterniond rotation() const;
// Return the (2l+1)x(2l+1) matrix for transforming the coefficients within
// band @l by the rotation. @l must be at least 0 and less than or equal to
// the order this rotation was initially constructed with.
const Eigen::MatrixXd& band_rotation(int l) const;
private:
explicit Rotation(int order, const Eigen::Quaterniond& rotation);
const int order_;
const Eigen::Quaterniond rotation_;
std::vector<Eigen::MatrixXd> band_rotations_;
};
} // namespace sh
#endif // SH_SPHERICAL_HARMONICS_H_
================================================
FILE: sh/spherical_harmonics_test.cc
================================================
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "sh/default_image.h"
#include "sh/spherical_harmonics.h"
#include "gtest/gtest.h"
namespace sh {
namespace {
#define EXPECT_TUPLE3_NEAR(expected, actual, tolerance) \
{ \
EXPECT_NEAR(expected(0), actual(0), tolerance); \
EXPECT_NEAR(expected(1), actual(1), tolerance); \
EXPECT_NEAR(expected(2), actual(2), tolerance); \
}
#define EXPECT_TUPLE2_NEAR(expected, actual, tolerance) \
{ \
EXPECT_NEAR(expected(0), actual(0), tolerance); \
EXPECT_NEAR(expected(1), actual(1), tolerance); \
}
const double kEpsilon = 1e-10;
const double kHardcodedError = 1e-5;
const double kCoeffErr = 5e-2;
// Use a lower sample count than the default so the tests complete faster.
const int kTestSampleCount = 5000;
// Use a very small image since computing the diffuse irradiance explicitly
// is an O(N^4) operation on the resolution.
const int kImageWidth = 32;
const int kImageHeight = 16;
const double kIrradianceError = 0.01;
// Because the test is limited to an environment of 32x16, the error is high
// but most irradiance values are on the order of 2 or 3 so relatively this is
// reasonable. This error factor is also dependent on the SH order used to
// represent the cosine lobe in irradiance calculations. Band 2 does add some
// ringing caused by the clamping function applied to the lobe.
const double kEnvMapIrradianceError = 0.08;
// Clamp the first argument to be greater than or equal to the second
// and less than or equal to the third.
double Clamp(double val, double min, double max) {
if (val < min) {
val = min;
}
if (val > max) {
val = max;
}
return val;
}
// Return true if the first value is within epsilon of the second value.
bool NearByMargin(double actual, double expected) {
double diff = actual - expected;
if (diff < 0.0) {
diff = -diff;
}
return diff < 1e-16;
}
void ExpectMatrixNear(const Eigen::MatrixXd& expected,
const Eigen::MatrixXd& actual, double tolerance) {
EXPECT_EQ(expected.rows(), actual.rows());
EXPECT_EQ(expected.cols(), actual.cols());
for (int i = 0; i < expected.rows(); i++) {
for (int j = 0; j < expected.cols(); j++) {
EXPECT_NEAR(expected(i, j), actual(i, j), tolerance);
}
}
}
void GenerateTestEnvironment(Image* env_map) {
for (int y = 0; y < kImageHeight; y++) {
for (int x = 0; x < kImageWidth; x++) {
float red = x < kImageWidth / 2 ? 1.0 : 0.0;
float green = x >= kImageWidth / 2 ? 1.0 : 0.0;
float blue = y > kImageHeight / 2 ? 1.0 : 0.0;
env_map->SetPixel(x, y, Eigen::Array3f(red, green, blue));
}
}
}
void ComputeExplicitDiffuseIrradiance(const Image& env_map,
Image* diffuse) {
double pixel_area = 2 * M_PI / env_map.width() * M_PI / env_map.height();
for (int y = 0; y < kImageHeight; y++) {
for (int x = 0; x < kImageWidth; x++) {
Eigen::Vector3d normal = ToVector((x + 0.5) * 2 * M_PI / kImageWidth,
(y + 0.5) * M_PI / kImageHeight);
Eigen::Array3f irradiance(0.0, 0.0, 0.0);
for (int ey = 0; ey < env_map.height(); ey++) {
double theta = (ey + 0.5) * M_PI / env_map.height();
double sa = pixel_area * sin(theta);
for (int ex = 0; ex < env_map.width(); ex++) {
Eigen::Vector3d light = ToVector(
(ex + 0.5) * 2 * M_PI / env_map.width(),
(ey + 0.5) * M_PI / env_map.height());
irradiance += sa * Clamp(light.dot(normal), 0.0, 1.0) *
env_map.GetPixel(ex, ey);
}
}
diffuse->SetPixel(x, y, irradiance);
}
}
}
} // namespace
TEST(SphericalHarmonicsTest, ProjectFunction) {
// The expected coefficients used to define the analytic spherical function
const std::vector<double> coeffs = {-1.028, 0.779, -0.275, 0.601, -0.256,
1.891, -1.658, -0.370, -0.772};
// Project and compare the fitted coefficients, which should be near identical
// to the initial coefficients
SphericalFunction func = [&] (double phi, double theta) {
return EvalSHSum(2, coeffs, phi, theta); };
std::unique_ptr<std::vector<double>> fitted = ProjectFunction(
2, func, kTestSampleCount);
ASSERT_TRUE(fitted != nullptr);
for (int i = 0; i < 9; i++) {
EXPECT_NEAR(coeffs[i], (*fitted)[i], kCoeffErr);
}
}
TEST(SphericalHarmonicsTest, ProjectSparseSamples) {
// These are the expected coefficients that define the sparse samples of
// the underyling spherical function
const std::vector<double> coeffs = {-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.818, 1.482};
// Generate sparse samples
std::vector<Eigen::Vector3d> sample_dirs;
std::vector<double> sample_vals;
for (int t = 0; t < 6; t++) {
double theta = t * M_PI / 6.0;
for (int p = 0; p < 8; p++) {
double phi = p * 2.0 * M_PI / 8.0;
Eigen::Vector3d dir = ToVector(phi, theta);
double value = EvalSHSum(2, coeffs, phi, theta);
sample_dirs.push_back(dir);
sample_vals.push_back(value);
}
}
// Compute the sparse fit and given that the samples were drawn from the
// spherical basis functions this should be a pretty ideal match
std::unique_ptr<std::vector<double>> fitted = ProjectSparseSamples(
2, sample_dirs, sample_vals);
ASSERT_TRUE(fitted != nullptr);
for (int i = 0; i < 9; i++) {
EXPECT_NEAR(coeffs[i], (*fitted)[i], kCoeffErr);
}
}
TEST(SphericalHarmonicsTest, ProjectEnvironment) {
// These are the expected coefficients that define the environment map
// passed into Project()
const std::vector<double> c_red = {-1.028, 0.779, -0.275, 0.601, -0.256,
1.891, -1.658, -0.370, -0.772};
const std::vector<double> c_green = {-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.818, 1.482};
const std::vector<double> c_blue = {-1.119, 0.559, 0.433, -0.680, -1.815,
-0.915, 1.345, 1.572, -0.622};
// Generate an environment map based off of c_red, c_green, and c_blue
DefaultImage env_map(64, 32); // This does not need to be a large map for the test
for (int t = 0; t < env_map.height(); t++) {
double theta = (t + 0.5) * M_PI / env_map.height();
for (int p = 0; p < env_map.width(); p++) {
double phi = (p + 0.5) * 2.0 * M_PI / env_map.width();
env_map.SetPixel(p, t, Eigen::Array3f(EvalSHSum(2, c_red, phi, theta),
EvalSHSum(2, c_green, phi, theta),
EvalSHSum(2, c_blue, phi, theta)));
}
}
// Fit the environment to spherical functions. Given that we formed it from
// the spherical basis we should get a very near perfect fit.
std::unique_ptr<std::vector<Eigen::Array3f>> c_fit = ProjectEnvironment(
2, env_map);
ASSERT_TRUE(c_fit != nullptr);
for (int i = 0; i < 9; i++) {
Eigen::Array3f fitted = (*c_fit)[i];
EXPECT_NEAR(c_red[i], fitted(0), kCoeffErr);
EXPECT_NEAR(c_green[i], fitted(1), kCoeffErr);
EXPECT_NEAR(c_blue[i], fitted(2), kCoeffErr);
}
}
TEST(SphericalHarmonicsTest, EvalSHSum) {
const std::vector<double> coeffs = {-1.119, 0.559, 0.433, -0.680, -1.815,
-0.915, 1.345, 1.572, -0.622};
double expected = 0.0;
for (int l = 0; l <= 2; l++) {
for (int m = -l; m <= l; m++) {
expected += coeffs[GetIndex(l, m)] * EvalSH(l, m, M_PI / 4, M_PI / 4);
}
}
EXPECT_EQ(expected, EvalSHSum(2, coeffs, M_PI / 4, M_PI / 4));
}
TEST(SphericalHarmonicsTest, EvalSHSumArray3f) {
const std::vector<double> coeffs_0 = {-1.119, 0.559, 0.433, -0.680, -1.815,
-0.915, 1.345, 1.572, -0.622};
const std::vector<double> coeffs_1 = {-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.818, 1.482};
const std::vector<double> coeffs_2 = {-1.119, 0.559, 0.433, -0.680, -1.815,
-0.915, 1.345, 1.572, -0.622};
std::vector<Eigen::Array3f> coeffs;
for (unsigned int i = 0; i < coeffs_0.size(); i++) {
coeffs.push_back(Eigen::Array3f(coeffs_0[i], coeffs_1[i], coeffs_2[i]));
}
Eigen::Array3f expected(0.0, 0.0, 0.0);
for (int l = 0; l <= 2; l++) {
for (int m = -l; m <= l; m++) {
expected += coeffs[GetIndex(l, m)] * EvalSH(l, m, M_PI / 4, M_PI / 4);
}
}
Eigen::Array3f actual = EvalSHSum(2, coeffs, M_PI / 4, M_PI / 4);
EXPECT_TUPLE3_NEAR(expected, actual, kEpsilon);
}
TEST(SphericalHarmonicsTest, GetIndex) {
// Indices are arranged from low band to high degree, and from low order
// to high order within a band.
EXPECT_EQ(0, GetIndex(0, 0));
EXPECT_EQ(1, GetIndex(1, -1));
EXPECT_EQ(2, GetIndex(1, 0));
EXPECT_EQ(3, GetIndex(1, 1));
EXPECT_EQ(4, GetIndex(2, -2));
EXPECT_EQ(5, GetIndex(2, -1));
EXPECT_EQ(6, GetIndex(2, 0));
EXPECT_EQ(7, GetIndex(2, 1));
EXPECT_EQ(8, GetIndex(2, 2));
}
TEST(SphericalHarmonicsTest, GetCoefficientCount) {
// For up to order n SH representation, there are (n+1)^2 coefficients.
EXPECT_EQ(1, GetCoefficientCount(0));
EXPECT_EQ(9, GetCoefficientCount(2));
EXPECT_EQ(16, GetCoefficientCount(3));
}
TEST(SphericalHarmonicsTest, ToVector) {
// Compare spherical coordinates with their known direction vectors.
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(1, 0, 0), ToVector(0.0, M_PI / 2),
kEpsilon);
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(0, 1, 0), ToVector(M_PI / 2, M_PI / 2),
kEpsilon);
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(0, 0, 1), ToVector(0.0, 0.0), kEpsilon);
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(0.5, 0.5, sqrt(0.5)),
ToVector(M_PI / 4, M_PI / 4), kEpsilon);
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(0.5, 0.5, -sqrt(0.5)),
ToVector(M_PI / 4, 3 * M_PI / 4), kEpsilon);
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(-0.5, 0.5, -sqrt(0.5)),
ToVector(3 * M_PI / 4, 3 * M_PI / 4), kEpsilon);
EXPECT_TUPLE3_NEAR(Eigen::Vector3d(0.5, -0.5, -sqrt(0.5)),
ToVector(-M_PI / 4, 3 * M_PI / 4), kEpsilon);
}
TEST(SphericalHarmonicsTest, ToSphericalCoords) {
// Compare vectors with their known spherical coordinates.
double phi, theta;
ToSphericalCoords(Eigen::Vector3d(1, 0, 0), &phi, &theta);
EXPECT_EQ(0.0, phi);
EXPECT_EQ(M_PI / 2, theta);
ToSphericalCoords(Eigen::Vector3d(0, 1, 0), &phi, &theta);
EXPECT_EQ(M_PI / 2, phi);
EXPECT_EQ(M_PI / 2, theta);
ToSphericalCoords(Eigen::Vector3d(0, 0, 1), &phi, &theta);
EXPECT_EQ(0.0, phi);
EXPECT_EQ(0.0, theta);
ToSphericalCoords(Eigen::Vector3d(0.5, 0.5, sqrt(0.5)), &phi, &theta);
EXPECT_EQ(M_PI / 4, phi);
EXPECT_EQ(M_PI / 4, theta);
ToSphericalCoords(Eigen::Vector3d(0.5, 0.5, -sqrt(0.5)), &phi, &theta);
EXPECT_EQ(M_PI / 4, phi);
EXPECT_EQ(3 * M_PI / 4, theta);
ToSphericalCoords(Eigen::Vector3d(-0.5, 0.5, -sqrt(0.5)), &phi, &theta);
EXPECT_EQ(3 * M_PI / 4, phi);
EXPECT_EQ(3 * M_PI / 4, theta);
ToSphericalCoords(Eigen::Vector3d(0.5, -0.5, -sqrt(0.5)), &phi, &theta);
EXPECT_EQ(-M_PI / 4, phi);
EXPECT_EQ(3 * M_PI / 4, theta);
}
TEST(SphericalHarmonicsTest, EvalSHSlow) {
// Compare the general SH implementation to the closed form functions for
// several bands, from: http://en.wikipedia.org/wiki/Table_of_spherical_harmonics#Real_spherical_harmonics
// It's assumed that if the implementation matches these for this subset, the
// probability it's correct overall is high.
//
// Note that for all cases |m|=1 below, we negate compared to what Wikipedia
// lists. After careful review, it seems they do not include the (-1)^m term
// (the Condon-Shortley phase) in their calculations.
const double phi = M_PI / 4;
const double theta = M_PI / 3;
const Eigen::Vector3d d = ToVector(phi, theta);
// l = 0
EXPECT_NEAR(0.5 * sqrt(1 / M_PI), EvalSHSlow(0, 0, phi, theta), kEpsilon);
// l = 1, m = -1
EXPECT_NEAR(-sqrt(3 / (4 * M_PI)) * d.y(), EvalSHSlow(1, -1, phi, theta),
kEpsilon);
// l = 1, m = 0
EXPECT_NEAR(sqrt(3 / (4 * M_PI)) * d.z(), EvalSHSlow(1, 0, phi, theta),
kEpsilon);
// l = 1, m = 1
EXPECT_NEAR(-sqrt(3 / (4 * M_PI)) * d.x(), EvalSHSlow(1, 1, phi, theta),
kEpsilon);
// l = 2, m = -2
EXPECT_NEAR(0.5 * sqrt(15 / M_PI) * d.x() * d.y(),
EvalSHSlow(2, -2, phi, theta), kEpsilon);
// l = 2, m = -1
EXPECT_NEAR(-0.5 * sqrt(15 / M_PI) * d.y() * d.z(),
EvalSHSlow(2, -1, phi, theta), kEpsilon);
// l = 2, m = 0
EXPECT_NEAR(0.25 * sqrt(5 / M_PI) *
(-d.x() * d.x() - d.y() * d.y() + 2 * d.z() * d.z()),
EvalSHSlow(2, 0, phi, theta), kEpsilon);
// l = 2, m = 1
EXPECT_NEAR(-0.5 * sqrt(15 / M_PI) * d.z() * d.x(),
EvalSHSlow(2, 1, phi, theta), kEpsilon);
// l = 2, m = 2
EXPECT_NEAR(0.25 * sqrt(15 / M_PI) * (d.x() * d.x() - d.y() * d.y()),
EvalSHSlow(2, 2, phi, theta), kEpsilon);
}
TEST(SphericalHarmonicsTest, EvalSHHardcoded) {
// Arbitrary coordinates
const double phi = 0.4296;
const double theta = 1.73234;
const Eigen::Vector3d d = ToVector(phi, theta);
for (int l = 0; l <= 4; l++) {
for (int m = -l; m <= l; m++) {
double expected = EvalSHSlow(l, m, phi, theta);
EXPECT_NEAR(expected, EvalSH(l, m, phi, theta), kHardcodedError)
<< "Spherical coord failure for l, m = (" << l << ", " << m << ")";
EXPECT_NEAR(expected, EvalSH(l, m, d), kHardcodedError)
<< "Vector failure for l, m = (" << l << ", " << m << ")";
}
}
}
TEST(SphericalHarmonicsDeathTest, EvalSHBadInputs) {
const double phi = M_PI / 4;
const double theta = M_PI / 3;
const Eigen::Vector3d d = ToVector(phi, theta);
// l < 0
EXPECT_DEATH(EvalSH(-1, 0, phi, theta), "l must be at least 0.");
EXPECT_DEATH(EvalSH(-1, 0, d), "l must be at least 0.");
// m > l
EXPECT_DEATH(EvalSH(1, 2, phi, theta), "m must be between -l and l.");
EXPECT_DEATH(EvalSH(1, 2, d), "m must be between -l and l.");
// m < -l
EXPECT_DEATH(EvalSH(1, -2, phi, theta), "m must be between -l and l.");
EXPECT_DEATH(EvalSH(1, -2, phi, theta), "m must be between -l and l.");
}
TEST(SphericalHarmonicsDeathTest, ProjectFunctionBadInputs) {
const std::vector<double> coeffs = {-1.028};
SphericalFunction func = [&] (double phi, double theta) {
return EvalSHSum(0, coeffs, phi, theta); };
// order < 0
EXPECT_DEATH(ProjectFunction(-1, func, kTestSampleCount),
"Order must be at least zero.");
// sample_count <= 0
EXPECT_DEATH(ProjectFunction(2, func, 0),
"Sample count must be at least one.");
EXPECT_DEATH(ProjectFunction(2, func, -1),
"Sample count must be at least one.");
}
TEST(SphericalHarmonicsDeathTest, ProjectEnvironmentBadInputs) {
DefaultImage env(64, 32);
// order < 0
EXPECT_DEATH(ProjectEnvironment(-1, env), "Order must be at least zero.");
}
TEST(SphericalHarmonicsDeathTest, ProjectSparseSamplesBadInputs) {
// These are the expected coefficients that define the sparse samples of
// the underyling spherical function
const std::vector<double> coeffs = {-0.591};
// Generate sparse samples
std::vector<Eigen::Vector3d> sample_dirs;
std::vector<double> sample_vals;
for (int t = 0; t < 6; t++) {
double theta = t * M_PI / 6.0;
for (int p = 0; p < 8; p++) {
double phi = p * 2.0 * M_PI / 8.0;
Eigen::Vector3d dir = ToVector(phi, theta);
double value = EvalSHSum<double>(0, coeffs, phi, theta);
sample_dirs.push_back(dir);
sample_vals.push_back(value);
}
}
// order < 0
EXPECT_DEATH(ProjectSparseSamples(-1, sample_dirs, sample_vals),
"Order must be at least zero.");
// unequal directions and values
sample_vals.push_back(0.4);
EXPECT_DEATH(ProjectSparseSamples(2, sample_dirs, sample_vals),
"Directions and values must have the same size.");
}
TEST(SphericalHarmonicsDeathTest, EvalSHSumBadInputs) {
// These are the expected coefficients that define the sparse samples of
// the underyling spherical function
const std::vector<double> coeffs = {-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.818, 1.482};
EXPECT_DEATH(EvalSHSum(3, coeffs, M_PI / 4, M_PI / 4),
"Incorrect number of coefficients provided.");
}
TEST(SphericalHarmonicsDeathTest, ToSphericalCoordsBadInputs) {
double phi, theta;
EXPECT_DEATH(ToSphericalCoords(Eigen::Vector3d(2.0, 0.0, 0.4), &phi, &theta),
"");
}
TEST(SphericalHarmonicsRotationTest, ClosedFormZAxisRotation) {
// The band-level rotation matrices for a rotation about the z-axis are
// relatively simple so we can compute them closed form and make sure the
// recursive general approach works properly.
// This closed form comes from [1].
double alpha = M_PI / 4.0;
Eigen::Quaterniond rz(Eigen::AngleAxisd(alpha, Eigen::Vector3d::UnitZ()));
std::unique_ptr<Rotation> rz_sh(Rotation::Create(3, rz));
// order 0
Eigen::MatrixXd r0(1, 1);
r0 << 1.0;
ExpectMatrixNear(r0, rz_sh->band_rotation(0), kEpsilon);
// order 1
Eigen::MatrixXd r1(3, 3);
r1 << cos(alpha), 0, sin(alpha),
0, 1, 0,
-sin(alpha), 0, cos(alpha);
ExpectMatrixNear(r1, rz_sh->band_rotation(1), kEpsilon);
// order 2
Eigen::MatrixXd r2(5, 5);
r2 << cos(2 * alpha), 0, 0, 0, sin(2 * alpha),
0, cos(alpha), 0, sin(alpha), 0,
0, 0, 1, 0, 0,
0, -sin(alpha), 0, cos(alpha), 0,
-sin(2 * alpha), 0, 0, 0, cos(2 * alpha);
ExpectMatrixNear(r2, rz_sh->band_rotation(2), kEpsilon);
// order 3
Eigen::MatrixXd r3(7, 7);
r3 << cos(3 * alpha), 0, 0, 0, 0, 0, sin(3 * alpha),
0, cos(2 * alpha), 0, 0, 0, sin(2 * alpha), 0,
0, 0, cos(alpha), 0, sin(alpha), 0, 0,
0, 0, 0, 1, 0, 0, 0,
0, 0, -sin(alpha), 0, cos(alpha), 0, 0,
0, -sin(2 * alpha), 0, 0, 0, cos(2 * alpha), 0,
-sin(3 * alpha), 0, 0, 0, 0, 0, cos(3 * alpha);
ExpectMatrixNear(r3, rz_sh->band_rotation(3), kEpsilon);
}
TEST(SphericalHarmonicsRotationTest, ClosedFormBands) {
// Use an arbitrary rotation
Eigen::Quaterniond r(Eigen::AngleAxisd(
0.423, Eigen::Vector3d(0.234, -0.642, 0.829).normalized()));
Eigen::Matrix3d r_mat = r.toRotationMatrix();
// Create rotation for band 1 and 2
std::unique_ptr<Rotation> sh_rot(Rotation::Create(2, r));
// For l = 1, the transformation matrix for the coefficients is relatively
// easy to derive. If R is the rotation matrix, the elements of the transform
// can be described as: Mij = integral_over_sphere Yi(R * s)Yj(s) ds.
// For l = 1, we have:
// Y0(s) = -0.5sqrt(3/pi)s.y
// Y1(s) = 0.5sqrt(3/pi)s.z
// Y2(s) = -0.5sqrt(3/pi)s.x
// Note that these Yi include the Condon-Shortely phase. The expectent matrix
// M is equal to:
// [ R11 -R12 R10
// -R21 R22 -R20
// R01 -R02 R00 ]
// In [1]'s Appendix summarizing [4], this is given without the negative signs
// and is a simple permutation, but that is because [4] does not include the
// Condon-Shortely phase in their definition of the SH basis functions.
Eigen::Matrix3d band_1 = sh_rot->band_rotation(1);
EXPECT_DOUBLE_EQ(r_mat(1, 1), band_1(0, 0));
EXPECT_DOUBLE_EQ(-r_mat(1, 2), band_1(0, 1));
EXPECT_DOUBLE_EQ(r_mat(1, 0), band_1(0, 2));
EXPECT_DOUBLE_EQ(-r_mat(2, 1), band_1(1, 0));
EXPECT_DOUBLE_EQ(r_mat(2, 2), band_1(1, 1));
EXPECT_DOUBLE_EQ(-r_mat(2, 0), band_1(1, 2));
EXPECT_DOUBLE_EQ(r_mat(0, 1), band_1(2, 0));
EXPECT_DOUBLE_EQ(-r_mat(0, 2), band_1(2, 1));
EXPECT_DOUBLE_EQ(r_mat(0, 0), band_1(2, 2));
// The l = 2 band transformation is significantly more complex in terms of R,
// and a CAS program was used to arrive at these equations (plus a fair
// amount of simplification by hand afterwards).
Eigen::MatrixXd band_2 = sh_rot->band_rotation(2);
EXPECT_NEAR(r_mat(0, 0) * r_mat(1, 1) + r_mat(0, 1) * r_mat(1, 0),
band_2(0, 0), kEpsilon);
EXPECT_NEAR(-r_mat(0, 1) * r_mat(1, 2) - r_mat(0, 2) * r_mat(1, 1),
band_2(0, 1), kEpsilon);
EXPECT_NEAR(-sqrt(3) / 3 * (r_mat(0, 0) * r_mat(1, 0) +
r_mat(0, 1) * r_mat(1, 1) -
2 * r_mat(0, 2) * r_mat(1, 2)),
band_2(0, 2), kEpsilon);
EXPECT_NEAR(-r_mat(0, 0) * r_mat(1, 2) - r_mat(0, 2) * r_mat(1, 0),
band_2(0, 3), kEpsilon);
EXPECT_NEAR(r_mat(0, 0) * r_mat(1, 0) - r_mat(0, 1) * r_mat(1, 1),
band_2(0, 4), kEpsilon);
EXPECT_NEAR(-r_mat(1, 0) * r_mat(2, 1) - r_mat(1, 1) * r_mat(2, 0),
band_2(1, 0), kEpsilon);
EXPECT_NEAR(r_mat(1, 1) * r_mat(2, 2) + r_mat(1, 2) * r_mat(2, 1),
band_2(1, 1), kEpsilon);
EXPECT_NEAR(sqrt(3) / 3* (r_mat(1, 0) * r_mat(2, 0) +
r_mat(1, 1) * r_mat(2, 1) -
2 * r_mat(1, 2) * r_mat(2, 2)),
band_2(1, 2), kEpsilon);
EXPECT_NEAR(r_mat(1, 0) * r_mat(2, 2) + r_mat(1, 2) * r_mat(2, 0),
band_2(1, 3), kEpsilon);
EXPECT_NEAR(-r_mat(1, 0) * r_mat(2, 0) + r_mat(1, 1) * r_mat(2, 1),
band_2(1, 4), kEpsilon);
EXPECT_NEAR(-sqrt(3) / 3 * (r_mat(0, 0) * r_mat(0, 1) +
r_mat(1, 0) * r_mat(1, 1) -
2 * r_mat(2, 0) * r_mat(2, 1)),
band_2(2, 0), kEpsilon);
EXPECT_NEAR(sqrt(3) / 3 * (r_mat(0, 1) * r_mat(0, 2) +
r_mat(1, 1) * r_mat(1, 2) -
2 * r_mat(2, 1) * r_mat(2, 2)),
band_2(2, 1), kEpsilon);
EXPECT_NEAR(-0.5 * (1 - 3 * r_mat(2, 2) * r_mat(2, 2)),
band_2(2, 2), kEpsilon);
EXPECT_NEAR(sqrt(3) / 3 * (r_mat(0, 0) * r_mat(0, 2) +
r_mat(1, 0) * r_mat(1, 2) -
2 * r_mat(2, 0) * r_mat(2, 2)),
band_2(2, 3), kEpsilon);
EXPECT_NEAR(sqrt(3) / 6 * (-r_mat(0, 0) * r_mat(0, 0) +
r_mat(0, 1) * r_mat(0, 1) -
r_mat(1, 0) * r_mat(1, 0) +
r_mat(1, 1) * r_mat(1, 1) +
2 * r_mat(2, 0) * r_mat(2, 0) -
2 * r_mat(2, 1) * r_mat(2, 1)),
band_2(2, 4), kEpsilon);
EXPECT_NEAR(-r_mat(0, 0) * r_mat(2, 1) - r_mat(0, 1) * r_mat(2, 0),
band_2(3, 0), kEpsilon);
EXPECT_NEAR(r_mat(0, 1) * r_mat(2, 2) + r_mat(0, 2) * r_mat(2, 1),
band_2(3, 1), kEpsilon);
EXPECT_NEAR(sqrt(3) / 3 * (r_mat(0, 0) * r_mat(2, 0) +
r_mat(0, 1) * r_mat(2, 1) -
2 * r_mat(0, 2) * r_mat(2, 2)),
band_2(3, 2), kEpsilon);
EXPECT_NEAR(r_mat(0, 0) * r_mat(2, 2) + r_mat(0, 2) * r_mat(2, 0),
band_2(3, 3), kEpsilon);
EXPECT_NEAR(-r_mat(0, 0) * r_mat(2, 0) + r_mat(0, 1) * r_mat(2, 1),
band_2(3, 4), kEpsilon);
EXPECT_NEAR(r_mat(0, 0) * r_mat(0, 1) - r_mat(1, 0) * r_mat(1, 1),
band_2(4, 0), kEpsilon);
EXPECT_NEAR(-r_mat(0, 1) * r_mat(0, 2) + r_mat(1, 1) * r_mat(1, 2),
band_2(4, 1), kEpsilon);
EXPECT_NEAR(sqrt(3) / 6 * (-r_mat(0, 0) * r_mat(0, 0) -
r_mat(0, 1) * r_mat(0, 1) +
r_mat(1, 0) * r_mat(1, 0) +
r_mat(1, 1) * r_mat(1, 1) +
2 * r_mat(0, 2) * r_mat(0, 2) -
2 * r_mat(1, 2) * r_mat(1, 2)),
band_2(4, 2), kEpsilon);
EXPECT_NEAR(-r_mat(0, 0) * r_mat(0, 2) + r_mat(1, 0) * r_mat(1, 2),
band_2(4, 3), kEpsilon);
EXPECT_NEAR(0.5 * (r_mat(0, 0) * r_mat(0, 0) -
r_mat(0, 1) * r_mat(0, 1) -
r_mat(1, 0) * r_mat(1, 0) +
r_mat(1, 1) * r_mat(1, 1)),
band_2(4, 4), kEpsilon);
}
TEST(SphericalHarmonicsRotationTest, CreateFromSHRotation) {
Eigen::Quaterniond r(Eigen::AngleAxisd(M_PI / 4.0, Eigen::Vector3d::UnitY()));
std::unique_ptr<Rotation> low_band(Rotation::Create(3, r));
std::unique_ptr<Rotation> high_band(Rotation::Create(5, r));
std::unique_ptr<Rotation> from_low(Rotation::Create(5, *low_band));
for (int l = 0; l <= 5; l++) {
ExpectMatrixNear(high_band->band_rotation(l),
from_low->band_rotation(l), kEpsilon);
}
}
TEST(SphericalHarmonicsRotationTest, RotateSymmetricFunction) {
SphericalFunction function = [] (double phi, double theta) {
Eigen::Vector3d d = ToVector(phi, theta);
return Eigen::Vector3d::UnitZ().dot(d);
};
std::unique_ptr<std::vector<double>> coeff = ProjectFunction(
3, function, kTestSampleCount);
// Rotation about the z-axis, but the function used is rotationally symmetric
// about the z-axis so the rotated coefficients should have little change.
for (double angle = 0.0; angle < 2.0 * M_PI; angle += M_PI / 8) {
Eigen::Quaterniond r1(Eigen::AngleAxisd(angle, Eigen::Vector3d::UnitZ()));
std::unique_ptr<Rotation> r1_sh(Rotation::Create(3, r1));
std::vector<double> r1_coeff;
r1_sh->Apply(*coeff, &r1_coeff);
// Compare the rotated coefficients to the coefficients fitted to the
// rotated source function.
for (int i = 0; i < 16; i++) {
// r1 was a rotation about the z-axis, so even though the rotation isn't
// the identity, the transformed coefficients should be equal to the
// original coefficients.
EXPECT_NEAR((*coeff)[i], r1_coeff[i], kCoeffErr);
}
}
// Rotate about more arbitrary angles to test a simple function's rotation.
Eigen::Vector3d axis = Eigen::Vector3d(0.234, -0.642, 0.829).normalized();
std::vector<double> rotated_coeff;
for (double angle = 0.0; angle < 2.0 * M_PI; angle += M_PI / 8) {
Eigen::Quaterniond rotation(Eigen::AngleAxisd(angle, axis));
Eigen::Quaterniond r_inv = rotation.inverse();
std::unique_ptr<Rotation> r_sh(Rotation::Create(3, rotation));
SphericalFunction rotated_function = [&] (double p, double t) {
const Eigen::Vector3d n(0, 0, 1);
Eigen::Vector3d d = r_inv * ToVector(p, t);
return n.dot(d);
};
std::unique_ptr<std::vector<double>> expected_coeff = ProjectFunction(
3, rotated_function, kTestSampleCount);
r_sh->Apply(*coeff, &rotated_coeff);
for (int i = 0; i < 16; i++) {
EXPECT_NEAR((*expected_coeff)[i], rotated_coeff[i], kCoeffErr);
}
}
}
TEST(SphericalHarmonicsRotationTest, RotateComplexFunction) {
const std::vector<double> coeff = {-1.028, 0.779, -0.275, 0.601, -0.256,
1.891, -1.658, -0.370, -0.772,
-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.370, -0.772,
-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543};
Eigen::Vector3d axis = Eigen::Vector3d(-.43, 0.19, 0.634).normalized();
std::vector<double> rotated_coeff;
for (double angle = 0.0; angle < 2.0 * M_PI; angle += M_PI / 8) {
Eigen::Quaterniond rotation(Eigen::AngleAxisd(angle, axis));
Eigen::Quaterniond r_inv = rotation.inverse();
std::unique_ptr<Rotation> r_sh(Rotation::Create(4, rotation));
SphericalFunction rotated_function = [&] (double p, double t) {
ToSphericalCoords(r_inv * ToVector(p, t), &p, &t);
return EvalSHSum(4, coeff, p, t);
};
std::unique_ptr<std::vector<double>> expected_coeff = ProjectFunction(
4, rotated_function, kTestSampleCount);
r_sh->Apply(coeff, &rotated_coeff);
for (int i = 0; i < 25; i++) {
EXPECT_NEAR((*expected_coeff)[i], rotated_coeff[i], kCoeffErr);
}
}
}
TEST(SphericalHarmonicsRotationTest, RotateInPlace) {
// Rotate the function about the y axis by pi/4, which is no longer an
// identity for the SH coefficients
const Eigen::Quaterniond r(Eigen::AngleAxisd(
M_PI / 4.0, Eigen::Vector3d::UnitY()));
const Eigen::Quaterniond r_inv = r.inverse();
std::unique_ptr<Rotation> r_sh(Rotation::Create(3, r));
SphericalFunction function = [] (double phi, double theta) {
Eigen::Vector3d d = ToVector(phi, theta);
return Clamp(Eigen::Vector3d::UnitZ().dot(d), 0.0, 1.0);
};
SphericalFunction rotated_function = [&] (double phi, double theta) {
Eigen::Vector3d d = r_inv * ToVector(phi, theta);
return Clamp(Eigen::Vector3d::UnitZ().dot(d), 0.0, 1.0);
};
std::unique_ptr<std::vector<double>> coeff = ProjectFunction(
3, function, kTestSampleCount);
std::unique_ptr<std::vector<double>> rotated_coeff = ProjectFunction(
3, rotated_function, kTestSampleCount);
r_sh->Apply(*coeff, coeff.get());
// Compare the rotated coefficients to the coefficients fitted to the
// rotated source function
for (int i = 0; i < 16; i++) {
EXPECT_NEAR((*rotated_coeff)[i], (*coeff)[i], kCoeffErr);
}
}
TEST(SphericalHarmonicsRotationTest, RotateArray3f) {
// The coefficients for red, green, and blue channels
const std::vector<double> c_red = {-1.028, 0.779, -0.275, 0.601, -0.256,
1.891, -1.658, -0.370, -0.772};
const std::vector<double> c_green = {-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.818, 1.482};
const std::vector<double> c_blue = {-1.119, 0.559, 0.433, -0.680, -1.815,
-0.915, 1.345, 1.572, -0.622};
// Combined as an Array3f
std::vector<Eigen::Array3f> combined;
for (unsigned int i = 0; i < c_red.size(); i++) {
combined.push_back(Eigen::Array3f(c_red[i], c_green[i], c_blue[i]));
}
// Rotate the function about the y axis by pi/4, which is no longer an
// identity for the SH coefficients
const Eigen::Quaterniond r(Eigen::AngleAxisd(
M_PI / 4.0, Eigen::Vector3d::UnitY()));
std::unique_ptr<Rotation> r_sh(Rotation::Create(2, r));
std::vector<double> rotated_r;
std::vector<double> rotated_g;
std::vector<double> rotated_b;
std::vector<Eigen::Array3f> rotated_combined;
r_sh->Apply(c_red, &rotated_r);
r_sh->Apply(c_green, &rotated_g);
r_sh->Apply(c_blue, &rotated_b);
r_sh->Apply(combined, &rotated_combined);
for (unsigned int i = 0; i < c_red.size(); i++) {
EXPECT_FLOAT_EQ(rotated_r[i], rotated_combined[i](0));
EXPECT_FLOAT_EQ(rotated_g[i], rotated_combined[i](1));
EXPECT_FLOAT_EQ(rotated_b[i], rotated_combined[i](2));
}
}
TEST(SphericalHarmonicsRotationTest, RotateArray3fInPlace) {
// The coefficients for red, green, and blue channels
const std::vector<double> c_red = {-1.028, 0.779, -0.275, 0.601, -0.256,
1.891, -1.658, -0.370, -0.772};
const std::vector<double> c_green = {-0.591, -0.713, 0.191, 1.206, -0.587,
-0.051, 1.543, -0.818, 1.482};
const std::vector<double> c_blue = {-1.119, 0.559, 0.433, -0.680, -1.815,
-0.915, 1.345, 1.572, -0.622};
// Combined as an Array3f
std::vector<Eigen::Array3f> combined;
for (unsigned int i = 0; i < c_red.size(); i++) {
combined.push_back(Eigen::Array3f(c_red[i], c_green[i], c_blue[i]));
}
// Rotate the function about the y axis by pi/4, which is no longer an
// identity for the SH coefficients
const Eigen::Quaterniond r(Eigen::AngleAxisd(
M_PI / 4.0, Eigen::Vector3d::UnitY()));
std::unique_ptr<Rotation> r_sh(Rotation::Create(2, r));
std::vector<double> rotated_r;
std::vector<double> rotated_g;
std::vector<double> rotated_b;
r_sh->Apply(c_red, &rotated_r);
r_sh->Apply(c_green, &rotated_g);
r_sh->Apply(c_blue, &rotated_b);
r_sh->Apply(combined, &combined);
for (unsigned int i = 0; i < c_red.size(); i++) {
EXPECT_FLOAT_EQ(rotated_r[i], combined[i](0));
EXPECT_FLOAT_EQ(rotated_g[i], combined[i](1));
EXPECT_FLOAT_EQ(rotated_b[i], combined[i](2));
}
}
TEST(SphericalHarmonicsRotationDeathTest, CreateFromMatrixBadInputs) {
Eigen::Quaterniond good_r(Eigen::AngleAxisd(
M_PI / 4.0, Eigen::Vector3d::UnitY()));
Eigen::Quaterniond bad_r(0.0, 0.0, 0.1, 0.3);
EXPECT_DEATH(Rotation::Create(-1, good_r), "Order must be at least 0.");
EXPECT_DEATH(Rotation::Create(2, bad_r), "Rotation must be normalized.");
}
TEST(SphericalHarmonicsRotationDeathTest, CreateFromSHBadInputs) {
Eigen::Quaterniond good_r(Eigen::AngleAxisd(
M_PI / 4.0, Eigen::Vector3d::UnitY()));
std::unique_ptr<Rotation> good_sh(Rotation::Create(2, good_r));
EXPECT_DEATH(Rotation::Create(-1, *good_sh), "Order must be at least 0.");
}
TEST(SphericalHarmonicsRotationDeathTest, RotateBadInputs) {
Eigen::Quaterniond r(Eigen::AngleAxisd(M_PI / 4.0, Eigen::Vector3d::UnitY()));
std::unique_ptr<Rotation> sh(Rotation::Create(2, r));
const std::vector<double> bad_coeffs = {-0.459, 0.3242};
std::vector<double> result;
EXPECT_DEATH(sh->Apply(bad_coeffs, &result),
"Incorrect number of coefficients provided.");
}
TEST(SphericalHarmonicsTest, ImageCoordsToSphericalCoordsTest) {
// Half-pixel angular increments.
double pixel_phi = M_PI / kImageWidth;
double pixel_theta = 0.5 * M_PI / kImageHeight;
EXPECT_NEAR(pixel_phi, ImageXToPhi(0, kImageWidth), kEpsilon);
EXPECT_NEAR(pixel_theta, ImageYToTheta(0, kImageHeight), kEpsilon);
EXPECT_NEAR(M_PI + pixel_phi, ImageXToPhi(kImageWidth / 2, kImageWidth),
kEpsilon);
EXPECT_NEAR(M_PI / 2 + pixel_theta,
ImageYToTheta(kImageHeight / 2, kImageHeight), kEpsilon);
EXPECT_NEAR(2 * M_PI - pixel_phi, ImageXToPhi(kImageWidth - 1, kImageWidth),
kEpsilon);
EXPECT_NEAR(M_PI - pixel_theta,
ImageYToTheta(kImageHeight - 1, kImageHeight), kEpsilon);
// Out of bounds pixels on either side of the image range
EXPECT_NEAR(-pixel_phi, ImageXToPhi(-1, kImageWidth), kEpsilon);
EXPECT_NEAR(-pixel_theta, ImageYToTheta(-1, kImageHeight), kEpsilon);
EXPECT_NEAR(2 * M_PI + pixel_phi, ImageXToPhi(kImageWidth, kImageWidth),
kEpsilon);
EXPECT_NEAR(M_PI + pixel_theta, ImageYToTheta(kImageHeight, kImageHeight),
kEpsilon);
}
TEST(SphericalHarmonicsTest, SphericalCoordsToImageCoordsTest) {
EXPECT_TUPLE2_NEAR(Eigen::Vector2d(0.0, 0.0),
ToImageCoords(0.0, 0.0, kImageWidth, kImageHeight),
kEpsilon);
EXPECT_TUPLE2_NEAR(Eigen::Vector2d(kImageWidth / 2.0, kImageHeight / 2.0),
ToImageCoords(M_PI, M_PI / 2.0, kImageWidth, kImageHeight),
kEpsilon);
EXPECT_TUPLE2_NEAR(Eigen::Vector2d(kImageWidth, kImageHeight),
ToImageCoords(2 * M_PI - kEpsilon * 1e-3,
M_PI, kImageWidth, kImageHeight), kEpsilon);
// Out of the normal phi, theta ranges
// Negative rotation half a pixel past 0 in the xy plane and a negative
// rotation half a pixel past 0 in the z axis. The equivalent in-range angles
// are a half pixel rotation along the z axis and a rotation just shy of
// 180 in the xy plane (full rotation to bring it into range, and a 180
// adjust for the z axis).
EXPECT_TUPLE2_NEAR(Eigen::Vector2d(kImageWidth / 2 - 0.5, 0.5),
ToImageCoords(-M_PI / kImageWidth,
-0.5 * M_PI / kImageHeight,
kImageWidth, kImageHeight), kEpsilon);
// A half pixel past one full rotation in the xy plane and the through the
// z axis. The equivalent in-range angles are a half pixel past 180 degrees
// in the xy plane and half a pixel shy of 180 along the z axis.
EXPECT_TUPLE2_NEAR(Eigen::Vector2d(kImageWidth / 2 + 0.5, kImageHeight - 0.5),
ToImageCoords(
(kImageWidth + 0.5) * 2 * M_PI / kImageWidth,
(kImageHeight + 0.5) * M_PI / kImageHeight,
kImageWidth, kImageHeight), kEpsilon);
}
TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceTest) {
// Use a simple function where it's convenient to analytically determine the
// diffuse irradiance. If the light is a constant for the positive z
// hemisphere, and the query normal is the positive z axis, then the diffuse
// irradiance is the integral of cos(theta) over that hemisphere, which is
// equal to:
// int(int(light * cos(theta) * sin(theta), 0, 2pi, phi), 0, pi/2, theta) ->
// 2pi * light * int(cos(theta) * sin(theta), 0, pi/2, theta) ->
// 2pi * light * (-0.5cos(theta)^2|0,pi/2) ->
// pi * light * (-cos(pi/2)+cos(0)) ->
// pi * light
// Set light = 1 for simplicity.
std::unique_ptr<std::vector<double>> env =
ProjectFunction(2, [] (double phi, double theta) {
return theta > M_PI / 2 ? 0.0 : 1.0;
}, 2500);
// Convert it to RGB
std::vector<Eigen::Array3f> env_rgb(9);
for (int i = 0; i < 9; i++) {
env_rgb[i] = Eigen::Array3f((*env)[i], (*env)[i], (*env)[i]);
}
Eigen::Array3f diffuse_irradiance = RenderDiffuseIrradiance(
env_rgb, Eigen::Vector3d::UnitZ());
EXPECT_TUPLE3_NEAR(Eigen::Array3f(M_PI, M_PI, M_PI), diffuse_irradiance,
kIrradianceError);
}
TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceMapTest) {
DefaultImage env_map(kImageWidth, kImageHeight);
DefaultImage expected_diffuse(kImageWidth, kImageHeight);
GenerateTestEnvironment(&env_map);
ComputeExplicitDiffuseIrradiance(env_map, &expected_diffuse);
DefaultImage rendered_diffuse(kImageWidth, kImageHeight);
RenderDiffuseIrradianceMap(env_map, &rendered_diffuse);
// Compare the two diffuse irradiance maps
for (int y = 0; y < expected_diffuse.height(); y++) {
for (int x = 0; x < expected_diffuse.width(); x++) {
Eigen::Array3f expected = expected_diffuse.GetPixel(x, y);
Eigen::Array3f rendered = rendered_diffuse.GetPixel(x, y);
EXPECT_TUPLE3_NEAR(expected, rendered, kEnvMapIrradianceError);
}
}
}
TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceMoreCoefficientsTest) {
// Coefficients of the expected size (9).
std::vector<Eigen::Array3f> coeffs9;
for (int i = 0; i < 9; i++) {
float c = static_cast<float>(i);
coeffs9.push_back({c, c, c});
}
// Coefficients equal to coeffs9 for the first 9 and then 3 additional
// coefficients that should be ignored.
std::vector<Eigen::Array3f> coeffs12;
for (int i = 0; i < 12; i++) {
float c = static_cast<float>(i);
coeffs12.push_back({c, c, c});
}
EXPECT_TUPLE3_NEAR(
RenderDiffuseIrradiance(coeffs9, Eigen::Vector3d::UnitZ()),
RenderDiffuseIrradiance(coeffs12, Eigen::Vector3d::UnitZ()), kEpsilon);
}
TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceFewCoefficientsTest) {
std::vector<Eigen::Array3f> coeffs3;
for (int i = 0; i < 3; i++) {
float c = static_cast<float>(i);
coeffs3.push_back({c, c, c});
}
// Coefficients of the expected size (9), padded with zeros so the first 3
// are equal to coeffs3.
std::vector<Eigen::Array3f> coeffs9 = coeffs3;
for (unsigned int i = coeffs3.size(); i < 9; i++) {
coeffs9.push_back({0.0, 0.0, 0.0});
}
EXPECT_TUPLE3_NEAR(RenderDiffuseIrradiance(coeffs9, Eigen::Vector3d::UnitZ()),
RenderDiffuseIrradiance(coeffs3, Eigen::Vector3d::UnitZ()),
kEpsilon);
}
TEST(SphericalHarmonicUtilsTest, RenderDiffuseIrradianceNoCoefficientsTest) {
EXPECT_TUPLE3_NEAR(Eigen::Array3f(0.0, 0.0, 0.0),
RenderDiffuseIrradiance({}, Eigen::Vector3d::UnitZ()),
kEpsilon);
}
} // namespace sh
================================================
FILE: third_party/BUILD
================================================
================================================
FILE: third_party/eigen3.BUILD
================================================
licenses(["restricted"]) # MPL2, portions GPL v3, LGPL v3, BSD-like
exports_files(["LICENSE"])
cc_library(
name = "eigen3",
visibility = ["//visibility:public"],
hdrs = glob(
include = ["Eigen/**"],
exclude = ["Eigen/**/CMakeLists.txt"],
),
defines = ["EIGEN_MPL2_ONLY"],
alwayslink = 1,
)
================================================
FILE: third_party/gtest.BUILD
================================================
cc_library(
name = "main",
srcs = glob(
include = ["googletest/src/*.h",
"googletest/include/gtest/internal/**/*.h",
"googletest/src/*.cc"],
exclude = ["googletest/src/gtest-all.cc"]
),
hdrs = glob(["googletest/include/gtest/*.h"]),
includes = ["googletest/",
"googletest/include"],
linkopts = ["-pthread"],
visibility = ["//visibility:public"],
)
gitextract_mqbhqjx2/
├── .gitignore
├── AUTHORS
├── CONTRIBUTING
├── CONTRIBUTORS
├── LICENSE
├── LICENSE_HEADER
├── README.md
├── WORKSPACE
├── sh/
│ ├── BUILD
│ ├── default_image.cc
│ ├── default_image.h
│ ├── image.h
│ ├── spherical_harmonics.cc
│ ├── spherical_harmonics.h
│ └── spherical_harmonics_test.cc
└── third_party/
├── BUILD
├── eigen3.BUILD
└── gtest.BUILD
SYMBOL INDEX (105 symbols across 6 files)
FILE: sh/default_image.cc
type sh (line 17) | namespace sh {
FILE: sh/default_image.h
function namespace (line 21) | namespace sh {
FILE: sh/image.h
function namespace (line 26) | namespace sh {
FILE: sh/spherical_harmonics.cc
type sh (line 21) | namespace sh {
function Zero (line 48) | double Zero() { return 0.0; }
function Zero (line 49) | float Zero() { return 0.0; }
function Zero (line 50) | Eigen::Array3f Zero() { return Eigen::Array3f::Zero(); }
function Clamp (line 73) | double Clamp(double val, double min, double max) {
function NearByMargin (line 84) | bool NearByMargin(double actual, double expected) {
function FastFMod (line 94) | double FastFMod(double x, double m) {
function HardcodedSH00 (line 103) | double HardcodedSH00(const Eigen::Vector3d& d) {
function HardcodedSH1n1 (line 108) | double HardcodedSH1n1(const Eigen::Vector3d& d) {
function HardcodedSH10 (line 113) | double HardcodedSH10(const Eigen::Vector3d& d) {
function HardcodedSH1p1 (line 118) | double HardcodedSH1p1(const Eigen::Vector3d& d) {
function HardcodedSH2n2 (line 123) | double HardcodedSH2n2(const Eigen::Vector3d& d) {
function HardcodedSH2n1 (line 128) | double HardcodedSH2n1(const Eigen::Vector3d& d) {
function HardcodedSH20 (line 133) | double HardcodedSH20(const Eigen::Vector3d& d) {
function HardcodedSH2p1 (line 138) | double HardcodedSH2p1(const Eigen::Vector3d& d) {
function HardcodedSH2p2 (line 143) | double HardcodedSH2p2(const Eigen::Vector3d& d) {
function HardcodedSH3n3 (line 148) | double HardcodedSH3n3(const Eigen::Vector3d& d) {
function HardcodedSH3n2 (line 153) | double HardcodedSH3n2(const Eigen::Vector3d& d) {
function HardcodedSH3n1 (line 158) | double HardcodedSH3n1(const Eigen::Vector3d& d) {
function HardcodedSH30 (line 164) | double HardcodedSH30(const Eigen::Vector3d& d) {
function HardcodedSH3p1 (line 170) | double HardcodedSH3p1(const Eigen::Vector3d& d) {
function HardcodedSH3p2 (line 176) | double HardcodedSH3p2(const Eigen::Vector3d& d) {
function HardcodedSH3p3 (line 181) | double HardcodedSH3p3(const Eigen::Vector3d& d) {
function HardcodedSH4n4 (line 186) | double HardcodedSH4n4(const Eigen::Vector3d& d) {
function HardcodedSH4n3 (line 191) | double HardcodedSH4n3(const Eigen::Vector3d& d) {
function HardcodedSH4n2 (line 196) | double HardcodedSH4n2(const Eigen::Vector3d& d) {
function HardcodedSH4n1 (line 201) | double HardcodedSH4n1(const Eigen::Vector3d& d) {
function HardcodedSH40 (line 206) | double HardcodedSH40(const Eigen::Vector3d& d) {
function HardcodedSH4p1 (line 212) | double HardcodedSH4p1(const Eigen::Vector3d& d) {
function HardcodedSH4p2 (line 217) | double HardcodedSH4p2(const Eigen::Vector3d& d) {
function HardcodedSH4p3 (line 223) | double HardcodedSH4p3(const Eigen::Vector3d& d) {
function HardcodedSH4p4 (line 228) | double HardcodedSH4p4(const Eigen::Vector3d& d) {
function Factorial (line 240) | double Factorial(int x) {
function DoubleFactorial (line 263) | double DoubleFactorial(int x) {
function EvalLegendrePolynomial (line 291) | double EvalLegendrePolynomial(int l, int m, double x) {
function KroneckerDelta (line 330) | double KroneckerDelta(int i, int j) {
function GetCenteredElement (line 344) | double GetCenteredElement(const Eigen::MatrixXd& r, int i, int j) {
function P (line 354) | double P(int i, int a, int b, int l, const std::vector<Eigen::MatrixXd...
function U (line 376) | double U(int m, int n, int l, const std::vector<Eigen::MatrixXd>& r) {
function V (line 382) | double V(int m, int n, int l, const std::vector<Eigen::MatrixXd>& r) {
function W (line 399) | double W(int m, int n, int l, const std::vector<Eigen::MatrixXd>& r) {
function ComputeUVWCoeff (line 412) | void ComputeUVWCoeff(int m, int n, int l, double* u, double* v, double...
function ComputeBandRotation (line 429) | void ComputeBandRotation(int l, std::vector<Eigen::MatrixXd>* rotation...
function ToVector (line 456) | Eigen::Vector3d ToVector(double phi, double theta) {
function ToSphericalCoords (line 461) | void ToSphericalCoords(const Eigen::Vector3d& dir, double* phi, double...
function ImageXToPhi (line 471) | double ImageXToPhi(int x, int width) {
function ImageYToTheta (line 477) | double ImageYToTheta(int y, int height) {
function ToImageCoords (line 481) | Eigen::Vector2d ToImageCoords(double phi, double theta, int width, int...
function EvalSHSlow (line 503) | double EvalSHSlow(int l, int m, double phi, double theta) {
function EvalSHSlow (line 520) | double EvalSHSlow(int l, int m, const Eigen::Vector3d& dir) {
function EvalSH (line 526) | double EvalSH(int l, int m, double phi, double theta) {
function EvalSH (line 537) | double EvalSH(int l, int m, const Eigen::Vector3d& dir) {
function ProjectFunction (line 619) | std::unique_ptr<std::vector<double>> ProjectFunction(
function ProjectEnvironment (line 667) | std::unique_ptr<std::vector<Eigen::Array3f>> ProjectEnvironment(
function ProjectSparseSamples (line 705) | std::unique_ptr<std::vector<double>> ProjectSparseSamples(
function T (line 747) | T EvalSHSum(int order, const std::vector<T>& coeffs, double phi, doubl...
function T (line 765) | T EvalSHSum(int order, const std::vector<T>& coeffs,
function RenderDiffuseIrradianceMap (line 897) | void RenderDiffuseIrradianceMap(const Image& env_map, Image* diffuse_o...
function RenderDiffuseIrradianceMap (line 903) | void RenderDiffuseIrradianceMap(const std::vector<Eigen::Array3f>& sh_...
function RenderDiffuseIrradiance (line 916) | Eigen::Array3f RenderDiffuseIrradiance(
FILE: sh/spherical_harmonics.h
function namespace (line 46) | namespace sh {
FILE: sh/spherical_harmonics_test.cc
type sh (line 19) | namespace sh {
function Clamp (line 58) | double Clamp(double val, double min, double max) {
function NearByMargin (line 69) | bool NearByMargin(double actual, double expected) {
function ExpectMatrixNear (line 77) | void ExpectMatrixNear(const Eigen::MatrixXd& expected,
function GenerateTestEnvironment (line 89) | void GenerateTestEnvironment(Image* env_map) {
function ComputeExplicitDiffuseIrradiance (line 101) | void ComputeExplicitDiffuseIrradiance(const Image& env_map,
function TEST (line 127) | TEST(SphericalHarmonicsTest, ProjectFunction) {
function TEST (line 145) | TEST(SphericalHarmonicsTest, ProjectSparseSamples) {
function TEST (line 176) | TEST(SphericalHarmonicsTest, ProjectEnvironment) {
function TEST (line 212) | TEST(SphericalHarmonicsTest, EvalSHSum) {
function TEST (line 225) | TEST(SphericalHarmonicsTest, EvalSHSumArray3f) {
function TEST (line 248) | TEST(SphericalHarmonicsTest, GetIndex) {
function TEST (line 262) | TEST(SphericalHarmonicsTest, GetCoefficientCount) {
function TEST (line 269) | TEST(SphericalHarmonicsTest, ToVector) {
function TEST (line 286) | TEST(SphericalHarmonicsTest, ToSphericalCoords) {
function TEST (line 312) | TEST(SphericalHarmonicsTest, EvalSHSlow) {
function TEST (line 356) | TEST(SphericalHarmonicsTest, EvalSHHardcoded) {
function TEST (line 373) | TEST(SphericalHarmonicsDeathTest, EvalSHBadInputs) {
function TEST (line 391) | TEST(SphericalHarmonicsDeathTest, ProjectFunctionBadInputs) {
function TEST (line 407) | TEST(SphericalHarmonicsDeathTest, ProjectEnvironmentBadInputs) {
function TEST (line 414) | TEST(SphericalHarmonicsDeathTest, ProjectSparseSamplesBadInputs) {
function TEST (line 443) | TEST(SphericalHarmonicsDeathTest, EvalSHSumBadInputs) {
function TEST (line 452) | TEST(SphericalHarmonicsDeathTest, ToSphericalCoordsBadInputs) {
function TEST (line 458) | TEST(SphericalHarmonicsRotationTest, ClosedFormZAxisRotation) {
function TEST (line 500) | TEST(SphericalHarmonicsRotationTest, ClosedFormBands) {
function TEST (line 622) | TEST(SphericalHarmonicsRotationTest, CreateFromSHRotation) {
function TEST (line 634) | TEST(SphericalHarmonicsRotationTest, RotateSymmetricFunction) {
function TEST (line 685) | TEST(SphericalHarmonicsRotationTest, RotateComplexFunction) {
function TEST (line 714) | TEST(SphericalHarmonicsRotationTest, RotateInPlace) {
function TEST (line 745) | TEST(SphericalHarmonicsRotationTest, RotateArray3f) {
function TEST (line 784) | TEST(SphericalHarmonicsRotationTest, RotateArray3fInPlace) {
function TEST (line 822) | TEST(SphericalHarmonicsRotationDeathTest, CreateFromMatrixBadInputs) {
function TEST (line 831) | TEST(SphericalHarmonicsRotationDeathTest, CreateFromSHBadInputs) {
function TEST (line 839) | TEST(SphericalHarmonicsRotationDeathTest, RotateBadInputs) {
function TEST (line 850) | TEST(SphericalHarmonicsTest, ImageCoordsToSphericalCoordsTest) {
function TEST (line 878) | TEST(SphericalHarmonicsTest, SphericalCoordsToImageCoordsTest) {
function TEST (line 913) | TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceTest) {
function TEST (line 942) | TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceMapTest) {
function TEST (line 963) | TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceMoreCoefficientsTe...
function TEST (line 982) | TEST(SphericalHarmonicsTest, RenderDiffuseIrradianceFewCoefficientsTes...
function TEST (line 1000) | TEST(SphericalHarmonicUtilsTest, RenderDiffuseIrradianceNoCoefficients...
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (119K chars).
[
{
"path": ".gitignore",
"chars": 68,
"preview": "#ignore editor backups\n*~\n\n#ignore bazel build directories\nbazel-*\n\n"
},
{
"path": "AUTHORS",
"chars": 317,
"preview": "# This is the official list of [Project Name] authors for copyright purposes.\n# This file is distinct from the CONTRIBUT"
},
{
"path": "CONTRIBUTING",
"chars": 1449,
"preview": "Want to contribute? Great! First, read this page (including the small print at the end).\n\n### Before you contribute\nBefo"
},
{
"path": "CONTRIBUTORS",
"chars": 482,
"preview": "# People who have agreed to one of the CLAs and can contribute patches.\n# The AUTHORS file lists the copyright holders; "
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "LICENSE_HEADER",
"chars": 573,
"preview": "Copyright 2015 Google Inc. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may"
},
{
"path": "README.md",
"chars": 6595,
"preview": "3D Graphics-oriented Spherical Harmonics Library\n================================================\n\nSpherical harmonics c"
},
{
"path": "WORKSPACE",
"chars": 658,
"preview": "workspace(name = \"spherical_harmonics\")\n\nload(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\")\n\nhttp_arch"
},
{
"path": "sh/BUILD",
"chars": 1032,
"preview": "config_setting(\n name = \"windows\",\n constraint_values = [\n \"@bazel_tools//platforms:windows\",\n ],\n)\n\ncc_"
},
{
"path": "sh/default_image.cc",
"chars": 1164,
"preview": "// Copyright 2015 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "sh/default_image.h",
"chars": 1167,
"preview": "// Copyright 2015 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "sh/image.h",
"chars": 1198,
"preview": "// Copyright 2015 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "sh/spherical_harmonics.cc",
"chars": 35530,
"preview": "// Copyright 2015 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "sh/spherical_harmonics.h",
"chars": 12231,
"preview": "// Copyright 2015 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "sh/spherical_harmonics_test.cc",
"chars": 40461,
"preview": "// Copyright 2015 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": "third_party/BUILD",
"chars": 0,
"preview": ""
},
{
"path": "third_party/eigen3.BUILD",
"chars": 333,
"preview": "licenses([\"restricted\"]) # MPL2, portions GPL v3, LGPL v3, BSD-like\n\nexports_files([\"LICENSE\"])\n\ncc_library(\n name ="
},
{
"path": "third_party/gtest.BUILD",
"chars": 446,
"preview": "cc_library(\n name = \"main\",\n srcs = glob(\n include = [\"googletest/src/*.h\",\n \"googletest/"
}
]
About this extraction
This page contains the full source code of the google/spherical-harmonics GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (112.4 KB), approximately 33.5k tokens, and a symbol index with 105 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.