TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역][해설]What do we want from a Diffuse BRDF?

jplee 2023. 9. 10. 16:25

최근 서브스트레이트 프레임워크 중심으로 여러가지 변경점, 개념, 시각적인 표현 모델 전반을 살펴보고 있습니다.

이미 2018년도에 구현되어 모더워페어 WW2 에 적용 된 여러가지 커스터마이징 토픽을 살펴 보면서 언리얼 엔진 서브스트레이트에 적용 된 Chan Diffuse 에 대해서 좀 더 구체적으로 살펴보고자 합니다.

2018년 Danny Chan 은 그것에 대해서 시그라프 2018 어드벤스드 리얼타임 렌더링 게임 섹션에서 발표 했습니다.

 

시그라프 2023 서브스트레이트 발표 자료 슬라이드에 언급 된 내용을 참조 하고 복기 합니다.

 


MaterialAdvancesInWWII 프레젠테이션의 일부 슬라이드를 참조 하여 복기 합니다.

디퓨즈 BRDF에서 원하는 것은 무엇인가요?

많은 논문에서 거친 표면에서 확산 조명이 평평해지는 것을 보여줍니다. 
달을 이 현상의 예로 생각해 보세요.
오렌-나야르와 같은 BRDF는 이를 모델링하려고 시도합니다.

그런 다음 매끄러운 표면에서 조명이 둥글어지는 현상을 설명하는 몇 가지 다른 논문에서 관찰한 내용이 있습니다.
이는 평탄화 효과와는 반대입니다. 

평탄화(Equalization)
앞서 설명한 정규화는 분포가 한곳에 집중되어 있는 경우에는 효과적이지만 그 집중된 영역에서 멀리 떨어진 값이 있을 경우에는 효과가 없습니다. 이런 경우 평탄화가 필요합니다.
평탄화는 각각의 값이 전체 분포에 차지하는 비중에 따라 분포를 재분배하므로 명암 대비를 개선하는 데 효과적
입니다.  (출처 : 티스토리 귀퉁이 서재 님 )

조명을 '반올림'하면 아래 구의 실루엣을 따라 빛의 감쇠가 더 심해집니다.
이는 스페큘러에 대한 에너지 손실로 설명할 수 있습니다.

오른쪽 이미지에서는 빛과 시야가 동일합니다.

대부분의 솔루션은 조명을 평평하게 하는 데 중점을 두지만(Oren-Nayar), 나중에 조명을 둥글게 하는 것도 중요하고 더 나은 재질 차별화를 제공한다는 것을 알게 될 것입니다.

[Ore94] 램버트 반사율 모델의 일반화
http://www1.cs.columbia.edu/CAVE/publications/pdfs/Oren_SIGGRAPH94.pdf

[Wol98] 컴퓨터 비전을 위한 개선된 확산 반사 모델
https://link.springer.com/article/10.1023%2FA%3A1008017513536

램버시안 디퓨즈는 적어도 최근까지 실시간 그래픽에서 디퓨즈 반응의 표준이었습니다.
표면이 램버시안 디퓨즈로만 조명되는 경우, 해당 표면은 어느 각도에서나 동일한 밝기로 나타납니다.

 

얼마 전까지만 해도 스페큘러 반사를 모델링하고 디퓨즈에 이 스페큘러 기여도를 추가했습니다. 
하지만 실제로는 에너지 보존을 위해 스페큘러로 반사된 에너지는 디퓨즈 리플렉션에 사용할 수 없게 됩니다.

 

 

확산 반사율은 스페큘러 반사되는 양만큼 감소합니다. 그렇다면 스페큘러 반사는 얼마나 많이 반사될까요?

 

 

이해해야 할 중요한 개념은 BRDF가 상호적이라는 것입니다.
즉, 빛은 양방향으로 동일한 경로를 따라 이동한다는 뜻입니다. 
즉, 빛 벡터와 뷰 벡터를 BRDF 함수로 바꿀 수 있지만 여전히 동일해야 합니다.
마이크로 패싯 방정식을 사용하여 빛 벡터(L)와 뷰 벡터(V)를 바꿔서 시도해 볼 수 있습니다. 참고로 반각 벡터(H)는 이 둘을 바꿔도 영향을 받지 않습니다.
정시 광원에 대해 생각할 때 일반적으로 빛이 음영 처리되는 지점을 향해 단일 경로로 이동하는 것을 상상합니다.
그런 다음 눈 쪽으로 반사되는 반사율이 얼마나 되는지 평가합니다. 
적어도 제가 정시광에 대해 생각했던 방식입니다.
녹색 엽으로 정의된 여러 방향으로 산란되는 반사광이 눈에 도달하지 않는 것을 알 수 있습니다.

 

 

 

하지만 다른 방식으로도 쉽게 상상할 수 있습니다.
BRDF 로브는 변하지 않지만 우리의 눈이 광원을 대신한다는 것을 알 수 있습니다.
빛이 반사되는 방향은 눈이며, BRDF 로브에 의해 정의된 방향으로 *모인* 빛에 의해 결정됩니다.
일반적으로 반사 프로브나 환경 맵을 통해 간접 정반사를 수집하는 것이 이러한 방식입니다.

 

 

 

이제 EnvBRDF 룩업 테이블을 다시 불러오겠습니다.
이 테이블은 이전에 반사광 에너지의 몇 퍼센트를 모아서 눈 쪽으로 반사해야 하는지 계산하는 데 사용되었습니다.
그러나 이 테이블은 정시 광원에서 표면으로 산란되는 빛 에너지를 계산하는 데에도 쉽게 사용할 수 있습니다.
이것은 확산 반사에 참여할 수 없는 정반사로 반사되는 에너지입니다.

 

 

램버시안 디퓨즈를 사용한 광택 머티리얼입니다.

 

 

여기서는 EnvBrdf 룩업 테이블을 사용하여 확산 반사에 사용할 수 있는 빛 에너지의 양을 줄이고 있습니다.
확산 반사에 사용할 수 있는 에너지를 줄임으로써 잠재적으로 상호성을 위반하는 BRDF를 생성하고 있다는 점에 유의해야 합니다. 
향후 작업의 일부는 다중 산란 스페큘러를 도입하고 이를 모두 하나로 묶어 이 문제를 해결하는 것입니다.
하지만 이 기법을 함께 제공하지 않은 이유는 다음에 논의할 방법에 에너지 절약 기능이 내장되어 있기 때문입니다.

 

 

다음으로 하이츠와 그의 공저자들이 최근 발표한 몇 가지 논문을 살펴봤습니다. 
첫 번째 논문은 램버시안 마이크로패싯을 사용한 확산 BRDF 모델링에 대해 설명합니다.
두 번째 논문은 마이크로 패싯을 사용한 다중 산란에 대한 일반적인 솔루션을 제공합니다.
램버시안 마이크로패싯은 확산 모델과 정반사 모델 간에 일관된 물리적 표현을 유지한다는 점에서 흥미롭게 들렸습니다. 
이러한 방식으로 확산 반사를 모델링하는 것에 반대하는 주장이 있습니다. 확산 거리에 따라 수신 마이크로페이싯이 광자의 재방출 마이크로페이싯이 아닐 수 있다는 의미입니다.
그러나 다음에 살펴보겠지만, 이 모델에서는 좋은 특성이 떨어집니다.

[Hei15] 확률론적 평가로 간단한 이방성 거친 확산 머티리얼 구현하기
https://drive.google.com/file/d/0BzvWIdpUpRx_M3ZmakxHYXZWaUk/view

[Hei16] 스미스 모델을 사용한 다중 산란 마이크로 패싯 BSDF 구현하기
https://eheitzresearch.wordpress.com/240-2/

 

Eric Heitz's Research Page

Multiple-Scattering Microfacet BSDFs with the Smith Model Eric Heitz, Johannes Hanika, Eugene d’Eon and Carsten Dachsbacher ACM SIGGRAPH 2016 Motivation Modeling multiple scattering in microf…

eheitzresearch.wordpress.com

 

