330 lines
14 KiB
C++
330 lines
14 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MonteCarlo.h"
|
|
|
|
namespace Lightmass
|
|
{
|
|
|
|
/** Generates valid X and Y axes of a coordinate system, given the Z axis. */
|
|
void GenerateCoordinateSystem(const FVector4f& ZAxis, FVector4f& XAxis, FVector4f& YAxis)
|
|
{
|
|
// Use the vector perpendicular to ZAxis and the Y axis as the XAxis
|
|
const FVector4f XAxisCandidate = ZAxis ^ FVector4f(0,1,0);
|
|
if (XAxisCandidate.SizeSquared3() < KINDA_SMALL_NUMBER)
|
|
{
|
|
// The vector was nearly equal to the Y axis, use the X axis instead
|
|
XAxis = (ZAxis ^ FVector4f(1,0,0)).GetUnsafeNormal3();
|
|
}
|
|
else
|
|
{
|
|
XAxis = XAxisCandidate.GetUnsafeNormal3();
|
|
}
|
|
|
|
YAxis = ZAxis ^ XAxis;
|
|
checkSlow(YAxis.IsUnit3());
|
|
}
|
|
|
|
/** Generates valid X and Y axes of a coordinate system, given the Z axis. */
|
|
void GenerateCoordinateSystem2(const FVector4f& ZAxis, FVector4f& XAxis, FVector4f& YAxis)
|
|
{
|
|
// This implementation is based off of the one from 'Physically Based Rendering'
|
|
if (FMath::Abs(ZAxis.X) > FMath::Abs(ZAxis.Y))
|
|
{
|
|
const float InverseLength = FMath::InvSqrt(ZAxis.X * ZAxis.X + ZAxis.Z * ZAxis.Z);
|
|
XAxis = FVector4f(-ZAxis.Z * InverseLength, 0.0f, ZAxis.X * InverseLength);
|
|
}
|
|
else
|
|
{
|
|
const float InverseLength = FMath::InvSqrt(ZAxis.Y * ZAxis.Y + ZAxis.Z * ZAxis.Z);
|
|
XAxis = FVector4f(0.0f, ZAxis.Z * InverseLength, -ZAxis.Y * InverseLength);
|
|
}
|
|
|
|
YAxis = ZAxis ^ XAxis;
|
|
checkSlow(YAxis.IsUnit3());
|
|
checkSlow(FMath::Abs(Dot3(XAxis, ZAxis)) <= THRESH_NORMALS_ARE_ORTHOGONAL);
|
|
checkSlow(FMath::Abs(Dot3(YAxis, ZAxis)) <= THRESH_NORMALS_ARE_ORTHOGONAL);
|
|
checkSlow(FMath::Abs(Dot3(XAxis, YAxis)) <= THRESH_NORMALS_ARE_ORTHOGONAL);
|
|
}
|
|
|
|
/** Generates a pseudo-random unit vector, uniformly distributed over all directions. */
|
|
FVector4f GetUnitVector(FLMRandomStream& RandomStream)
|
|
{
|
|
return GetUnitPosition(RandomStream).GetUnsafeNormal3();
|
|
}
|
|
|
|
/** Generates a pseudo-random position inside the unit sphere, uniformly distributed over the volume of the sphere. */
|
|
FVector4f GetUnitPosition(FLMRandomStream& RandomStream)
|
|
{
|
|
FVector4f Result;
|
|
// Use rejection sampling to generate a valid sample
|
|
do
|
|
{
|
|
Result.X = RandomStream.GetFraction() * 2 - 1;
|
|
Result.Y = RandomStream.GetFraction() * 2 - 1;
|
|
Result.Z = RandomStream.GetFraction() * 2 - 1;
|
|
} while( Result.SizeSquared3() > 1.f );
|
|
return Result;
|
|
}
|
|
|
|
/**
|
|
* Generates a pseudo-random unit vector in the Z > 0 hemisphere whose PDF == 1 / (2 * PI) in solid angles,
|
|
* Or sin(theta) / (2 * PI) in hemispherical coordinates, which is a uniform distribution over the area of the hemisphere.
|
|
*/
|
|
FVector4f GetUniformHemisphereVector(FLMRandomStream& RandomStream, float MaxTheta)
|
|
{
|
|
const float Theta = FMath::Min(FMath::Acos(RandomStream.GetFraction()), MaxTheta - DELTA);
|
|
const float Phi = 2.0f * (float)PI * RandomStream.GetFraction();
|
|
checkSlow(Theta >= 0 && Theta <= (float)HALF_PI);
|
|
checkSlow(Phi >= 0 && Phi <= 2.0f * (float)PI);
|
|
const float SinTheta = FMath::Sin(Theta);
|
|
// Convert to Cartesian
|
|
return FVector4f(FMath::Cos(Phi) * SinTheta, FMath::Sin(Phi) * SinTheta, FMath::Cos(Theta));
|
|
}
|
|
|
|
/**
|
|
* Generates a pseudo-random unit vector in the Z > 0 hemisphere whose PDF == cos(theta) / PI in solid angles,
|
|
* Which is sin(theta)cos(theta) / PI in hemispherical coordinates.
|
|
*/
|
|
FVector4f GetCosineHemisphereVector(FLMRandomStream& RandomStream, float MaxTheta)
|
|
{
|
|
const float Theta = FMath::Min(FMath::Acos(FMath::Sqrt(RandomStream.GetFraction())), MaxTheta - DELTA);
|
|
const float Phi = 2.0f * (float)PI * RandomStream.GetFraction();
|
|
checkSlow(Theta >= 0 && Theta <= (float)HALF_PI);
|
|
checkSlow(Phi >= 0 && Phi <= 2.0f * (float)PI);
|
|
const float SinTheta = FMath::Sin(Theta);
|
|
// Convert to Cartesian
|
|
return FVector4f(FMath::Cos(Phi) * SinTheta, FMath::Sin(Phi) * SinTheta, FMath::Cos(Theta));
|
|
}
|
|
|
|
/**
|
|
* Generates a pseudo-random unit vector in the Z > 0 hemisphere,
|
|
* Whose PDF == (SpecularPower + 1) / (2.0f * PI) * cos(Alpha) ^ SpecularPower in solid angles,
|
|
* Where Alpha is the angle between the perfect specular direction and the outgoing direction.
|
|
*/
|
|
FVector4f GetModifiedPhongSpecularVector(FLMRandomStream& RandomStream, const FVector4f& TangentSpecularDirection, float SpecularPower)
|
|
{
|
|
checkSlow(TangentSpecularDirection.Z >= 0.0f);
|
|
checkSlow(SpecularPower > 0.0f);
|
|
|
|
FVector4f GeneratedTangentVector;
|
|
do
|
|
{
|
|
// Generate hemispherical coordinates in the local frame of the perfect specular direction
|
|
// Don't allow a value of 0, since that results in a PDF of 0 with large specular powers due to floating point imprecision
|
|
const float Alpha = FMath::Min(FMath::Acos(FMath::Pow(FMath::Max(RandomStream.GetFraction(), DELTA), 1.0f / (SpecularPower + 1.0f))), (float)HALF_PI - DELTA);
|
|
const float Phi = 2.0f * (float)PI * RandomStream.GetFraction();
|
|
|
|
// Convert to Cartesian, still in the coordinate space of the perfect specular direction
|
|
const float SinTheta = FMath::Sin(Alpha);
|
|
const FVector4f GeneratedSpecularTangentVector(FMath::Cos(Phi) * SinTheta, FMath::Sin(Phi) * SinTheta, FMath::Cos(Alpha));
|
|
|
|
// Generate the X and Y axes of the coordinate space whose Z is the perfect specular direction
|
|
FVector4f SpecularTangentX = (TangentSpecularDirection ^ FVector4f(0,1,0)).GetUnsafeNormal3();
|
|
if (SpecularTangentX.SizeSquared3() < KINDA_SMALL_NUMBER)
|
|
{
|
|
// The specular direction was nearly equal to the Y axis, use the X axis instead
|
|
SpecularTangentX = (TangentSpecularDirection ^ FVector4f(1,0,0)).GetUnsafeNormal3();
|
|
}
|
|
else
|
|
{
|
|
SpecularTangentX = SpecularTangentX.GetUnsafeNormal3();
|
|
}
|
|
const FVector4f SpecularTangentY = TangentSpecularDirection ^ SpecularTangentX;
|
|
|
|
// Rotate the generated coordinates into the local frame of the tangent space normal (0,0,1)
|
|
const FVector4f SpecularTangentRow0(SpecularTangentX.X, SpecularTangentY.X, TangentSpecularDirection.X);
|
|
const FVector4f SpecularTangentRow1(SpecularTangentX.Y, SpecularTangentY.Y, TangentSpecularDirection.Y);
|
|
const FVector4f SpecularTangentRow2(SpecularTangentX.Z, SpecularTangentY.Z, TangentSpecularDirection.Z);
|
|
GeneratedTangentVector = FVector4f(
|
|
Dot3(SpecularTangentRow0, GeneratedSpecularTangentVector),
|
|
Dot3(SpecularTangentRow1, GeneratedSpecularTangentVector),
|
|
Dot3(SpecularTangentRow2, GeneratedSpecularTangentVector)
|
|
);
|
|
}
|
|
// Regenerate an Alpha as long as the direction is outside of the tangent space Z > 0 hemisphere,
|
|
// Since some part of the cosine lobe around the specular direction can be outside of the hemisphere around the surface normal.
|
|
while (GeneratedTangentVector.Z < DELTA);
|
|
return GeneratedTangentVector;
|
|
}
|
|
|
|
/**
|
|
* Generates a pseudo-random position within a unit disk,
|
|
* Whose PDF == 1 / PI, which is a uniform distribution over the area of the disk.
|
|
*/
|
|
FVector2f GetUniformUnitDiskPosition(FLMRandomStream& RandomStream)
|
|
{
|
|
const float Theta = 2.0f * (float)PI * RandomStream.GetFraction();
|
|
const float Radius = FMath::Sqrt(RandomStream.GetFraction());
|
|
return FVector2f(Radius * FMath::Cos(Theta), Radius * FMath::Sin(Theta));
|
|
}
|
|
|
|
/**
|
|
* Generates a pseudo-random direction within a cone,
|
|
* Whose PDF == 1 / (2 * PI * (1 - CosMaxConeTheta)), which is a uniform distribution over the directions in the cone.
|
|
*/
|
|
FVector4f UniformSampleCone(FLMRandomStream& RandomStream, float CosMaxConeTheta, const FVector4f& XAxis, const FVector4f& YAxis, const FVector4f& ZAxis)
|
|
{
|
|
checkSlow(CosMaxConeTheta >= 0.0f && CosMaxConeTheta <= 1.0f);
|
|
const float CosTheta = FMath::Lerp(CosMaxConeTheta, 1.0f, RandomStream.GetFraction());
|
|
const float SinTheta = FMath::Sqrt(1.0f - CosTheta * CosTheta);
|
|
const float Phi = RandomStream.GetFraction() * 2.0f * (float)PI;
|
|
return FMath::Cos(Phi) * SinTheta * XAxis + FMath::Sin(Phi) * SinTheta * YAxis + CosTheta * ZAxis;
|
|
}
|
|
|
|
FVector4f UniformSampleCone(float CosMaxConeTheta, const FVector4f& XAxis, const FVector4f& YAxis, const FVector4f& ZAxis, float Uniform1, float Uniform2)
|
|
{
|
|
checkSlow(CosMaxConeTheta >= 0.0f && CosMaxConeTheta <= 1.0f);
|
|
const float CosTheta = FMath::Lerp(CosMaxConeTheta, 1.0f, Uniform1);
|
|
const float SinTheta = FMath::Sqrt(1.0f - CosTheta * CosTheta);
|
|
const float Phi = Uniform2 * 2.0f * (float)PI;
|
|
return FMath::Cos(Phi) * SinTheta * XAxis + FMath::Sin(Phi) * SinTheta * YAxis + CosTheta * ZAxis;
|
|
}
|
|
|
|
/** Calculates the PDF for a sample generated by UniformSampleCone */
|
|
float UniformConePDF(float CosMaxConeTheta)
|
|
{
|
|
checkSlow(CosMaxConeTheta >= 0.0f && CosMaxConeTheta <= 1.0f);
|
|
return 1.0f / (2.0f * (float)PI * (1.0f - CosMaxConeTheta));
|
|
}
|
|
|
|
FVector4f UniformSampleHemisphere(float Uniform1, float Uniform2)
|
|
{
|
|
const float R = FMath::Sqrt(1.0f - Uniform1 * Uniform1);
|
|
const float Phi = 2.0f * (float)PI * Uniform2;
|
|
|
|
// Convert to Cartesian
|
|
return FVector4f(FMath::Cos(Phi) * R, FMath::Sin(Phi) * R, Uniform1);
|
|
}
|
|
|
|
/** Generates unit length, stratified and uniformly distributed direction samples in a hemisphere. */
|
|
void GenerateStratifiedUniformHemisphereSamples(int32 NumThetaSteps, int32 NumPhiSteps, FLMRandomStream& RandomStream, TArray<FVector4f>& Samples, TArray<FVector2f>& Uniforms)
|
|
{
|
|
Samples.Empty(NumThetaSteps * NumPhiSteps);
|
|
for (int32 ThetaIndex = 0; ThetaIndex < NumThetaSteps; ThetaIndex++)
|
|
{
|
|
for (int32 PhiIndex = 0; PhiIndex < NumPhiSteps; PhiIndex++)
|
|
{
|
|
const float U1 = RandomStream.GetFraction();
|
|
const float U2 = RandomStream.GetFraction();
|
|
|
|
const float Fraction1 = (ThetaIndex + U1) / (float)NumThetaSteps;
|
|
const float Fraction2 = (PhiIndex + U2) / (float)NumPhiSteps;
|
|
|
|
const float R = FMath::Sqrt(1.0f - Fraction1 * Fraction1);
|
|
|
|
const float Phi = 2.0f * (float)PI * Fraction2;
|
|
// Convert to Cartesian
|
|
Samples.Add(FVector4f(FMath::Cos(Phi) * R, FMath::Sin(Phi) * R, Fraction1));
|
|
Uniforms.Add(FVector2f(Fraction1, Fraction2));
|
|
}
|
|
}
|
|
}
|
|
|
|
void GenerateStratifiedCosineHemisphereSamples(int32 NumThetaSteps, int32 NumPhiSteps, FLMRandomStream& RandomStream, TArray<FVector4f>& Samples)
|
|
{
|
|
Samples.Empty(NumThetaSteps * NumPhiSteps);
|
|
|
|
for (int32 ThetaIndex = 0; ThetaIndex < NumThetaSteps; ThetaIndex++)
|
|
{
|
|
for (int32 PhiIndex = 0; PhiIndex < NumPhiSteps; PhiIndex++)
|
|
{
|
|
const float U1 = RandomStream.GetFraction();
|
|
const float U2 = RandomStream.GetFraction();
|
|
|
|
const float Fraction1 = (ThetaIndex + U1) / (float)NumThetaSteps;
|
|
const float Fraction2 = (PhiIndex + U2) / (float)NumPhiSteps;
|
|
|
|
const float Theta = FMath::Acos(FMath::Sqrt(Fraction1));
|
|
const float Phi = 2.0f * (float)PI * Fraction2;
|
|
checkSlow(Theta >= 0 && Theta <= (float)HALF_PI);
|
|
checkSlow(Phi >= 0 && Phi <= 2.0f * (float)PI);
|
|
const float SinTheta = FMath::Sin(Theta);
|
|
// Convert to Cartesian
|
|
Samples.Add(FVector4f(FMath::Cos(Phi) * SinTheta, FMath::Sin(Phi) * SinTheta, FMath::Cos(Theta)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Multiple importance sampling power heuristic of two functions with a power of two.
|
|
* From Veach's PHD thesis titled "Robust Monte Carlo Methods for Light Transport Simulation", page 273.
|
|
*/
|
|
float PowerHeuristic(int32 NumF, float fPDF, int32 NumG, float gPDF)
|
|
{
|
|
const float fWeight = NumF * fPDF;
|
|
const float gWeight = NumG * gPDF;
|
|
return fWeight * fWeight / (fWeight * fWeight + gWeight * gWeight);
|
|
}
|
|
|
|
/** Calculates the step 1D cumulative distribution function for the given unnormalized probability distribution function. */
|
|
void CalculateStep1dCDF(const TArray<float>& PDF, TArray<float>& CDF, float& UnnormalizedIntegral)
|
|
{
|
|
CDF.Empty(PDF.Num());
|
|
float RunningUnnormalizedIntegral = 0;
|
|
CDF.Add(0.0f);
|
|
for (int32 i = 1; i < PDF.Num(); i++)
|
|
{
|
|
RunningUnnormalizedIntegral += PDF[i - 1];
|
|
CDF.Add(RunningUnnormalizedIntegral);
|
|
}
|
|
UnnormalizedIntegral = RunningUnnormalizedIntegral + PDF.Last();
|
|
if (UnnormalizedIntegral > 0.0f)
|
|
{
|
|
// Normalize the CDF
|
|
for (int32 i = 1; i < CDF.Num(); i++)
|
|
{
|
|
CDF[i] /= UnnormalizedIntegral;
|
|
}
|
|
}
|
|
check(CDF.Num() == PDF.Num());
|
|
}
|
|
|
|
/** Generates a Sample from the given step 1D probability distribution function. */
|
|
void Sample1dCDF(const TArray<float>& PDFArray, const TArray<float>& CDFArray, float UnnormalizedIntegral, FLMRandomStream& RandomStream, float& PDF, float& Sample)
|
|
{
|
|
checkSlow(PDFArray.Num() > 0);
|
|
checkSlow(PDFArray.Num() == CDFArray.Num());
|
|
|
|
// See pages 641-644 of the "Physically Based Rendering" book for an excellent description of
|
|
// How to sample from a piecewise-constant 1d function, which this implementation is based on.
|
|
if (PDFArray.Num() > 1)
|
|
{
|
|
// Get a uniformly distributed pseudo-random number
|
|
const float RandomFraction = RandomStream.GetFraction();
|
|
int32 GreaterElementIndex = -1;
|
|
// Find the index of where the step function becomes greater or equal to the generated number
|
|
//@todo - CDFArray is monotonically increasing so we can do better than a linear time search
|
|
for (int32 i = 1; i < CDFArray.Num(); i++)
|
|
{
|
|
if (CDFArray[i] >= RandomFraction)
|
|
{
|
|
GreaterElementIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (GreaterElementIndex >= 0)
|
|
{
|
|
check(GreaterElementIndex >= 1 && GreaterElementIndex < CDFArray.Num());
|
|
// Find the fraction that the generated number is from the element before the greater or equal element.
|
|
const float OffsetAlongCDFSegment = (RandomFraction - CDFArray[GreaterElementIndex - 1]) / (CDFArray[GreaterElementIndex] - CDFArray[GreaterElementIndex - 1]);
|
|
// Access the probability that this element was selected and normalize it
|
|
PDF = PDFArray[GreaterElementIndex - 1] / UnnormalizedIntegral;
|
|
Sample = (GreaterElementIndex - 1 + OffsetAlongCDFSegment) / (float)CDFArray.Num();
|
|
}
|
|
else
|
|
{
|
|
// The last element in the 1d CDF was selected
|
|
const float OffsetAlongCDFSegment = (RandomFraction - CDFArray.Last()) / (1.0f - CDFArray.Last());
|
|
PDF = PDFArray.Last() / UnnormalizedIntegral;
|
|
Sample = FMath::Clamp((CDFArray.Num() - 1 + OffsetAlongCDFSegment) / (float)CDFArray.Num(), 0.0f, 1.0f - DELTA);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PDF = 1.0f;
|
|
Sample = 0;
|
|
}
|
|
}
|
|
|
|
}
|