games202-hw/hw2/prt/ext/spherical-harmonics/sh/spherical_harmonics.cc

1002 lines
35 KiB
C++
Raw Permalink Normal View History

// 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