본문 바로가기
Shader

유니티 Lit 셰이더 PBR 뒤져보기3

by 대마왕J 2024. 10. 23.
inline void InitializeBRDFDataDirect(half3 albedo, half3 diffuse, half3 specular, half reflectivity, half oneMinusReflectivity, half smoothness, inout half alpha, out BRDFData outBRDFData)
{
    outBRDFData = (BRDFData)0;
    outBRDFData.albedo = albedo;
    outBRDFData.diffuse = diffuse;
    outBRDFData.specular = specular;
    outBRDFData.reflectivity = reflectivity;

    outBRDFData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
    outBRDFData.roughness           = max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN_SQRT);
    outBRDFData.roughness2          = max(outBRDFData.roughness * outBRDFData.roughness, HALF_MIN);
    outBRDFData.grazingTerm         = saturate(smoothness + reflectivity);
    outBRDFData.normalizationTerm   = outBRDFData.roughness * half(4.0) + half(2.0);
    outBRDFData.roughness2MinusOne  = outBRDFData.roughness2 - half(1.0);

    // Input is expected to be non-alpha-premultiplied while ROP is set to pre-multiplied blend.
    // We use input color for specular, but (pre-)multiply the diffuse with alpha to complete the standard alpha blend equation.
    // In shader: Cs' = Cs * As, in ROP: Cs' + Cd(1-As);
    // i.e. we only alpha blend the diffuse part to background (transmittance).
    #if defined(_ALPHAPREMULTIPLY_ON)
        // TODO: would be clearer to multiply this once to accumulated diffuse lighting at end instead of the surface property.
        outBRDFData.diffuse *= alpha;
    #endif
}

여기서부터 봅니다. 
BRDF.hlsl 파일을 봐야 해요 . 이전시간에 얘길 안했네. 

BRDFData 구조체를 채우는 곳인데요. 
outBRDFData.albedo = albedo;
알베도는 뭐 알베도고 
outBRDFData.diffuse = diffuse;
디퓨즈네요. 디퓨즈는 이전 시간에 뭐였죠? 
albedo * oneMinusReflectivity 이거였어요. 알베도에 1- 스페큘러 반사율을 곱한거.
outBRDFData.specular 
그럼 스페큘러는? 
lerp(kDielectricSpec.rgb, albedo, metallic)
이거죠. 비금속일 경우 0.4의 색이 들어가고, 금속일 경우 알베도가 100% 들어가요. floa3의 색이죠
outBRDFData.reflectivity
반사율은? 
half(1.0) - oneMinusReflectivity 이죠? 이거 뭐.. 스페큘러 반사율이예요. 
비메탈이면 0.96, 메탈이면 0 이면 나오는게 oneMinusReflectivity 니까, 
이건 반대로 비메탈이면 0.04, 메탈이면 1이 나오는.. 즉 '정반사율' 인거죠. 이건 float 이예요. 
하 설명하기 복잡하게도 해놨네 

 outBRDFData.perceptualRoughness
다음은 이거. 퍼셉츄얼러프니스. 직역하면 '지각 거칠기' 
이거 타고 들어가면  
PerceptualSmoothnessToPerceptualRoughness(smoothness)
이고 이건 (1.0 - perceptualSmoothness) 이거든요. 
즉 스무스니스 값을 받아서 1- 해준값이예요. 러프니스랑 스무스니스는 반대니까..

outBRDFData.roughness

그리고 이제 러프니스 값은 
max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN_SQRT);
하하하하 지각 거칠기 값을 그냥 거칠기값으로 변환시킨다네요 
PerceptualRoughnessToRoughness 는 그냥 perceptualRoughness * perceptualRoughness 예요. 
근데 이건 전처리가 붙어 있죠 #ifndef BUILTIN_TARGET_API
이건 즉 빌트인 상태일때만이라는 뜻일까나? 그땐 감마 모드일테니 이렇게 하라는 것일지도..
여하간 이건 뭐 심각하게 중요한건 아니고.
HALF_MIN_SQRT 는 float 연산에서 최소 값 이상의 정밀도를 보장하기 위해 사용됩니다. 언더플로우 방지용 숫자라고 볼 수 있겠네요 

outBRDFData.roughness2

은 러프니스 2제곱이예요. 

outBRDFData.grazingTerm

그레이징텀. 이거 그 노말과 조명벡터가 거의 90도가 되는 면을 의미해요 
saturate(smoothness + reflectivity) 란 공식이 들어 있으니 smoothness  + 비메탈이면 0.04, 메탈이면 1 이네요. 

