public const int numSamples = 7 ; // Must be an odd number
[SerializeField, ColorUsage(false, true, 0.05f, 2.0f, 1.0f, 1.0f)]
Color m_StdDev1 ;
public Color stdDev1 ;
Color m_StdDev2 ;
public Color stdDev2 ;
float m_LerpWeight ;
public float lerpWeight ;
[SerializeField]
public bool enableTransmittance ;
Vector4 [ ] m_FilterKernel ;
Vector4 [ ] m_FilterKernel ;
Vector4 m_HalfRcpVariance ;
Vector3 [ ] m_HalfRcpVariances ;
public bool m_KernelNeedsUpdate ;
Vector4 m_HalfRcpWeightedVariances ;
m_StdDev1 = new Color ( 0.3f , 0.3f , 0.3f , 0.0f ) ;
m_StdDev2 = new Color ( 1.0f , 1.0f , 1.0f , 0.0f ) ;
m_LerpWeight = 0.5f ;
ComputeKernel ( ) ;
}
stdDev1 = new Color ( 0.3f , 0.3f , 0.3f , 0.0f ) ;
stdDev2 = new Color ( 1.0f , 1.0f , 1.0f , 0.0f ) ;
lerpWeight = 0.5f ;
enableTransmittance = true ;
m_FilterKernel = null ;
m_HalfRcpVariances = null ;
public Color stdDev1
{
get { return m_StdDev1 ; }
set { if ( m_StdDev1 ! = value ) { m_StdDev1 = value ; m_KernelNeedsUpdate = true ; } }
}
public Color stdDev2
{
get { return m_StdDev2 ; }
set { if ( m_StdDev2 ! = value ) { m_StdDev2 = value ; m_KernelNeedsUpdate = true ; } }
}
public float lerpWeight
{
get { return m_LerpWeight ; }
set { if ( m_LerpWeight ! = value ) { m_LerpWeight = value ; m_KernelNeedsUpdate = true ; } }
UpdateKernelAndVarianceData ( ) ;
get { if ( m_KernelNeedsUpdate ) ComputeKernel ( ) ; return m_FilterKernel ; }
// Set via UpdateKernelAndVarianceData().
get { return m_FilterKernel ; }
public Vector4 halfRcpVariance
{
get { if ( m_KernelNeedsUpdate ) ComputeKernel ( ) ; return m_HalfRcpVariance ; }
}
public void SetDirtyFlag ( )
{
m_KernelNeedsUpdate = true ;
}
// --- Private Methods ---
static float Gaussian ( float x , float stdDev )
{
float variance = stdDev * stdDev ;
return Mathf . Exp ( - x * x / ( 2 * variance ) ) / Mathf . Sqrt ( 2 * Mathf . PI * variance ) ;
public Vector3 [ ] halfRcpVariances
{
// Set via UpdateKernelAndVarianceData().
get { return m_HalfRcpVariances ; }
static float GaussianCombination ( float x , float stdDev1 , float stdDev2 , float lerpWeight )
public Vector4 halfRcpWeightedVariances
return Mathf . Lerp ( Gaussian ( x , stdDev1 ) , Gaussian ( x , stdDev2 ) , lerpWeight ) ;
// Set via UpdateKernelAndVarianceData().
get { return m_HalfRcpWeightedVariances ; }
static float RationalApproximation ( float t )
public void UpdateKernelAndVarianceData ( )
// Abramowitz and Stegun formula 26.2.23.
// The absolute value of the error should be less than 4.5 e-4.
float [ ] c = { 2.515517f , 0.802853f , 0.010328f } ;
float [ ] d = { 1.432788f , 0.189269f , 0.001308f } ;
return t - ( ( c [ 2 ] * t + c [ 1 ] ) * t + c [ 0 ] ) / ( ( ( d [ 2 ] * t + d [ 1 ] ) * t + d [ 0 ] ) * t + 1.0f ) ;
}
// Ref: https://www.johndcook.com/blog/csharp_phi_inverse/
static float NormalCdfInverse ( float p , float stdDev )
{
float x ;
if ( p < 0.5 )
if ( m_FilterKernel = = null )
// F^-1(p) = - G^-1(p)
x = - RationalApproximation ( Mathf . Sqrt ( - 2.0f * Mathf . Log ( p ) ) ) ;
}
else
{
// F^-1(p) = G^-1(1-p)
x = RationalApproximation ( Mathf . Sqrt ( - 2.0f * Mathf . Log ( 1.0f - p ) ) ) ;
m_FilterKernel = new Vector4 [ numSamples ] ;
return x * stdDev ;
}
static float GaussianCombinationCdfInverse ( float p , float stdDev1 , float stdDev2 , float lerpWeight )
{
return Mathf . Lerp ( NormalCdfInverse ( p , stdDev1 ) , NormalCdfInverse ( p , stdDev2 ) , lerpWeight ) ;
}
void ComputeKernel ( )
{
if ( m_FilterKernel = = null | | m_FilterKernel . Length ! = numSamples )
if ( m_HalfRcpVariances = = null )
m_FilterKernel = new Vector4 [ numSamples ] ;
m_HalfRcpVariances = new Vector3 [ 2 ] ;
}
// Our goal is to blur the image using a filter which is represented
// It is separable by design, but generally not radially symmetric.
// Find the widest Gaussian across 3 color channels.
float maxStdDev1 = Mathf . Max ( m_S tdDev1. r , m_S tdDev1. g , m_S tdDev1. b ) ;
float maxStdDev2 = Mathf . Max ( m_S tdDev2. r , m_S tdDev2. g , m_S tdDev2. b ) ;
float maxStdDev1 = Mathf . Max ( s tdDev1. r , s tdDev1. g , s tdDev1. b ) ;
float maxStdDev2 = Mathf . Max ( s tdDev2. r , s tdDev2. g , s tdDev2. b ) ;
Vector3 weightSum = new Vector3 ( 0 , 0 , 0 ) ;
float u = ( i + 0.5f ) / numSamples ;
float pos = GaussianCombinationCdfInverse ( u , maxStdDev1 , maxStdDev2 , m_L erpWeight) ;
float pdf = GaussianCombination ( pos , maxStdDev1 , maxStdDev2 , m_L erpWeight) ;
float pos = GaussianCombinationCdfInverse ( u , maxStdDev1 , maxStdDev2 , l erpWeight) ;
float pdf = GaussianCombination ( pos , maxStdDev1 , maxStdDev2 , l erpWeight) ;
val . x = GaussianCombination ( pos , m_S tdDev1. r , m_S tdDev2. r , m_L erpWeight) ;
val . y = GaussianCombination ( pos , m_S tdDev1. g , m_S tdDev2. g , m_L erpWeight) ;
val . z = GaussianCombination ( pos , m_S tdDev1. b , m_S tdDev2. b , m_L erpWeight) ;
val . x = GaussianCombination ( pos , s tdDev1. r , s tdDev2. r , l erpWeight) ;
val . y = GaussianCombination ( pos , s tdDev1. g , s tdDev2. g , l erpWeight) ;
val . z = GaussianCombination ( pos , s tdDev1. b , s tdDev2. b , l erpWeight) ;
// We do not divide by 'numSamples' since we will renormalize, anyway.
m_FilterKernel [ i ] . x = val . x * ( 1 / pdf ) ;
m_FilterKernel [ i ] . z * = 1 / weightSum . z ;
}
// Store (1 / (2 * Variance)) per color channel per Gaussian.
m_HalfRcpVariances [ 0 ] . x = 0.5f / ( stdDev1 . r * stdDev1 . r ) ;
m_HalfRcpVariances [ 0 ] . y = 0.5f / ( stdDev1 . g * stdDev1 . g ) ;
m_HalfRcpVariances [ 0 ] . z = 0.5f / ( stdDev1 . b * stdDev1 . b ) ;
m_HalfRcpVariances [ 1 ] . x = 0.5f / ( stdDev2 . r * stdDev2 . r ) ;
m_HalfRcpVariances [ 1 ] . y = 0.5f / ( stdDev2 . g * stdDev2 . g ) ;
m_HalfRcpVariances [ 1 ] . z = 0.5f / ( stdDev2 . b * stdDev2 . b ) ;
weightedStdDev . x = Mathf . Lerp ( m_StdDev1 . r , m_StdDev2 . r , m_LerpWeight ) ;
weightedStdDev . y = Mathf . Lerp ( m_StdDev1 . g , m_StdDev2 . g , m_LerpWeight ) ;
weightedStdDev . z = Mathf . Lerp ( m_StdDev1 . b , m_StdDev2 . b , m_LerpWeight ) ;
weightedStdDev . w = Mathf . Lerp ( maxStdDev1 , maxStdDev2 , m_LerpWeight ) ;
weightedStdDev . x = Mathf . Lerp ( stdDev1 . r , stdDev2 . r , lerpWeight ) ;
weightedStdDev . y = Mathf . Lerp ( stdDev1 . g , stdDev2 . g , lerpWeight ) ;
weightedStdDev . z = Mathf . Lerp ( stdDev1 . b , stdDev2 . b , lerpWeight ) ;
weightedStdDev . w = Mathf . Lerp ( maxStdDev1 , maxStdDev2 , lerpWeight ) ;
// Store (1 / (2 * Variance)) per color channel.
m_HalfRcpVariance . x = 0.5f / ( weightedStdDev . x * weightedStdDev . x ) ;
m_HalfRcpVariance . y = 0.5f / ( weightedStdDev . y * weightedStdDev . y ) ;
m_HalfRcpVariance . z = 0.5f / ( weightedStdDev . z * weightedStdDev . z ) ;
m_HalfRcpVariance . w = 0.5f / ( weightedStdDev . w * weightedStdDev . w ) ;
// Store (1 / (2 * WeightedVariance)) per color channel.
m_HalfRcpWeightedVariances . x = 0.5f / ( weightedStdDev . x * weightedStdDev . x ) ;
m_HalfRcpWeightedVariances . y = 0.5f / ( weightedStdDev . y * weightedStdDev . y ) ;
m_HalfRcpWeightedVariances . z = 0.5f / ( weightedStdDev . z * weightedStdDev . z ) ;
m_HalfRcpWeightedVariances . w = 0.5f / ( weightedStdDev . w * weightedStdDev . w ) ;
}
// --- Private Methods ---
static float Gaussian ( float x , float stdDev )
{
float variance = stdDev * stdDev ;
return Mathf . Exp ( - x * x / ( 2 * variance ) ) / Mathf . Sqrt ( 2 * Mathf . PI * variance ) ;
}
static float GaussianCombination ( float x , float stdDev1 , float stdDev2 , float lerpWeight )
{
return Mathf . Lerp ( Gaussian ( x , stdDev1 ) , Gaussian ( x , stdDev2 ) , lerpWeight ) ;
}
static float RationalApproximation ( float t )
{
// Abramowitz and Stegun formula 26.2.23.
// The absolute value of the error should be less than 4.5 e-4.
float [ ] c = { 2.515517f , 0.802853f , 0.010328f } ;
float [ ] d = { 1.432788f , 0.189269f , 0.001308f } ;
return t - ( ( c [ 2 ] * t + c [ 1 ] ) * t + c [ 0 ] ) / ( ( ( d [ 2 ] * t + d [ 1 ] ) * t + d [ 0 ] ) * t + 1.0f ) ;
}
// Ref: https://www.johndcook.com/blog/csharp_phi_inverse/
static float NormalCdfInverse ( float p , float stdDev )
{
float x ;
if ( p < 0.5 )
{
// F^-1(p) = - G^-1(p)
x = - RationalApproximation ( Mathf . Sqrt ( - 2.0f * Mathf . Log ( p ) ) ) ;
}
else
{
// F^-1(p) = G^-1(1-p)
x = RationalApproximation ( Mathf . Sqrt ( - 2.0f * Mathf . Log ( 1.0f - p ) ) ) ;
}
return x * stdDev ;
}
static float GaussianCombinationCdfInverse ( float p , float stdDev1 , float stdDev2 , float lerpWeight )
{
return Mathf . Lerp ( NormalCdfInverse ( p , stdDev1 ) , NormalCdfInverse ( p , stdDev2 ) , lerpWeight ) ;
}
}
[SerializeField]
int m_NumProfiles ;
int m_NumProfiles ;
int m_TransmittanceFlags ;
[SerializeField]
Vector4 [ ] m_HalfRcpVariancesAndLerpWeights ;
[SerializeField]
Vector4 [ ] m_HalfRcpWeightedVariances ;
[SerializeField]
Vector4 [ ] m_FilterKernels ;
// --- Public Methods ---
{
m_Profiles [ i ] = new SubsurfaceScatteringProfile ( ) ;
}
OnValidate ( ) ;
public SubsurfaceScatteringProfile [ ] profiles { set { m_Profiles = value ; OnValidate ( ) ; } get { return m_Profiles ; } }
public SubsurfaceScatteringProfile [ ] profiles {
// Set via serialization.
get { return m_Profiles ; }
}
// Returns a bit mask s.t. the i-th bit indicates whether the i-th profile requires transmittance evaluation.
// Supplies '_TransmittanceFlags' to Lit.hlsl.
public int transmittanceFlags {
// Set during OnValidate().
get { return m_TransmittanceFlags ; }
}
// Supplies '_HalfRcpVariancesAndLerpWeights' to Lit.hlsl.
public Vector4 [ ] halfRcpVariancesAndLerpWeights {
// Set during OnValidate().
get { return m_HalfRcpVariancesAndLerpWeights ; }
}
// Supplies '_HalfRcpWeightedVariances' to CombineSubsurfaceScattering.shader.
public Vector4 [ ] halfRcpWeightedVariances {
// Set during OnValidate().
get { return m_HalfRcpWeightedVariances ; }
}
public void SetDirtyFlag ( )
// Supplies '_FilterKernels' to CombineSubsurfaceScattering.shader.
public Vector4 [ ] filterKernels
for ( int i = 0 ; i < m_Profiles . Length ; i + + )
{
m_Profiles [ i ] . SetDirtyFlag ( ) ;
}
// Set during OnValidate().
get { return m_FilterKernels ; }
}
// --- Private Methods ---
Array . Resize ( ref m_Profiles , maxNumProfiles ) ;
}
m_NumProfiles = m_Profiles . Length ;
m_NumProfiles = m_Profiles . Length ;
m_TransmittanceFlags = 0 ;
if ( m_HalfRcpVariancesAndLerpWeights = = null )
{
m_HalfRcpVariancesAndLerpWeights = new Vector4 [ maxNumProfiles * 2 ] ;
}
if ( m_HalfRcpWeightedVariances = = null )
{
m_HalfRcpWeightedVariances = new Vector4 [ maxNumProfiles ] ;
}
if ( m_FilterKernels = = null )
{
m_FilterKernels = new Vector4 [ maxNumProfiles * SubsurfaceScatteringProfile . numSamples ] ;
}
m_TransmittanceFlags | = ( m_Profiles [ i ] . enableTransmittance ? 1 : 0 ) < < i ;
c . r = Mathf . Clamp ( m_Profiles [ i ] . stdDev1 . r , 0.05f , 2.0f ) ;
c . g = Mathf . Clamp ( m_Profiles [ i ] . stdDev1 . g , 0.05f , 2.0f ) ;
c . b = Mathf . Clamp ( m_Profiles [ i ] . stdDev1 . b , 0.05f , 2.0f ) ;
m_Profiles [ i ] . stdDev2 = c ;
m_Profiles [ i ] . lerpWeight = Mathf . Clamp01 ( m_Profiles [ i ] . lerpWeight ) ;
m_Profiles [ i ] . UpdateKernelAndVarianceData ( ) ;
}
// Use the updated data to fill the cache.
for ( int i = 0 ; i < m_NumProfiles ; i + + )
{
m_HalfRcpVariancesAndLerpWeights [ 2 * i ] = m_Profiles [ i ] . halfRcpVariances [ 0 ] ;
m_HalfRcpVariancesAndLerpWeights [ 2 * i ] . w = 1.0f - m_Profiles [ i ] . lerpWeight ;
m_HalfRcpVariancesAndLerpWeights [ 2 * i + 1 ] = m_Profiles [ i ] . halfRcpVariances [ 1 ] ;
m_HalfRcpVariancesAndLerpWeights [ 2 * i + 1 ] . w = m_Profiles [ i ] . lerpWeight ;
m_HalfRcpWeightedVariances [ i ] = m_Profiles [ i ] . halfRcpWeightedVariances ;
for ( int j = 0 , n = SubsurfaceScatteringProfile . numSamples ; j < n ; j + + )
{
m_FilterKernels [ n * i + j ] = m_Profiles [ i ] . filterKernel [ j ] ;
}
}
}
}
{
private class Styles
{
public readonly GUIContent sssCategory = new GUIContent ( "Subsurface scattering" ) ;
public readonly GUIContent sssProfileStdDev1 = new GUIContent ( "SSS profile standard deviation #1" , "Determines the shape of the 1st Gaussian filter. Increases the strength and the radius of the blur of the corresponding color channel." ) ;
public readonly GUIContent sssProfileStdDev2 = new GUIContent ( "SSS profile standard deviation #2" , "Determines the shape of the 2nd Gaussian filter. Increases the strength and the radius of the blur of the corresponding color channel." ) ;
public readonly GUIContent sssProfileLerpWeight = new GUIContent ( "SSS profile filter interpolation" , "Controls linear interpolation between the two Gaussian filters." ) ;
public readonly GUIContent sssCategory = new GUIContent ( "Subsurface scattering" ) ;
public readonly GUIContent sssProfileStdDev1 = new GUIContent ( "Standard deviation #1" , "Determines the shape of the 1st Gaussian filter. Increases the strength and the radius of the blur of the corresponding color channel." ) ;
public readonly GUIContent sssProfileStdDev2 = new GUIContent ( "Standard deviation #2" , "Determines the shape of the 2nd Gaussian filter. Increases the strength and the radius of the blur of the corresponding color channel." ) ;
public readonly GUIContent sssProfileLerpWeight = new GUIContent ( "Filter interpolation" , "Controls linear interpolation between the two Gaussian filters." ) ;
public readonly GUIContent sssProfileTransmittance = new GUIContent ( "Enable transmittance" , "Toggles simulation of light passing through thin objects. Depends on the thickness of the material." ) ;
private SerializedProperty m_Profiles ;
// --- Public Methods ---
{
serializedObject . Update ( ) ;
SerializedProperty profiles = serializedObject . FindProperty ( "m_Profiles" ) ;
EditorGUILayout . PropertyField ( m_Profiles , true ) ;
EditorGUILayout . PropertyField ( profiles , true ) ;
// Serialization does not invoke setters, but does call OnValidate().
// Serialization ignores setters.
( ( SubsurfaceScatteringParameters ) target ) . SetDirtyFlag ( ) ;
}
// --- Private Methods ---
void OnEnable ( )
{
m_Profiles = serializedObject . FindProperty ( "m_Profiles" ) ;
}
}
#endif