static void Main( string[] args )
{
try
{
// Analyze arguments
if ( args.Length != 3 )
throw new Exception( "Usage: BRDFLafortuneFitting \"Path to MERL BRDF\" \"Path to Lafortune Coeffs file\" AmountOfCosineLobes" );
FileInfo SourceBRDF = new FileInfo( args[0] );
if ( !SourceBRDF.Exists )
throw new Exception( "Source BRDF file \"" + SourceBRDF.FullName + "\" does not exist!" );
FileInfo TargetFile = new FileInfo( args[1] );
if ( !TargetFile.Directory.Exists )
throw new Exception( "Target coefficient file's \"" + TargetFile.FullName + "\" directory does not exist!" );
int LobesCount = 0;
if ( !int.TryParse( args[2], out LobesCount ) )
throw new Exception( "3rd argument must be an integer number!" );
if ( LobesCount <= 0 || LobesCount > 100 )
throw new Exception( "Number of lobes must be in [1,100]!" );
// Attempt to create the target file first, just to ensure we don't bump into a problem after the lengthy minimization process...
try
{
using ( TargetFile.Create() ) {}
}
catch ( Exception _e )
{
throw new Exception( "Failed to create the target coefficients file: " + _e.Message );
}
// Load the BRDF
double[][] BRDF = LoadBRDF( SourceBRDF );
// // DEBUG CHECK
// { // Generate a bunch of incoming/outgoing directions, get their Half/Diff angles then regenerate back incoming/outgoing directions from these angles and check the relative incoming/outgoing directions are conserved
// // This is important to ensure we sample only the relevant (i.e. changing) parts of the BRDF in our minimization scheme
// // (I want to actually sample the BRDF using the half/diff angles and generate incoming/outgoing vectors from these, rather than sample all the possible 4D space)
// //
// Random TempRNG = new Random( 1 );
// Vector3 TempIn = new Vector3(), TempOut = new Vector3();
// double MinThetaHalf = double.MaxValue, MaxThetaHalf = -double.MaxValue;
// double MinPhiHalf = double.MaxValue, MaxPhiHalf = -double.MaxValue;
// double MinThetaDiff = double.MaxValue, MaxThetaDiff = -double.MaxValue;
// double MinPhiDiff = double.MaxValue, MaxPhiDiff = -double.MaxValue;
// for ( int i=0; i < 10000; i++ )
// {
// double Phi_i = 2.0 * Math.PI * (TempRNG.NextDouble() - 0.5);
// double Theta_i = 0.5 * Math.PI * TempRNG.NextDouble();
// double Phi_r = 2.0 * Math.PI * (TempRNG.NextDouble() - 0.5);
// double Theta_r = 0.5 * Math.PI * TempRNG.NextDouble();
//
// double Theta_half, Phi_half, Theta_diff, Phi_diff;
// std_coords_to_half_diff_coords( Theta_i, Phi_i, Theta_r, Phi_r, out Theta_half, out Phi_half, out Theta_diff, out Phi_diff );
//
//
// // MinThetaHalf = Math.Min( MinThetaHalf, Theta_half );
// // MaxThetaHalf = Math.Max( MaxThetaHalf, Theta_half );
// // MinThetaDiff = Math.Min( MinThetaDiff, Theta_diff );
// // MaxThetaDiff = Math.Max( MaxThetaDiff, Theta_diff );
// // MinPhiHalf = Math.Min( MinPhiHalf, Phi_half );
// // MaxPhiHalf = Math.Max( MaxPhiHalf, Phi_half );
// // MinPhiDiff = Math.Min( MinPhiDiff, Phi_diff );
// // MaxPhiDiff = Math.Max( MaxPhiDiff, Phi_diff );
//
// // if ( Theta_half > MaxThetaHalf )
// // {
// // MaxThetaHalf = Theta_half;
// // MaxPhiHalf = Phi_half;
// // MaxThetaDiff = Theta_diff;
// // MaxPhiDiff = Phi_diff;
// // }
//
// // Convert back...
// double NewTheta_i, NewPhi_i, NewTheta_r, NewPhi_r;
// half_diff_coords_to_std_coords( Theta_half, Phi_half, Theta_diff, Phi_diff, out NewTheta_i, out NewPhi_i, out NewTheta_r, out NewPhi_r );
//
// // Convert back into directions
// half_diff_coords_to_std_coords( Theta_half, Phi_half, Theta_diff, Phi_diff, ref TempIn, ref TempOut );
//
// // Check
// const double Tol = 1e-4;
// if ( Math.Abs( NewTheta_i - Theta_i ) > Tol
// || Math.Abs( NewTheta_r - Theta_r ) > Tol )
// throw new Exception( "ARGH THETA!" );
// if ( Math.Abs( NewPhi_i - Phi_i ) > Tol
// || Math.Abs( NewPhi_r - Phi_r ) > Tol )
// throw new Exception( "ARGH PHI!" );
//
// if ( NewTheta_i > 0.5 * Math.PI )
// throw new Exception( "Incoming direction below surface!" );
// if ( NewTheta_r > 0.5 * Math.PI )
// throw new Exception( "Outgoing direction below surface!" );
// if ( TempIn.z < 0.0 )
// throw new Exception( "VECTOR Incoming direction below surface!" );
// if ( TempOut.z < 0.0 )
// throw new Exception( "VECTOR Outgoing direction below surface!" );
// }
// }
// // DEBUG CHECK
// DEBUG CHECK
// I can't understand the purpose of certain "wrong angles" in the BRDF table
// When Theta_half is near PI/2, and difference angles make the incoming or outgoing directions go BELOW the fucking surface, what does it mean????
// {
// double ThetaHalf = 0.5*Math.PI;
// double ThetaDiff = 0.1*Math.PI; // This should make the incoming direction go BELOW the surface...
// double PhiDiff = 0.0;
//
// // Check it's below the surface
// Vector3 TempIn = new Vector3(), TempOut = new Vector3();
// half_diff_coords_to_std_coords( ThetaHalf, 0, ThetaDiff, PhiDiff, ref TempIn, ref TempOut );
//
// // Get BRDF index
// int TableIndex = PhiDiff_index( PhiDiff );
// TableIndex += (BRDF_SAMPLING_RES_PHI_D / 2) * ThetaDiff_index( ThetaDiff );
// TableIndex += (BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D / 2) * ThetaHalf_index( ThetaHalf );
//
// // What the fuck is there??
// double Value = BRDF[0][TableIndex];
//
// // A tiny negative value...
// }
// DEBUG CHECK
// DEBUG Clear out BRDF values to make sure we only read positive samples. If we find a negative sample then there is an error!
// for ( int PhiDiffIndex=0; PhiDiffIndex < BRDF_SAMPLING_RES_PHI_D/2; PhiDiffIndex++ )
// for ( int ThetaDiffIndex=0; ThetaDiffIndex < BRDF_SAMPLING_RES_THETA_D; ThetaDiffIndex++ )
// for ( int ThetaHalfIndex=0; ThetaHalfIndex < BRDF_SAMPLING_RES_THETA_H; ThetaHalfIndex++ )
// {
// int TableIndex = PhiDiffIndex;
// TableIndex += (BRDF_SAMPLING_RES_PHI_D / 2) * ThetaDiffIndex;
// TableIndex += (BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D / 2) * ThetaHalfIndex;
//
// BRDF[0][TableIndex] = -1.0f;
// }
// DEBUG
// Generate the sampling base
// => We generate and store as many samples as possible in the 90*90*360/2 source array
// => We generate the associated incoming/outgoing direction
// => We store the 3 dot product coefficients for the sample, which we will use to evaluate the cosine lobe
//
Random RNG = new Random( 1 );
double dPhi = Math.PI / (2*SAMPLES_COUNT_THETA);
double dTheta = 0.5*Math.PI / SAMPLES_COUNT_THETA;
Vector3 MinValues = new Vector3() { x=+double.MaxValue, y=+double.MaxValue, z=+double.MaxValue };
Vector3 MaxValues = new Vector3() { x=-double.MaxValue, y=-double.MaxValue, z=-double.MaxValue };
Vector3 In = new Vector3();
Vector3 Out = new Vector3();
List<BRDFSample> Samples = new List<BRDFSample>();
for ( int PhiDiffIndex=0; PhiDiffIndex < 2*SAMPLES_COUNT_THETA; PhiDiffIndex++ )
{
for ( int ThetaDiffIndex=0; ThetaDiffIndex < SAMPLES_COUNT_THETA; ThetaDiffIndex++ )
{
for ( int ThetaHalfIndex=0; ThetaHalfIndex < SAMPLES_COUNT_THETA; ThetaHalfIndex++ )
{
// Generate random stratified samples
double PhiDiff = dPhi * (PhiDiffIndex + RNG.NextDouble());
double ThetaDiff = dTheta * (ThetaDiffIndex + RNG.NextDouble());
double ThetaHalf = dTheta * (ThetaHalfIndex + RNG.NextDouble());
// double PhiDiff = dPhi * PhiDiffIndex;
// double ThetaDiff = dTheta * ThetaDiffIndex;
// double ThetaHalf = dTheta * ThetaHalfIndex;
// Retrieve incoming/outgoing vectors
half_diff_coords_to_std_coords( ThetaHalf, 0.0, ThetaDiff, PhiDiff, ref In, ref Out );
// Build the general BRDF index
int TableIndex = PhiDiff_index( PhiDiff );
TableIndex += (BRDF_SAMPLING_RES_PHI_D / 2) * ThetaDiff_index( ThetaDiff );
TableIndex += (BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D / 2) * ThetaHalf_index( ThetaHalf );
// Check the in & out directions are valid (i.e. ABOVE the surface)
const double Z_TOL = 0.001;
if ( In.z <= Z_TOL || Out.z <= Z_TOL )
continue; // Invalid sample...
//////////////////////////////////////////////////////////////////////////
// Replace any invalid value
double R = BRDF[0][TableIndex];
double G = BRDF[1][TableIndex];
double B = BRDF[2][TableIndex];
double SumValidValues = 0.0;
int ValidValuesCount = 0;
if ( IsValid( R ) )
{ // Red is valid
SumValidValues += R;
ValidValuesCount++;
}
if ( IsValid( G ) )
{ // Green is valid
SumValidValues += G;
ValidValuesCount++;
}
if ( IsValid( B ) )
{ // Blue is valid
SumValidValues += B;
ValidValuesCount++;
}
if ( ValidValuesCount != 3 )
{
SumValidValues /= Math.Max( 1, ValidValuesCount ); // Get the average of valid values
if ( !IsValid( R ) )
R = SumValidValues; // Replace Red by average...
if ( !IsValid( G ) )
G = SumValidValues; // Replace Green by average...
if ( !IsValid( B ) )
B = SumValidValues; // Replace Blue by average...
BRDF[0][TableIndex] = R;
BRDF[1][TableIndex] = G;
BRDF[2][TableIndex] = B;
}
//////////////////////////////////////////////////////////////////////////
BRDFSample Sample = new BRDFSample();
Samples.Add( Sample );
Sample.m_BRDFIndex = TableIndex;
// Store the cos(ThetaIn)
Sample.m_CosThetaIn = In.z;
// Build the dot product coefficients used by the Lafortune model
Sample.m_DotProduct.Set(
In.x*Out.x,
In.y*Out.y,
In.z*Out.z
);
// DEBUG Keep min/max values to have an idea of what we're manipulating here...
MinValues.x = Math.Min( MinValues.x, R );
MaxValues.x = Math.Max( MaxValues.x, R );
MinValues.y = Math.Min( MinValues.y, G );
MaxValues.y = Math.Max( MaxValues.y, G );
MinValues.z = Math.Min( MinValues.z, B );
MaxValues.z = Math.Max( MaxValues.z, B );
// DEBUG
// DEBUG
// Patch the BRDF to make it look like an obvious cosine lobe from a standard Phong reflection
// {
// Vector3 Reflect = new Vector3() { x=-In.x, y=-In.y, z=In.z };
// double Dot = Reflect.Dot( ref Out );
//
// // Vector3 Half = new Vector3() { x=In.x+Out.x, y=In.y+Out.y, z=In.z+Out.z };
// // Half.Normalize();
// // double Dot = Half.z;
// Dot = Math.Max( 0, Dot );
// Dot = Math.Pow( Dot, 17.2 ); // The exponent is very particular
// // Dot /= In.z; // / cos(ThetaIn)
//
// BRDF[0][TableIndex] = Dot;
// }
// DEBUG
}
}
}
// We got our samples!
ms_BRDFSamples = Samples.ToArray();
double PercentageOfTableUsed = (double) ms_BRDFSamples.Length / (BRDF_SAMPLING_RES_THETA_H*BRDF_SAMPLING_RES_THETA_D*BRDF_SAMPLING_RES_PHI_D / 2);
// Show modeless progress form
ms_ProgressForm = new ProgressForm();
ms_ProgressForm.Show();
ms_ProgressForm.BRDFComponentIndex = 0;
ms_ProgressForm.Progress = 0.0;
//////////////////////////////////////////////////////////////////////////
// Setup the solver
{
ms_Solver.Init( ms_BRDFSamples.Length, 4*LobesCount, 45 );
ms_Solver.AbsoluteTolerance = 0.005;
ms_Solver.RelativeTolerance = 0.01;
for ( int LobeIndex=0; LobeIndex < LobesCount; LobeIndex++ )
{
ms_Solver.VectorConstraints.SetConstraint( 4*LobeIndex+0, -2.0, +2.0 ); // Cx
ms_Solver.VectorConstraints.SetConstraint( 4*LobeIndex+1, -2.0, +2.0 ); // Cy
ms_Solver.VectorConstraints.SetConstraint( 4*LobeIndex+2, 0.0, +1.5 ); // Cz
ms_Solver.VectorConstraints.SetConstraint( 4*LobeIndex+3, 1.0, +128 ); // Exponent
}
}
// Build a list of initial guesses
CosineLobe[] InitialGuesses = new CosineLobe[10];
InitialGuesses[0] = new CosineLobe(
new Vector3() { x=-1, y=-1, z=1 }, // Standard Phong reflection
// new Vector3() { x=0, y=0, z=1 }, // Dumb test
// 17.2 );
1.0 );
for ( int i=1; i < InitialGuesses.Length; i++ )
{
Vector3 Direction = new Vector3() { x=2.0*RNG.NextDouble()-1.0, y=2.0*RNG.NextDouble()-1.0, z=RNG.NextDouble() };
InitialGuesses[i] = new CosineLobe( Direction, 1.0 );
}
// Perform local minimization for each R,G,B component
CosineLobe[][] CosineLobes = new CosineLobe[3][];
double[][] RMSErrors = new double[3][];
try
{
for ( int ComponentIndex=0; ComponentIndex < 3; ComponentIndex++ )
{
CosineLobes[ComponentIndex] = new CosineLobe[LobesCount];
RMSErrors[ComponentIndex] = new double[LobesCount];
ms_ProgressForm.BRDFComponentIndex = ComponentIndex;
FitBRDF( BRDF[ComponentIndex], CosineLobes[ComponentIndex], InitialGuesses, BFGS_CONVERGENCE_TOLERANCE, RMSErrors[ComponentIndex], ShowProgress );
// // CHECK We get the same result starting from another initial guess
// CosineLobe[] TempLobes = new CosineLobe[LobesCount];
// InitialGuesses[0] = new CosineLobe( new Vector3() { x=0, y=0, z=1 }, 1 );
// FitBRDF( BRDF[ComponentIndex], TempLobes, InitialGuesses, BFGS_CONVERGENCE_TOLERANCE, RMSErrors[ComponentIndex], ShowProgress );
// // CHECK
}
}
catch ( Exception _e )
{
throw new Exception( "BRDF Fitting failed: " + _e.Message );
}
ms_ProgressForm.Dispose();
// Save the result
try
{
using ( FileStream Stream = TargetFile.Create() )
using ( BinaryWriter Writer = new BinaryWriter( Stream ) )
{
// Write the amount of lobes
Writer.Write( LobesCount );
// Write the coefficients
for ( int ComponentIndex=0; ComponentIndex < 3; ComponentIndex++ )
{
for ( int LobeIndex=0; LobeIndex < LobesCount; LobeIndex++ )
{
CosineLobe Lobe = CosineLobes[ComponentIndex][LobeIndex];
Writer.Write( Lobe.C.x );
Writer.Write( Lobe.C.y );
Writer.Write( Lobe.C.z );
Writer.Write( Lobe.N );
}
}
}
}
catch ( Exception _e )
{
throw new Exception( "Error writing the result cosine lobe coefficients: " + _e.Message );
}
}
catch ( Exception _e )
{
MessageBox.Show( "An error occurred!\r\n\r\n" + _e.Message + "\r\n\r\n" + _e.StackTrace, "BRDF Fitting", MessageBoxButtons.OK, MessageBoxIcon.Warning );
}
}