outBRDFData.normalizationTerm

노말라이제이션 텀. outBRDFData.roughness * half(4.0) + half(2.0); 
러프니스값에 4를 곱하고 2를 더했네요 PBR 기반에서 수치 보정용. 나중에 써요 

outBRDFData.roughness2MinusOne

이것도 마찬가지. outBRDFData.roughness2 - half(1.0) 이렇게 되어 있네요 러프니스 2제곱에서 1빼기. 
나중에 계산식이 간편하게 되기 위해서 미리 구해놓은거예요 

그 아래는 #if defined(_ALPHAPREMULTIPLY_ON) 이니까 알파 프리멀티플라이 할때 알파를 곱하는거라 뭐.. 안 중요하니까 패스. 

휴.. PBR 연산하느라 기본 설정하는게 너무 많아서 헷갈리는군요. 
여기까지가 다 전처리라는게 참 거시기함. 

자 다시 연산을 보죠. 

 
half3 LightingPhysicallyBased(BRDFData brdfData, BRDFData brdfDataClearCoat,
    half3 lightColor, half3 lightDirectionWS, float lightAttenuation,
    half3 normalWS, half3 viewDirectionWS,
    half clearCoatMask, bool specularHighlightsOff)
{
    half NdotL = saturate(dot(normalWS, lightDirectionWS));
    half3 radiance = lightColor * (lightAttenuation * NdotL);

    half3 brdf = brdfData.diffuse;
    brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);

    return brdf * radiance;
}

이게 직광 라이트였잖아요? 
클리어코트는 신경 안써도 되고.. 

    half NdotL = saturate(dot(normalWS, lightDirectionWS));
    half3 radiance = lightColor * (lightAttenuation * NdotL);

그럼 일단 radiance (발광)은 NdotL 에 라이트칼라죠 사실. 
그리고 

 half3 brdf = brdfData.diffuse;

니까 현재 brdf는 albedo * oneMinusReflectivity 예요. 이거 뭐 칼라.. 말하는거지 
금속일 경우는 0가 되겠고 비금속일 경우는 albedo * 0.96 이 되겠죠 
그 다음은 

brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);

인데 이거 스페큘러가 좀 중요하죠. 
brdfData.specular 는 일단 lerp(kDielectricSpec.rgb, albedo, metallic) 니까. 무슨 색이 나오냐 얘기예요. 금속일때와 비금속일때의 결과가 다르죠. 
그리고 여기

DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);

가 진짜 핵심 부분인 스페큘러예요. 

휴우.. 자 그럼 볼까..

Specular

// Computes the scalar specular term for Minimalist CookTorrance BRDF
// NOTE: needs to be multiplied with reflectance f0, i.e. specular color to complete
half DirectBRDFSpecular(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS)
{
    float3 lightDirectionWSFloat3 = float3(lightDirectionWS);
    float3 halfDir = SafeNormalize(lightDirectionWSFloat3 + float3(viewDirectionWS));

    float NoH = saturate(dot(float3(normalWS), halfDir));
    half LoH = half(saturate(dot(lightDirectionWSFloat3, halfDir)));

    // GGX Distribution multiplied by combined approximation of Visibility and Fresnel
    // BRDFspec = (D * V * F) / 4.0
    // D = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2
    // V * F = 1.0 / ( LoH^2 * (roughness + 0.5) )
    // See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course

    // Final BRDFspec = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 * (LoH^2 * (roughness + 0.5) * 4.0)
    // We further optimize a few light invariant terms
    // brdfData.normalizationTerm = (roughness + 0.5) * 4.0 rewritten as roughness * 4.0 + 2.0 to a fit a MAD.
    float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f;

    half LoH2 = LoH * LoH;
    half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm);

    // On platforms where half actually means something, the denominator has a risk of overflow
    // clamp below was added specifically to "fix" that, but dx compiler (we convert bytecode to metal/gles)
    // sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...))
#if REAL_IS_HALF
    specularTerm = specularTerm - HALF_MIN;
    // Update: Conservative bump from 100.0 to 1000.0 to better match the full float specular look.
    // Roughly 65504.0 / 32*2 == 1023.5,
    // or HALF_MAX / ((mobile) MAX_VISIBLE_LIGHTS * 2),
    // to reserve half of the per light range for specular and half for diffuse + indirect + emissive.
    specularTerm = clamp(specularTerm, 0.0, 1000.0); // Prevent FP16 overflow on mobiles
