You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
404 lines
19 KiB
404 lines
19 KiB
using System;
|
|
using System.IO;
|
|
using Unity.Collections;
|
|
using Unity.Jobs;
|
|
|
|
namespace UnityEngine.Rendering.HighDefinition.LTC
|
|
{
|
|
internal class LTCTableGenerator
|
|
{
|
|
internal enum LTCTableParametrization
|
|
{
|
|
CosTheta,
|
|
Theta
|
|
}
|
|
|
|
// Minimal roughness to avoid singularities
|
|
const float k_MinRoughness = 0.001f;
|
|
const int k_MaxIterations = 100;
|
|
const float k_FitExploreDelta = 0.05f;
|
|
const float k_Tolerance = 1e-5f;
|
|
|
|
// Holds all the information required to achieve a LTC table generation
|
|
internal class BRDFGenerator
|
|
{
|
|
public Type type;
|
|
public IBRDF brdf;
|
|
public LTCTableParametrization parametrization;
|
|
public bool shouldGenerate;
|
|
public int tableResolution;
|
|
public int sampleCount;
|
|
public string outputDir;
|
|
|
|
public BRDFGenerator(Type targetType, int tableResolution, int sampleCount, LTCTableParametrization parametrization, string outputDir)
|
|
{
|
|
this.type = targetType;
|
|
this.brdf = (IBRDF)Activator.CreateInstance(targetType);
|
|
this.shouldGenerate = true;
|
|
this.tableResolution = tableResolution;
|
|
this.sampleCount = sampleCount;
|
|
this.outputDir = outputDir;
|
|
this.parametrization = parametrization;
|
|
}
|
|
};
|
|
|
|
struct BRDFGeneratorJob : IJobParallelFor
|
|
{
|
|
[NativeDisableParallelForRestriction]
|
|
public NativeArray<LTCData> ltcData;
|
|
public int tableResolution;
|
|
public int sampleCount;
|
|
public LTCLightingModel lightingModel;
|
|
public LTCTableParametrization parametrization;
|
|
|
|
public void Fit(int roughnessIndex, int thetaIndex, NelderMead fitter, IBRDF brdf)
|
|
{
|
|
// Compute the roughness and cosTheta for this sample
|
|
float roughness, cosTheta;
|
|
GetRoughnessAndAngle(roughnessIndex, thetaIndex, tableResolution, parametrization, out roughness, out cosTheta);
|
|
|
|
// Compute the matching view vector
|
|
Vector3 tsView = new Vector3(Mathf.Sqrt(1 - cosTheta * cosTheta), 0, cosTheta);
|
|
|
|
// Compute BRDF's magnitude and average direction
|
|
LTCData currentLTCData;
|
|
LTCDataUtilities.Initialize(out currentLTCData);
|
|
LTCDataUtilities.ComputeAverageTerms(brdf, ref tsView, roughness, sampleCount, ref currentLTCData);
|
|
|
|
// Otherwise use average direction as Z vector
|
|
int previousLTCDataIndex = (thetaIndex - 1) * tableResolution + roughnessIndex;
|
|
LTCData previousLTC = ltcData[previousLTCDataIndex];
|
|
currentLTCData.m11 = previousLTC.m11;
|
|
currentLTCData.m22 = previousLTC.m22;
|
|
currentLTCData.m13 = previousLTC.m13;
|
|
|
|
LTCDataUtilities.Update(ref currentLTCData);
|
|
|
|
// Find best-fit LTC lobe (scale, alphax, alphay)
|
|
if (currentLTCData.magnitude > 1e-6)
|
|
{
|
|
double[] startFit = LTCDataUtilities.GetFittingParms(in currentLTCData);
|
|
double[] resultFit = new double[startFit.Length];
|
|
|
|
int localSampleCount = sampleCount;
|
|
currentLTCData.error = (float)fitter.FindFit(resultFit, startFit, (double)k_FitExploreDelta, (double)k_Tolerance, k_MaxIterations, (double[] parameters) =>
|
|
{
|
|
LTCDataUtilities.SetFittingParms(ref currentLTCData, parameters, false);
|
|
return ComputeError(currentLTCData, brdf, localSampleCount, ref tsView, roughness);
|
|
});
|
|
currentLTCData.iterationsCount = fitter.m_lastIterationsCount;
|
|
|
|
// Update LTC with final best fitting values
|
|
LTCDataUtilities.SetFittingParms(ref currentLTCData, resultFit, false);
|
|
}
|
|
|
|
// Store new valid result
|
|
int currentLTCDataIndex = thetaIndex * tableResolution + roughnessIndex;
|
|
ltcData[currentLTCDataIndex] = currentLTCData;
|
|
}
|
|
|
|
public void Execute(int roughnessIndex)
|
|
{
|
|
// Create the fitter
|
|
NelderMead fitter = new NelderMead(3);
|
|
IBRDF brdf = LTCAreaLight.GetBRDFInterface(lightingModel);
|
|
// Compute all the missing LTCData (0 of the first line is already done)
|
|
for (int thetaIndex = 1; thetaIndex < tableResolution; thetaIndex++)
|
|
{
|
|
Fit(roughnessIndex, thetaIndex, fitter, brdf);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void GetRoughnessAndAngle(int roughnessIndex, int thetaIndex, int tableResolution, LTCTableParametrization parametrization, out float alpha, out float cosTheta)
|
|
{
|
|
float perceptualRoughness = (float)roughnessIndex / (tableResolution - 1);
|
|
alpha = Mathf.Max(k_MinRoughness, perceptualRoughness * perceptualRoughness);
|
|
|
|
if (parametrization == LTCTableParametrization.CosTheta)
|
|
{
|
|
// Parameterised by sqrt(1 - cos(theta))
|
|
float v = (float)thetaIndex / (tableResolution - 1);
|
|
cosTheta = 1.0f - v * v;
|
|
// Clamp to cos(1.57)
|
|
cosTheta = Mathf.Max(3.7540224885647058065387021283285e-4f, cosTheta);
|
|
}
|
|
else
|
|
{
|
|
float theta = Mathf.Min(1.57f, thetaIndex / (float)(tableResolution - 1) * 1.57079f);
|
|
cosTheta = Mathf.Cos(theta);
|
|
}
|
|
}
|
|
|
|
static public void FitInitial(BRDFGenerator brdfGenerator, NelderMead fitter, NativeArray<LTCData> ltcData, int roughnessIndex, int thetaIndex)
|
|
{
|
|
// Compute the roughness and cosTheta for this sample
|
|
float roughness, cosTheta;
|
|
GetRoughnessAndAngle(roughnessIndex, thetaIndex, brdfGenerator.tableResolution, brdfGenerator.parametrization, out roughness, out cosTheta);
|
|
|
|
// Compute the matching view vector
|
|
Vector3 tsView = new Vector3(Mathf.Sqrt(1 - cosTheta * cosTheta), 0, cosTheta);
|
|
|
|
// Compute BRDF's magnitude and average direction
|
|
LTCData currentLTCData;
|
|
LTCDataUtilities.Initialize(out currentLTCData);
|
|
LTCDataUtilities.ComputeAverageTerms(brdfGenerator.brdf, ref tsView, roughness, brdfGenerator.sampleCount, ref currentLTCData);
|
|
|
|
// if theta == 0 the lobe is rotationally symmetric and aligned with Z = (0 0 1)
|
|
currentLTCData.X.x = 1;
|
|
currentLTCData.X.y = 0;
|
|
currentLTCData.X.z = 0;
|
|
|
|
currentLTCData.Y.x = 0;
|
|
currentLTCData.Y.y = 1;
|
|
currentLTCData.Y.z = 0;
|
|
|
|
currentLTCData.Z.x = 0;
|
|
currentLTCData.Z.y = 0;
|
|
currentLTCData.Z.z = 1;
|
|
|
|
if (roughnessIndex == (brdfGenerator.tableResolution - 1))
|
|
{
|
|
// roughness = 1 or no available result
|
|
currentLTCData.m11 = 1.0f;
|
|
currentLTCData.m22 = 1.0f;
|
|
}
|
|
else
|
|
{
|
|
// init with roughness of previous fit
|
|
LTCData previousLTC = ltcData[roughnessIndex + 1];
|
|
currentLTCData.m11 = previousLTC.m11;
|
|
currentLTCData.m22 = previousLTC.m22;
|
|
}
|
|
currentLTCData.m13 = 0;
|
|
|
|
LTCDataUtilities.Update(ref currentLTCData);
|
|
|
|
// Find best-fit LTC lobe (scale, alphax, alphay)
|
|
if (currentLTCData.magnitude > 1e-6)
|
|
{
|
|
double[] startFit = LTCDataUtilities.GetFittingParms(in currentLTCData);
|
|
double[] resultFit = new double[startFit.Length];
|
|
|
|
currentLTCData.error = (float)fitter.FindFit(resultFit, startFit, k_FitExploreDelta, k_Tolerance, k_MaxIterations, (double[] parameters) =>
|
|
{
|
|
LTCDataUtilities.SetFittingParms(ref currentLTCData, parameters, true);
|
|
return ComputeError(currentLTCData, brdfGenerator.brdf, brdfGenerator.sampleCount, ref tsView, roughness);
|
|
});
|
|
currentLTCData.iterationsCount = fitter.m_lastIterationsCount;
|
|
|
|
// Update LTC with final best fitting values
|
|
LTCDataUtilities.SetFittingParms(ref currentLTCData, resultFit, true);
|
|
}
|
|
|
|
// Store new valid result
|
|
ltcData[roughnessIndex] = currentLTCData;
|
|
}
|
|
|
|
// Compute the error between the BRDF and the LTC using Multiple Importance Sampling
|
|
static float ComputeError(LTCData ltcData, IBRDF brdf, int sampleCount, ref Vector3 _tsView, float _alpha)
|
|
{
|
|
Vector3 tsLight = Vector3.zero;
|
|
|
|
double pdf_BRDF, eval_BRDF;
|
|
double pdf_LTC, eval_LTC;
|
|
|
|
float sumError = 0.0f;
|
|
for (int j = 0; j < sampleCount; ++j)
|
|
{
|
|
for (int i = 0; i < sampleCount; ++i)
|
|
{
|
|
float U1 = (i + 0.5f) / sampleCount;
|
|
float U2 = (j + 0.5f) / sampleCount;
|
|
|
|
// importance sample LTC
|
|
{
|
|
// sample
|
|
LTCDataUtilities.GetSamplingDirection(ltcData, U1, U2, ref tsLight);
|
|
|
|
eval_BRDF = brdf.Eval(ref _tsView, ref tsLight, _alpha, out pdf_BRDF);
|
|
eval_LTC = (float)LTCDataUtilities.Eval(ltcData, ref tsLight);
|
|
pdf_LTC = eval_LTC / ltcData.magnitude;
|
|
|
|
// error with MIS weight
|
|
float error = Mathf.Abs((float)(eval_BRDF - eval_LTC));
|
|
error = error * error * error; // Use L3 norm to favor large values over smaller ones
|
|
if (error != 0.0f)
|
|
error /= (float)pdf_LTC + (float)pdf_BRDF;
|
|
|
|
if (double.IsNaN(error))
|
|
{
|
|
// SHOULD NEVER HAPPEN
|
|
}
|
|
sumError += error;
|
|
}
|
|
|
|
// importance sample BRDF
|
|
{
|
|
// sample
|
|
brdf.GetSamplingDirection(ref _tsView, _alpha, U1, U2, ref tsLight);
|
|
|
|
// error with MIS weight
|
|
eval_BRDF = brdf.Eval(ref _tsView, ref tsLight, _alpha, out pdf_BRDF);
|
|
eval_LTC = LTCDataUtilities.Eval(ltcData, ref tsLight);
|
|
pdf_LTC = eval_LTC / ltcData.magnitude;
|
|
float error = Mathf.Abs((float)(eval_BRDF - eval_LTC));
|
|
error = error * error * error; // Use L3 norm to favor large values over smaller ones
|
|
|
|
if (error != 0.0f)
|
|
error /= (float)pdf_LTC + (float)pdf_BRDF;
|
|
|
|
if (double.IsNaN(error))
|
|
{
|
|
// SHOULD NEVER HAPPEN
|
|
}
|
|
sumError += error;
|
|
}
|
|
}
|
|
}
|
|
|
|
return sumError / ((float)sampleCount * sampleCount);
|
|
}
|
|
|
|
static public void ExecuteFittingJob(BRDFGenerator brdfGenerator, bool parallel)
|
|
{
|
|
// When dispatching the table on the two dimensions (X, Y) a set of constrains apply:
|
|
// - Every element (Xi, Yi) has a dependency on the previous one on the same column.
|
|
// - The first element of a column has a dependency on the first element of the previous column.
|
|
// - The element (0,0) doesn't have a dependency on any element.
|
|
// To be able to dispatch this as a job, we need to compute the first line linearly and then dispatch every column starting from the second element.
|
|
|
|
using (var ltcData = new NativeArray<LTCData>(brdfGenerator.tableResolution * brdfGenerator.tableResolution, Allocator.TempJob))
|
|
{
|
|
// Create the fitter
|
|
NelderMead fitter = new NelderMead(3);
|
|
|
|
Debug.Log("Running fitting job on the " + brdfGenerator.type.Name + " BRDF.");
|
|
|
|
// Fill the first line
|
|
for (int roughnessIndex = brdfGenerator.tableResolution - 1; roughnessIndex >= 0; roughnessIndex--)
|
|
FitInitial(brdfGenerator, fitter, ltcData, roughnessIndex, 0);
|
|
|
|
BRDFGeneratorJob brdfJob = new BRDFGeneratorJob
|
|
{
|
|
ltcData = ltcData,
|
|
tableResolution = brdfGenerator.tableResolution,
|
|
sampleCount = brdfGenerator.sampleCount,
|
|
lightingModel = brdfGenerator.brdf.GetLightingModel(),
|
|
parametrization = brdfGenerator.parametrization,
|
|
};
|
|
|
|
if (parallel)
|
|
{
|
|
// Create, run the job and wait for its completion.
|
|
JobHandle fittingJob = brdfJob.Schedule(brdfGenerator.tableResolution, 1);
|
|
fittingJob.Complete();
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < brdfGenerator.tableResolution; ++i)
|
|
{
|
|
brdfJob.Execute(i);
|
|
}
|
|
}
|
|
|
|
Debug.Log("Fitting done. Exporting the file");
|
|
|
|
// Export the table to disk
|
|
string BRDFName = brdfGenerator.type.Name;
|
|
FileInfo CSharpFileName = new FileInfo(Path.Combine(brdfGenerator.outputDir, "LtcData." + BRDFName + ".cs"));
|
|
ExportToCSharp(ltcData, brdfGenerator.tableResolution, brdfGenerator.parametrization, CSharpFileName, BRDFName);
|
|
}
|
|
}
|
|
|
|
static void ExportToCSharp(NativeArray<LTCData> ltcDataArray, int tableResolution, LTCTableParametrization parametrization, FileInfo _CSharpFileName, string brdfName)
|
|
{
|
|
string sourceCode = "";
|
|
|
|
LTCData ltcData;
|
|
ltcData.magnitude = 0.0f;
|
|
|
|
string tableName = "s_LtcMatrixData_" + brdfName;
|
|
|
|
sourceCode += "using UnityEngine;\n"
|
|
+ "using System;\n"
|
|
+ "\n"
|
|
+ "namespace UnityEngine.Rendering.HighDefinition\n"
|
|
+ "{\n"
|
|
+ " internal partial class LTCAreaLight\n"
|
|
+ " {\n"
|
|
+ " // [GENERATED CONTENT " + DateTime.Now.ToString("dd MMM yyyy HH:mm:ss") + "]\n"
|
|
+ " // Table contains 3x3 matrix coefficients of M^-1 for the fitting of the " + brdfName + " BRDF using the LTC technique\n"
|
|
+ " // Only the 0,2,4,6 of the 3x3 coefficients are emitted, and they are encoded into FP16\n"
|
|
+ " // From \"Real-Time Polygonal-Light Shading with Linearly Transformed Cosines\" 2016 (https://eheitzresearch.wordpress.com/415-2/)\n"
|
|
+ " //\n"
|
|
+ " // The table is accessed via LTCAreaLight." + tableName + "[<roughnessIndex> + 64 * <thetaIndex>] // Theta values are along the Y axis, Roughness values are along the X axis\n"
|
|
+ " // • roughness = ( <roughnessIndex> / " + (tableResolution - 1) + " )^2 (the table is indexed by perceptual roughness)\n";
|
|
if (parametrization == LTCTableParametrization.CosTheta)
|
|
sourceCode += " // • cosTheta = 1 - ( <thetaIndex> / " + (tableResolution - 1) + " )^2\n";
|
|
else
|
|
sourceCode += " // • theta = ( <thetaIndex> / " + (tableResolution - 1) + " )\n";
|
|
sourceCode += " //\n"
|
|
+ " internal static ushort[] " + tableName + " = new ushort[" + tableResolution + " * " + tableResolution + " * 4]\n"
|
|
+ " {";
|
|
|
|
string lotsOfSpaces = " ";
|
|
float alpha, cosTheta;
|
|
for (int thetaIndex = 0; thetaIndex < tableResolution; thetaIndex++)
|
|
{
|
|
GetRoughnessAndAngle(0, thetaIndex, tableResolution, parametrization, out alpha, out cosTheta);
|
|
sourceCode += "\n";
|
|
if (parametrization == LTCTableParametrization.CosTheta)
|
|
sourceCode += " // Cos (theta) = " + cosTheta + "\n";
|
|
else
|
|
sourceCode += " // Theta = " + Mathf.Acos(cosTheta) + "\n";
|
|
|
|
for (int roughnessIndex = 0; roughnessIndex < tableResolution; roughnessIndex++)
|
|
{
|
|
// Compute the current ltc data index
|
|
int currentIndexData = roughnessIndex + thetaIndex * tableResolution;
|
|
ltcData = ltcDataArray[currentIndexData];
|
|
|
|
GetRoughnessAndAngle(roughnessIndex, thetaIndex, tableResolution, parametrization, out alpha, out cosTheta);
|
|
|
|
// Export the matrix as a list of 3x3 doubles, columns first. Only emit 0,2,4,6 elements of the list
|
|
// since others are zeroes or ones.
|
|
double factor = 1.0 / ltcData.invM.m22;
|
|
|
|
float val0 = (float) (factor * ltcData.invM.m00);
|
|
float val2 = (float) (factor * ltcData.invM.m20);
|
|
float val4 = (float) (factor * ltcData.invM.m11);
|
|
float val6 = (float) (factor * ltcData.invM.m02);
|
|
|
|
float fp16Max = 65504.0f;
|
|
Debug.Assert(Mathf.Abs(val0) <= fp16Max, "This FP32 value is too large to be converted to FP16.");
|
|
Debug.Assert(Mathf.Abs(val2) <= fp16Max, "This FP32 value is too large to be converted to FP16.");
|
|
Debug.Assert(Mathf.Abs(val4) <= fp16Max, "This FP32 value is too large to be converted to FP16.");
|
|
Debug.Assert(Mathf.Abs(val6) <= fp16Max, "This FP32 value is too large to be converted to FP16.");
|
|
|
|
string line = $" {Mathf.FloatToHalf(val0)}, {Mathf.FloatToHalf(val2)}, {Mathf.FloatToHalf(val4)}, {Mathf.FloatToHalf(val6)},";
|
|
if (line.Length < 45)
|
|
line += lotsOfSpaces.Substring(lotsOfSpaces.Length - (45 - line.Length)); // Pad with spaces
|
|
sourceCode += line;
|
|
sourceCode += "// alpha = " + alpha + "\n";
|
|
}
|
|
}
|
|
|
|
sourceCode += " };\n";
|
|
|
|
// End comment
|
|
sourceCode += "\n";
|
|
sourceCode += " // NOTE: Formerly, we needed to also export and create a table for the BRDF's amplitude factor + fresnel coefficient\n";
|
|
sourceCode += " // but it turns out these 2 factors are actually already precomputed and available in the FGD table corresponding\n";
|
|
sourceCode += " // to the " + brdfName + " BRDF, therefore they are no longer exported...\n";
|
|
|
|
// Close class and namespace
|
|
sourceCode += " }\n";
|
|
sourceCode += "}\n";
|
|
|
|
// Write content
|
|
using (StreamWriter W = _CSharpFileName.CreateText())
|
|
W.Write(sourceCode);
|
|
}
|
|
}
|
|
}
|