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.
1366 lines
60 KiB
1366 lines
60 KiB
//-----------------------------------------------------------------------------
|
|
// SurfaceData and BSDFData
|
|
//-----------------------------------------------------------------------------
|
|
// SurfaceData is defined in Hair.cs which generates Hair.cs.hlsl
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Hair/Hair.cs.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/NormalBuffer.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/VolumeRendering.hlsl"
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Texture and constant buffer declaration
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/LTCAreaLight/LTCAreaLight.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/PreIntegratedFGD/PreIntegratedFGD.hlsl"
|
|
|
|
#ifndef _ENVIRONMENT_LIGHT_SAMPLE_COUNT
|
|
#define _ENVIRONMENT_LIGHT_SAMPLE_COUNT 1
|
|
#endif
|
|
|
|
#ifndef _AREA_LIGHT_SAMPLE_COUNT
|
|
#define _AREA_LIGHT_SAMPLE_COUNT 1
|
|
#endif
|
|
|
|
#define DEFAULT_HAIR_SPECULAR_VALUE 0.0465 // Hair is IOR 1.55
|
|
|
|
// These H offset values (-1, 1) are used to approximate the integral for far-field azimuthal scattering.
|
|
// For TT, the dominant contribution comes from light transmitted straight through the fiber (thus 0).
|
|
// For TRT, a similar observation is made and v3/2 is used to approximate.
|
|
#define HAIR_H_TT 0.0
|
|
#define HAIR_H_TRT 0.86602540378
|
|
|
|
// #define HAIR_DISPLAY_REFERENCE_BSDF
|
|
// #define HAIR_DISPLAY_REFERENCE_IBL
|
|
|
|
// Extra material feature flag we utilize to compile different versions of BSDF evaluation (for pre-integration, etc.)
|
|
#define MATERIALFEATUREFLAGS_HAIR_MARSCHNER_SKIP_R (1 << 16)
|
|
#define MATERIALFEATUREFLAGS_HAIR_MARSCHNER_SKIP_TT (1 << 17)
|
|
#define MATERIALFEATUREFLAGS_HAIR_MARSCHNER_SKIP_TRT (1 << 18)
|
|
#define MATERIALFEATUREFLAGS_HAIR_MARSCHNER_SKIP_LONGITUDINAL (1 << 19)
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Absorption Parameterization Mappings
|
|
//-----------------------------------------------------------------------------
|
|
|
|
float GetAbsorptionDenominator(float azimuthalRoughness)
|
|
{
|
|
const float beta = azimuthalRoughness;
|
|
|
|
#if 0
|
|
float beta2 = beta * beta;
|
|
float beta3 = beta2 * beta;
|
|
float beta4 = beta3 * beta;
|
|
float beta5 = beta4 * beta;
|
|
|
|
// Least squares fit of an inverse mapping between scattering parameters and scattering albedo.
|
|
return 5.969 - (0.215 * beta) + (2.532 * beta2) - (10.73 * beta3) + (5.574 * beta4) + (0.245 * beta5);
|
|
#else
|
|
// Simplified version of the above.
|
|
return (((((0.245f * beta) + 5.574f) * beta - 10.73f) * beta + 2.532f) * beta - 0.215f) * beta + 5.969f;
|
|
#endif
|
|
}
|
|
|
|
// Ref: A Practical and Controllable Hair and Fur Model for Production Path Tracing Eq. 9
|
|
float3 AbsorptionFromReflectance(float3 diffuseColor, float azimuthalRoughness)
|
|
{
|
|
// Enforce a minimum value to prevent NaNs.
|
|
diffuseColor = max(diffuseColor, 1e-3);
|
|
|
|
return Sq(log(diffuseColor) / GetAbsorptionDenominator(azimuthalRoughness));
|
|
}
|
|
|
|
// Require an inverse mapping, as we parameterize the LUTs by reflectance wavelength (or for approximation that rely on diffuse).
|
|
float3 ReflectanceFromAbsorption(float3 absorption, float azimuthalRoughness)
|
|
{
|
|
// Enforce a minimum value to prevent NaNs.
|
|
absorption = max(absorption, 0.0);
|
|
|
|
return exp(-sqrt(absorption) * GetAbsorptionDenominator(azimuthalRoughness));
|
|
}
|
|
|
|
// Ref: An Energy-Conserving Hair Reflectance Model Sec. 6.1
|
|
float3 AbsorptionFromMelanin(float eumelanin, float pheomelanin)
|
|
{
|
|
const float3 eA = float3(0.419, 0.697, 1.37);
|
|
const float3 eP = float3(0.187, 0.4, 1.05);
|
|
|
|
return (eumelanin * eA) + (pheomelanin * eP);
|
|
}
|
|
|
|
float3 ReflectanceFromMelanin(float eumelanin, float pheomelanin, float azimuthalRoughness)
|
|
{
|
|
return ReflectanceFromAbsorption( AbsorptionFromMelanin(eumelanin, pheomelanin), azimuthalRoughness );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Helper functions/variable specific to this material
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Ref: "Light Scattering from Human Hair Fibers"
|
|
// Longitudinal scattering as modeled by a normal distribution.
|
|
// To be used as an approximation to d'Eon et al's Energy Conserving Longitudinal Scattering Function.
|
|
// TODO: Move me to BSDF.hlsl
|
|
|
|
half3 Gaussian(half3 thetaH, half3 beta)
|
|
{
|
|
beta = max(beta, 1e-5); // zero-div guard
|
|
|
|
// NOTE: This gaussian assumes that beta is already squared.
|
|
return rcp(sqrt(TWO_PI * beta)) * exp(-Sq(thetaH) / (2 * beta));
|
|
}
|
|
|
|
float GetHFromTube(float3 L, float3 N, float3 T)
|
|
{
|
|
// Angle of inclination from normal plane.
|
|
float sinTheta = dot(L, T);
|
|
|
|
// Project w to the normal plane.
|
|
float3 LProj = SafeNormalize(L - sinTheta * T);
|
|
|
|
// Find gamma in the normal plane.
|
|
float cosGamma = dot(LProj, N);
|
|
|
|
// Need to account for the sign to recover -1..1
|
|
float sgn = sign(dot(N, cross(LProj, T)));
|
|
|
|
// Length along the fiber width.
|
|
return SafeSqrt(1 - Sq(cosGamma)) * sgn;
|
|
}
|
|
|
|
float ModifiedRefractionIndex(float cosThetaD)
|
|
{
|
|
// Original derivation of modified refraction index for arbitrary IOR.
|
|
// float sinThetaD = sqrt(1 - Sq(cosThetaD));
|
|
// return sqrt(Sq(eta) - Sq(sinThetaD)) / cosThetaD;
|
|
|
|
// Karis approximation for the modified refraction index for human hair (1.55)
|
|
return 1.19 / cosThetaD + (0.36 * cosThetaD);
|
|
}
|
|
|
|
float4 GetDiffuseOrDefaultColor(BSDFData bsdfData, float replace)
|
|
{
|
|
return float4(bsdfData.diffuseColor, 0.0);
|
|
}
|
|
|
|
float3 GetNormalForShadowBias(BSDFData bsdfData)
|
|
{
|
|
return bsdfData.geomNormalWS;
|
|
}
|
|
|
|
float GetAmbientOcclusionForMicroShadowing(BSDFData bsdfData)
|
|
{
|
|
// Don't do micro shadow for hair, don't really make sense
|
|
return 1.0;
|
|
}
|
|
|
|
// This function is use to help with debugging and must be implemented by any lit material
|
|
// Implementer must take into account what are the current override component and
|
|
// adjust SurfaceData properties accordingdly
|
|
void ApplyDebugToSurfaceData(float3x3 tangentToWorld, inout SurfaceData surfaceData)
|
|
{
|
|
#ifdef DEBUG_DISPLAY
|
|
// NOTE: THe _Debug* uniforms come from /HDRP/Debug/DebugDisplay.hlsl
|
|
|
|
// Override value if requested by user
|
|
// this can be use also in case of debug lighting mode like diffuse only
|
|
bool overrideAlbedo = _DebugLightingAlbedo.x != 0.0;
|
|
bool overrideSmoothness = _DebugLightingSmoothness.x != 0.0;
|
|
bool overrideNormal = _DebugLightingNormal.x != 0.0;
|
|
bool overrideAO = _DebugLightingAmbientOcclusion.x != 0.0;
|
|
|
|
if (overrideAlbedo)
|
|
{
|
|
float3 overrideAlbedoValue = _DebugLightingAlbedo.yzw;
|
|
surfaceData.diffuseColor = overrideAlbedoValue;
|
|
}
|
|
|
|
if (overrideSmoothness)
|
|
{
|
|
float overrideSmoothnessValue = _DebugLightingSmoothness.y;
|
|
surfaceData.perceptualSmoothness = overrideSmoothnessValue;
|
|
surfaceData.secondaryPerceptualSmoothness = overrideSmoothnessValue;
|
|
}
|
|
|
|
if (overrideNormal)
|
|
{
|
|
surfaceData.normalWS = tangentToWorld[2];
|
|
}
|
|
|
|
if (overrideAO)
|
|
{
|
|
float overrideAOValue = _DebugLightingAmbientOcclusion.y;
|
|
surfaceData.ambientOcclusion = overrideAOValue;
|
|
}
|
|
|
|
if (_DebugFullScreenMode == FULLSCREENDEBUGMODE_VALIDATE_DIFFUSE_COLOR)
|
|
{
|
|
surfaceData.diffuseColor = pbrDiffuseColorValidate(surfaceData.diffuseColor, DEFAULT_HAIR_SPECULAR_VALUE, false, false).xyz;
|
|
}
|
|
else if (_DebugFullScreenMode == FULLSCREENDEBUGMODE_VALIDATE_SPECULAR_COLOR)
|
|
{
|
|
surfaceData.diffuseColor = pbrSpecularColorValidate(surfaceData.diffuseColor, DEFAULT_HAIR_SPECULAR_VALUE, false, false).xyz;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Note: This will be available and used in ShaderPassForward.hlsl since in Hair.shader,
|
|
// just before including the core code of the pass (ShaderPassForward.hlsl) we include
|
|
// Material.hlsl (or Lighting.hlsl which includes it) which in turn includes us,
|
|
// Hair.shader, via the #if defined(UNITY_MATERIAL_*) glue mechanism.
|
|
void ApplyDebugToBSDFData(inout BSDFData bsdfData)
|
|
{
|
|
#ifdef DEBUG_DISPLAY
|
|
// Override value if requested by user
|
|
// this can be use also in case of debug lighting mode like specular only
|
|
bool overrideSpecularColor = _DebugLightingSpecularColor.x != 0.0;
|
|
|
|
if (overrideSpecularColor)
|
|
{
|
|
float3 overrideSpecularColor = _DebugLightingSpecularColor.yzw;
|
|
bsdfData.fresnel0 = overrideSpecularColor;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
NormalData ConvertSurfaceDataToNormalData(SurfaceData surfaceData)
|
|
{
|
|
NormalData normalData;
|
|
normalData.normalWS = surfaceData.normalWS;
|
|
normalData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surfaceData.perceptualSmoothness);
|
|
return normalData;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// conversion function for forward
|
|
//-----------------------------------------------------------------------------
|
|
|
|
float RoughnessToBlinnPhongSpecularExponent(float roughness)
|
|
{
|
|
return clamp(2 * rcp(max(roughness * roughness, FLT_EPS)) - 2, FLT_EPS, rcp(FLT_EPS));
|
|
}
|
|
|
|
BSDFData ConvertSurfaceDataToBSDFData(uint2 positionSS, SurfaceData surfaceData)
|
|
{
|
|
BSDFData bsdfData;
|
|
ZERO_INITIALIZE(BSDFData, bsdfData);
|
|
|
|
// IMPORTANT: All enable flags are statically know at compile time, so the compiler can do compile time optimization
|
|
bsdfData.materialFeatures = surfaceData.materialFeatures;
|
|
|
|
bsdfData.ambientOcclusion = surfaceData.ambientOcclusion;
|
|
bsdfData.specularOcclusion = surfaceData.specularOcclusion;
|
|
|
|
bsdfData.diffuseColor = surfaceData.diffuseColor;
|
|
|
|
bsdfData.normalWS = surfaceData.normalWS;
|
|
bsdfData.geomNormalWS = surfaceData.geomNormalWS;
|
|
|
|
// Enforce a maximum smoothness to prevent NaNs.
|
|
bsdfData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(min(1.0 - 1e-2, surfaceData.perceptualSmoothness));
|
|
|
|
// This value will be override by the value in diffusion profile
|
|
bsdfData.fresnel0 = DEFAULT_HAIR_SPECULAR_VALUE;
|
|
bsdfData.transmittance = surfaceData.transmittance;
|
|
bsdfData.rimTransmissionIntensity = surfaceData.rimTransmissionIntensity;
|
|
|
|
// This is the hair tangent (which represents the hair strand direction, root to tip).
|
|
bsdfData.hairStrandDirectionWS = surfaceData.hairStrandDirectionWS;
|
|
|
|
// Kajiya kay
|
|
if (HasFlag(surfaceData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_KAJIYA_KAY))
|
|
{
|
|
bsdfData.secondaryPerceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surfaceData.secondaryPerceptualSmoothness);
|
|
bsdfData.specularTint = surfaceData.specularTint;
|
|
bsdfData.secondarySpecularTint = surfaceData.secondarySpecularTint;
|
|
bsdfData.specularShift = surfaceData.specularShift;
|
|
bsdfData.secondarySpecularShift = surfaceData.secondarySpecularShift;
|
|
|
|
float roughness1 = PerceptualRoughnessToRoughness(bsdfData.perceptualRoughness);
|
|
float roughness2 = PerceptualRoughnessToRoughness(bsdfData.secondaryPerceptualRoughness);
|
|
|
|
bsdfData.specularExponent = RoughnessToBlinnPhongSpecularExponent(roughness1);
|
|
bsdfData.secondarySpecularExponent = RoughnessToBlinnPhongSpecularExponent(roughness2);
|
|
|
|
bsdfData.anisotropy = 0.8; // For hair we fix the anisotropy
|
|
}
|
|
|
|
// Marschner
|
|
if (HasFlag(surfaceData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER) ||
|
|
HasFlag(surfaceData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER_CINEMATIC))
|
|
{
|
|
// Cuticle Angle
|
|
const float cuticleAngle = radians(surfaceData.cuticleAngle);
|
|
bsdfData.cuticleAngle = -cuticleAngle;
|
|
bsdfData.cuticleAngleR = -cuticleAngle;
|
|
bsdfData.cuticleAngleTT = cuticleAngle * 0.5;
|
|
bsdfData.cuticleAngleTRT = cuticleAngle * 1.5;
|
|
|
|
// Longitudinal Roughness
|
|
const float roughnessL = bsdfData.perceptualRoughness;
|
|
bsdfData.roughnessR = PerceptualRoughnessToRoughness(roughnessL);
|
|
bsdfData.roughnessTT = PerceptualRoughnessToRoughness(roughnessL * 0.5);
|
|
bsdfData.roughnessTRT = PerceptualRoughnessToRoughness(roughnessL * 2.0);
|
|
|
|
// Azimuthal Roughness
|
|
bsdfData.perceptualRoughnessRadial = PerceptualSmoothnessToPerceptualRoughness(min(1.0 - 1e-2, surfaceData.perceptualRadialSmoothness));
|
|
|
|
// Absorption. Note: We require diffuse color to parameterize LUTs and for approximation purposes.
|
|
#if _ABSORPTION_FROM_COLOR
|
|
bsdfData.absorption = AbsorptionFromReflectance(surfaceData.diffuseColor, bsdfData.perceptualRoughnessRadial);
|
|
#elif _ABSORPTION_FROM_MELANIN
|
|
bsdfData.absorption = AbsorptionFromMelanin(surfaceData.eumelanin, surfaceData.pheomelanin);
|
|
bsdfData.diffuseColor = ReflectanceFromMelanin(surfaceData.eumelanin, surfaceData.pheomelanin, bsdfData.perceptualRoughnessRadial);
|
|
#else
|
|
bsdfData.absorption = surfaceData.absorption;
|
|
bsdfData.diffuseColor = ReflectanceFromAbsorption(bsdfData.absorption, bsdfData.perceptualRoughnessRadial);
|
|
#endif
|
|
|
|
#if _MATERIAL_FEATURE_HAIR_MARSCHNER_CINEMATIC
|
|
bsdfData.strandCountProbe = surfaceData.strandCountProbe;
|
|
|
|
#if !_USE_SPLINE_VISIBILITY_FOR_MULTIPLE_SCATTERING
|
|
// The user has specified that they would like to derive self-shadowing data only from the volumetric grid.
|
|
bsdfData.visibility = -1;
|
|
#endif
|
|
#endif
|
|
|
|
// By default the normalization factor should be 1 and overridden by area lights.
|
|
bsdfData.distributionNormalizationFactor = 1;
|
|
|
|
// Only necesarry for reference.
|
|
// bsdfData.h = -1 + 2 * InterleavedGradientNoise(positionSS, _TaaFrameInfo.z);
|
|
}
|
|
|
|
ApplyDebugToBSDFData(bsdfData);
|
|
|
|
return bsdfData;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Debug method (use to display values)
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// This function call the generated debug function and allow to override the debug output if needed
|
|
void GetSurfaceDataDebug(uint paramId, SurfaceData surfaceData, inout float3 result, inout bool needLinearToSRGB)
|
|
{
|
|
GetGeneratedSurfaceDataDebug(paramId, surfaceData, result, needLinearToSRGB);
|
|
|
|
// Overide debug value output to be more readable
|
|
switch (paramId)
|
|
{
|
|
case DEBUGVIEW_HAIR_SURFACEDATA_NORMAL_VIEW_SPACE:
|
|
// Convert to view space
|
|
{
|
|
float3 vsNormal = TransformWorldToViewDir(surfaceData.normalWS);
|
|
result = IsNormalized(vsNormal) ? vsNormal * 0.5 + 0.5 : float3(1.0, 0.0, 0.0);
|
|
break;
|
|
}
|
|
case DEBUGVIEW_HAIR_SURFACEDATA_GEOMETRIC_NORMAL_VIEW_SPACE:
|
|
{
|
|
float3 vsGeomNormal = TransformWorldToViewDir(surfaceData.geomNormalWS);
|
|
result = IsNormalized(vsGeomNormal) ? vsGeomNormal * 0.5 + 0.5 : float3(1.0, 0.0, 0.0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function call the generated debug function and allow to override the debug output if needed
|
|
void GetBSDFDataDebug(uint paramId, BSDFData bsdfData, inout float3 result, inout bool needLinearToSRGB)
|
|
{
|
|
GetGeneratedBSDFDataDebug(paramId, bsdfData, result, needLinearToSRGB);
|
|
|
|
// Overide debug value output to be more readable
|
|
switch (paramId)
|
|
{
|
|
case DEBUGVIEW_HAIR_BSDFDATA_NORMAL_VIEW_SPACE:
|
|
// Convert to view space
|
|
{
|
|
float3 vsNormal = TransformWorldToViewDir(bsdfData.normalWS);
|
|
result = IsNormalized(vsNormal) ? vsNormal * 0.5 + 0.5 : float3(1.0, 0.0, 0.0);
|
|
break;
|
|
}
|
|
case DEBUGVIEW_HAIR_BSDFDATA_GEOMETRIC_NORMAL_VIEW_SPACE:
|
|
{
|
|
float3 vsGeomNormal = TransformWorldToViewDir(bsdfData.geomNormalWS);
|
|
result = IsNormalized(vsGeomNormal) ? vsGeomNormal * 0.5 + 0.5 : float3(1.0, 0.0, 0.0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GetPBRValidatorDebug(SurfaceData surfaceData, inout float3 result)
|
|
{
|
|
result = surfaceData.diffuseColor;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// PreLightData
|
|
//
|
|
// Make sure we respect naming conventions to reuse ShaderPassForward as is,
|
|
// ie struct (even if opaque to the ShaderPassForward) name is PreLightData,
|
|
// GetPreLightData prototype.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Precomputed lighting data to send to the various lighting functions
|
|
struct PreLightData
|
|
{
|
|
float NdotV; // Could be negative due to normal mapping, use ClampNdotV()
|
|
|
|
// IBL
|
|
float3 iblR; // Reflected specular direction, used for IBL in EvaluateBSDF_Env()
|
|
float iblPerceptualRoughness;
|
|
|
|
// Area lights (17 VGPRs)
|
|
// TODO: 'orthoBasisViewNormal' is just a rotation around the normal and should thus be just 1x VGPR.
|
|
float3x3 orthoBasisViewNormal; // Right-handed view-dependent orthogonal basis around the normal (6x VGPRs)
|
|
float3x3 ltcTransformDiffuse; // Inverse transformation for Lambertian or Disney Diffuse (4x VGPRs)
|
|
float3x3 ltcTransformSpecular; // Inverse transformation for GGX (4x VGPRs)
|
|
|
|
float3 specularFGD; // Store preintegrated BSDF for both specular and diffuse
|
|
float diffuseFGD;
|
|
};
|
|
|
|
//
|
|
// ClampRoughness helper specific to this material
|
|
//
|
|
void ClampRoughness(inout PreLightData preLightData, inout BSDFData bsdfData, float minRoughness)
|
|
{
|
|
bsdfData.perceptualRoughness = max(RoughnessToPerceptualRoughness(minRoughness), bsdfData.perceptualRoughness);
|
|
bsdfData.secondaryPerceptualRoughness = max(RoughnessToPerceptualRoughness(minRoughness), bsdfData.secondaryPerceptualRoughness);
|
|
|
|
bsdfData.roughnessR = max(minRoughness, bsdfData.roughnessR);
|
|
bsdfData.roughnessTT = max(minRoughness, bsdfData.roughnessTT);
|
|
bsdfData.roughnessTRT = max(minRoughness, bsdfData.roughnessTRT);
|
|
}
|
|
|
|
// This function is call to precompute heavy calculation before lightloop
|
|
PreLightData GetPreLightData(float3 V, PositionInputs posInput, inout BSDFData bsdfData)
|
|
{
|
|
PreLightData preLightData;
|
|
// Don't init to zero to allow to track warning about uninitialized data
|
|
|
|
#if _USE_LIGHT_FACING_NORMAL
|
|
float3 N = ComputeViewFacingNormal(V, bsdfData.hairStrandDirectionWS);
|
|
|
|
// Silence the imaginary square root compiler warning for the cosThetaParam.
|
|
// The compiler seems to think that the dot product result between view vector and view facing normal produces
|
|
// a result > 1, thus producing an imaginary square root for sqrt(1 - x).
|
|
V = normalize(V);
|
|
#else
|
|
float3 N = bsdfData.normalWS;
|
|
#endif
|
|
|
|
preLightData.NdotV = dot(N, V);
|
|
float clampedNdotV = ClampNdotV(preLightData.NdotV);
|
|
|
|
float unused;
|
|
|
|
// Both models share usage of GGX for now due to anisotropic LTC area light limitation, and Marschner invokes the BSDF directly for the environment evaluation.
|
|
{
|
|
// Note: For Kajiya hair we currently rely on a single cubemap sample instead of two, as in practice smoothness of both lobe aren't too far from each other.
|
|
// and we take smoothness of the secondary lobe as it is often more rough (it is the colored one).
|
|
preLightData.iblPerceptualRoughness = bsdfData.secondaryPerceptualRoughness;
|
|
// TODO: adjust for Blinn-Phong here?
|
|
GetPreIntegratedFGDGGXAndDisneyDiffuse(clampedNdotV, preLightData.iblPerceptualRoughness, bsdfData.fresnel0, preLightData.specularFGD, preLightData.diffuseFGD, unused);
|
|
// We used lambert for hair for now
|
|
// Note: this normalization term is wrong, correct one is (1/(Pi^2)).
|
|
preLightData.diffuseFGD = 1.0;
|
|
}
|
|
|
|
// Stretch hack... Copy-pasted from GGX, ALU-optimized for hair.
|
|
// float3 iblN = normalize(lerp(bsdfData.normalWS, N, bsdfData.anisotropy));
|
|
float3 iblN = N;
|
|
preLightData.iblR = reflect(-V, iblN);
|
|
|
|
if (HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_KAJIYA_KAY))
|
|
{
|
|
preLightData.iblPerceptualRoughness *= saturate(1.2 - abs(bsdfData.anisotropy));
|
|
}
|
|
else
|
|
{
|
|
// Marschner utilizes the lowest environment mip and treats it as a directional light source to invoke the BSDF directly.
|
|
preLightData.iblPerceptualRoughness = 1.0;
|
|
}
|
|
|
|
// Area light
|
|
preLightData.ltcTransformDiffuse = k_identity3x3;
|
|
// IMPORTANT NOTE: For the time being, until we solve issues with Kajiya Kay anisotropy and LTC tables, hair will fall-back on GGX.
|
|
// To be replaced with LTCLIGHTINGMODEL_KAJIYA_KAY_SPECULAR when that table is going to be valid.
|
|
preLightData.ltcTransformSpecular = SampleLtcMatrix(bsdfData.perceptualRoughness, clampedNdotV, LTCLIGHTINGMODEL_GGX);
|
|
|
|
// Construct a right-handed view-dependent orthogonal basis around the normal
|
|
preLightData.orthoBasisViewNormal = GetOrthoBasisViewNormal(V, N, preLightData.NdotV);
|
|
|
|
return preLightData;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// bake lighting function
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// This define allow to say that we implement a ModifyBakedDiffuseLighting function to be call in PostInitBuiltinData
|
|
#define MODIFY_BAKED_DIFFUSE_LIGHTING
|
|
|
|
void ModifyBakedDiffuseLighting(float3 V, PositionInputs posInput, PreLightData preLightData, BSDFData bsdfData, inout BuiltinData builtinData)
|
|
{
|
|
// Add GI transmission contribution to bakeDiffuseLighting, we then drop backBakeDiffuseLighting (i.e it is not used anymore, this save VGPR)
|
|
{
|
|
// TODO: disabled until further notice (not clear how to handle occlusion).
|
|
//builtinData.bakeDiffuseLighting += builtinData.backBakeDiffuseLighting * bsdfData.transmittance;
|
|
}
|
|
|
|
if (HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_KAJIYA_KAY))
|
|
{
|
|
// Premultiply (back) bake diffuse lighting information with diffuse pre-integration
|
|
builtinData.bakeDiffuseLighting *= preLightData.diffuseFGD * bsdfData.diffuseColor;
|
|
}
|
|
else
|
|
{
|
|
// Marschner model has no diffuse component.
|
|
// Thus we do not want GI to influence it (we handle this in the specular term with IBL lighting).
|
|
builtinData.bakeDiffuseLighting = 0;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// light transport functions
|
|
//-----------------------------------------------------------------------------
|
|
|
|
LightTransportData GetLightTransportData(SurfaceData surfaceData, BuiltinData builtinData, BSDFData bsdfData)
|
|
{
|
|
LightTransportData lightTransportData;
|
|
|
|
// DiffuseColor for lightmapping
|
|
lightTransportData.diffuseColor = bsdfData.diffuseColor;
|
|
lightTransportData.emissiveColor = builtinData.emissiveColor;
|
|
|
|
return lightTransportData;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// LightLoop related function (Only include if required)
|
|
// HAS_LIGHTLOOP is define in Lighting.hlsl
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#ifdef HAS_LIGHTLOOP
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// BSDF share between directional light, punctual light and area light (reference)
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
bool IsNonZeroBSDF(float3 V, float3 L, PreLightData preLightData, BSDFData bsdfData)
|
|
{
|
|
return true; // Due to either reflection or transmission being always active
|
|
}
|
|
|
|
struct HairAngle
|
|
{
|
|
half sinThetaI;
|
|
half sinThetaO;
|
|
half cosThetaI;
|
|
half cosThetaO;
|
|
half cosThetaD;
|
|
half thetaH;
|
|
half phiI;
|
|
half phiO;
|
|
half phi;
|
|
half cosPhi;
|
|
half sinThetaT;
|
|
half cosThetaT;
|
|
};
|
|
|
|
void GetHairAngleLocal(float3 wo, float3 wi, inout HairAngle angles)
|
|
{
|
|
angles.sinThetaO = wo.x;
|
|
angles.sinThetaI = wi.x;
|
|
|
|
half thetaO = FastASin(angles.sinThetaO);
|
|
half thetaI = FastASin(angles.sinThetaI);
|
|
angles.thetaH = (thetaI + thetaO) * 0.5;
|
|
|
|
angles.cosThetaD = cos((thetaO - thetaI) * 0.5);
|
|
angles.cosThetaI = SafeSqrt(1 - Sq(angles.sinThetaI));
|
|
angles.cosThetaO = SafeSqrt(1 - Sq(angles.sinThetaO));
|
|
|
|
angles.phiI = FastAtan2(wi.z, wi.y);
|
|
angles.phiO = FastAtan2(wo.z, wo.y);
|
|
angles.phi = angles.phiI - angles.phiO;
|
|
|
|
angles.cosPhi = cos(angles.phi);
|
|
|
|
angles.sinThetaT = angles.sinThetaO / 1.55;
|
|
angles.cosThetaT = SafeSqrt(1 - Sq(angles.sinThetaT));
|
|
}
|
|
|
|
void GetHairAngleWorld(float3 V, float3 L, float3 T, inout HairAngle angles)
|
|
{
|
|
angles.sinThetaO = dot(T, V);
|
|
angles.sinThetaI = dot(T, L);
|
|
|
|
half thetaO = FastASin(angles.sinThetaO);
|
|
half thetaI = FastASin(angles.sinThetaI);
|
|
angles.thetaH = (thetaI + thetaO) * 0.5;
|
|
|
|
angles.cosThetaD = cos((thetaO - thetaI) * 0.5);
|
|
angles.cosThetaO = cos(thetaO);
|
|
angles.cosThetaI = cos(thetaI);
|
|
|
|
// Projection onto the normal plane, and since phi is the relative angle, we take the cosine in this projection.
|
|
half3 VProj = V - angles.sinThetaO * T;
|
|
half3 LProj = L - angles.sinThetaI * T;
|
|
angles.cosPhi = dot(LProj, VProj) * rsqrt(dot(LProj, LProj) * dot(VProj, VProj) + 1e-5); // zero-div guard
|
|
angles.phi = FastACos(angles.cosPhi);
|
|
|
|
// Fixed for approximate human hair IOR
|
|
angles.sinThetaT = angles.sinThetaO / 1.55;
|
|
angles.cosThetaT = SafeSqrt(1 - Sq(angles.sinThetaT));
|
|
}
|
|
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Hair/Reference/HairReference.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Hair/PreIntegratedAzimuthalScattering.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Hair/MultipleScattering/HairMultipleScattering.hlsl"
|
|
|
|
CBSDF EvaluateBSDF(float3 V, float3 L, PreLightData preLightData, BSDFData bsdfData)
|
|
{
|
|
CBSDF cbsdf;
|
|
ZERO_INITIALIZE(CBSDF, cbsdf);
|
|
|
|
half3 T = bsdfData.hairStrandDirectionWS;
|
|
half3 N = bsdfData.normalWS;
|
|
|
|
#if _USE_LIGHT_FACING_NORMAL
|
|
// The Kajiya-Kay model has a "built-in" transmission, and the 'NdotL' is always positive.
|
|
half cosTL = dot(T, L);
|
|
half sinTL = sqrt(saturate(1.0 - cosTL * cosTL));
|
|
half NdotL = sinTL; // Corresponds to the cosine w.r.t. the light-facing normal
|
|
#else
|
|
// Double-sided Lambert.
|
|
half NdotL = dot(N, L);
|
|
#endif
|
|
|
|
half NdotV = preLightData.NdotV;
|
|
half clampedNdotV = ClampNdotV(NdotV);
|
|
half clampedNdotL = saturate(NdotL);
|
|
|
|
if (HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_KAJIYA_KAY))
|
|
{
|
|
float LdotV, NdotH, LdotH, invLenLV;
|
|
GetBSDFAngle(V, L, NdotL, NdotV, LdotV, NdotH, LdotH, invLenLV);
|
|
|
|
float3 t1 = ShiftTangent(T, N, bsdfData.specularShift);
|
|
float3 t2 = ShiftTangent(T, N, bsdfData.secondarySpecularShift);
|
|
|
|
float3 H = (L + V) * invLenLV;
|
|
|
|
// Balancing energy between lobes, as well as between diffuse and specular is left to artists.
|
|
float3 hairSpec1 = bsdfData.specularTint * D_KajiyaKay(t1, H, bsdfData.specularExponent);
|
|
float3 hairSpec2 = bsdfData.secondarySpecularTint * D_KajiyaKay(t2, H, bsdfData.secondarySpecularExponent);
|
|
|
|
float3 F = F_Schlick(bsdfData.fresnel0, LdotH);
|
|
|
|
#if _USE_LIGHT_FACING_NORMAL
|
|
// See "Analytic Tangent Irradiance Environment Maps for Anisotropic Surfaces".
|
|
cbsdf.diffR = rcp(PI * PI) * clampedNdotL;
|
|
// Transmission is built into the model, and it's not exactly clear how to split it.
|
|
cbsdf.diffT = 0;
|
|
#else
|
|
// Double-sided Lambert.
|
|
cbsdf.diffR = Lambert() * clampedNdotL;
|
|
#endif
|
|
// Bypass the normal map...
|
|
float geomNdotV = dot(bsdfData.geomNormalWS, V);
|
|
|
|
// G = NdotL * NdotV.
|
|
cbsdf.specR = 0.25 * F * (hairSpec1 + hairSpec2) * clampedNdotL * saturate(geomNdotV * FLT_MAX);
|
|
|
|
// Yibing's and Morten's hybrid scatter model hack.
|
|
float scatterFresnel1 = pow(saturate(-LdotV), 9.0) * pow(saturate(1.0 - geomNdotV * geomNdotV), 12.0);
|
|
float scatterFresnel2 = saturate(PositivePow((1.0 - geomNdotV), 20.0));
|
|
|
|
cbsdf.specT = scatterFresnel1 + bsdfData.rimTransmissionIntensity * scatterFresnel2;
|
|
}
|
|
|
|
if (HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER) ||
|
|
HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER_CINEMATIC))
|
|
{
|
|
// Approximation of the three primary paths in a hair fiber (R, TT, TRT), with concepts from:
|
|
// "Strand-Based Hair Rendering in Frostbite" (Tafuri 2019)
|
|
// "A Practical and Controllable Hair and Fur Model for Production Path Tracing" (Chiang 2016)
|
|
// "Physically Based Hair Shading in Unreal" (Karis 2016)
|
|
// "An Energy-Conserving Hair Reflectance Model" (d'Eon 2011)
|
|
// "Light Scattering from Human Hair Fibers" (Marschner 2003)
|
|
|
|
// Reminder: All of these flags are known at compile time and the compiler will strip away the unused paths.
|
|
|
|
// Retrieve angles via spherical coordinates in the hair shading space.
|
|
HairAngle angles;
|
|
ZERO_INITIALIZE(HairAngle, angles);
|
|
#if 0
|
|
// Transform to the local frame for spherical coordinates,
|
|
// Note that the strand direction is assumed to lie pointing down the +X axis.
|
|
const float3x3 frame = GetLocalFrame(bsdfData.geomNormalWS, bsdfData.hairStrandDirectionWS);
|
|
const float3 wo = mul(V, transpose(frame));
|
|
const float3 wi = mul(L, transpose(frame));
|
|
GetHairAngleLocal(wi, wo, angles);
|
|
#else
|
|
GetHairAngleWorld(V, L, T, angles);
|
|
#endif
|
|
|
|
const half3 alpha = half3(
|
|
bsdfData.cuticleAngleR,
|
|
bsdfData.cuticleAngleTT,
|
|
bsdfData.cuticleAngleTRT
|
|
);
|
|
|
|
const half3 beta = half3(
|
|
bsdfData.roughnessR,
|
|
bsdfData.roughnessTT,
|
|
bsdfData.roughnessTRT
|
|
);
|
|
|
|
// The index of refraction that can be used to analyze scattering in the normal plane (Bravais' Law).
|
|
const half etaPrime = ModifiedRefractionIndex(angles.cosThetaD);
|
|
|
|
// Reduced absorption coefficient.
|
|
const half3 mu = bsdfData.absorption;
|
|
|
|
// Various misc. terms reused between lobe evaluation.
|
|
half3 F, Tr, S = 0;
|
|
|
|
// Per-path attenuations.
|
|
half3 A[3];
|
|
|
|
half3 M;
|
|
if (!HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER_CINEMATIC))
|
|
{
|
|
// For non-cinematic hair shading, use a cheaper gaussian for longitudinal scattering.
|
|
M = Gaussian(angles.thetaH - alpha, beta) * bsdfData.distributionNormalizationFactor;
|
|
}
|
|
else
|
|
{
|
|
// Evaluate the energy conserving longitudinal scattering for all three paths.
|
|
M = GetEnergyConservingLongitudinalScattering(angles.sinThetaI, angles.sinThetaO, bsdfData.perceptualRoughness);
|
|
}
|
|
|
|
// Fetch the preintegrated azimuthal distributions for each path
|
|
const half3 N = GetRoughenedAzimuthalScatteringDistribution(angles.phi, angles.cosThetaD, bsdfData.perceptualRoughnessRadial);
|
|
|
|
// Solve the first three lobes (R, TT, TRT).
|
|
|
|
// R
|
|
{
|
|
// Attenuation for this path as proposed by d'Eon et al, replaced with a trig identity for cos half phi.
|
|
A[0] = F_Schlick(bsdfData.fresnel0, sqrt(0.5 + 0.5 * dot(L, V)));
|
|
S += M[0] * A[0] * N[0];
|
|
}
|
|
|
|
// TT
|
|
if (!HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER_SKIP_TT))
|
|
{
|
|
// Attenutation (Simplified for H = 0)
|
|
half cosGammaO = SafeSqrt(1 - Sq(HAIR_H_TT));
|
|
half cosTheta = angles.cosThetaO * cosGammaO;
|
|
F = F_Schlick(bsdfData.fresnel0, cosTheta);
|
|
|
|
half sinGammaT = HAIR_H_TT / etaPrime;
|
|
half cosGammaT = SafeSqrt(1 - Sq(sinGammaT));
|
|
Tr = exp(-mu * (2 * cosGammaT / angles.cosThetaT));
|
|
|
|
A[1] = Sq(1 - F) * Tr;
|
|
|
|
S += M[1] * A[1] * N[1];
|
|
}
|
|
else
|
|
A[1] = 0; // Required to fully initialize.
|
|
|
|
// TRT
|
|
{
|
|
// Attenutation (Simplified for H = √3/2)
|
|
half cosGammaO = SafeSqrt(1 - Sq(HAIR_H_TRT));
|
|
half cosTheta = angles.cosThetaO * cosGammaO;
|
|
F = F_Schlick(bsdfData.fresnel0, cosTheta);
|
|
|
|
half sinGammaT = HAIR_H_TRT / etaPrime;
|
|
half cosGammaT = SafeSqrt(1 - Sq(sinGammaT));
|
|
Tr = exp(-mu * (2 * cosGammaT / angles.cosThetaT));
|
|
|
|
A[2] = Sq(1 - F) * F * Sq(Tr);
|
|
|
|
S += M[2] * A[2] * N[2];
|
|
}
|
|
|
|
// TODO: Residual TRRT+ Lobe. (accounts for ~15% energy otherwise lost by the first three lobes).
|
|
// Transmission event is built into the model.
|
|
// Some stubborn NaNs have cropped up due to the angle optimization, we suppress them here with a max for now.
|
|
cbsdf.specR = max(S, 0);
|
|
|
|
// Multiple Scattering
|
|
cbsdf.specR = ComputeDualScattering(bsdfData, angles, DecodeHairStrandCount(L, bsdfData.strandCountProbe), cbsdf.specR);
|
|
}
|
|
|
|
return cbsdf;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Surface shading (all light types) below
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#if _MATERIAL_FEATURE_HAIR_MARSCHNER || _MATERIAL_FEATURE_HAIR_MARSCHNER_CINEMATIC
|
|
// The Marschner model has no diffuse component. Thus we request the light loop to compile without APV / light probe evaluation.
|
|
#define LIGHT_EVALUATION_SKIP_INDIRECT_DIFFUSE
|
|
|
|
// Extra configuration for multiple scattering in the Marschner model.
|
|
#if _MATERIAL_FEATURE_HAIR_MARSCHNER_CINEMATIC
|
|
// Inform the light loop to compile with bsdf visibility information.
|
|
#define LIGHT_EVALUATION_BSDF_HANDLES_VISIBILITY
|
|
// Disable the contact shadow in case of multiple scattering.
|
|
#define LIGHT_EVALUATION_NO_CONTACT_SHADOWS
|
|
#else
|
|
// Force contact shadows to skip the NdotL computation (allows to mitigate glowing heads for un-shadow mapped lights).
|
|
#define LIGHT_EVALUATION_CONTACT_SHADOW_DISABLE_NDOTL
|
|
#endif
|
|
#endif
|
|
|
|
// Hair used precomputed transmittance, no thick transmittance required
|
|
#define MATERIAL_INCLUDE_PRECOMPUTED_TRANSMISSION
|
|
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightEvaluation.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/MaterialEvaluation.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/SurfaceShading.hlsl"
|
|
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/MostRepresentativePoint.hlsl"
|
|
|
|
float3 RayPlaneIntersect(in float3 rayOrigin, in float3 rayDirection, in float3 planeOrigin, in float3 planeNormal)
|
|
{
|
|
float dist = dot(planeNormal, planeOrigin - rayOrigin) / dot(planeNormal, rayDirection);
|
|
return rayOrigin + rayDirection * dist;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EvaluateBSDF_Directional
|
|
//-----------------------------------------------------------------------------
|
|
|
|
DirectLighting EvaluateBSDF_Directional(LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput, PreLightData preLightData,
|
|
DirectionalLightData lightData, BSDFData bsdfData,
|
|
BuiltinData builtinData)
|
|
{
|
|
return ShadeSurface_Directional(lightLoopContext, posInput, builtinData,
|
|
preLightData, lightData, bsdfData, V);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EvaluateBSDF_Punctual (supports spot, point and projector lights)
|
|
//-----------------------------------------------------------------------------
|
|
|
|
DirectLighting EvaluateBSDF_Punctual(LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, LightData lightData, BSDFData bsdfData, BuiltinData builtinData)
|
|
{
|
|
return ShadeSurface_Punctual(lightLoopContext, posInput, builtinData,
|
|
preLightData, lightData, bsdfData, V);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EvaluateBSDF_Line - Approximation with Linearly Transformed Cosines
|
|
//-----------------------------------------------------------------------------
|
|
|
|
DirectLighting EvaluateBSDF_Line( LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, LightData lightData, BSDFData bsdfData, BuiltinData builtinData)
|
|
{
|
|
DirectLighting lighting;
|
|
ZERO_INITIALIZE(DirectLighting, lighting);
|
|
|
|
// Translate the light s.t. the shaded point is at the origin of the coordinate system.
|
|
float3 unL = lightData.positionRWS - posInput.positionWS;
|
|
|
|
// These values could be precomputed on CPU to save VGPR or ALU.
|
|
float halfLength = lightData.size.x * 0.5;
|
|
|
|
float intensity = CapsuleWindowing(unL, lightData.right, halfLength,
|
|
lightData.rangeAttenuationScale, lightData.rangeAttenuationBias);
|
|
|
|
// Terminate if the shaded point is too far away.
|
|
if (intensity > 0)
|
|
{
|
|
lightData.diffuseDimmer *= intensity;
|
|
lightData.specularDimmer *= intensity;
|
|
|
|
float3 center = mul(preLightData.orthoBasisViewNormal, unL);
|
|
float3 axis = mul(preLightData.orthoBasisViewNormal, lightData.right);
|
|
|
|
float ltcValue;
|
|
|
|
// ----- 1. Evaluate the diffuse part -----
|
|
|
|
ltcValue = I_ltc_line(transpose(preLightData.ltcTransformDiffuse), center, axis, halfLength);
|
|
|
|
// We don't multiply by 'bsdfData.diffuseColor' here. It's done only once in PostEvaluateBSDF().
|
|
lighting.diffuse += ltcValue * lightData.diffuseDimmer;
|
|
|
|
// Transmission Lobe
|
|
{
|
|
// Flip the surface while maintaining the view direction.
|
|
float3x3 flipMatrix = float3x3(1, 0, 0,
|
|
0, -1, 0,
|
|
0, 0, -1);
|
|
|
|
// Transform the vectors instead of transforming the basis.
|
|
// Use the Lambertian approximation for performance reasons.
|
|
// TODO: performing the evaluation twice is very inefficient!
|
|
ltcValue = I_ltc_line(k_identity3x3, mul(flipMatrix, center), mul(flipMatrix, axis), halfLength);
|
|
|
|
// We use diffuse lighting for accumulation since it is going to be blurred during the SSS pass.
|
|
// We don't multiply by 'bsdfData.diffuseColor' here. It's done only once in PostEvaluateBSDF().
|
|
lighting.diffuse += bsdfData.transmittance * (ltcValue * lightData.diffuseDimmer);
|
|
}
|
|
|
|
// ----- 2. Evaluate the specular part -----
|
|
|
|
ltcValue = I_ltc_line(transpose(preLightData.ltcTransformSpecular), center, axis, halfLength);
|
|
|
|
lighting.specular += ltcValue * lightData.specularDimmer;
|
|
|
|
// We need to multiply by the magnitude of the integral of the BRDF
|
|
// ref: http://advances.realtimerendering.com/s2016/s2016_ltc_fresnel.pdf
|
|
lighting.diffuse *= lightData.color * preLightData.diffuseFGD;
|
|
lighting.specular *= lightData.color * preLightData.specularFGD;
|
|
|
|
// ----- 3. Debug display -----
|
|
|
|
#ifdef DEBUG_DISPLAY
|
|
if (_DebugLightingMode == DEBUGLIGHTINGMODE_LUX_METER)
|
|
{
|
|
ltcValue = I_ltc_line(k_identity3x3, center, axis, halfLength);
|
|
|
|
// Only lighting, not BSDF
|
|
lighting.diffuse = lightData.color * (ltcValue * lightData.diffuseDimmer);
|
|
// Apply area light on lambert then multiply by PI to cancel Lambert
|
|
lighting.diffuse *= PI;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return lighting;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EvaluateBSDF_Rect_MRP - Approximation with Most Representative Point
|
|
//-----------------------------------------------------------------------------
|
|
|
|
DirectLighting EvaluateBSDF_Rect_MRP(LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, LightData lightData, BSDFData bsdfData, BuiltinData builtinData)
|
|
{
|
|
DirectLighting lighting;
|
|
ZERO_INITIALIZE(DirectLighting, lighting);
|
|
|
|
// Ref: Moving Frostbite to PBR (Appendix E)
|
|
// Solve the area lighting using the Most Representative Point detection method.
|
|
// This is a stop-gap solution until further research is given to LTC support for anisotropic BSDFs.
|
|
// In the future, when strand-space shading is added, it might be doable to take a "structured sampling" approach.
|
|
const float3 positionWS = posInput.positionWS;
|
|
|
|
#if SHADEROPTIONS_BARN_DOOR
|
|
// Apply the barn door modification to the light data
|
|
RectangularLightApplyBarnDoor(lightData, positionWS);
|
|
#endif
|
|
|
|
float3 unL = lightData.positionRWS - positionWS;
|
|
|
|
if (dot(lightData.forward, unL) < FLT_EPS)
|
|
{
|
|
const float halfWidth = lightData.size.x * 0.5;
|
|
const float halfHeight = lightData.size.y * 0.5;
|
|
|
|
// Solid angle computation (brute force or approximate routine).
|
|
// In our measurements the brute force is slightly more expensive but not by much. Additionally MRP is faster overall
|
|
// than LTC so we accept the cost for the quality benefit.
|
|
#if 1
|
|
float4x3 lightVerts;
|
|
lightVerts[0] = lightData.positionRWS + lightData.right * -halfWidth + lightData.up * -halfHeight; // LL
|
|
lightVerts[1] = lightData.positionRWS + lightData.right * -halfWidth + lightData.up * halfHeight; // UL
|
|
lightVerts[2] = lightData.positionRWS + lightData.right * halfWidth + lightData.up * halfHeight; // UR
|
|
lightVerts[3] = lightData.positionRWS + lightData.right * halfWidth + lightData.up * -halfHeight; // LR
|
|
|
|
const float solidAngle = SolidAngleRectangle(positionWS, lightVerts);
|
|
#else
|
|
const float solidAngle = SolidAngleRightPyramid(positionWS, lightData.positionRWS, halfWidth, halfHeight);
|
|
#endif
|
|
|
|
float3 L;
|
|
|
|
if (HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER))
|
|
{
|
|
// Let's choose a dominant direction with the same philosophy as we have for marschner IBL, using the the vector in the
|
|
// tangent-camera plane that is orthogonal to the tangent. This provides well-behaved results (though not perfect)
|
|
// with respect to the reference, instead of the classic reflection vector.
|
|
const float3 dh = ComputeViewFacingNormal(V, bsdfData.hairStrandDirectionWS);
|
|
|
|
// Intersect the dominant specular direction with the light plane.
|
|
float3 ph = RayPlaneIntersect(positionWS, dh, lightData.positionRWS, lightData.forward);
|
|
|
|
// Compute the closest position on the rectangle.
|
|
ph = ClosestPointRectangle(ph, lightData.positionRWS, -lightData.right, lightData.up, halfWidth, halfHeight);
|
|
|
|
// Determine the dominant hemisphere direction based on the camera-light plane angle.
|
|
const float LdotV = max(dot(-lightData.forward, V), 0);
|
|
|
|
// Construct the most representative direction.
|
|
// We must consider multiple specular lobes, on the backward (R, TRT) and forward (TT) scattering hemisphere.
|
|
// For the backward hemisphere we handle R and TRT similarly, and use the MRP result (based on the "fake" normal just like how we use it for IBL).
|
|
// For the forward hemisphere we need to approximate harsher. We can get away with falling back to the light center and modifying the roughness.
|
|
const float3 LBHemisphere = ph - positionWS;
|
|
const float3 LFHemisphere = unL;
|
|
L = SafeNormalize(lerp(LFHemisphere, LBHemisphere, LdotV));
|
|
|
|
// Define a factor here to weight the solid angle contribution term to match the reference as close as possible for varying sizes.
|
|
const float solidAngleFactor = 0.1;
|
|
const float roughnessTTPrime = saturate(bsdfData.roughnessTT + solidAngleFactor * solidAngle);
|
|
|
|
// Modify the roughness for the forward hemisphere scattering.
|
|
bsdfData.roughnessTT = lerp(roughnessTTPrime, bsdfData.roughnessTT, LdotV);
|
|
|
|
// Attempt at energy normalization for rectangular lights.
|
|
// Similar in spirit to the "Specular D Normalization" heuristic (eq. 10 Real Shading in Unreal Engine 4)
|
|
// Choose this solid angle-based heuristic to attempt to normalize the longitudinal distribution.
|
|
const float3 alpha = float3(
|
|
bsdfData.roughnessR,
|
|
bsdfData.roughnessTT,
|
|
bsdfData.roughnessTRT
|
|
);
|
|
|
|
const float3 alphaPrime = saturate(alpha + solidAngle);
|
|
|
|
bsdfData.distributionNormalizationFactor = sqrt(alpha / alphaPrime);
|
|
}
|
|
else
|
|
{
|
|
// For Kajiya instead of MRP, fall back to the light center and modulate the roughnesses by the solid angle.
|
|
// This isn't perfect for respecting the shape's orientation but generally good enough at widening the distribution for rects of varying size.
|
|
L = normalize(lightData.positionRWS - positionWS);
|
|
|
|
const float roughness1 = PerceptualRoughnessToRoughness(bsdfData.perceptualRoughness);
|
|
const float roughness2 = PerceptualRoughnessToRoughness(bsdfData.secondaryPerceptualRoughness);
|
|
|
|
// Again, define a factor to fudge the solid angle to closely match the reference.
|
|
const float solidAngleFactor = 0.05;
|
|
|
|
bsdfData.specularExponent = RoughnessToBlinnPhongSpecularExponent(saturate(roughness1 + solidAngleFactor * solidAngle));
|
|
bsdfData.secondarySpecularExponent = RoughnessToBlinnPhongSpecularExponent(saturate(roughness2 + solidAngleFactor * solidAngle));
|
|
}
|
|
|
|
// Configure a theoretically placed point light at the most important position contributing the area light irradiance.
|
|
float3 lightColor = lightData.color * solidAngle;
|
|
|
|
// Only apply cookie if there is one
|
|
if ( lightData.cookieMode != COOKIEMODE_NONE )
|
|
{
|
|
// Compute cookie's mip count.
|
|
const float cookieWidth = lightData.cookieScaleOffset.x * _CookieAtlasSize.x; // Guaranteed power of two.
|
|
const float cookieMips = round(log2(cookieWidth));
|
|
|
|
// Normalize the solid angle against the hemisphere surface area to determine a weight for choosing the mip.
|
|
const float cookieMip = cookieMips - (cookieMips * solidAngle * INV_TWO_PI);
|
|
|
|
LightData lightDataFlipped = lightData;
|
|
{
|
|
// Flip the matrix since the cookie seems flipped incorrectly otherwise.
|
|
lightDataFlipped.right = -lightDataFlipped.right;
|
|
}
|
|
|
|
// Sample the cookie as if it were a typical punctual light.
|
|
lightColor *= EvaluateCookie_Punctual(lightLoopContext, lightDataFlipped, -unL, cookieMip).rgb;
|
|
}
|
|
|
|
// Raytracing shadow algorithm require to evaluate lighting without shadow, so it defined SKIP_RASTERIZED_AREA_SHADOWS
|
|
// This is only present in Lit Material as it is the only one using the improved shadow algorithm.
|
|
#ifndef SKIP_RASTERIZED_AREA_SHADOWS
|
|
SHADOW_TYPE shadow = EvaluateShadow_RectArea(lightLoopContext, posInput, lightData, builtinData, bsdfData.normalWS, normalize(lightData.positionRWS), length(lightData.positionRWS));
|
|
lightColor.rgb *= ComputeShadowColor(shadow, lightData.shadowTint, lightData.penumbraTint);
|
|
#endif
|
|
|
|
// Simulate a sphere/disk light with this hack.
|
|
// Note that it is not correct with our precomputation of PartLambdaV
|
|
// (means if we disable the optimization it will not have the
|
|
// same result) but we don't care as it is a hack anyway.
|
|
ClampRoughness(preLightData, bsdfData, lightData.minRoughness);
|
|
|
|
lighting = ShadeSurface_Infinitesimal(preLightData, bsdfData, V, L, lightColor.rgb,
|
|
lightData.diffuseDimmer, lightData.specularDimmer);
|
|
}
|
|
|
|
return lighting;
|
|
}
|
|
|
|
void IntegrateBSDF_AreaRef(float3 V, float3 positionWS,
|
|
PreLightData preLightData, LightData lightData, BSDFData bsdfData,
|
|
out float3 diffuseLighting, out float3 specularLighting,
|
|
uint sampleCount = _AREA_LIGHT_SAMPLE_COUNT)
|
|
{
|
|
diffuseLighting = float3(0.0, 0.0, 0.0);
|
|
specularLighting = float3(0.0, 0.0, 0.0);
|
|
|
|
for (uint i = 0; i < sampleCount; ++i)
|
|
{
|
|
float3 P = float3(0.0, 0.0, 0.0); // Sample light point. Random point on the light shape in local space.
|
|
float3 Ns = float3(0.0, 0.0, 0.0); // Unit surface normal at P
|
|
float lightPdf = 0.0; // Pdf of the light sample
|
|
|
|
float2 u = Hammersley2d(i, sampleCount);
|
|
|
|
// Lights in Unity point backward.
|
|
float4x4 localToWorld = float4x4(float4(lightData.right, 0.0), float4(lightData.up, 0.0), float4(-lightData.forward, 0.0), float4(lightData.positionRWS, 1.0));
|
|
|
|
switch (lightData.lightType)
|
|
{
|
|
case GPULIGHTTYPE_RECTANGLE:
|
|
SampleRectangle(u, localToWorld, lightData.size.x, lightData.size.y, lightPdf, P, Ns);
|
|
break;
|
|
}
|
|
|
|
// Get distance
|
|
float3 unL = P - positionWS;
|
|
float sqrDist = dot(unL, unL);
|
|
float3 L = normalize(unL);
|
|
|
|
// Cosine of the angle between the light direction and the normal of the light's surface.
|
|
float cosLNs = saturate(dot(-L, Ns));
|
|
|
|
// We calculate area reference light with the area integral rather than the solid angle one.
|
|
float NdotL = saturate(dot(bsdfData.normalWS, L));
|
|
float illuminance = cosLNs / (sqrDist * lightPdf);
|
|
|
|
if (illuminance > 0.0)
|
|
{
|
|
CBSDF cbsdf = EvaluateBSDF(V, L, preLightData, bsdfData);
|
|
|
|
// Note: Again we do not accumulate diffuse here since marschner has no diffuse lobe.
|
|
specularLighting += cbsdf.specR * lightData.color * illuminance * lightData.specularDimmer;
|
|
}
|
|
}
|
|
|
|
specularLighting /= float(sampleCount);
|
|
}
|
|
|
|
DirectLighting EvaluateBSDF_Area(LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, LightData lightData,
|
|
BSDFData bsdfData, BuiltinData builtinData)
|
|
{
|
|
if (lightData.lightType == GPULIGHTTYPE_TUBE)
|
|
{
|
|
return EvaluateBSDF_Line(lightLoopContext, V, posInput, preLightData, lightData, bsdfData, builtinData);
|
|
}
|
|
else
|
|
{
|
|
if (!HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER_CINEMATIC))
|
|
return EvaluateBSDF_Rect_MRP(lightLoopContext, V, posInput, preLightData, lightData, bsdfData, builtinData);
|
|
else
|
|
{
|
|
// Raytracing shadow algorithm require to evaluate lighting without shadow, so it defined SKIP_RASTERIZED_AREA_SHADOWS
|
|
// This is only present in Lit Material as it is the only one using the improved shadow algorithm.
|
|
#ifndef SKIP_RASTERIZED_AREA_SHADOWS
|
|
SHADOW_TYPE shadow = EvaluateShadow_RectArea(lightLoopContext, posInput, lightData, builtinData, bsdfData.normalWS, normalize(lightData.positionRWS), length(lightData.positionRWS));
|
|
|
|
#ifdef LIGHT_EVALUATION_BSDF_HANDLES_VISIBILITY
|
|
if (AreaOccluderInRendererBounds(lightLoopContext, lightData, posInput))
|
|
{
|
|
#if _USE_SPLINE_VISIBILITY_FOR_MULTIPLE_SCATTERING
|
|
// Use the shadow sample as a visibility term. Otherwise the volumetric data will be used to compute the self-shadow.
|
|
bsdfData.visibility = shadow;
|
|
#endif
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
lightData.color.rgb *= ComputeShadowColor(shadow, lightData.shadowTint, lightData.penumbraTint);
|
|
}
|
|
#endif
|
|
|
|
DirectLighting lighting;
|
|
ZERO_INITIALIZE(DirectLighting, lighting);
|
|
|
|
IntegrateBSDF_AreaRef(V, posInput.positionWS, preLightData, lightData, bsdfData, lighting.diffuse, lighting.specular);
|
|
|
|
return lighting;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EvaluateBSDF_SSLighting for screen space lighting
|
|
// ----------------------------------------------------------------------------
|
|
|
|
IndirectLighting EvaluateBSDF_ScreenSpaceReflection(PositionInputs posInput,
|
|
PreLightData preLightData,
|
|
BSDFData bsdfData,
|
|
inout float reflectionHierarchyWeight)
|
|
{
|
|
IndirectLighting lighting;
|
|
ZERO_INITIALIZE(IndirectLighting, lighting);
|
|
|
|
// TODO: this texture is sparse (mostly black). Can we avoid reading every texel? How about using Hi-S?
|
|
float4 ssrLighting = LOAD_TEXTURE2D_X(_SsrLightingTexture, posInput.positionSS);
|
|
InversePreExposeSsrLighting(ssrLighting);
|
|
|
|
// Apply the weight on the ssr contribution (if required)
|
|
ApplyScreenSpaceReflectionWeight(ssrLighting);
|
|
|
|
// TODO: we should multiply all indirect lighting by the FGD value only ONCE.
|
|
lighting.specularReflected = ssrLighting.rgb * preLightData.specularFGD;
|
|
reflectionHierarchyWeight = ssrLighting.a;
|
|
|
|
return lighting;
|
|
}
|
|
|
|
IndirectLighting EvaluateBSDF_ScreenspaceRefraction(LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, BSDFData bsdfData,
|
|
EnvLightData envLightData,
|
|
inout float hierarchyWeight)
|
|
{
|
|
IndirectLighting lighting;
|
|
ZERO_INITIALIZE(IndirectLighting, lighting);
|
|
|
|
// TODO
|
|
|
|
return lighting;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EvaluateBSDF_Env
|
|
// ----------------------------------------------------------------------------
|
|
|
|
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Sampling/Sampling.hlsl"
|
|
|
|
// _preIntegratedFGD and _CubemapLD are unique for each BRDF
|
|
IndirectLighting EvaluateBSDF_Env( LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, EnvLightData lightData, BSDFData bsdfData,
|
|
int influenceShapeType, int GPUImageBasedLightingType,
|
|
inout float hierarchyWeight)
|
|
{
|
|
IndirectLighting lighting;
|
|
ZERO_INITIALIZE(IndirectLighting, lighting);
|
|
|
|
if (GPUImageBasedLightingType == GPUIMAGEBASEDLIGHTINGTYPE_REFRACTION)
|
|
return lighting;
|
|
|
|
float3 envLighting = 0;
|
|
float3 positionWS = posInput.positionWS;
|
|
float weight = 1.0;
|
|
|
|
float3 R = preLightData.iblR;
|
|
|
|
// Note: using influenceShapeType and projectionShapeType instead of (lightData|proxyData).shapeType allow to make compiler optimization in case the type is know (like for sky)
|
|
// Note: Even though intersection distance is not used in cinematic sampling we still need to compute the correct weight for the hierarchy.
|
|
float intersectionDistance = EvaluateLight_EnvIntersection(positionWS, bsdfData.normalWS, lightData, influenceShapeType, R, weight);
|
|
|
|
// For non-cinematic hair shading, fall back to more optimal environment evaluation routines.
|
|
if (!HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_MARSCHNER_CINEMATIC))
|
|
{
|
|
float4 preLD = SampleEnvWithDistanceBaseRoughness(lightLoopContext, posInput, lightData, R, preLightData.iblPerceptualRoughness, intersectionDistance);
|
|
weight *= preLD.a; // Used by planar reflection to discard pixel
|
|
|
|
if (HasFlag(bsdfData.materialFeatures, MATERIALFEATUREFLAGS_HAIR_KAJIYA_KAY))
|
|
{
|
|
envLighting = preLightData.specularFGD * preLD.rgb;
|
|
|
|
// We tint the HDRI with the secondary lob specular as it is more representatative of indirect lighting on hair.
|
|
envLighting *= bsdfData.secondarySpecularTint;
|
|
}
|
|
else
|
|
{
|
|
// For now we approximate Marschner IBL as proposed by Brian Karis in "Physically Based Hair Shading in Unreal":
|
|
// With slight variant in approach, instead of sampling a spherical harmonic of the environment, sample from the lowest mip.
|
|
|
|
// Modify the roughness to approximate a larger area light source.
|
|
bsdfData.roughnessR = saturate(bsdfData.roughnessR + 0.1);
|
|
bsdfData.roughnessTRT = saturate(bsdfData.roughnessTRT + 0.1);
|
|
|
|
// Skip TT for the environment sample (compiler will optimizate for these two different BSDF versions)
|
|
bsdfData.materialFeatures |= MATERIALFEATUREFLAGS_HAIR_MARSCHNER_SKIP_TT;
|
|
|
|
// This sample is treated as a directional light source and we evaluate the BSDF with it directly.
|
|
CBSDF cbsdf = EvaluateBSDF(V, bsdfData.normalWS, preLightData, bsdfData);
|
|
|
|
envLighting = cbsdf.specR * preLD.rgb * PI;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Transform to the local frame for spherical coordinates,
|
|
// Note that the strand direction is assumed to lie pointing down the X axis, as this is expected by the BSDF.
|
|
half3x3 localToWorld = GetLocalFrame(bsdfData.normalWS, bsdfData.hairStrandDirectionWS);
|
|
|
|
// Rely on the spherical harmonic for visibility.
|
|
bsdfData.visibility = -1;
|
|
|
|
// TODO: This sample count is a good opportunity for varying quality levels.
|
|
const int sampleCount = _ENVIRONMENT_LIGHT_SAMPLE_COUNT;
|
|
const half rcpSampleCount = rcp(sampleCount);
|
|
|
|
#ifdef LINE_RENDERING_OFFSCREEN_SHADING
|
|
// For HQ Line rendering, unfortunately since the shading occurs in object space we can't really pull off jittered samples with TAA.
|
|
// The penalty is that we have to pay the cost of more samples to stabilize the result. In the future, we should explore
|
|
// re-coupling the shading rate to the visibility of strands to benefit from TAA / Upscalers which drastically reduce the shading cost
|
|
// that normally comes with coupled shading/visibility. For the moment, this incurred extra cost can be offset with the shading atlas
|
|
// history which can allow artist control over the percentage of shading points to compute in the current frame.
|
|
half2 sampleJitter = InitRandom(V.xy * 0.5 + 0.5);
|
|
#else
|
|
// Rely on TAA to get some extra samples.
|
|
half sampleJitterAngle = InterleavedGradientNoise(posInput.positionSS.xy, _TaaFrameInfo.z) * 2.0 * PI;
|
|
half2 sampleJitter = float2(sin(sampleJitterAngle), cos(sampleJitterAngle));
|
|
#endif
|
|
|
|
UNITY_LOOP
|
|
for (uint i = 0; i < (uint)sampleCount; ++i)
|
|
{
|
|
float2 u = Hammersley2d(i, sampleCount);
|
|
u = frac(u + sampleJitter);
|
|
|
|
half3 localL = SampleSphereUniform(u.x, u.y);
|
|
half3 L = mul(localL, localToWorld);
|
|
|
|
half4 val = SampleEnv(lightLoopContext, lightData.envIndex, L, 4, lightData.rangeCompressionFactorCompensation, 0.5);
|
|
|
|
// Invoke the fiber scattering function.
|
|
CBSDF cbsdf = EvaluateBSDF(V, L, preLightData, bsdfData);
|
|
|
|
envLighting += (cbsdf.specR * abs(localL.z) * rcpSampleCount * val.rgb) / INV_FOUR_PI;
|
|
}
|
|
}
|
|
|
|
UpdateLightingHierarchyWeights(hierarchyWeight, weight);
|
|
envLighting *= weight * lightData.multiplier;
|
|
lighting.specularReflected = envLighting;
|
|
|
|
return lighting;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// PostEvaluateBSDF
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void PostEvaluateBSDF( LightLoopContext lightLoopContext,
|
|
float3 V, PositionInputs posInput,
|
|
PreLightData preLightData, BSDFData bsdfData, BuiltinData builtinData, AggregateLighting lighting,
|
|
out LightLoopOutput lightLoopOutput)
|
|
{
|
|
AmbientOcclusionFactor aoFactor;
|
|
GetScreenSpaceAmbientOcclusionMultibounce(posInput.positionSS, preLightData.NdotV, bsdfData.perceptualRoughness, bsdfData.ambientOcclusion, bsdfData.specularOcclusion, bsdfData.diffuseColor, bsdfData.fresnel0, aoFactor);
|
|
ApplyAmbientOcclusionFactor(aoFactor, builtinData, lighting);
|
|
|
|
// Apply the albedo to the direct diffuse lighting (only once). The indirect (baked)
|
|
// diffuse lighting has already multiply the albedo in ModifyBakedDiffuseLighting().
|
|
lightLoopOutput.diffuseLighting = bsdfData.diffuseColor * lighting.direct.diffuse + builtinData.bakeDiffuseLighting + builtinData.emissiveColor;
|
|
lightLoopOutput.specularLighting = lighting.direct.specular + lighting.indirect.specularReflected;
|
|
|
|
#ifdef DEBUG_DISPLAY
|
|
PostEvaluateBSDFDebugDisplay(aoFactor, builtinData, lighting, bsdfData.diffuseColor, lightLoopOutput);
|
|
#endif
|
|
}
|
|
|
|
#endif // #ifdef HAS_LIGHTLOOP
|