GGX NDF와 함께 하이츠 멀티스캐터링 시뮬레이터를 사용했습니다. 
다중 산란 모델을 사용한다는 것은 확산 반사된 에너지가 인접한 마이크로 패싯에 의해 다시 반사될 수 있다는 것을 의미합니다. 
우리는 그의 모델에 몇 가지 가정을 추가했습니다:
1) 각 마이크로패싯을 100%의 확산 반사체로 취급합니다.
2) 프레넬을 적용하여 확산 반사된 에너지를 결정합니다. 
3) 암시적 F0 = 0.04를 사용하여 정반사로 반사되고 확산 반사에 참여할 수 없는 에너지를 결정합니다.

흥미롭게도 우리가 원하는 속성은 이 모델에서 자동으로 빠져나옵니다: 
강한 스쳐가는 역반사 반응으로 인한 거친 표면의 경우 평탄화, 
에너지 손실부터 스페큘러까지 매끄러운 표면을 위한 반올림. 

 

피팅 프로세스의 일반적인 개요는 다음과 같습니다.
먼저 다양한 광택 값으로 멀티스캐터링 디퓨즈 BRDF를 시뮬레이션하여 등방성 3D BRDF 파일을 생성하고 BRDFExplorer에 로드할 수 있습니다.
그런 다음 BRDF의 2D 슬라이스로 축소합니다.
마지막으로 광택을 파라미터로 포함하는 함수 근사치를 찾습니다.

[Brd12] BRDF 탐색기
https://www.disneyanimation.com/technology/brdf.html

반구에 대한 확산 응답 테이블을 생성하기 위해 수정한 하이츠의 다중 산란 마이크로 패싯 모델을 사용하여 광자의 많은 바운스를 시뮬레이션했습니다. 
이 작업은 프로세서 집약적이지만 쉽게 병렬화할 수 있었기 때문에 네트워크에 있는 여러 컴퓨터에 분산했습니다.
BRDFExplorer가 파일을 열고 검사할 수 있도록 MERL 데이터베이스 형식으로 파일을 생성했습니다.
다음은 BRDFExplorer의 극좌표 플롯으로, 디퓨즈 모델을 사용하여 거친 표면에서 발생하는 강한 방목 역반사 효과를 보여줍니다.

다음은 조명이 켜진 구체의 실제 모습입니다. 
구의 왼쪽 절반은 램버트를 사용하여 라이팅되었습니다,
오른쪽 절반은 완전히 시뮬레이션된 멀티스캐터링 디퓨즈 BRDF를 사용하여 라이팅합니다.
가장 왼쪽 구체에서 램버트와 비교하여 멀티스캐터링 디퓨즈의 조명이 평평한 것을 확인할 수 있습니다.
가장 오른쪽 구체에서 멀티스캐터링 디퓨즈는 실루엣이 어두워지는 결과를 가져옵니다.

 

 

전체 등방성 BRDF는 3차원을 갖습니다.
문제의 차원을 줄이는 방법이 있습니다. 
3D 볼륨에서 2D 슬라이스를 가져옵니다.
일반적으로 2D 슬라이스는 전체 BRDF를 꽤 잘 표현합니다.
2D 슬라이스의 또 다른 장점은 시각적 해석에 적합하다는 점입니다.
다음은 BRDF 슬라이스의 두 가지 예시입니다. 
왼쪽은 멀티스캐터링 디퓨즈 모델을 사용한 거친 머티리얼을 나타냅니다. 
오른쪽은 표준 녹색 광택 머티리얼입니다.
이제 다양한 광택 값에 대해 BRDF 슬라이스를 생성합니다.
가장 먼저 시도한 것은 다음에 설명하는 2D Rational 함수를 사용하여 이러한 슬라이스를 맞추는 것입니다. 

[Bur12] 디즈니의 물리 기반 셰이딩

[Pac12] Rational BRDF

 

SIGGRAPH 2012 Course: Practical Physically Based Shading in Film and Game Production

Course Description Physically based shading is becoming of increasing importance to both film and game production. By adhering to physically based, energy-conserving shading models, one can easily create high quality, realistic materials that maintain that

blog.selfshadow.com

 

Rational BRDF

Over the last two decades, much effort has been devoted to accurately measure Bidirectional Reflectance Distribution Functions (BRDFs) of real-world materials and to use efficiently the resulting data for rendering. Because of their large size, it is diffi

inria.hal.science

 

광택이 0일 때 BRDF 슬라이스에 적합한 15계수의 합리적인 함수를 찾았습니다.
궁극적으로는 0에서 1까지의 전체 광택 범위를 렌더링할 수 있어야 합니다.
다른 파라미터로 광택을 추가하여 3D 유리 함수로 확장하려고 시도했습니다,
하지만 이 경우 더 많은 계수가 필요했습니다. 

[Pes15] 물리 기반 렌더링을 위한 근사 모델
[Joh] NLopt

 

SIGGRAPH 2015 Course: Physically Based Shading in Theory and Practice

© Disney 2014. Course Description Physically based shading is transforming the way we approach production rendering, and simplifying the lives of artists in the process. By adhering to physically based, energy-conserving models, one can easily create real

blog.selfshadow.com

 

NLopt - NLopt Documentation

NLopt NLopt NLopt is a free/open-source library for nonlinear optimization, providing a common interface for a number of different free optimization routines available online as well as original implementations of various other algorithms. Its features inc

nlopt.readthedocs.io

 

 

 

이 시점에서 저희는 BRDF를 수작업으로 피팅하기로 결정했습니다. 
수작업으로 피팅을 하면 우리가 볼 수 있는 스쳐가는 역반사 효과와 같은 두드러진 특성을 보존할 수 있었습니다.
이 작업은 디즈니의 BRDF익스플로러를 사용했습니다.
앞서 언급했듯이, 생성된 전체 3D 아이소트로픽 BRDF와 2D 슬라이스를 모두 읽을 수 있도록 BRDFExplorer를 수정했습니다. 
BRDFExplorer에서 제공하는 다양한 그래프를 사용하여 BRDF를 분석함으로써 BRDF를 관리하기 쉬운 여러 부분으로 나누고, 이를 결합하여 전체 결과를 만들 수 있었습니다.
첫 번째 부분은 러프 파운데이션이라고 부르는 부분입니다.
하단에는 이 러프 파운데이션을 나타내는 2D BRDF 슬라이스가 있습니다.

 

 

두 번째 부분은 스무스 디퓨즈 BRDF입니다.
시그라프 2012의 디즈니의 디퓨즈 모델이 광택 = 1일 때 멀티스캐터링 디퓨즈로 본 것과 매우 유사하다는 것을 발견했습니다.

[Bur12] 디즈니의 물리 기반 셰이딩
http://blog.selfshadow.com/publications/s2012-shading-course/

 

SIGGRAPH 2012 Course: Practical Physically Based Shading in Film and Game Production

Course Description Physically based shading is becoming of increasing importance to both film and game production. By adhering to physically based, energy-conserving shading models, one can easily create high quality, realistic materials that maintain that

blog.selfshadow.com

 

 

 

그런 다음 광택을 기준으로 러프 모델과 스무스 모델 사이를 보간합니다.

 

 

또한 거친 소재에 가장 중요한 요소인 방목 각도(Grazing term) 재귀반사 성분을 추가했습니다.

 

 

그리고 최종 결과에 도달했습니다.
이것이 2차 세계대전에서 배송된 것입니다.

* 편집 * 원본 슬라이드에서는 f_r에 대한 rho/PI가 생략되었습니다. Rho는 확산 알베도입니다.

 

 

각 구의 왼쪽 절반은 전체 시뮬레이션된 멀티스캐터링 BRDF입니다.
오른쪽 절반은 최종 수작업으로 맞춘 멀티스캐터링 모델입니다.
앞서 언급했듯이, 전체 모델을 먼저 BRDF 슬라이스로 2D로 축소한 다음 적합도를 찾았습니다.

 

 

다음은 램버시안 디퓨즈와 GGX 스페큘러를 사용하여 광택이 0인 머티리얼입니다.

 

 

새로운 멀티스캐터링 디퓨즈 BRDF를 사용한 것입니다.
멀티스캐터링 디퓨즈 BRDF를 사용하여 울퉁불퉁한 콘크리트 바닥에 밝기가 추가된 것을 확인할 수 있습니다. 
이는 이 섹션의 시작 부분에서 울퉁불퉁한 콘크리트에 대한 측정 결과와 일치합니다.
이 머티리얼의 라이팅이 미묘하게 평평해지는 것을 확인할 수 있습니다.

 

 

