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 f*****g 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 f**k 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; // 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 ); } }
/// <summary> /// Computes the square difference between a current cosine lobe estimate and a goal BRDF given a set of samples /// </summary> /// <param name="_SamplesCollection">The collection of samples to use for the computation</param> /// <param name="_Normalizer">The normalizer for the final result</param> /// <param name="_GoalBDRF">The goal BRDF function to compute square difference from</param> /// <param name="_LobeEstimates">The cosine lobes matching the BRDF</param> /// <returns>The square difference between goal and estimate</returns> private static double ComputeSummedDifferences( BRDFSample[] _Samples, double _Normalizer, double[] _GoalBDRF, CosineLobe[] _LobeEstimates ) { // Sum differences between current ZH estimates and current SH goal estimates double SumSquareDifference = 0.0; double GoalValue, CurrentValue, TempLobeDot; int SamplesCount = _Samples.Length; int LobesCount = _LobeEstimates.Length; for ( int SampleIndex=0; SampleIndex < SamplesCount; SampleIndex++ ) { BRDFSample Sample = _Samples[SampleIndex]; // Get BRDF value for that sample GoalValue = _GoalBDRF[Sample.m_BRDFIndex]; // if ( GoalValue < 0.0 ) // throw new Exception( "Unexpected negative value!" ); if ( double.IsNaN( GoalValue ) ) throw new Exception( "Unexpected NaN!" ); // DEBUG => Multiply by cos(ThetaIn) to test if it gets us a better conditioning GoalValue *= Sample.m_CosThetaIn; // Estimate cosine lobe value in that direction CurrentValue = 0.0; for ( int LobeIndex=0; LobeIndex < LobesCount; LobeIndex++ ) { CosineLobe Lobe = _LobeEstimates[LobeIndex]; TempLobeDot = Lobe.C.x * Sample.m_DotProduct.x + Lobe.C.y * Sample.m_DotProduct.y + Lobe.C.z * Sample.m_DotProduct.z; TempLobeDot = Math.Max( 0.0, TempLobeDot ); TempLobeDot = Math.Pow( TempLobeDot, Lobe.N ); CurrentValue += TempLobeDot; } // Sum difference between estimate and goal SumSquareDifference += (CurrentValue - GoalValue) * (CurrentValue - GoalValue); // SumSquareDifference += Math.Abs( CurrentValue - GoalValue ); // Just to avoid super large numbers! } // Normalize SumSquareDifference *= _Normalizer; return SumSquareDifference; }