본문 바로가기
Shader

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

by 대마왕J 2024. 10. 2.

맨날 찾아볼 때마다 까먹은데다가 

설명해 놓은 PPT도 분실해 버리는 바람에 
여기다 정리해 봅니다. 쳇

일단 유니티 Lit 셰이더의 PBR 연산은 LitForwardPass 에서 시작하고 , 거기 안에서 조명 연산 부분은
half4 color = UniversalFragmentPBR(inputData, surfaceData); 이게 사실 전부인데, 

UniversalFragmentPBR 은 Lighting.hlsl에 정의되어 있고 여기서 
UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
라는 함수로 정의되어 있습니다. 뭐야 실체는 다 밖에 있는거네.

그럼 이제 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData) 함수를 봐야겠죠 

PBR 함수 정리 

////////////////////////////////////////////////////////////////////////////////
/// PBR lighting...
////////////////////////////////////////////////////////////////////////////////
half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData)
{
    #if defined(_SPECULARHIGHLIGHTS_OFF)
    bool specularHighlightsOff = true;
    #else
    bool specularHighlightsOff = false;
    #endif
    BRDFData brdfData;

    // NOTE: can modify "surfaceData"...
    InitializeBRDFData(surfaceData, brdfData);

    #if defined(DEBUG_DISPLAY)
    half4 debugColor;

    if (CanDebugOverrideOutputColor(inputData, surfaceData, brdfData, debugColor))
    {
        return debugColor;
    }
    #endif

    // Clear-coat calculation...
    BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
    half4 shadowMask = CalculateShadowMask(inputData);
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
    uint meshRenderingLayers = GetMeshRenderingLayer();
    Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);

    // 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);
#ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
#endif
    {
        lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                                              mainLight,
                                                              inputData.normalWS, inputData.viewDirectionWS,
                                                              surfaceData.clearCoatMask, specularHighlightsOff);
    }

    #if defined(_ADDITIONAL_LIGHTS)
    uint pixelLightCount = GetAdditionalLightsCount();

    #if USE_FORWARD_PLUS
    [loop] for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
    {
        FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK

        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);

#ifdef _LIGHT_LAYERS
        if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    }
    #endif

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);

#ifdef _LIGHT_LAYERS
        if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    LIGHT_LOOP_END
    #endif

    #if defined(_ADDITIONAL_LIGHTS_VERTEX)
    lightingData.vertexLightingColor += inputData.vertexLighting * brdfData.diffuse;
    #endif

#if REAL_IS_HALF
    // Clamp any half.inf+ to HALF_MAX
    return min(CalculateFinalColor(lightingData, surfaceData.alpha), HALF_MAX);
#else
    return CalculateFinalColor(lightingData, surfaceData.alpha);
#endif
}

꽤 깁니다. 이거 뭐 왜 긴지는 뻔한데.. 
이거저거요거 다 들어 있어서 그래요 
게다가 각종 디파인이 들어 있어서 그런데요. 저놈의 디파인 땜에 우버셰이더는 무거워지는거지.. 
게다가 공부할때 힘들게 만든 요인도 저 녀석입니다. 

자 그럼.. 위의 디파인은 다음과 같습니다. 

#if defined(_SPECULARHIGHLIGHTS_OFF)
스페큘러 연산할때 조명 하이라이트 연산은 날리느냐 가져가느냐예요 
#if defined(DEBUG_DISPLAY)
디버그 디스플레이 모드일때 렌더링 되는 데이터예요 
#ifdef _LIGHT_LAYERS
라이트 레이어 옵션 제어하는데예요. 여기에 해당하는 라이트 레이어만 렌더링하죠 
#if defined(_ADDITIONAL_LIGHTS)
Additinal Light 계산하는 부분이예요. 메인 디렉셔널 하나 빼고는 전부 Additinal Lihgt죠 
#if USE_FORWARD_PLUS
Forward Plus 렌더링 모드냐 물어보는 거예요. 포워드+ 는 포워드를 개량해서 라이트를 더 많이 쓸 수 있게 만들죠 
그러면 Additinal Light 사용할 수 있는 양이 늘어나니까 Additinal Light 안에 옵션이 있어요 

#if defined(_ADDITIONAL_LIGHTS_VERTEX)
버텍스 라이트 쓸 때 도는 모드예요. URP 에서는 버텍스 라이트를 쓸 일이 많이 줄었지만, 옵션을 낮추면 Additinal Light가 버텍스 라이트로 돌아요 그 때 씀. 
#if REAL_IS_HALF
유니티에서는 float 이나 half , fixed 말고 real 이라는 형식을 하나 만들었는데요, 이건 뭐 별건 아니고 플렛폼과 상황에 따라 float  이나 half 를 동적으로 와리가리 할 수 있게 하는 거예요 . 
즉 지금은 real 이 half 일때 정밀도 같은 이유 때문에 계산을 추가해 주는것. 

휴 각종 디파인이 있군요. 
저 디파인은 사실 보는데 걸리적대니까 다 삭제해 버리면 메인 라이트 연산 하나만 남겠죠?

그래서 저 디파인을 다 삭제해 보면, 깔끔해집니다.

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);

    // 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);
}