램버시안 디퓨즈와 GGX 스페큘러를 사용하는 광택 = 1의 머티리얼입니다.

 

이는 새로운 멀티스캐터링 디퓨즈 BRDF를 사용합니다.
한 가지 명심해야 할 점은 모든 중간 광택이 평면과 원형 디퓨즈 조명 사이의 보간을 제공한다는 것입니다.
그 결과 광택 맵을 기반으로 머티리얼 차별화가 향상됩니다. 
오브젝트의 오른쪽 실루엣이 어두워지는 것을 볼 수 있는데, 이는 소위 조명의 "라운딩"입니다.
머티리얼이 좀 더 풍부해졌습니다.

 

 

여기서 흥미로운 점이 있습니다.
보통은 부드럽고 표면 아래 산란이 있는 스킨에 멀티스캐터링 디퓨즈를 사용하는 것을 고려하지 않지만, 디버그 메뉴에 숨겨진 실험적인 기능이었을 때 캐릭터 아티스트가 먼저 사용을 요청했습니다.
캐릭터 머리에 적용된 램버시안 디퓨즈입니다.

 

 

 

멀티스캐터링 디퓨즈가 적용된 모습입니다.
아티스트들은 멀티스캐터링 디퓨즈를 사용할 때 어두운 실루엣이 줄어드는 것을 선호했습니다. 향후 작업에서는 사진으로 이를 검증할 예정입니다. 

BRDF.ush

언리얼 엔진 5.3.0 서브스트레이트에 적용 된 Diffuse Chan model.

// [ Chan 2018, "Material Advances in Call of Duty: WWII" ]
// 여기에서는 레트로 반사율 기여를 피하기 위해 영역 조명에서 역반사 기여도를 페이드 아웃하도록 확장되었습니다.
float3 Diffuse_Chan( float3 DiffuseColor, float a2, float NoV, float NoL, float VoH, float NoH, float RetroReflectivityWeight)
{
    // 범위를 벗어난 음수 값으로 인해 메시의 가장자리가 이상하게 어두워지는 것을 방지하기 위해 각 입력을 포화시킵니다(탄젠트 공간 보간으로 인해 발생).
    NoV = saturate(NoV);
    NoL = saturate(NoL);
    VoH = saturate(VoH);
    NoH = saturate(NoH);

    // a2 = 2 / ( 1 + exp2( 18 * g )
    float g = saturate( (1.0 / 18.0) * log2( 2 * rcpFast(a2) - 1 ) );

    float F0 = VoH + Pow5( 1 - VoH );
    float FdV = 1 - 0.75 * Pow5( 1 - NoV );
    float FdL = 1 - 0.75 * Pow5( 1 - NoL );

    // 러프(F0)에서 스무스(FdV * FdL)로의 응답 보간
    float Fd = lerp( F0, FdV * FdL, saturate( 2.2 * g - 0.5 ) );

    // 레트로 반사율 기여도.
    float Fb = ( (34.5 * g - 59 ) * g + 24.5 ) * VoH * exp2( -max( 73.2 * g - 21.2, 8.9 ) * sqrtFast( NoH ) );
    // It fades out when lights become area lights in order to avoid visual artefacts.
    Fb *= RetroReflectivityWeight;
    
    return DiffuseColor * ( (1 / PI) * ( Fd + Fb ) );
}

 

StrataEvaluation.ush

