Files
UnrealEngine/Engine/Source/ThirdParty/MaterialX/MaterialX-1.38.10/source/MaterialXRender/Harmonics.cpp
2025-05-18 13:04:45 +08:00

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