#endif

    return specularTerm;
}

키엑.

보통 쿡 토렌스 공식을 쓰는데요. 유니티는 이걸 
See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course 
https://community.arm.com/events/1155

 

Moving Mobile Graphics - SIGGRAPH 2015 Course - Calendar - Arm Community - Arm Community

 

community.arm.com

이 식으로 모바일용 최적화를 해놨어요. 

쿡 토런스 공식을 그냥 써도 되겠죠. 여하간 이게 물리 기반에서 스페큘러를 구하는 공식 축약판이예요. 

즉 이렇게 되면 

마지막에 
return brdf * radiance;

radiance를 곱해주죠. 즉 NdotL*라이트칼라 를 곱해주고 끝이예요. 
보통 파이로 나눠서 광량을 표시하곤 하는데 유니티 라이트는 걍 1 이라는 비물리적 수치라서 그냥 둔 모양. 

 

이렇게 다이렉트 라이트가 완성되었구요. 
LightingPhysicallyBased 

이게 다이렉트 라이트였으니
이제 인다이렉트 라이트도 동일하게 해줘야죠 

inDirectLight

인다이렉트 라이트는 

half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
{
    BRDFData brdfData;
    // NOTE: can modify "surfaceData"...
    InitializeBRDFData(surfaceData, brdfData);

    // Clear-coat calculation...
    BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
    half4 shadowMask = CalculateShadowMask(inputData);
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
    Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);
    InitializeBRDFData
    // NOTE: We don't apply AO to the GI here because it's done in the lighting calculation below...
    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);

    LightingData lightingData = CreateLightingData(inputData, surfaceData);

    lightingData.giColor = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
                                              inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
                                              inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);

    {
        lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                                              mainLight,
                                                              inputData.normalWS, inputData.viewDirectionWS,
                                                              surfaceData.clearCoatMask, specularHighlightsOff);
    }

    return CalculateFinalColor(lightingData, surfaceData.alpha);
}

여기 안에 있는

lightingData.giColor = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
                                              inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
                                              inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);

이 부분이 인다이렉트 라이트입니다. 

뭐 얘도 동일하죠. 

좋아요 그럼 GlobalIllumination 을 가봐야겠지.. 
GlobalIllumination.hlsl 에 있는 녀석을 가져옵니다. 

half3 GlobalIllumination(BRDFData brdfData, BRDFData brdfDataClearCoat, float clearCoatMask,
    half3 bakedGI, half occlusion, float3 positionWS,
    half3 normalWS, half3 viewDirectionWS, float2 normalizedScreenSpaceUV)
{
    half3 reflectVector = reflect(-viewDirectionWS, normalWS);
    half NoV = saturate(dot(normalWS, viewDirectionWS));
    half fresnelTerm = Pow4(1.0 - NoV);

    half3 indirectDiffuse = bakedGI;
    half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfData.perceptualRoughness, 1.0h, normalizedScreenSpaceUV);

    half3 color = EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);

    if (IsOnlyAOLightingFeatureEnabled())
    {
        color = half3(1,1,1); // "Base white" for AO debug lighting mode
    }

#if defined(_CLEARCOAT) || defined(_CLEARCOATMAP)
    half3 coatIndirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfDataClearCoat.perceptualRoughness, 1.0h, normalizedScreenSpaceUV);
    // TODO: "grazing term" causes problems on full roughness
    half3 coatColor = EnvironmentBRDFClearCoat(brdfDataClearCoat, clearCoatMask, coatIndirectSpecular, fresnelTerm);

    // Blend with base layer using khronos glTF recommended way using NoV
    // Smooth surface & "ambiguous" lighting
    // NOTE: fresnelTerm (above) is pow4 instead of pow5, but should be ok as blend weight.
    half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * fresnelTerm;
    return (color * (1.0 - coatFresnel * clearCoatMask) + coatColor) * occlusion;
#else
    return color * occlusion;
#endif
}

역시 필요없는걸 날려보죠