상당히 줄었네요. 
이제 보면 크게 함수들이 보이는데요 
이 함수들이 하는 일은 다음과 같습니다. 

InitializeBRDFData(surfaceData, brdfData);
surfaceData와 brdfData를 초기화 시켜줍니다. surfaceData 는 주로 텍스쳐나 입력값들이 들어 있고요

struct SurfaceData
{
    half3 albedo;
    half3 specular;
    half  metallic;
    half  smoothness;
    half3 normalTS;
    half3 emission;
    half  occlusion;
    half  alpha;
    half  clearCoatMask;
    half  clearCoatSmoothness;
};

brdfData는 PBR 조명 연산을 위한 각종 값들이 들어 있습니다. 
둘은 가끔 같은 값을 가지고 있는 경우도 많아요. 

struct BRDFData
{
    half3 albedo;
    half3 diffuse;
    half3 specular;
    half reflectivity;
    half perceptualRoughness;
    half roughness;
    half roughness2;
    half grazingTerm;

    // We save some light invariant BRDF terms so we don't have to recompute
    // them in the light loop. Take a look at DirectBRDF function for detailed explaination.
    half normalizationTerm;     // roughness * 4.0 + 2.0
    half roughness2MinusOne;    // roughness^2 - 1.0
};


CreateClearCoatBRDFData(surfaceData, brdfData);
클리어코드용 값 초기화 함수죠. 

CalculateShadowMask(inputData);
셰도우 마스크 연산을 위한 함수입니다. 라이트맵에서 셰도우 마스크 방식과 연관있습니다. 

CreateAmbientOcclusionFactor(inputData, surfaceData);
SSAO 연산을 위한 함수입니다. SSAO 연산을 안할거면 별 필요 없죠 

GetMainLight(inputData, shadowMask, aoFactor);
조명의 여러 값들을 받아옵니다. 

 MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);
리얼타임 GI와 BakedGI를 합쳐줍니다. 라이트 프로브랑 라이트맵을 합쳐준다 뭐 그런 쪽이죠

CreateLightingData(inputData, surfaceData);
라이팅 데이터 구조체를 정리해 줍니다.

GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
                                              inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
                                              inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);
lightingData.giColor 를 만들어 주는 함수죠. PBR 에서 간접광 연산이 여기서 전부 일어난다고 볼 수 있습니다. 

LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                                              mainLight,
                                                              inputData.normalWS, inputData.viewDirectionWS,
                                                              surfaceData.clearCoatMask, specularHighlightsOff);
사실 이게 진짜입니다. 이게 진짜 PBR 조명연산을 처리하죠. 여기에서 나온 값들을 전부 lightingData 구조체로 보내서 나중에 합쳐 줍니다. CalculateFinalColor(lightingData, surfaceData.alpha); 함수를 통해서요

 

CalculateFinalColor 함수는 어떻게 되어 있는가?

우선 공식을 보려면 마지막에 전부 더해주는 부분을 체크해 보는게 좋죠 
CalculateFinalColor 함수는 일단 아래걸 사용하네요 

half4 CalculateFinalColor(LightingData lightingData, half alpha)
{
    half3 finalColor = CalculateLightingColor(lightingData, 1);

    return half4(finalColor, alpha);
}

그렇다면 이번엔 

CalculateLightingColor(lightingData, 1)

함수로 들어간다는게 보이죠? 역시 또 고구마줄기처럼 저길 파고 들어가야죠 
그럼 아래와 같은 고구마가 보입니다. 

half3 CalculateLightingColor(LightingData lightingData, half3 albedo)
{
    half3 lightingColor = 0;

    if (IsOnlyAOLightingFeatureEnabled())
    {
        return lightingData.giColor; // Contains white + AO
    }

    if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_GLOBAL_ILLUMINATION))
    {
        lightingColor += lightingData.giColor;
    }

    if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_MAIN_LIGHT))
    {
        lightingColor += lightingData.mainLightColor;
    }

    if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_ADDITIONAL_LIGHTS))
    {
        lightingColor += lightingData.additionalLightsColor;
    }

    if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_VERTEX_LIGHTING))
    {
        lightingColor += lightingData.vertexLightingColor;
    }

    lightingColor *= albedo;

    if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_EMISSION))
    {
        lightingColor += lightingData.emissionColor;
    }

    return lightingColor;
}

여기가 뭐냐면 .. 

여기에 사용되는 놈이예요 

여기에 보면 각종 연산값들을 검증해 볼 수 있잖아요 

이런것들을 위해 출력해주는 부분입니다. 그래서 보기 힘듬. 

차근차근히 보죠. 
우선 giColor 를 받아오고 
거기에 mainLightColor를 더해줍니다. 
그리고  additionalLightsColor 를 더해주고요 
다시 거기에 vertexLightingColor 를 더해줍니다. 
그렇게 더해준 라이트칼라 값에 albedo를 곱해주네요. 
그리고 필요하면 emissionColor 를 더해주는 방식으로 되어 있습니다. 흠 
입력받는 albedo 가 1이라서 albedo를 나중에 곱해주는 방식이 이채롭네요 

자 뭐 어려운건 없어요. 이제 어떻게 최종연산되는지 알았으니 역으로 추적해 가면 끝

반응형

댓글