276 lines
11 KiB
C#

using System;
using UnityEditor.Rendering.HighDefinition;
namespace UnityEngine.Rendering.HighDefinition
{
[GenerateHLSL]
class DiffusionProfileConstants
{
public const int DIFFUSION_PROFILE_COUNT = 16; // Max. number of profiles, including the slot taken by the neutral profile
public const int DIFFUSION_PROFILE_NEUTRAL_ID = 0; // Does not result in blurring
public const int SSS_PIXELS_PER_SAMPLE = 4;
}
enum DefaultSssSampleBudgetForQualityLevel
{
Low = 20,
Medium = 40,
High = 80,
Max = 1000
}
[Serializable]
class DiffusionProfile : IEquatable<DiffusionProfile>
{
public enum TexturingMode : uint
{
PreAndPostScatter = 0,
PostScatter = 1
}
public enum TransmissionMode : uint
{
Regular = 0,
ThinObject = 1
}
[ColorUsage(false, true)]
public Color scatteringDistance; // Per color channel (no meaningful units)
[ColorUsage(false, true)]
public Color transmissionTint; // HDR color
public TexturingMode texturingMode;
public TransmissionMode transmissionMode;
public Vector2 thicknessRemap; // X = min, Y = max (in millimeters)
public float worldScale; // Size of the world unit in meters
public float ior; // 1.4 for skin (mean ~0.028)
public Vector3 shapeParam { get; private set; } // RGB = shape parameter: S = 1 / D
public float filterRadius { get; private set; } // In millimeters
public float maxScatteringDistance { get; private set; } // No meaningful units
// Unique hash used in shaders to identify the index in the diffusion profile array
public uint hash = 0;
// Here we need to have one parameter in the diffusion profile parameter because the deserialization call the default constructor
public DiffusionProfile(bool dontUseDefaultConstructor)
{
ResetToDefault();
}
public void ResetToDefault()
{
scatteringDistance = Color.grey;
transmissionTint = Color.white;
texturingMode = TexturingMode.PreAndPostScatter;
transmissionMode = TransmissionMode.ThinObject;
thicknessRemap = new Vector2(0f, 5f);
worldScale = 1f;
ior = 1.4f; // Typical value for skin specular reflectance
}
internal void Validate()
{
thicknessRemap.y = Mathf.Max(thicknessRemap.y, 0f);
thicknessRemap.x = Mathf.Clamp(thicknessRemap.x, 0f, thicknessRemap.y);
worldScale = Mathf.Max(worldScale, 0.001f);
ior = Mathf.Clamp(ior, 1.0f, 2.0f);
UpdateKernel();
}
// Ref: Approximate Reflectance Profiles for Efficient Subsurface Scattering by Pixar.
void UpdateKernel()
{
Vector3 sd = (Vector3)(Vector4)scatteringDistance;
// Rather inconvenient to support (S = Inf).
shapeParam = new Vector3(Mathf.Min(16777216, 1.0f / sd.x),
Mathf.Min(16777216, 1.0f / sd.y),
Mathf.Min(16777216, 1.0f / sd.z));
// Filter radius is, strictly speaking, infinite.
// The magnitude of the function decays exponentially, but it is never truly zero.
// To estimate the radius, we can use adapt the "three-sigma rule" by defining
// the radius of the kernel by the value of the CDF which corresponds to 99.7%
// of the energy of the filter.
float cdf = 0.997f;
// Importance sample the normalized diffuse reflectance profile for the computed value of 's'.
// ------------------------------------------------------------------------------------
// R[r, phi, s] = s * (Exp[-r * s] + Exp[-r * s / 3]) / (8 * Pi * r)
// PDF[r, phi, s] = r * R[r, phi, s]
// CDF[r, s] = 1 - 1/4 * Exp[-r * s] - 3/4 * Exp[-r * s / 3]
// ------------------------------------------------------------------------------------
// We importance sample the color channel with the widest scattering distance.
maxScatteringDistance = Mathf.Max(sd.x, sd.y, sd.z);
filterRadius = SampleBurleyDiffusionProfile(cdf, maxScatteringDistance);
}
static float DisneyProfile(float r, float s)
{
return s * (Mathf.Exp(-r * s) + Mathf.Exp(-r * s * (1.0f / 3.0f))) / (8.0f * Mathf.PI * r);
}
static float DisneyProfilePdf(float r, float s)
{
return r * DisneyProfile(r, s);
}
static float DisneyProfileCdf(float r, float s)
{
return 1.0f - 0.25f * Mathf.Exp(-r * s) - 0.75f * Mathf.Exp(-r * s * (1.0f / 3.0f));
}
static float DisneyProfileCdfDerivative1(float r, float s)
{
return 0.25f * s * Mathf.Exp(-r * s) * (1.0f + Mathf.Exp(r * s * (2.0f / 3.0f)));
}
static float DisneyProfileCdfDerivative2(float r, float s)
{
return (-1.0f / 12.0f) * s * s * Mathf.Exp(-r * s) * (3.0f + Mathf.Exp(r * s * (2.0f / 3.0f)));
}
// The CDF is not analytically invertible, so we use Halley's Method of root finding.
// { f(r, s, p) = CDF(r, s) - p = 0 } with the initial guess { r = (10^p - 1) / s }.
static float DisneyProfileCdfInverse(float p, float s)
{
// Supply the initial guess.
float r = (Mathf.Pow(10f, p) - 1f) / s;
float t = float.MaxValue;
while (true)
{
float f0 = DisneyProfileCdf(r, s) - p;
float f1 = DisneyProfileCdfDerivative1(r, s);
float f2 = DisneyProfileCdfDerivative2(r, s);
float dr = f0 / (f1 * (1f - f0 * f2 / (2f * f1 * f1)));
if (Mathf.Abs(dr) < t)
{
r = r - dr;
t = Mathf.Abs(dr);
}
else
{
// Converged to the best result.
break;
}
}
return r;
}
// https://zero-radiance.github.io/post/sampling-diffusion/
// Performs sampling of a Normalized Burley diffusion profile in polar coordinates.
// 'u' is the random number (the value of the CDF): [0, 1).
// rcp(s) = 1 / ShapeParam = ScatteringDistance.
// Returns the sampled radial distance, s.t. (u = 0 -> r = 0) and (u = 1 -> r = Inf).
static float SampleBurleyDiffusionProfile(float u, float rcpS)
{
u = 1 - u; // Convert CDF to CCDF
float g = 1 + (4 * u) * (2 * u + Mathf.Sqrt(1 + (4 * u) * u));
float n = Mathf.Pow(g, -1.0f / 3.0f); // g^(-1/3)
float p = (g * n) * n; // g^(+1/3)
float c = 1 + p + n; // 1 + g^(+1/3) + g^(-1/3)
float x = 3 * Mathf.Log(c / (4 * u));
return x * rcpS;
}
public bool Equals(DiffusionProfile other)
{
if (other == null)
return false;
return scatteringDistance == other.scatteringDistance &&
transmissionTint == other.transmissionTint &&
texturingMode == other.texturingMode &&
transmissionMode == other.transmissionMode &&
thicknessRemap == other.thicknessRemap &&
worldScale == other.worldScale &&
ior == other.ior;
}
}
[HelpURL(Documentation.baseURL + Documentation.version + Documentation.subURL + "Diffusion-Profile" + Documentation.endURL)]
internal partial class DiffusionProfileSettings : ScriptableObject
{
[SerializeField]
internal DiffusionProfile profile;
[NonSerialized] internal Vector4 worldScaleAndFilterRadiusAndThicknessRemap; // X = meters per world unit, Y = filter radius (in mm), Z = remap start, W = end - start
[NonSerialized] internal Vector4 shapeParamAndMaxScatterDist; // RGB = S = 1 / D, A = d = RgbMax(D)
[NonSerialized] internal Vector4 transmissionTintAndFresnel0; // RGB = color, A = fresnel0
[NonSerialized] internal Vector4 disabledTransmissionTintAndFresnel0; // RGB = black, A = fresnel0 - For debug to remove the transmission
[NonSerialized] internal int updateCount;
void OnEnable()
{
if (profile == null)
profile = new DiffusionProfile(true);
profile.Validate();
UpdateCache();
#if UNITY_EDITOR
if (m_Version != MigrationDescription.LastVersion<Version>())
{
// We delay the upgrade of the diffusion profile because in the OnEnable we are still
// in the import of the current diffusion profile, so we can't create new assets of the same
// type from here otherwise it will freeze the editor in an infinite import loop.
// Thus we delay the upgrade of one editor frame so the import of this asset is finished.
UnityEditor.EditorApplication.delayCall += TryToUpgrade;
}
UnityEditor.Rendering.HighDefinition.DiffusionProfileHashTable.UpdateDiffusionProfileHashNow(this);
#endif
}
#if UNITY_EDITOR
internal void Reset()
{
if (profile != null && profile.hash == 0)
{
profile.ResetToDefault();
profile.hash = DiffusionProfileHashTable.GenerateUniqueHash(this);
}
}
#endif
internal void UpdateCache()
{
worldScaleAndFilterRadiusAndThicknessRemap = new Vector4(profile.worldScale,
profile.filterRadius,
profile.thicknessRemap.x,
profile.thicknessRemap.y - profile.thicknessRemap.x);
shapeParamAndMaxScatterDist = profile.shapeParam;
shapeParamAndMaxScatterDist.w = profile.maxScatteringDistance;
// Convert ior to fresnel0
float fresnel0 = (profile.ior - 1.0f) / (profile.ior + 1.0f);
fresnel0 *= fresnel0; // square
transmissionTintAndFresnel0 = new Vector4(profile.transmissionTint.r * 0.25f, profile.transmissionTint.g * 0.25f, profile.transmissionTint.b * 0.25f, fresnel0); // Premultiplied
disabledTransmissionTintAndFresnel0 = new Vector4(0.0f, 0.0f, 0.0f, fresnel0);
updateCount++;
}
internal bool HasChanged(int update)
{
return update == updateCount;
}
/// <summary>
/// Initialize the settings for the default diffusion profile.
/// </summary>
public void SetDefaultParams()
{
worldScaleAndFilterRadiusAndThicknessRemap = new Vector4(1, 0, 0, 1);
shapeParamAndMaxScatterDist = new Vector4(16777216, 16777216, 16777216, 0);
transmissionTintAndFresnel0.w = 0.04f; // Match DEFAULT_SPECULAR_VALUE defined in Lit.hlsl
}
}
}