335 lines
12 KiB
C++
335 lines
12 KiB
C++
//
|
|
// Copyright Contributors to the MaterialX Project
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
#include <MaterialXRender/Harmonics.h>
|
|
|
|
#include <iostream>
|
|
|
|
MATERIALX_NAMESPACE_BEGIN
|
|
|
|
namespace
|
|
{
|
|
|
|
const double PI = std::acos(-1.0);
|
|
|
|
const double BASIS_CONSTANT_0 = std::sqrt( 1.0 / ( 4.0 * PI));
|
|
const double BASIS_CONSTANT_1 = std::sqrt( 3.0 / ( 4.0 * PI));
|
|
const double BASIS_CONSTANT_2 = std::sqrt(15.0 / ( 4.0 * PI));
|
|
const double BASIS_CONSTANT_3 = std::sqrt( 5.0 / (16.0 * PI));
|
|
const double BASIS_CONSTANT_4 = std::sqrt(15.0 / (16.0 * PI));
|
|
|
|
const double COSINE_CONSTANT_0 = 1.0;
|
|
const double COSINE_CONSTANT_1 = 2.0 / 3.0;
|
|
const double COSINE_CONSTANT_2 = 1.0 / 4.0;
|
|
|
|
const Color3d LUMA_COEFFS_REC709(0.2126, 0.7152, 0.0722);
|
|
|
|
double imageXToPhi(unsigned int x, unsigned int width)
|
|
{
|
|
// Align spherical coordinates with texel centers by adding 0.5.
|
|
return 2.0 * PI * (x + 0.5) / width;
|
|
}
|
|
|
|
double imageYToTheta(unsigned int y, unsigned int height)
|
|
{
|
|
return PI * (y + 0.5) / height;
|
|
}
|
|
|
|
Vector3d sphericalToCartesian(double theta, double phi)
|
|
{
|
|
double r = std::sin(theta);
|
|
return Vector3d(-r * std::sin(phi), -std::cos(theta), r * std::cos(phi));
|
|
}
|
|
|
|
double texelSolidAngle(unsigned int y, unsigned int width, unsigned int height)
|
|
{
|
|
// Return the solid angle of a texel within a lat-long environment map.
|
|
//
|
|
// Reference:
|
|
// https://en.wikipedia.org/wiki/Solid_angle#Latitude-longitude_rectangle
|
|
|
|
double dTheta = std::cos(y * PI / height) - std::cos((y + 1) * PI / height);
|
|
double dPhi = 2.0 * PI / width;
|
|
return dTheta * dPhi;
|
|
}
|
|
|
|
Sh3ScalarCoeffs evalDirection(const Vector3d& dir)
|
|
{
|
|
// Evaluate the spherical harmonic basis functions for the given direction,
|
|
// returning the first three bands of coefficients.
|
|
//
|
|
// References:
|
|
// https://cseweb.ucsd.edu/~ravir/papers/envmap/envmap.pdf
|
|
// http://orlandoaguilar.github.io/sh/spherical/harmonics/irradiance/map/2017/02/12/SphericalHarmonics.html
|
|
|
|
const double& x = dir[0];
|
|
const double& y = dir[1];
|
|
const double& z = dir[2];
|
|
|
|
return Sh3ScalarCoeffs(
|
|
{
|
|
BASIS_CONSTANT_0,
|
|
BASIS_CONSTANT_1 * y,
|
|
BASIS_CONSTANT_1 * z,
|
|
BASIS_CONSTANT_1 * x,
|
|
BASIS_CONSTANT_2 * x * y,
|
|
BASIS_CONSTANT_2 * y * z,
|
|
BASIS_CONSTANT_3 * (3.0 * z * z - 1.0),
|
|
BASIS_CONSTANT_2 * x * z,
|
|
BASIS_CONSTANT_4 * (x * x - y * y)
|
|
});
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
Sh3ColorCoeffs projectEnvironment(ConstImagePtr env, bool irradiance)
|
|
{
|
|
Sh3ColorCoeffs shEnv;
|
|
|
|
for (unsigned int y = 0; y < env->getHeight(); y++)
|
|
{
|
|
double theta = imageYToTheta(y, env->getHeight());
|
|
double texelWeight = texelSolidAngle(y, env->getWidth(), env->getHeight());
|
|
|
|
for (unsigned int x = 0; x < env->getWidth(); x++)
|
|
{
|
|
// Sample the color at these coordinates.
|
|
Color4 color = env->getTexelColor(x, y);
|
|
|
|
// Compute the direction vector.
|
|
double phi = imageXToPhi(x, env->getWidth());
|
|
Vector3d dir = sphericalToCartesian(theta, phi);
|
|
|
|
// Evaluate the given direction as SH coefficients.
|
|
Sh3ScalarCoeffs shDir = evalDirection(dir);
|
|
|
|
// Combine color with texel weight.
|
|
Color3d weightedColor(color[0] * texelWeight,
|
|
color[1] * texelWeight,
|
|
color[2] * texelWeight);
|
|
|
|
// Update coefficients for the influence of this texel.
|
|
for (size_t i = 0; i < shEnv.NUM_COEFFS; i++)
|
|
{
|
|
shEnv[i] += weightedColor * shDir[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If irradiance is requested, then apply constant factors to convolve the
|
|
// signal by a clamped cosine kernel.
|
|
if (irradiance)
|
|
{
|
|
shEnv[0] *= COSINE_CONSTANT_0;
|
|
shEnv[1] *= COSINE_CONSTANT_1;
|
|
shEnv[2] *= COSINE_CONSTANT_1;
|
|
shEnv[3] *= COSINE_CONSTANT_1;
|
|
shEnv[4] *= COSINE_CONSTANT_2;
|
|
shEnv[5] *= COSINE_CONSTANT_2;
|
|
shEnv[6] *= COSINE_CONSTANT_2;
|
|
shEnv[7] *= COSINE_CONSTANT_2;
|
|
shEnv[8] *= COSINE_CONSTANT_2;
|
|
}
|
|
|
|
return shEnv;
|
|
}
|
|
|
|
ImagePtr normalizeEnvironment(ConstImagePtr env, float envRadiance, float maxTexelRadiance)
|
|
{
|
|
// Compute the radiance of the original environment map.
|
|
double origEnvRadiance = 0.0;
|
|
for (unsigned int y = 0; y < env->getHeight(); y++)
|
|
{
|
|
double texelWeight = texelSolidAngle(y, env->getWidth(), env->getHeight());
|
|
|
|
for (unsigned int x = 0; x < env->getWidth(); x++)
|
|
{
|
|
// Sample the color at these coordinates.
|
|
Color4 color = env->getTexelColor(x, y);
|
|
|
|
// Apply maximum texel radiance.
|
|
double texelRadiance = Color3d(color[0], color[1], color[2]).dot(LUMA_COEFFS_REC709);
|
|
if ((float) texelRadiance > maxTexelRadiance)
|
|
{
|
|
color *= maxTexelRadiance / (float) texelRadiance;
|
|
}
|
|
|
|
// Combine color with texel weight.
|
|
Color3d weightedColor(color[0] * texelWeight,
|
|
color[1] * texelWeight,
|
|
color[2] * texelWeight);
|
|
|
|
// Add to environment radiance.
|
|
double texelContribution = weightedColor.dot(LUMA_COEFFS_REC709);
|
|
origEnvRadiance += texelContribution;
|
|
}
|
|
}
|
|
|
|
// Generate the normalized map.
|
|
ImagePtr normEnv = Image::create(env->getWidth(), env->getHeight(), env->getChannelCount(), env->getBaseType());
|
|
normEnv->createResourceBuffer();
|
|
float envNormFactor = origEnvRadiance ? (float) (envRadiance / origEnvRadiance) : 1.0f;
|
|
for (unsigned int y = 0; y < env->getHeight(); y++)
|
|
{
|
|
for (unsigned int x = 0; x < env->getWidth(); x++)
|
|
{
|
|
// Sample the color at these coordinates.
|
|
Color4 color = env->getTexelColor(x, y);
|
|
|
|
// Apply maximum texel radiance.
|
|
double texelRadiance = Color3d(color[0], color[1], color[2]).dot(LUMA_COEFFS_REC709);
|
|
if ((float) texelRadiance > maxTexelRadiance)
|
|
{
|
|
color *= maxTexelRadiance / (float) texelRadiance;
|
|
}
|
|
|
|
// Store the normalized color.
|
|
normEnv->setTexelColor(x, y, color * envNormFactor);
|
|
}
|
|
}
|
|
|
|
return normEnv;
|
|
}
|
|
|
|
void computeDominantLight(ConstImagePtr env, Vector3& lightDir, Color3& lightColor)
|
|
{
|
|
// Reference:
|
|
// https://seblagarde.wordpress.com/2011/10/09/dive-in-sh-buffer-idea/
|
|
|
|
// Project the environment to spherical harmonics.
|
|
Sh3ColorCoeffs shEnv = projectEnvironment(env);
|
|
|
|
// Handle empty environments.
|
|
if (shEnv == Sh3ColorCoeffs())
|
|
{
|
|
lightDir = Vector3(0.0f, -1.0f, 0.0f);
|
|
lightColor = Color3(0.0f);
|
|
return;
|
|
}
|
|
|
|
// Compute the dominant light direction.
|
|
Vector3d dir = Vector3d(shEnv[3].dot(LUMA_COEFFS_REC709),
|
|
shEnv[1].dot(LUMA_COEFFS_REC709),
|
|
shEnv[2].dot(LUMA_COEFFS_REC709)).getNormalized();
|
|
|
|
// Evaluate the dominant direction as spherical harmonics.
|
|
Sh3ScalarCoeffs shDir = evalDirection(dir);
|
|
Vector4d vDir(shDir[0], shDir[1], shDir[2], shDir[3]);
|
|
|
|
// Compute the dominant light color.
|
|
Vector4d vEnvR(shEnv[0][0], shEnv[1][0], shEnv[2][0], shEnv[3][0]);
|
|
Vector4d vEnvG(shEnv[0][1], shEnv[1][1], shEnv[2][1], shEnv[3][1]);
|
|
Vector4d vEnvB(shEnv[0][2], shEnv[1][2], shEnv[2][2], shEnv[3][2]);
|
|
Color3d color = Color3d(
|
|
std::max(vDir.dot(vEnvR), 0.0),
|
|
std::max(vDir.dot(vEnvG), 0.0),
|
|
std::max(vDir.dot(vEnvB), 0.0)) / vDir.dot(vDir);
|
|
|
|
// Convert to single-precision floats.
|
|
lightDir = Vector3((float) dir[0], (float) dir[1], (float) dir[2]);
|
|
lightColor = Color3((float) color[0], (float) color[1], (float) color[2]);
|
|
}
|
|
|
|
ImagePtr renderEnvironment(const Sh3ColorCoeffs& shEnv, unsigned int width, unsigned int height)
|
|
{
|
|
ImagePtr env = Image::create(width, height, 3, Image::BaseType::FLOAT);
|
|
env->createResourceBuffer();
|
|
|
|
for (unsigned int y = 0; y < env->getHeight(); y++)
|
|
{
|
|
double theta = imageYToTheta(y, env->getHeight());
|
|
for (unsigned int x = 0; x < env->getWidth(); x++)
|
|
{
|
|
// Compute the direction vector.
|
|
double phi = imageXToPhi(x, env->getWidth());
|
|
Vector3d dir = sphericalToCartesian(theta, phi);
|
|
|
|
// Evaluate the given direction as SH coefficients.
|
|
Sh3ScalarCoeffs shDir = evalDirection(dir);
|
|
|
|
// Compute the signal color in this direction.
|
|
Color3d signalColor;
|
|
for (size_t i = 0; i < shEnv.NUM_COEFFS; i++)
|
|
{
|
|
signalColor += shEnv[i] * shDir[i];
|
|
}
|
|
|
|
// Clamp the color and store as an environment texel.
|
|
Color4 outputColor(
|
|
(float) std::max(signalColor[0], 0.0),
|
|
(float) std::max(signalColor[1], 0.0),
|
|
(float) std::max(signalColor[2], 0.0),
|
|
1.0f);
|
|
env->setTexelColor(x, y, outputColor);
|
|
}
|
|
}
|
|
|
|
return env;
|
|
}
|
|
|
|
ImagePtr renderReferenceIrradiance(ConstImagePtr env, unsigned int width, unsigned int height)
|
|
{
|
|
std::cout << "Rendering reference irradiance map..." << std::endl;
|
|
ImagePtr outImage = Image::create(width, height, 3, Image::BaseType::FLOAT);
|
|
outImage->createResourceBuffer();
|
|
|
|
// Iterate through output texels.
|
|
for (unsigned int outY = 0; outY < outImage->getHeight(); outY++)
|
|
{
|
|
std::cout << "Rendering irradiance map row " << outY << " of " << outImage->getHeight() << "..." << std::endl;
|
|
double outTheta = imageYToTheta(outY, outImage->getHeight());
|
|
for (unsigned int outX = 0; outX < outImage->getWidth(); outX++)
|
|
{
|
|
// Compute the output direction vector.
|
|
double outPhi = imageXToPhi(outX, outImage->getWidth());
|
|
Vector3d outDir = sphericalToCartesian(outTheta, outPhi);
|
|
|
|
// Initialize output texel color.
|
|
Color3d outColor;
|
|
|
|
// Iterate through input texels.
|
|
for (unsigned int inY = 0; inY < env->getHeight(); inY++)
|
|
{
|
|
double inTheta = imageYToTheta(inY, env->getHeight());
|
|
if (std::abs(inTheta - outTheta) >= PI / 2.0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
double inTexelWeight = texelSolidAngle(inY, env->getWidth(), env->getHeight());
|
|
for (unsigned int inX = 0; inX < env->getWidth(); inX++)
|
|
{
|
|
// Compute the input direction vector.
|
|
double inPhi = imageXToPhi(inX, env->getWidth());
|
|
Vector3d inDir = sphericalToCartesian(inTheta, inPhi);
|
|
|
|
// Compute the cosine weight.
|
|
double cosineWeight = inDir.dot(outDir);
|
|
if (cosineWeight <= 0.0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Sample the input environment at these coordinates.
|
|
Color4 envColor = env->getTexelColor(inX, inY);
|
|
|
|
// Apply the influence of this input texel.
|
|
outColor += Color3d(envColor[0], envColor[1], envColor[2]) * inTexelWeight * cosineWeight;
|
|
}
|
|
}
|
|
|
|
// Normalize and store the output texel.
|
|
outImage->setTexelColor(outX, outY, Color4((float) (outColor[0] / PI),
|
|
(float) (outColor[1] / PI),
|
|
(float) (outColor[2] / PI),
|
|
1.0f));
|
|
}
|
|
}
|
|
|
|
return outImage;
|
|
}
|
|
|
|
MATERIALX_NAMESPACE_END
|