half3 GlobalIllumination(BRDFData brdfData, BRDFData brdfDataClearCoat, float clearCoatMask,
    half3 bakedGI, half occlusion, float3 positionWS,
    half3 normalWS, half3 viewDirectionWS, float2 normalizedScreenSpaceUV)
{
    half3 reflectVector = reflect(-viewDirectionWS, normalWS);
    half NoV = saturate(dot(normalWS, viewDirectionWS));
    half fresnelTerm = Pow4(1.0 - NoV);

    half3 indirectDiffuse = bakedGI;
    half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfData.perceptualRoughness, 1.0h, normalizedScreenSpaceUV);

    half3 color = EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);
    return color * occlusion;
}

음 좀 단순해졌다.. 

자 이것도 디퓨즈랑 스페큘러로 나눠져 있겠죠?

half3 indirectDiffuse = bakedGI;

하핫 indirectDiffuse는 쉽네요. bakedGI 를 씁니다. 이건 라이트 프로브 + 라이트맵이라고 볼 수 있어요. 거기에 알베도가 곱해진... 
그래요 뭐 이미 구워진 놈들이죠 . 틀리지 않지

심플하네. 그냥 다 되어 있어요 디퓨즈는 끝났네?

이젠 스페큘러를 봅시다. 

half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfData.perceptualRoughness, 1.0h, normalizedScreenSpaceUV);

헤에 너는 좀 복잡하구나? 

half3 GlossyEnvironmentReflection(half3 reflectVector, half perceptualRoughness, half occlusion)
{
#if !defined(_ENVIRONMENTREFLECTIONS_OFF)
    half3 irradiance;
    half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness);
    half4 encodedIrradiance = half4(SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip));

    irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);

    return irradiance * occlusion;
#else

    return _GlossyEnvironmentColor.rgb * occlusion;
#endif // _ENVIRONMENTREFLECTIONS_OFF
}

역시 정리해보면 별 거 없어요 

half3 GlossyEnvironmentReflection(half3 reflectVector, half perceptualRoughness, half occlusion)
{
 
    half3 irradiance;
    half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness);
    half4 encodedIrradiance = half4(SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip));

    irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);

    return irradiance * occlusion;
}

댑따 쉽죠. 
일단 mip 레벨을 구해요. 리플렉션 프로브가 반사잖아요. 얘가 스페큘러 텍스쳐예요. 
근데 예를 스무스니스에 따라 반응하게 하려면 

// The *approximated* version of the non-linear remapping. It works by
// approximating the cone of the specular lobe, and then computing the MIP map level
// which (approximately) covers the footprint of the lobe with a single texel.
// Improves the perceptual roughness distribution.
real PerceptualRoughnessToMipmapLevel(real perceptualRoughness, uint maxMipLevel)
{
    perceptualRoughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);

    return perceptualRoughness * maxMipLevel;
}

이걸로 밉 레벨이 적당히 반응하게 만들어요 
그리고 

half4 encodedIrradiance = half4(SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip));

이걸로 그냥 큐브맵 받아와서 밉 레벨 적용하는거예요. 유니티 리플렉션 프로브요

irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);

그리고 다음줄에서 HDR로 들어오는 이녀석을 디코드 시키죠. 
여기에 오클루젼 곱해주면 끝. 
 return irradiance * occlusion;

와 설명할 것도 없네?

자 다시. GlobalIllumination 함수에서 

    half3 indirectDiffuse = bakedGI;
    half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdfData.perceptualRoughness, 1.0h, normalizedScreenSpaceUV);

    half3 color = EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);

이 부분에서 위에 두 개가 구해졌으므로 (라이트 프로브와 리플렉션 프로브) 
마지막에 EnvironmentBRDF 함수에서 금속 비금속 어쩌구 해주면 될듯. 

half3 EnvironmentBRDF(BRDFData brdfData, half3 indirectDiffuse, half3 indirectSpecular, half fresnelTerm)
{
    half3 c = indirectDiffuse * brdfData.diffuse;
    c += indirectSpecular * EnvironmentBRDFSpecular(brdfData, fresnelTerm);
    return c;
}

역시 텍스쳐 곱해주는 곳으로 연결되는군요. 

indirectDiffuse 는 brdfData.diffuse 랑 곱해줘서 금속 비금속 텍스쳐 작동하게 만들죠 
indirectSpecular 는 EnvironmentBRDFSpecular로 가는데, 

half3 EnvironmentBRDFSpecular(BRDFData brdfData, half fresnelTerm)
{
    float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0);
    return half3(surfaceReduction * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm));
}

이거예요. 이 공식으로 스페큘러쪽 처리를 해주고 더해주면 GI도 끝. 

 

 

 

 

 

 

 

 

반응형

댓글