Files
filament/tools/cmgen/src/CubemapSH.cpp
Romain Guy bb553bf53d Fix uninitialized memory bug in cmgen (#93)
We recently changed the default constructor of vector to not initialize the vector to 0. This caused subtle issues in cmgen which was creating arrays of vectors. This change simply forces 0 initialization of those arrays.
2018-08-13 22:24:23 -07:00

444 lines
14 KiB
C++

/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 "CubemapSH.h"
#include <math/mat4.h>
#include "Cubemap.h"
#include "CubemapUtils.h"
#include "utilities.h"
using namespace math;
// -----------------------------------------------------------------------------------------------
// A few useful utilities
// -----------------------------------------------------------------------------------------------
/*
* returns n! / d!
*/
static double factorial(size_t n, size_t d = 1) {
d = std::max(size_t(1), d);
n = std::max(size_t(1), n);
double r = 1.0;
if (n == d) {
// intentionally left blank
} else if (n > d) {
for ( ; n>d ; n--) {
r *= n;
}
} else {
for ( ; d>n ; d--) {
r *= d;
}
r = 1.0 / r;
}
return r;
}
// -----------------------------------------------------------------------------------------------
/*
* SH scaling factors:
* returns sqrt((2*l + 1) / 4*pi) * sqrt( (l-|m|)! / (l+|m|)! )
*/
double CubemapSH::Kml(ssize_t m, size_t l) {
m = std::abs(m);
const double K = (2*l + 1) * factorial(size_t(l-m), size_t(l+m));
return std::sqrt(K) * (M_2_SQRTPI * 0.25);
}
// < cos(theta) > SH coefficients pre-multiplied by 1 / K(0,l)
double CubemapSH::computeTruncatedCosSh(size_t l) {
if (l == 0) {
return M_PI;
} else if (l == 1) {
return 2 * M_PI / 3;
} else if (l & 1) {
return 0;
}
const size_t l_2 = l / 2;
double A0 = ((l_2 & 1) ? 1.0 : -1.0) / ((l + 2) * (l - 1));
double A1 = factorial(l, l_2) / (factorial(l_2) * (1 << l));
return 2 * M_PI * A0 * A1;
}
/*
* Calculates non-normalized SH bases, i.e.:
* m > 0, cos(m*phi) * P(m,l)
* m < 0, sin(|m|*phi) * P(|m|,l)
* m = 0, P(0,l)
*/
void CubemapSH::computeShBasis(
double* UTILS_RESTRICT SHb,
size_t numBands,
const double3& s)
{
#if 0
// Reference implementation
double phi = atan2(s.x, s.y);
for (size_t l = 0; l < numBands; l++) {
SHb[SHindex(0, l)] = Legendre(l, 0, s.z);
for (size_t m = 1; m <= l; m++) {
double p = Legendre(l, m, s.z);
SHb[SHindex(-m, l)] = std::sin(m * phi) * p;
SHb[SHindex( m, l)] = std::cos(m * phi) * p;
}
}
#endif
/*
* TODO: all the Legendre computation below is identical for all faces, so it
* might make sense to pre-compute it once. Also note that there is
* a fair amount of symmetry within a face (which we could take advantage of
* to reduce the pre-compute table).
*/
/*
* Below, we compute the associated Legendre polynomials using recursion.
* see: http://mathworld.wolfram.com/AssociatedLegendrePolynomial.html
*
* Note [0]: s.z == cos(theta) ==> we only need to compute P(s.z)
*
* Note [1]: We in fact compute P(s.z) / sin(theta)^|m|, by removing
* the "sqrt(1 - s.z*s.z)" [i.e.: sin(theta)] factor from the recursion.
* This is later corrected in the ( cos(m*phi), sin(m*phi) ) recursion.
*/
// s = (x, y, z) = (sin(theta)*cos(phi), sin(theta)*sin(phi), cos(theta))
// handle m=0 separately, since it produces only one coefficient
double Pml_2 = 0;
double Pml_1 = 1;
SHb[0] = Pml_1;
for (size_t l=1; l<numBands; l++) {
double Pml = ((2*l-1.0)*Pml_1*s.z - (l-1.0)*Pml_2) / l;
Pml_2 = Pml_1;
Pml_1 = Pml;
SHb[SHindex(0, l)] = Pml;
}
double Pmm = 1;
for (size_t m=1 ; m<numBands ; m++) {
Pmm = (1.0 - 2*m) * Pmm; // See [1], divide by sqrt(1 - s.z*s.z);
Pml_2 = Pmm;
Pml_1 = (2*m + 1.0)*Pmm*s.z;
// l == m
SHb[SHindex(-m, m)] = Pml_2;
SHb[SHindex( m, m)] = Pml_2;
if (m+1 < numBands) {
// l == m+1
SHb[SHindex(-m, m+1)] = Pml_1;
SHb[SHindex( m, m+1)] = Pml_1;
for (size_t l=m+2 ; l<numBands ; l++) {
double Pml = ((2*l - 1.0)*Pml_1*s.z - (l + m - 1.0)*Pml_2) / (l-m);
Pml_2 = Pml_1;
Pml_1 = Pml;
SHb[SHindex(-m, l)] = Pml;
SHb[SHindex( m, l)] = Pml;
}
}
}
// At this point, SHb contains the associated Legendre polynomials divided
// by sin(theta)^|m|. Below we compute the SH basis.
//
// ( cos(m*phi), sin(m*phi) ) recursion:
// cos(m*phi + phi) == cos(m*phi)*cos(phi) - sin(m*phi)*sin(phi)
// sin(m*phi + phi) == sin(m*phi)*cos(phi) + cos(m*phi)*sin(phi)
// cos[m+1] == cos[m]*s.x - sin[m]*s.y
// sin[m+1] == sin[m]*s.x + cos[m]*s.y
//
// Note that (d.x, d.y) == (cos(phi), sin(phi)) * sin(theta), so the
// code below actually evaluates:
// (cos((m*phi), sin(m*phi)) * sin(theta)^|m|
double Cm = s.x;
double Sm = s.y;
for (size_t m = 1; m <= numBands; m++) {
for (size_t l = m; l < numBands; l++) {
SHb[SHindex(-m, l)] *= Sm;
SHb[SHindex( m, l)] *= Cm;
}
double Cm1 = Cm * s.x - Sm * s.y;
double Sm1 = Sm * s.x + Cm * s.y;
Cm = Cm1;
Sm = Sm1;
}
}
std::unique_ptr<double3[]> CubemapSH::computeSH(const Cubemap& cm, size_t numBands, bool irradiance) {
const size_t numCoefs = numBands*numBands;
std::unique_ptr<double3[]> SH(new double3[numCoefs]{});
struct State {
State() = default;
explicit State(size_t numCoefs) : numCoefs(numCoefs) { }
State& operator=(State const & rhs) {
SH.reset(new double3[rhs.numCoefs]{}); // NOLINT(modernize-make-unique)
SHb.reset(new double[rhs.numCoefs]{}); // NOLINT(modernize-make-unique)
return *this;
}
size_t numCoefs = 0;
std::unique_ptr<double3[]> SH;
std::unique_ptr<double[]> SHb;
} prototype(numCoefs);
CubemapUtils::process<State>(cm,
[&](State& state, size_t y, Cubemap::Face f, Cubemap::Texel const* data, size_t dim) {
for (size_t x=0 ; x<dim ; ++x, ++data) {
double3 s(cm.getDirectionFor(f, x, y));
// sample a color
double3 color(Cubemap::sampleAt(data));
// take solid angle into account
color *= CubemapUtils::solidAngle(dim, x, y);
computeShBasis(state.SHb.get(), numBands, s);
// apply coefficients to the sampled color
for (size_t i=0 ; i<numCoefs ; i++) {
state.SH[i] += color * state.SHb[i];
}
}
},
[&](State& state) {
for (size_t i=0 ; i<numCoefs ; i++) {
SH[i] += state.SH[i];
}
}, prototype);
// precompute the scaling factor K
for (size_t l = 0; l < numBands; l++) {
const double truncatedCosSh = irradiance ? computeTruncatedCosSh(size_t(l)) : 1;
double K = Kml(0, l);
SH[SHindex(0, l)] *= K * truncatedCosSh;
for (size_t m = 1; m <= l; m++) {
K = M_SQRT2 * Kml(m, l);
SH[SHindex(-m, l)] *= K * truncatedCosSh;
SH[SHindex( m, l)] *= K * truncatedCosSh;
}
}
return SH;
}
void CubemapSH::renderSH(const Cubemap& cm,
const std::unique_ptr<double3[]>& sh, size_t numBands)
{
const size_t numCoefs = numBands*numBands;
std::vector<double> K(numCoefs);
// precompute the scaling factor K
for (size_t l=0 ; l<numBands ; l++) {
K[SHindex(0, l)] = Kml(0, l);
for (size_t m=1 ; m<=l ; m++) {
K[SHindex( m, l)] =
K[SHindex(-m, l)] = M_SQRT2 * Kml(m, l);
}
}
CubemapUtils::process<CubemapUtils::EmptyState>(cm,
[&](CubemapUtils::EmptyState&, size_t y,
Cubemap::Face f, Cubemap::Texel* data, size_t dim) {
std::vector<double> SHb(numCoefs);
for (size_t x=0 ; x<dim ; ++x, ++data) {
double3 s(cm.getDirectionFor(f, x, y));
computeShBasis(SHb.data(), numBands, s);
double3 c;
for (size_t i=0 ; i<numCoefs ; i++) {
c += sh[i] * (K[i] * SHb[i]);
}
Cubemap::writeAt(data, Cubemap::Texel(c));
}
});
}
/*
* This computes the 3-bands SH coefficients of the Cubemap convoluted by the
* truncated cos(theta) (i.e.: saturate(s.z)), pre-scaled by the reconstruction
* factors.
*/
std::unique_ptr<double3[]> CubemapSH::computeIrradianceSH3Bands(const Cubemap& cm) {
const size_t numCoefs = 9;
std::unique_ptr<double3[]> SH(new double3[numCoefs]{});
std::unique_ptr<double[]> A(new double[numCoefs]{});
const double c0 = computeTruncatedCosSh(0);
const double c1 = computeTruncatedCosSh(1);
const double c2 = computeTruncatedCosSh(2);
A[0] = sq(M_2_SQRTPI / 4) * c0;
A[1] = sq(M_2_SQRTPI / 4) * 3 * c1;
A[2] = sq(M_2_SQRTPI / 4) * 3 * c1;
A[3] = sq(M_2_SQRTPI / 4) * 3 * c1;
A[4] = sq(M_2_SQRTPI / 4) * 15 * c2;
A[5] = sq(M_2_SQRTPI / 4) * 15 * c2;
A[6] = sq(M_2_SQRTPI / 8) * 5 * c2;
A[7] = sq(M_2_SQRTPI / 4) * 15 * c2;
A[8] = sq(M_2_SQRTPI / 8) * 15 * c2;
struct State {
double3 SH[9] = { };
};
CubemapUtils::process<State>(cm,
[&](State& state, size_t y, Cubemap::Face f, Cubemap::Texel const* data, size_t dim) {
for (size_t x=0 ; x<dim ; ++x, ++data) {
double3 s(cm.getDirectionFor(f, x, y));
// sample a color
double3 color(Cubemap::sampleAt(data));
// take solid angle into account
color *= CubemapUtils::solidAngle(dim, x, y);
state.SH[0] += color * A[0];
state.SH[1] += color * A[1] * s.y;
state.SH[2] += color * A[2] * s.z;
state.SH[3] += color * A[3] * s.x;
state.SH[4] += color * A[4] * s.y*s.x;
state.SH[5] += color * A[5] * s.y*s.z;
state.SH[6] += color * A[6] * (3*s.z*s.z - 1);
state.SH[7] += color * A[7] * s.z*s.x;
state.SH[8] += color * A[8] * (s.x*s.x - s.y*s.y);
}
},
[&](State& state) {
for (size_t i=0 ; i<numCoefs ; i++) {
SH[i] += state.SH[i];
}
});
return SH;
}
void CubemapSH::renderPreScaledSH3Bands(
const Cubemap& cm, const std::unique_ptr<math::double3[]>& sh) {
CubemapUtils::process<CubemapUtils::EmptyState>(cm,
[&](CubemapUtils::EmptyState&, size_t y, Cubemap::Face f, Cubemap::Texel* data, size_t dim) {
for (size_t x=0 ; x<dim ; ++x, ++data) {
double3 s(cm.getDirectionFor(f, x, y));
double3 c;
c += sh[0];
c += sh[1] * s.y;
c += sh[2] * s.z;
c += sh[3] * s.x;
c += sh[4] * s.y*s.x;
c += sh[5] * s.y*s.z;
c += sh[6] * (3*s.z*s.z - 1);
c += sh[7] * s.z*s.x;
c += sh[8] * (s.x*s.x - s.y*s.y);
Cubemap::writeAt(data, Cubemap::Texel(c));
}
});
}
// -----------------------------------------------------------------------------------------------
// Only used for debugging
// -----------------------------------------------------------------------------------------------
double __UNUSED CubemapSH::Legendre(ssize_t l, ssize_t m, double x) {
// evaluate an Associated Legendre Polynomial P(l,m,x) at x
double pmm = 1.0;
if (m > 0) {
double somx2 = sqrt((1.0 - x) * (1.0 + x));
double fact = 1.0;
for (int i = 1; i <= m; i++) {
pmm *= (-fact) * somx2;
fact += 2.0;
}
}
if (l == m)
return pmm;
double pmmp1 = x * (2.0 * m + 1.0) * pmm;
if (l == m + 1)
return pmmp1;
double pll = 0.0;
for (ssize_t ll = m + 2; ll <= l; ++ll) {
pll = ((2.0 * ll - 1.0) * x * pmmp1 - (ll + m - 1.0) * pmm) / (ll - m);
pmm = pmmp1;
pmmp1 = pll;
}
return pll;
}
// Only used for debugging
double __UNUSED CubemapSH::TSH(int l, int m, const double3& d) {
if (l==0 && m==0) {
return 1 / (2*sqrt(M_PI));
} else if (l==1 && m==-1) {
return -(sqrt(3)*d.y)/(2*sqrt(M_PI));
} else if (l==1 && m==0) {
return (sqrt(3)*d.z)/(2*sqrt(M_PI));
} else if (l==1 && m==1) {
return -(sqrt(3)*d.x)/(2*sqrt(M_PI));
} else if (l==2 && m==-2) {
return (sqrt(15)*d.y*d.x)/(2*sqrt(M_PI));
} else if (l==2 && m==-1) {
return -(sqrt(15)*d.y*d.z)/(2*sqrt(M_PI));
} else if (l==2 && m==0) {
return (sqrt(5)*(3*d.z*d.z-1))/(4*sqrt(M_PI));
} else if (l==2 && m==1) {
return -(sqrt(15)*d.z*d.x)/(2*sqrt(M_PI));
} else if (l==2 && m==2) {
return (sqrt(15)*(d.x*d.x - d.y*d.y))/(4*sqrt(M_PI));
}
return 0;
}
void __UNUSED CubemapSH::printShBase(std::ostream& out, int l, int m) {
if (l<3 && std::abs(m) <= l) {
const char* d = nullptr;
double c = 0;
if (l==0 && m==0) {
c = M_2_SQRTPI * 0.25;
d = " ";
} else if (l==1 && m==-1) {
c = -M_2_SQRTPI * sqrt(3) * 0.25;
d = " * y; ";
} else if (l==1 && m==0) {
c = M_2_SQRTPI * sqrt(3) * 0.25;
d = " * z; ";
} else if (l==1 && m==1) {
c = -M_2_SQRTPI * sqrt(3) * 0.25;
d = " * x; ";
} else if (l==2 && m==-2) {
c = M_2_SQRTPI * sqrt(15) * 0.25;
d = " * y*x; ";
} else if (l==2 && m==-1) {
c = -M_2_SQRTPI * sqrt(15) * 0.25;
d = " * y*z; ";
} else if (l==2 && m==0) {
c = M_2_SQRTPI * sqrt(5) * 0.125;
d = " * (3*z*z -1); ";
} else if (l==2 && m==1) {
c = -M_2_SQRTPI * sqrt(15) * 0.25;
d = " * z*x; ";
} else if (l==2 && m==2) {
c = M_2_SQRTPI * sqrt(15) * 0.125;
d = " * (x*x - y*y);";
}
out << "SHb[" << SHindex(m, size_t(l)) << "] = ";
out << std::fixed << std::setprecision(15) << std::setw(18) << c << d;
out << " // L" << l << m;
}
}