FStrataEvaluateResult StrataEvaluateBSDFCommon(FStrataBSDFContext BSDFContext, FShadowTerms ShadowTerms, FAreaLightIntegrateContext AreaLightContext, FStrataIntegrationSettings Settings, int IntegrationType)
{
    FStrataEvaluateResult Sample = (FStrataEvaluateResult)0;

    const float OpaqueBSDFThroughput = 0.0f;

    const uint BSDFType = BSDF_GETTYPE(BSDFContext.BSDF);
    switch (BSDFType)
    {
       case STRATA_BSDF_TYPE_SLAB:
       {
          float3 DiffuseColor          = SLAB_DIFFUSEALBEDO(BSDFContext.BSDF);
          float3 F0              = SLAB_F0(BSDFContext.BSDF);
          float3 F90             = SLAB_F90(BSDFContext.BSDF);
          const float SafeRoughness  = MakeRoughnessSafe(SLAB_ROUGHNESS(BSDFContext.BSDF));
          const bool bHasAnisotropy  = BSDF_GETHASANISOTROPY(BSDFContext.BSDF);
          const bool bHaziness      = BSDF_GETHASHAZINESS(BSDFContext.BSDF);

          // Specular occlusion is only used here once to affect the F90 source parameters. F0 will decrease naturally to 0.
          F90 *= F0RGBToMicroOcclusion(F0);

          if (Settings.bForceFullyRough)
          {
             // When rendering reflection captures, the BSDF roughness has already been forced to 1 using View.RoughnessOverrideParameter (see StrataSanitizeBSDF).
             EnvBRDFApproxFullyRough(DiffuseColor, F0, F90);
          }

          float Alpha2Spec = Pow4(SafeRoughness);

          float NoV, VoH, NoH;
       #if STRATA_COMPLEXPATH
          BRANCH
          if (bHasAnisotropy)
          {
             Init(BSDFContext.Context, BSDFContext.N, BSDFContext.X, BSDFContext.Y, BSDFContext.V, AreaLightContext.L);

             NoV = BSDFContext.Context.NoV;
             VoH = BSDFContext.Context.VoH;
             NoH = BSDFContext.Context.NoH;
          }
          else
       #endif
          {
             Init(BSDFContext.Context, BSDFContext.N, BSDFContext.V, AreaLightContext.L);

             NoV = BSDFContext.Context.NoV;
             VoH = BSDFContext.Context.VoH;
             NoH = BSDFContext.Context.NoH;

             SphereMaxNoH(BSDFContext.Context, AreaLightContext.AreaLight.SphereSinAlpha, true);
          }
          BSDFContext.Context.NoV = saturate(max(abs(BSDFContext.Context.NoV), STRATA_EPSILON));

          ////
          //// Evaluate the diffuse component.
          ////
#if MATERIAL_ROUGHDIFFUSE
          if (Settings.bRoughDiffuseEnabled && any(DiffuseColor > 0))
          {
             // * If the specular layer is anisotropic, the diffuse takes the 'main' roughness value rather than the tangent/bitangent value
             // * The Chan model bakes transmittance specular directional albedo assuming F=0.04. In previous code we reapplied 
             //   this transmittance, as the energy preservation code we remove it as well. However the visual effect was small 
             //   and the cost was rather large (~8%). This is why we removed it in recent iteration
             Sample.DiffusePathValue = Diffuse_Chan(DiffuseColor, Alpha2Spec, NoV, AreaLightContext.NoL, VoH, NoH, GetAreaLightDiffuseMicroReflWeight(AreaLightContext.AreaLight));
          }
          else
#endif
          {
             Sample.DiffusePathValue = Diffuse_Lambert(DiffuseColor);
          }
          Sample.IntegratedDiffuseValue += (ShadowTerms.SurfaceShadow * AreaLightContext.NoL * AreaLightContext.Falloff) * Sample.DiffusePathValue * AreaLightContext.AreaLight.FalloffColor;
          Sample.DiffuseColor       = DiffuseColor;
          Sample.DiffusePDF     = BSDFContext.SatNoL / PI;
          Sample.bSubsurface    = BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION || BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION_PROFILE;




          ////
          //// Evaluate the specular component.
          //// This takes into account multiple scattering, micro occlusion.
          //// Note: anisotropy completely disables area integrations. Lights fall back to punctual.
          ////

          const bool bIsRectLight = IntegrationType == INTEGRATION_AREA_LIGHT_RECT;

          // Primary highlight
          float PDF = 0;
          float DirectionalAlbedo_SpecularTransmission = 1.0f;
          {
             FBxDFEnergyType MSScale = 1.0f;
             {
                FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(SafeRoughness, BSDFContext.Context.NoV, F0, F90);
                DirectionalAlbedo_SpecularTransmission = ComputeEnergyPreservation(EnergyTerms);
                MSScale = ComputeEnergyConservation(EnergyTerms);
             }
             // Apply energy conservation on the diffuse component
             // If the specular layer is anisotropic, the energy term is computed onto the 'main' roughness [Kulla 2019]
             //
             Sample.DiffusePathValue *= DirectionalAlbedo_SpecularTransmission;
             Sample.IntegratedDiffuseValue *= DirectionalAlbedo_SpecularTransmission;
          
             float D = 0;
             float Vis = 0;

             float3 RectLightSpec = 0;
             #if STRATA_COMPLEXPATH
             BRANCH
             if (bHasAnisotropy)
             {
                // Generalized microfacet specular
                {
                   float  Alpha  = Square(SafeRoughness);
                   float2 AlphaXY = 0;
                   GetAnisotropicRoughness(Alpha, SLAB_ANISOTROPY(BSDFContext.BSDF), AlphaXY.x, AlphaXY.y);

                #if STRATA_COMPLEXSPECIALPATH
                   // Glint shading only in the complex path
                   BRANCH
                   if (BSDF_GETHASGLINT(BSDFContext.BSDF))
                   {
                      const float2 GGXRoughnessXY = float2(sqrtFast(AlphaXY.x), sqrtFast(AlphaXY.y));
                      FBeckmannDesc Beckmann = GGXToBeckmann(GGXRoughnessXY);
                      D = f_P(BSDFContext.TangentV, BSDFContext.TangentL, float3(Beckmann.Sigma, Beckmann.Rho),
                         SLAB_GLINT_VALUE(BSDFContext.BSDF), SLAB_GLINT_UV(BSDFContext.BSDF), SLAB_GLINT_UVDDX(BSDFContext.BSDF), SLAB_GLINT_UVDDY(BSDFContext.BSDF));
                   }
                   else
                #endif
                   {
                      D = Strata_D_GGX_Aniso(AlphaXY.x, AlphaXY.y, BSDFContext.Context.NoH, BSDFContext.Context.XoH, BSDFContext.Context.YoH);
                   }

                   Vis = Strata_Vis_GGX_Aniso(AlphaXY.x, AlphaXY.y, BSDFContext.Context.NoV, BSDFContext.SatNoL, BSDFContext.Context.XoV, BSDFContext.Context.XoL, BSDFContext.Context.YoV, BSDFContext.Context.YoL);
                   const float H_PDF = VisibleGGXPDF_aniso(BSDFContext.TangentV, BSDFContext.TangentH, AlphaXY);
                   PDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
                }
             }
             else
             #endif
             {
                BRANCH
                if (bIsRectLight)
                {
                   // In this case, we set D to 1 and Vis will contain the area light / GGX integration
                   {
                      float3 MeanLightWorldDirection = 0.0f;
                      RectLightSpec = RectGGXApproxLTC(SafeRoughness, F0, BSDFContext.N, BSDFContext.V, AreaLightContext.AreaLight.Rect, AreaLightContext.AreaLight.Texture, MeanLightWorldDirection);
                      
                      // Now combine the rectangular area light with Glints.
                   #if STRATA_COMPLEXSPECIALPATH
                      BRANCH
                      if (BSDF_GETHASGLINT(BSDFContext.BSDF))
                      {
                         float3 MeanLightLocalDirection = normalize(mul(BSDFContext.TangentBasis, MeanLightWorldDirection));
                         FBeckmannDesc Beckmann = GGXToBeckmann(SafeRoughness);

                         // It is wrong to multiply again the GGXevaluation together with the glint function (representing Beckmann D function).
                         // But visually it works and that is an acceptable solution in the meantime we find something more correct.
                         RectLightSpec *= f_P(BSDFContext.TangentV, MeanLightLocalDirection, float3(Beckmann.Sigma.xx, Beckmann.Rho), 
                            SLAB_GLINT_VALUE(BSDFContext.BSDF), SLAB_GLINT_UV(BSDFContext.BSDF), SLAB_GLINT_UVDDX(BSDFContext.BSDF), SLAB_GLINT_UVDDY(BSDFContext.BSDF));
                      }
                   #endif // STRATA_COMPLEXPATH

                      const float H_PDF = VisibleGGXPDF(BSDFContext.TangentV, BSDFContext.TangentH, Alpha2Spec);
                      PDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
                   }
                }
                else
                {
                   // Special override for roughness for supporting area light integrator with Sphere/Tube/Disk light, which modifies/increase roughness.

                   // Generalized microfacet specular
                   {
                      if(IntegrationType == INTEGRATION_PUNCTUAL_LIGHT)
                      {
                      #if STRATA_COMPLEXSPECIALPATH
                         BRANCH
                         if (BSDF_GETHASGLINT(BSDFContext.BSDF))
                         {
                            FBeckmannDesc Beckmann = GGXToBeckmann(SafeRoughness);
                            D = f_P(BSDFContext.TangentV, BSDFContext.TangentL, float3(Beckmann.Sigma.xx, Beckmann.Rho),
                               SLAB_GLINT_VALUE(BSDFContext.BSDF), SLAB_GLINT_UV(BSDFContext.BSDF), SLAB_GLINT_UVDDX(BSDFContext.BSDF), SLAB_GLINT_UVDDY(BSDFContext.BSDF));
                         }
                         else
                      #endif // STRATA_COMPLEXPATH
                         {
                            D = Strata_D_GGX(SafeRoughness, Alpha2Spec, BSDFContext.Context.NoH);
                         }
                      }
                      else
                      {
                         const float Energy = EnergyNormalization(Alpha2Spec, BSDFContext.Context.VoH, AreaLightContext.AreaLight);

                      #if STRATA_COMPLEXSPECIALPATH
                         BRANCH
                         if (BSDF_GETHASGLINT(BSDFContext.BSDF))
                         {
                            FBeckmannDesc Beckmann = GGXToBeckmann(SafeRoughness);
                            D = Energy * f_P(BSDFContext.TangentV, BSDFContext.TangentL, float3(Beckmann.Sigma.xx, Beckmann.Rho),
                               SLAB_GLINT_VALUE(BSDFContext.BSDF), SLAB_GLINT_UV(BSDFContext.BSDF), SLAB_GLINT_UVDDX(BSDFContext.BSDF), SLAB_GLINT_UVDDY(BSDFContext.BSDF));
                         }
                         else
                      #endif // STRATA_COMPLEXPATH
                         {
                            D = Energy * Strata_D_GGX(SafeRoughness, Alpha2Spec, BSDFContext.Context.NoH);
                         }
                      }

                      Vis = Strata_Vis_GGX(SafeRoughness, Alpha2Spec, BSDFContext.Context.NoV, AreaLightContext.NoL);
                      const float H_PDF = VisibleGGXPDF(BSDFContext.TangentV, BSDFContext.TangentH, Alpha2Spec);
                      PDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
                   }
                }
             }

             if (bIsRectLight)
             {
                Sample.SpecularPathValue = MSScale * RectLightSpec;
             }
             else
             {
                const float3 FresnelTerm = Strata_F_GGX(F0, F90, BSDFContext.Context.VoH);
                Sample.SpecularPathValue = D * Vis * MSScale * FresnelTerm;
             }

             Sample.SpecularPathProbability     = 1.0f;
             Sample.SpecularHazePathProbability = 0.0f;
             Sample.IntegratedSpecularValue = Sample.SpecularPathValue;
          }

          // Secondary highlight
          float HazePDF = 0;
          #if STRATA_FASTPATH==0
          BRANCH
          if (bHaziness)
          {
             // F0 / F90 have already been affected by specular micro occlusion.
             float3 HazeF0 = F0;
             float3 HazeF90= F90;
          
             const FHaziness Haziness = UnpackHaziness(SLAB_HAZINESS(BSDFContext.BSDF));
             const float HazeWeight = Haziness.Weight;
             const float HazeSafeRoughness = MakeRoughnessSafe(Haziness.Roughness);
             const bool bHazeAsSimpleClearCoat = Haziness.bSimpleClearCoat;

             if (bHazeAsSimpleClearCoat)
             {
                HazeF0 = 0.04f;
                HazeF90 = 1.0f;
                // In this case, no specular micro occlusion happens.
             }

             FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(HazeSafeRoughness, BSDFContext.Context.NoV, HazeF0, HazeF90);
             FBxDFEnergyType HazeMSScale = ComputeEnergyConservation(EnergyTerms);

             float HazeD = 0;
             float HazeVis = 0;

             float3 RectLightSpecHaze = 0;
             #if STRATA_COMPLEXPATH
             BRANCH
             if (bHasAnisotropy)
             {
                {
                   float2 HazeAlpha = 0;
                   GetAnisotropicRoughness(HazeSafeRoughness, SLAB_ANISOTROPY(BSDFContext.BSDF), HazeAlpha.x, HazeAlpha.y);

                   HazeD   = Strata_D_GGX_Aniso(HazeAlpha.x, HazeAlpha.y, BSDFContext.Context.NoH, BSDFContext.Context.XoH, BSDFContext.Context.YoH);
                   HazeVis = Strata_Vis_GGX_Aniso(HazeAlpha.x, HazeAlpha.y, BSDFContext.Context.NoV, BSDFContext.Context.NoL, BSDFContext.Context.XoV, BSDFContext.Context.XoL, BSDFContext.Context.YoV, BSDFContext.Context.YoL);
                   const float H_PDF = VisibleGGXPDF_aniso(BSDFContext.TangentV, BSDFContext.TangentH, HazeAlpha);
                   HazePDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
                }
             }
             else
             #endif
             {
                if (bIsRectLight)
                {
                   {
                      RectLightSpecHaze = RectGGXApproxLTC(HazeSafeRoughness, HazeF0, BSDFContext.N, BSDFContext.V, AreaLightContext.AreaLight.Rect, AreaLightContext.AreaLight.Texture);
                
                      const float H_PDF = VisibleGGXPDF(BSDFContext.TangentV, BSDFContext.TangentH, Pow4(HazeSafeRoughness));
                      HazePDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
                   }
                }
                else
                {
                   // Special override for roughness for supporting area light integrator with Sphere/Tube/Disk light, which modifies/increase roughness.
                   {
                      float Alpha2SpecHaze = Pow4(HazeSafeRoughness);
                      if (IntegrationType == INTEGRATION_PUNCTUAL_LIGHT)
                      {
                         HazeD = Strata_D_GGX(HazeSafeRoughness, Alpha2SpecHaze, BSDFContext.Context.NoH);
                      }
                      else
                      {
                         const float Energy = EnergyNormalization(Alpha2SpecHaze, BSDFContext.Context.VoH, AreaLightContext.AreaLight);
                         HazeD = Strata_D_GGX(HazeSafeRoughness, Alpha2SpecHaze, BSDFContext.Context.NoH) * Energy;
                      }
                      HazeVis = Strata_Vis_GGX(HazeSafeRoughness, Alpha2SpecHaze, BSDFContext.Context.NoV, AreaLightContext.NoL);
                      const float H_PDF = VisibleGGXPDF(BSDFContext.TangentV, BSDFContext.TangentH, Alpha2SpecHaze);
                      HazePDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
                   }
                }
             }

             const float3 HazeFresnelTerm = Strata_F_GGX(HazeF0, HazeF90, BSDFContext.Context.VoH);
             if (bIsRectLight)
             {              
                Sample.SpecularHazePathValue = RectLightSpecHaze * HazeMSScale;
             }
             else
             {
                Sample.SpecularHazePathValue = HazeD * HazeVis * HazeMSScale * HazeFresnelTerm;
             }

             // Reminder: 
             // - bHazeAsSimpleClearCoat == false means we have two specular lobes that are lerped.
             // - bHazeAsSimpleClearCoat == true  means we have a bottom specular lobe (roughness and F0 user specified) and a top specular lob (hard coded absortion, F0=0.04 and user specified roughness.
             BRANCH
             if (bHazeAsSimpleClearCoat)
             {
                const BxDFContext ClearCoatContext = RefractClearCoatContext(BSDFContext.Context);
                const float3 HazeClearCoatTransmittance = SimpleClearCoatTransmittance(ClearCoatContext.NoL, ClearCoatContext.NoV, StrataGetBSDFMetallic(BSDFContext.BSDF), StrataGetBSDFBaseColor(BSDFContext.BSDF));

                const float TopLayerCoverage = HazeWeight;
                const float TopLayerSpecularTransmittionApprox = saturate(1.0f - HazeFresnelTerm.x); // Simple and do not use the LUT for now. With bHazeAsSimpleClearCoat, Fresnel is achromatic
                const float3 TopLayerThrouput = lerp(1.0f, HazeClearCoatTransmittance * TopLayerSpecularTransmittionApprox, TopLayerCoverage);

                const float TopLayerThrouputGrey = dot(TopLayerThrouput, (1.0 / 3.0).xxx);
                Sample.SpecularPathProbability = TopLayerThrouputGrey / (TopLayerCoverage + TopLayerThrouputGrey);
                Sample.SpecularHazePathProbability = TopLayerCoverage / (TopLayerCoverage + TopLayerThrouputGrey);

                // Clear coat applied over bottom layer diffuse component
                Sample.DiffusePathValue *= TopLayerThrouput;
                Sample.IntegratedDiffuseValue *= TopLayerThrouput;
                // And to the bottom layer specular affected by top layer throughput, on top of which we add the top layer specular
                Sample.IntegratedSpecularValue = Sample.SpecularPathValue * TopLayerThrouput + Sample.SpecularHazePathValue * TopLayerCoverage;
             }
             else
             {
                Sample.SpecularPathProbability     = (1.0f - HazeWeight);
                Sample.SpecularHazePathProbability = HazeWeight;
                Sample.IntegratedSpecularValue = lerp(Sample.SpecularPathValue, Sample.SpecularHazePathValue, HazeWeight);
             }
          }
          #endif

          // Lighting LUT
          #if STRATA_FASTPATH==0 && STRATA_COMPLEXSPECIALPATH
          BRANCH
          if (BSDF_GETHASSPECPROFILE(BSDFContext.BSDF))
          {
             Sample.IntegratedSpecularValue *= EvaluateSpecularProfile(SLAB_SPECPROFILEID(BSDFContext.BSDF), BSDFContext.SatNoV, BSDFContext.SatNoL, BSDFContext.Context.VoH, BSDFContext.Context.NoH);
          }
          #endif

          {
             float3 CommonTerm = 0.0f;
             if (bIsRectLight)
             {
                CommonTerm = ShadowTerms.SurfaceShadow; /* AreaLightContext.NoL and falloff is part of the LTC integration already */
             }
             else
             {
                CommonTerm = (ShadowTerms.SurfaceShadow * AreaLightContext.NoL * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor;
             }
             Sample.IntegratedSpecularValue *= CommonTerm;
          }

          Sample.SpecularPDF = PDF * Sample.SpecularPathProbability;
          Sample.SpecularHazePDF = HazePDF * Sample.SpecularHazePathProbability;


          ////
          //// Evaluate emissive and set the sample throughput (transmittance to next layer) corresponding to an opaque slab of matter.
          ////
          
          // we do not need to add emissive for the BRDF TotalSpec or TotalDiff values as this is handled separately
          Sample.EmissivePathValue = BSDF_GETEMISSIVE(BSDFContext.BSDF);

          Sample.ThroughputV       = OpaqueBSDFThroughput;
          Sample.TransmittanceAlongN = OpaqueBSDFThroughput;

          ////
          //// Evaluate cloth fuzz layered on top of the slab.
          ////

          #if STRATA_FASTPATH==0
          BRANCH
          if (BSDF_GETHASFUZZ(BSDFContext.BSDF))
          {
             const float  FuzzAmount = SLAB_FUZZ_AMOUNT(BSDFContext.BSDF);
             const float3 FuzzF0  = SLAB_FUZZ_COLOR(BSDFContext.BSDF);
             const float  FuzzRoughness = MakeRoughnessSafe(SLAB_FUZZ_ROUGHNESS(BSDFContext.BSDF), STRATA_MIN_FUZZ_ROUGHNESS);

             // F0 is ignored, as energy preservation is handle solely based on the white directional albedo and FuzzAmount
             FBxDFEnergyTermsA EnergyTerms = (FBxDFEnergyTermsA)1.f;
             float3 ClothSpecularPathValueNoL = 0;

             #if STRATA_SHEEN_QUALITY == 1
             // Disney LTC
             {
                float DirectionalAlbedo = 1;
                BRANCH
                if (bIsRectLight)
                {
                   ClothSpecularPathValueNoL = FuzzF0 * RectSheenApproxLTC(FuzzRoughness, BSDFContext.N, BSDFContext.V, AreaLightContext.AreaLight.Rect, AreaLightContext.AreaLight.Texture, DirectionalAlbedo);
                }
                else
                {
                   ClothSpecularPathValueNoL = FuzzF0 * SheenLTC_Eval(BSDFContext.V, AreaLightContext.L, BSDFContext.N, BSDFContext.Context.NoV, FuzzRoughness, View.SheenLTCTexture, View.SheenLTCSampler, DirectionalAlbedo);
                }
                EnergyTerms.E = DirectionalAlbedo;
                EnergyTerms.W = 1.f;
             }
             #else
             // Charlie cloth modle for D and Ashikhmin for visibility (more efficient than Charlie).
             {
                // ComputeEnergyConservation(EnergyTerms) is not taken into account because we do not white white fuzz to reflect all incoming energy
                // The fuzz should have some transmission to the lower lobes. In order to pass a furnace test, a combination of white diffuse+white fuzz is required
                EnergyTerms = ComputeClothEnergyTermsA(FuzzRoughness, BSDFContext.Context.NoV);

                float ClothD   = D_Charlie(FuzzRoughness, BSDFContext.Context.NoH);
                float ClothVis = Vis_Ashikhmin(BSDFContext.Context.NoV, AreaLightContext.NoL);
                float3 ClothF  = F_Schlick(FuzzF0, BSDFContext.Context.VoH);
                ClothSpecularPathValueNoL = (ClothD * ClothVis) * ClothF * ComputeEnergyConservation(EnergyTerms) * AreaLightContext.NoL;
             }
             #endif //STRATA_SHEEN_QUALITY

             // The specular and diffuse components below the fuzz are only attenuated linearly according to the amount of fuzz.
             const float3 Cloth_DirectionalAlbedo_SpecularTransmission = lerp(1.0, ComputeEnergyPreservation(EnergyTerms), FuzzAmount);

             // Area light are not supported by the cloth BRDF
             float3 ClothIntegratedSpecularValue = (ShadowTerms.SurfaceShadow * AreaLightContext.Falloff * FuzzAmount) * AreaLightContext.AreaLight.FalloffColor * ClothSpecularPathValueNoL;
             
             // Apply specular transmittance to diffuse and specular lob from slab medium and interface sitting below the layer of fuzz.
             Sample.DiffusePathValue          *= Cloth_DirectionalAlbedo_SpecularTransmission;
             Sample.IntegratedDiffuseValue  *= Cloth_DirectionalAlbedo_SpecularTransmission;
             Sample.SpecularPathValue      *= Cloth_DirectionalAlbedo_SpecularTransmission;
             Sample.IntegratedSpecularValue *= Cloth_DirectionalAlbedo_SpecularTransmission;
             
             Sample.SpecularPathValue      += ClothSpecularPathValueNoL;
             Sample.IntegratedSpecularValue  += ClothIntegratedSpecularValue;

             // This is not good. We should really have a separate PDF for cloth it self associated with a probability.
             // We should also output probability to sample cloth, diffuse, secular, etc.
             // We will revisit when the path tracer is getting up with Strata.
             //Sample.SpecularPDF = lerp(Sample.SpecularPDF, BSDFContext.SatNoV / PI, FuzzAmount); // Per "Production Friendly Microfacet Sheen BRDF", hemispherical sampling give good result as the roughness is usually high.
             // STRATA_TODO we should have a separated cloth PDF and Weight

             if (bHaziness)
             {
                Sample.SpecularHazePathValue*= Cloth_DirectionalAlbedo_SpecularTransmission;
                Sample.SpecularHazePathValue+= ClothSpecularPathValueNoL;

                // STRATA_TODO we should have a separated cloth PDF and Weight
                //Sample.SpecularHazePDF      = lerp(Sample.SpecularHazePDF,       BSDFContext.SatNoV / PI,   Fuzz);;
             }

             Sample.ThroughputV *= Cloth_DirectionalAlbedo_SpecularTransmission;
             // The specular transmission is ignored towards the direction of the light. (TransmittanceAlongN can only store transmittance)
             break;
          }
          #endif

          ////
          //// Evaluate approximated SSS (using wrap lighting). Temp code. STRATA_TODO: unify evaluation
          ////

          #if STRATA_FASTPATH==0
          if (BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_WRAP)
          {
             // The wrap lighting is a non-physically based shading which intends to match legacy behavior.
             // For doing so the look MFP & phase function anisotropy are respectively reinterpreted as 'SubSurfaceColor' and 'Opacity', per legacy shading model
             const bool bIsThin = BSDF_GETISTHIN(BSDFContext.BSDF);
             const float TransmittanceNoL = 1.0f;
             const float3 SlabDiffuseColor = bIsThin ? DiffuseColor : float3(1, 1, 1);
             const FParticipatingMedia PM = StrataSlabCreateParticipatingMedia(SlabDiffuseColor, SLAB_SSSMFP(BSDFContext.BSDF));
             const float3 SubSurfaceColor = IsotropicMediumSlabTransmittance(PM, STRATA_SIMPLEVOLUME_THICKNESS_M, TransmittanceNoL);
             const float Opacity = 1.f - abs(SLAB_SSSPHASEANISOTROPY(BSDFContext.BSDF));

             float3 TransmissionThroughput = 0;
             if (bIsThin)
             {
                // Legacy Foliage
                
                // http://blog.stevemcauley.com/2011/12/03/energy-conserving-wrapped-diffuse/
                const float Wrap = 0.5;
                const float WrapNoL = saturate((-dot(BSDFContext.N, BSDFContext.L) + Wrap) / Square(1 + Wrap));
                // Scatter distribution
                const float VoL = dot(BSDFContext.V, BSDFContext.L);
                const float Scatter = Strata_D_GGX(0.6, 0.6 * 0.6, saturate(-VoL));

                TransmissionThroughput = (WrapNoL * Scatter) * SubSurfaceColor;
             }
             else
             {
                // Legacy Subsurface
                 
                // To get an effect when you see through the material (hard coded pow constant)
                const half InScatter = pow(saturate(dot(BSDFContext.L, -BSDFContext.V)), 12) * lerp(3, .1f, Opacity);
                // Wrap around lighting, 
                // * /(PI*2) to be energy consistent (hack do get some view dependnt and light dependent effect)
                // * Opacity of 0 gives no normal dependent lighting, Opacity of 1 gives strong normal contribution    
                // * Simplified version (with w=.5,  n=1.5): 
                //     half WrappedDiffuse = 2 * pow(saturate((dot(N, L) + w) / (1.0f + w)), n) * (n + 1) / (2 * (1 + w));
                //     NormalContribution = WrappedDiffuse * Opacity + 1 - Opacity;
                const half WrappedDiffuse = pow(saturate(AreaLightContext.NoL * (1.f / 1.5f) + (0.5f / 1.5f)), 1.5f) * (2.5f / 1.5f);
                const half NormalContribution = lerp(1.f, WrappedDiffuse, Opacity);
                const half BackScatter = /* GBuffer.GBufferAO */ NormalContribution / (PI * 2);

                // Transmission
                // * Emulate Beer-Lambert absorption by retrieving extinction coefficient from SubSurfaceColor. Subsurface is interpreted as a 'transmittance color' 
                //   at a certain 'normalized' distance (SubSurfaceColorAsTransmittanceAtDistanceInMeters). This is a coarse approximation for getting hue-shiting. 
                // * TransmittedColor is computed for the 1-normalized distance, and then transformed back-and-forth in HSV space to preserving the luminance value 
                //   of the original color, but getting hue shifting
                const half3 ExtinctionCoefficients = TransmittanceToExtinction(SubSurfaceColor, View.SubSurfaceColorAsTransmittanceAtDistanceInMeters);
                const half3 RawTransmittedColor = ExtinctionToTransmittance(ExtinctionCoefficients, 1.0f /*At 1 meters, as we use normalized units*/);
                const half3 TransmittedColor = HSV_2_LinearRGB(half3(LinearRGB_2_HSV(RawTransmittedColor).xy, LinearRGB_2_HSV(SubSurfaceColor).z));

                // Lerp to never exceed 1 (energy conserving)
                TransmissionThroughput = lerp(BackScatter, 1, InScatter) * lerp(TransmittedColor, SubSurfaceColor, ShadowTerms.TransmissionShadow);
             }

             Sample.TransmissionPDF = 1.0f / (4.0f * PI); // STRATA_TODO this currently match the uniform sphere sampling from StrataImportanceSampleBSDF
             Sample.TransmissionPathValue = TransmissionThroughput * DirectionalAlbedo_SpecularTransmission;
             Sample.IntegratedDiffuseValue += (ShadowTerms.TransmissionShadow * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor * Sample.TransmissionPathValue;
          }
          #endif

          ////
          //// Evaluate transmitted light through a mesh due to sub surface scattering.
          ////
          #if STRATA_FASTPATH==0 && STRATA_SSS_TRANSMISSION
          if (BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION_PROFILE || BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION)
          {
             float ThicknessInCm = DecodeThickness(ShadowTerms.TransmissionThickness) * SSSS_MAX_TRANSMISSION_PROFILE_DISTANCE;

             float OneOverIOR = 1.0f;
             float PhaseFunctionAnisotropy = 0.0f;
             float3 TransmissionThroughput = 1.0f;
             if (BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION_PROFILE)
             {
                ThicknessInCm = BSDF_GETISTHIN(BSDFContext.BSDF) ? SLAB_SSSPROFILETHICKNESSCM(BSDFContext.BSDF) : ThicknessInCm;

                const uint ProfileId = StrataSubsurfaceProfileIdTo8bits(SLAB_SSSPROFILEID(BSDFContext.BSDF)); // TODO move this into the PackStrataOut( function, to avoid this decode here
                const FTransmissionProfileParams TransmissionParams = GetTransmissionProfileParams(ProfileId);
                TransmissionThroughput = GetTransmissionProfile(ProfileId, ThicknessInCm).rgb;
                PhaseFunctionAnisotropy = TransmissionParams.ScatteringDistribution;
                OneOverIOR          = TransmissionParams.OneOverIOR;
             }
             else
             {
                // When the surface 'IsThin' MFP is rescaled to STRATA_SIMPLEVOLUME_THICKNESS_CM
                ThicknessInCm = BSDF_GETISTHIN(BSDFContext.BSDF) ? STRATA_SIMPLEVOLUME_THICKNESS_CM : ThicknessInCm;

                const float3 MeanFreePathInCm = SLAB_SSSMFP(BSDFContext.BSDF);
                const float3 SubsurfaceAlebdo = SLAB_DIFFUSEALBEDO(BSDFContext.BSDF);
                TransmissionThroughput = GetBurleyTransmission(SubsurfaceAlebdo, MeanFreePathInCm, ThicknessInCm).xyz;

                PhaseFunctionAnisotropy = SLAB_SSSPHASEANISOTROPY(BSDFContext.BSDF);
                OneOverIOR          = 1.f / DielectricF0ToIor(F0.y);
             }

             const float3 RefracV = refract(BSDFContext.V, -BSDFContext.N, OneOverIOR);
             const float PhaseFunction = HenyeyGreensteinPhase(PhaseFunctionAnisotropy, dot(BSDFContext.L, RefracV));

             Sample.ThroughputV          = OpaqueBSDFThroughput; // SSS is not translucent as of today
             Sample.TransmittanceAlongN    = OpaqueBSDFThroughput; // idem
             Sample.TransmissionPathValue   = TransmissionThroughput * PhaseFunction;
             Sample.TransmissionPDF       = 1.0f / (4.0f * PI);     // STRATA_TODO this currently match the uniform sphere sampling from StrataImportanceSampleBSDF

             Sample.IntegratedDiffuseValue += (ShadowTerms.TransmissionShadow * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor * Sample.TransmissionPathValue;
          }
          #endif

          ////
          //// Evaluate a layer of participating media: scattering and transmittance.
          //// This is used for optically thin translucent objects, or non-bottom layer of a material.
          ////

          #if STRATA_FASTPATH==0
          BRANCH
          if (BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_SIMPLEVOLUME)
          {
             FParticipatingMedia PM = StrataSlabCreateParticipatingMedia(DiffuseColor, SLAB_SSSMFP(BSDFContext.BSDF));
             const float DiffuseToVolumeBlend = StrataSlabDiffuseToVolumeBlend(PM);

          #if USE_NEW_SIMPLEVOLUME
             const float3 SlabDirectionalAlbedo = IsotropicMediumSlabPunctualDirectionalAlbedoLUT(PM, BSDFContext.Context.NoV, BSDFContext.Context.NoL);
          #else
             const float3 SlabDirectionalAlbedo = IsotropicMediumSlabPunctualDirectionalAlbedo(PM);
          #endif

             // Shading point <-> View throughput
             const float3 SlabTransmittanceV = IsotropicMediumSlabTransmittance(PM, STRATA_SIMPLEVOLUME_THICKNESS_M, BSDFContext.Context.NoV);
             const float SpecularTransmissionV = DirectionalAlbedo_SpecularTransmission;

             // Shading point <-> Light throughput
             // Compute the transmittance at normal incidence (instead of BSDFContext.Context.NoL), and will compute the final value during evaluation 
             const float3 SlabTransmittanceN = IsotropicMediumSlabTransmittance(PM, STRATA_SIMPLEVOLUME_THICKNESS_M, 1.f /*NoL with L==N*/);
             // The specular transmission is ignored towards the direction of the light. (TransmittanceAlongN can only store transmittance)

             const float Phase = IsotropicPhase();

             // Now lerp between the optically thick and optically thin medium models.
             // The diffuse and throughput account for the GGX interface SpecularTransmission.
             Sample.DiffusePathValue       = lerp(Sample.DiffusePathValue,       SlabDirectionalAlbedo * SpecularTransmissionV,    DiffuseToVolumeBlend);
             Sample.DiffusePDF        = lerp(Sample.DiffusePDF,        Phase,                                  DiffuseToVolumeBlend);
             Sample.ThroughputV       = lerp(Sample.ThroughputV,       SlabTransmittanceV    * SpecularTransmissionV,    DiffuseToVolumeBlend);
             Sample.TransmittanceAlongN = lerp(Sample.TransmittanceAlongN, SlabTransmittanceN,                            DiffuseToVolumeBlend);

             Sample.bSubsurface = false;    // It should already be the case because Enforce in this case because BSDF_GETHASSSS should be false with SSS_TYPE_SIMPLEVOLUME is true

             // Use Context.NoL which is not saturate, and allows to handle backface lighting, necessary for medium support
             #if STRATA_TRANSLUCENT_ENABLED
             const float MediumNoL = abs(BSDFContext.Context.NoL);
             #else
             const float MediumNoL = saturate(BSDFContext.Context.NoL);
             #endif

             Sample.IntegratedDiffuseValue = lerp(
                Sample.IntegratedDiffuseValue, 
                (ShadowTerms.SurfaceShadow * MediumNoL * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor * SlabDirectionalAlbedo * DirectionalAlbedo_SpecularTransmission,
                DiffuseToVolumeBlend);
             Sample.DiffuseColor       = SlabDirectionalAlbedo;
          }
          #endif

          break;
       }

       #if STRATA_FASTPATH==0
       case STRATA_BSDF_TYPE_HAIR:
       {
          FGBufferData GBuffer   = (FGBufferData)0;
          GBuffer.BaseColor     = HAIR_BASECOLOR(BSDFContext.BSDF);
          GBuffer.Specular      = HAIR_SPECULAR(BSDFContext.BSDF);
          GBuffer.Roughness     = HAIR_ROUGHNESS(BSDFContext.BSDF);
          GBuffer.Metallic      = HAIR_SCATTER(BSDFContext.BSDF);
          GBuffer.CustomData.z   = HAIR_BACKLIT(BSDFContext.BSDF);
          GBuffer.ShadingModelID = SHADINGMODELID_HAIR;
          GBuffer.WorldNormal       = BSDFContext.N;

          FHairTransmittanceData HairTransmittance = InitHairTransmittanceData();
          if (HAIR_COMPLEXTRANSMITTANCE(BSDFContext.BSDF))
          {
             HairTransmittance = EvaluateDualScattering(GBuffer.BaseColor, BSDFContext.N, GBuffer.Roughness, BSDFContext.V, BSDFContext.L);
             HairTransmittance.OpaqueVisibility = ShadowTerms.SurfaceShadow;
          }

          float BacklitEnabled = 1.0f;
          float Area = 0.0f;
          uint2 Random = uint2(0, 0);
          Sample.SpecularPathValue   = HairShading(GBuffer, BSDFContext.L, BSDFContext.V, BSDFContext.N, ShadowTerms.TransmissionShadow, HairTransmittance, BacklitEnabled, Area, Random);
          Sample.SpecularPDF       = 1.0f / (4.0f * PI);     // STRATA_TODO this currently match the uniform sphere sampling from StrataImportanceSampleBSDF
          Sample.ThroughputV       = OpaqueBSDFThroughput; 
          Sample.TransmittanceAlongN = OpaqueBSDFThroughput;
          Sample.IntegratedSpecularValue = (ShadowTerms.TransmissionShadow * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor * Sample.SpecularPathValue;
       }
       break;
       
       case STRATA_BSDF_TYPE_EYE:
       {
          float3 DiffuseColor    = EYE_DIFFUSEALBEDO(BSDFContext.BSDF);
          float3 F0        = EYE_F0(BSDFContext.BSDF);
          float3 F90       = EYE_F90(BSDFContext.BSDF);
          const float SafeRoughness  = MakeRoughnessSafe(EYE_ROUGHNESS(BSDFContext.BSDF));

          // Specular occlusion is only used here once to affect the F90 source parameters. F0 will decrease naturally to 0.
          F90 *= F0RGBToMicroOcclusion(F0);

          if (Settings.bForceFullyRough)
          {
             // When rendering reflection captures, the BSDF roughness has already been forced to 1 using View.RoughnessOverrideParameter (see StrataSanitizeBSDF).
             EnvBRDFApproxFullyRough(DiffuseColor, F0, F90);
          }

          float Alpha2 = Pow4(SafeRoughness);

          Init(BSDFContext.Context, BSDFContext.N, BSDFContext.V, AreaLightContext.L);
          SphereMaxNoH(BSDFContext.Context, AreaLightContext.AreaLight.SphereSinAlpha, true);

          BSDFContext.Context.NoV = saturate(max(abs(BSDFContext.Context.NoV), STRATA_EPSILON));

          ////
          //// Evaluate the diffuse component.
          ////

          const float IrisNoL = saturate(dot(EYE_IRISNORMAL(BSDFContext.BSDF), BSDFContext.L));

          // Blend in the negative intersection normal to create some concavity
          // Not great as it ties the concavity to the convexity of the cornea surface
          // No good justification for that. On the other hand, if we're just looking to
          // introduce some concavity, this does the job.
          const float3 CausticNormal = normalize(lerp(EYE_IRISPLANENORMAL(BSDFContext.BSDF), -BSDFContext.N, EYE_IRISMASK(BSDFContext.BSDF) * EYE_IRISDISTANCE(BSDFContext.BSDF)));

          // Add-hoc legacy shading mode for eye. 
          const float Power = lerp(12, 1, IrisNoL);
          const float Caustic = 0.8 + 0.2 * (Power + 1) * pow(saturate(dot(CausticNormal, BSDFContext.L)), Power);

          const float Iris   = IrisNoL * Caustic;
          const float Sclera = BSDFContext.Context.NoL;

          Sample.DiffusePathValue = Diffuse_Lambert(DiffuseColor) * lerp(Sclera, Iris, EYE_IRISMASK(BSDFContext.BSDF));
          Sample.IntegratedDiffuseValue += (ShadowTerms.SurfaceShadow * AreaLightContext.NoL * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor * Sample.DiffusePathValue;

          Sample.DiffuseColor       = DiffuseColor;
          Sample.DiffusePDF     = BSDFContext.SatNoL / PI;
          Sample.bSubsurface    = BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION || BSDF_GETSSSTYPE(BSDFContext.BSDF) == SSS_TYPE_DIFFUSION_PROFILE;

          //
          // Apply energy conservation on the diffuse component
          // If the specular layer is anisotropic, the energy term is computed onto the 'main' roughness [Kulla 2019]
          //
          FBxDFEnergyType MSScale = 1;
          float DirectionalAlbedo_SpecularTransmission = 1.0f;
          {
             FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(SafeRoughness, BSDFContext.Context.NoV, F0, F90);
             DirectionalAlbedo_SpecularTransmission = ComputeEnergyPreservation(EnergyTerms);
             MSScale = ComputeEnergyConservation(EnergyTerms);
          }
          
          Sample.DiffusePathValue *= DirectionalAlbedo_SpecularTransmission;
          Sample.IntegratedDiffuseValue *= DirectionalAlbedo_SpecularTransmission;

          ////
          //// Evaluate the specular component.
          //// This takes into account multiple scattering, micro occlusion.
          //// Note: anisotropy completely disables area integrations. Lights fall back to punctual.
          ////

          float3 RectLightSpec = 0;
          float3 RectLightSpecHaze = 0;
          float D = 0;
          float Vis = 0;
          float PDF = 0;
          const bool bIsRectLight = IntegrationType == INTEGRATION_AREA_LIGHT_RECT;

          if (bIsRectLight)
          {
             // In this case, we set D to 1 and Vis will contain the area light / GGX integration
             RectLightSpec = RectGGXApproxLTC(SafeRoughness, F0, BSDFContext.N, BSDFContext.V, AreaLightContext.AreaLight.Rect, AreaLightContext.AreaLight.Texture);

             const float H_PDF = VisibleGGXPDF(BSDFContext.TangentV, BSDFContext.TangentH, Alpha2);
             PDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
          }
          else
          {
             // Special override for roughness for supporting area light integrator with Sphere/Tube/Disk light, which modifies/increase roughness.

             // Generalized microfacet specular
             float Alpha2Spec = Alpha2;
             if(IntegrationType == INTEGRATION_PUNCTUAL_LIGHT)
             {
                D = Strata_D_GGX(SafeRoughness, Alpha2Spec, BSDFContext.Context.NoH);
             }
             else
             {
                const float Energy = EnergyNormalization(Alpha2Spec, BSDFContext.Context.VoH, AreaLightContext.AreaLight);
                D = Strata_D_GGX(SafeRoughness, Alpha2Spec, BSDFContext.Context.NoH) * Energy;
             }
             Vis = Strata_Vis_GGX(SafeRoughness, Alpha2Spec, BSDFContext.Context.NoV, AreaLightContext.NoL);
             const float H_PDF = VisibleGGXPDF(BSDFContext.TangentV, BSDFContext.TangentH, Alpha2Spec);
             PDF = RayPDFToReflectionRayPDF(BSDFContext.Context.VoH, H_PDF);
          }

          const float3 FresnelTerm = Strata_F_GGX(F0, F90, BSDFContext.Context.VoH);

          Sample.SpecularPathProbability = 1.0f;
          Sample.SpecularPDF          = PDF * Sample.SpecularPathProbability;

          if (bIsRectLight)
          {
             Sample.SpecularPathValue       = RectLightSpec * MSScale;
             Sample.IntegratedSpecularValue = (ShadowTerms.SurfaceShadow * /* AreaLightContext.NoL and falloff is part of the LTC integration already */ Sample.SpecularPathProbability) * Sample.SpecularPathValue;
          }
          else
          {
             Sample.SpecularPathValue        = D * Vis * MSScale * FresnelTerm;
             Sample.IntegratedSpecularValue += (ShadowTerms.SurfaceShadow * AreaLightContext.NoL * AreaLightContext.Falloff) * AreaLightContext.AreaLight.FalloffColor * Sample.SpecularPathValue * Sample.SpecularPathProbability;
          }

          ////
          //// Evaluate emissive and set the sample throughput (transmittance to next layer) corresponding to an opaque slab of matter.
          ////
          
          // we do not need to add emissive for the BRDF TotalSpec or TotalDiff values as this is handled separately
          Sample.EmissivePathValue = BSDF_GETEMISSIVE(BSDFContext.BSDF);

          Sample.ThroughputV       = OpaqueBSDFThroughput;
          Sample.TransmittanceAlongN = OpaqueBSDFThroughput;

          break;
       }
       break;

       //case STRATA_BSDF_TYPE_VOLUMETRICFOGCLOUD:
       //case STRATA_BSDF_TYPE_UNLIT:
       //case STRATA_BSDF_TYPE_SINGLELAYERWATER:
       //Nothing to do in this case because these BSDF are evaluated in other specialised passes.
       //break;
       #endif
    }

    return Sample;
}