ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] Shader ResourceBinding [3/3] - UniformBuffer와 리소스 바인딩
    UE4 & UE5/Rendering 2021. 9. 13. 07:00

    [UE5] Shader ResourceBinding [3/3] - UniformBuffer와 리소스 바인딩

    최초 작성 : 2021-09-13
    마지막 수정 : 2021-09-13
    최재호

     

    목차

    1. 환경
    2. 목표
    3. 내용

      3.1. UE5의 UniformBuffer 의 정의

      3.2. UniformBuffer 의 예제

        3.2.1. UniformBuffer 매크로의 확장

      3.3. 컴파일된 쉐이더의 리플렉션 데이터를 FShader에 저장

      3.4. 실제 리소스들의 쉐이더 바인딩

      3.5. 렌더링을 위해서 바인딩한 리소스들을 Graphics API 전달

      3.6. 결론

    4. 레퍼런스

     

    1. 환경

    Unreal Engine 5 (ue5-main branch acc8c5f399ca01f6f549108be1fb75381fecbca8)

    UE4.26.2 코드와 거의 유사하므로 UE4에 익숙하시면 보시는데 문제가 없을 것 같습니다.
    일부 코드는 줄 바꿈 되거나 접은 상태로 중요한 코드를 위주로 설명하니 보실 때 참고해주세요.
    또한 코드를 직접 붙여 넣어서 진행했기 때문에 작아서 잘 보이지 않는 코드는 클릭하여 봐주세요.

    개인적으로 분석한 내용이라 틀린 점이 있을 수 있습니다. 그런 부분은 알려주시면 감사하겠습니다.

    필요한 사전 지식
    Graphic API 기본 이해
    Shader 기본 이해

     

    2. 목표

    1. UniformBuffer를 어떻게 정의하는지와 그 리플렉션 데이터인 FShaderParametersMetadata 생성과정을 확인해봅시다.

    2. 컴파일된 쉐이더가 실제로 사용될 때 리소스의 바인딩이 어떻게 일어나는지 확인해 봅시다.

    첫 번째 글 "[UE5] Shader [1/3] - 주요 클래스 파악" 에서는 UE5 Shader 컴파일되는 과정에 필요한 기본 클래스들과 쉐이더가 컴파일되면 어떻게 캐싱되는지를 알아봤습니다.

    두 번째 글 "[UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정" 에서는 UE5 Shader가 어떻게 컴파일되고, 인스턴스화 되어 사용되는지 알아봤습니다.

     

    3. 내용

    3.1. UE5의 UniformBuffer 의 정의

    UniformBuffer는 소스코드와 쉐이더 코드 양쪽에 각각 정의하고, 런타임에서 서로 바인딩 과정이 필요합니다. 언리얼에서는 소스와 쉐이더 코드에 각각 정의하는 것을 피하기 위해서 소스코드에서 매크로를 통해 UniformBuffer를 정의합니다. 그리고 이 UniformBuffer로부터 쉐이더 코드를 생성하여 쉐이더 컴파일 시에 참조합니다.

    UniformBuffer는 또 다른 UniformBuffer를 포함할 수도 있고 텍스쳐나 일반 변수 타입 등을 멤버로 가질 수 있습니다. 

     

    3.2. UniformBuffer 의 예제

    UniformBuffer는 매크로로 간단히 선언될 수 있습니다. FLightShaftPixelShaderParameters 라는 UniformBuffer 매크로는 또 다른 UniformBuffer인 FViewUniformShaderParameters를 View라는 변수로 가지고 있고, 4개의 FVector4와 FVector2D 변수 하나, float 변수 하나를 가집니다. 이 UniformBuffer의 멤버 변수들은 SHADER_PARAMETER 매크로로 정의합니다. 매크로의 첫 번째 파라메터는 타입, 두번재 파라메터는 멤버 변수의 이름입니다. 계속해서 이 매크로가 어떻게 확장될지 알아봅시다.

    코드블럭1.
    
    BEGIN_SHADER_PARAMETER_STRUCT(FLightShaftPixelShaderParameters, )
    	SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
    	SHADER_PARAMETER(FVector4, UVMinMax)
    	SHADER_PARAMETER(FVector4, AspectRatioAndInvAspectRatio)
    	SHADER_PARAMETER(FVector4, LightShaftParameters)
    	SHADER_PARAMETER(FVector4, BloomTintAndThreshold)
    	SHADER_PARAMETER(FVector2D, TextureSpaceBlurOrigin)
    	SHADER_PARAMETER(float, BloomMaxBrightness)
    END_SHADER_PARAMETER_STRUCT()

     

    3.2.1. UniformBuffer 매크로의 확장

    매크로를 풀어보면 UniformBuffer는 FLightShaftPixelShaderParameters 클래스로 생성됩니다. 이 클래스에는 실제 멤버 변수를 담습니다. 그리고 이 클래스에 대한 리플렉션 정보는 FShaderParametersMetadata 클래스로 캡슐화됩니다. 리플렉션 정보는 FLightShaftPixelShaderParameters::FTypeInfo::GetStructMetadata() 함수로 얻어올 수 있습니다.

    그림1. UniformBuffer의 리플렉션 정보인 FShaderParametersMetadata

     

    아래 코드블록2는 FLightShaftPixelShaderParameters 에 대한 매크로를 풀어본 것입니다. 그리고 주석으로 어떤 식으로 이 매크로가 확장되는지 설명하였습니다. 한번 정도 매크로를 분석해보는 것은 추후 코드를 보는데 도움이 될 것이라 생각합니다. 코드블럭2를 보면 FLightShaftPixelShaderParameters 는 자신의 멤버 변수를 선언하고 그 멤버 변수의 타입 정보를 생성하기 위해서 static TArray<FShaderParametersMetadata::FMember> zzGetMembers() 함수를 사용합니다.

     

    UniformBuffer를 정의하고 나면 이 UniformBuffer가 또 다른 UniformBuffer의 멤버가 될 수 있습니다. 이 말은 이 UniformBuffer의 리플렉션 정보를 만들 수 있어야 된다는 의미입니다. 이런 타입 정보는 아래 그림2를 사용하여 얻어올 수 있습니다. 그리고 기본 데이터 타입 또한 TShaderParameterTypeInfo로 미리 정의되어있는 것을 볼 수 있습니다.

    그림2. TShaderPrameterTypeInfo를 사용하여 타입의 정보를 얻어옴

    코드블럭2.
    
    // BEGIN_SHADER_PARAMETER_STRUCT(FLightShaftPixelShaderParameters, )
    __declspec(align(16)) class FLightShaftPixelShaderParameters
    {
    public:
      FLightShaftPixelShaderParameters()
      {
      }
    
      // 이 UniformStruct의 TypeInfo 명세
      struct FTypeInfo
      {
        static constexpr int32 NumRows = 1;
        static constexpr int32 NumColumns = 1;
        static constexpr int32 NumElements = 0;
        static constexpr int32 Alignment = 16;
        static constexpr bool bIsStoredInConstantBuffer = true;
        static constexpr const ANSICHAR* const FileName = "LightShaftRendering.cpp";
        static constexpr int32 FileLine = 132;
        using TAlignedType = FLightShaftPixelShaderParameters;
    
        // 이 UniformBuffer의 리플렉션 데이터를 반환하는 함수 - FShaderParametersMetadata는 UniformBuffer의 리플렉션 데이터
        static inline const FShaderParametersMetadata* GetStructMetadata()
        {
          static FShaderParametersMetadata StaticStructMetadata(
            FShaderParametersMetadata::EUseCase::ShaderParameterStruct, EUniformBufferBindingFlags::Shader,
            L"FLightShaftPixelShaderParameters", L"FLightShaftPixelShaderParameters", nullptr, nullptr,
            FTypeInfo::FileName, FTypeInfo::FileLine, sizeof(FLightShaftPixelShaderParameters),
            FLightShaftPixelShaderParameters::zzGetMembers());  // zzGetMemebers로 현재 UniformStruct의 멤버들의 리플렉션 정보를 모음
          return &StaticStructMetadata;
        }
      };
    
      static FUniformBufferRHIRef CreateUniformBuffer(const FLightShaftPixelShaderParameters& InContents,
                                                      EUniformBufferUsage InUsage) { return nullptr; }
    
    private:
      typedef FLightShaftPixelShaderParameters zzTThisStruct;
    
        // 첫번째 멤버의 zzAppendMemberGetPrev를 선언하기 위해서 만든 구조체.
        // * 멤버다가 이 struct를 zzAppendMemberGetPrev의 파라메터로 사용하면 함수 시그니쳐들간 달라진다. 그래서 서로 다른 함수가 생성되게 해줌.
      struct zzFirstMemberId
      {
        enum { HasDeclaredResource = 0 };
      };
    
      typedef void* zzFuncPtr;
      typedef zzFuncPtr (*zzMemberFunc)(zzFirstMemberId, TArray<FShaderParametersMetadata::FMember>*);
    
        // 첫번재 멤버의 Type 정보를 생성해주는 함수
      static zzFuncPtr zzAppendMemberGetPrev(zzFirstMemberId, TArray<FShaderParametersMetadata::FMember>*)
      {
        return nullptr;
      }
    
        // 첫번째 멤버는 View이며, typedef (struct zzFirstMemberId) (zzMemberId + 멤버이름) 형태로 만들어줌.
      typedef zzFirstMemberId
      // SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
      zzMemberIdView;
    public:
        // 첫번째 멤버 Aligned 된 타입으로 선언
      TShaderParameterTypeInfo<TUniformBufferBinding<FViewUniformShaderParameters>>::TAlignedType View;
      static_assert(UBMT_REFERENCED_STRUCT != UBMT_INVALID, "Invalid type "
        "TUniformBufferBinding<FViewUniformShaderParameters>" " of member " "View" ".");
    private:
        // 첫번째 멤버의 zzAppendMemberGetPrev를 선언하기 위해서 만든 구조체
      struct zzNextMemberIdView
      {
        enum
        {
          HasDeclaredResource = zzMemberIdView::HasDeclaredResource || !TShaderParameterTypeInfo<TUniformBufferBinding
            <FViewUniformShaderParameters>>::bIsStoredInConstantBuffer              // Constant Buffer에 안들어가면 HasDeclaredResource 는 0
        };
      };
    
        // 첫번재 멤버의 멤버타입 정보를 저장. 그리고 이전 멤버의 zzAppendMemberGetPrev(zzFirstMemberId 사용)를 리턴함.
        // zzAppendMemberGetPrev 함수의 특징
        //  - 현재 멤버(View)가 아닌 다음 Member의 struct (zzNextMemberId + Type명)를 파라메터로 넘김
        //  - 그려면 Memebers에 현재 멤버(View)의 Type정보를 담아줌.
        //  - 리턴값은 현재 바로 전에 정의 된 멤버타입 정보를 얻어올 수 있는 함수 포인터 리턴. 즉, zzAppendMemberGetPrev(zzFirstMemberId ...)
      static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdView, TArray<FShaderParametersMetadata::FMember>* Members)
      {
        static_assert(TShaderParameterTypeInfo<TUniformBufferBinding<
          FViewUniformShaderParameters>>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
          decltype(L"FViewUniformShaderParameters"), TCHAR>::Value, "No shader type for " "View" ".");
        static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->View))) & (
            TShaderParameterTypeInfo<TUniformBufferBinding<FViewUniformShaderParameters>>::Alignment - 1)) == 0,
          "Misaligned uniform buffer struct member " "View" ".");
        Members->Add(FShaderParametersMetadata::FMember(L"View", L"FViewUniformShaderParameters", 183,
                                                        ((::size_t)&reinterpret_cast<char const volatile&>((((
                                                          zzTThisStruct*)0)->View))),
                                                        EUniformBufferBaseType(UBMT_REFERENCED_STRUCT),
                                                        EShaderPrecisionModifier::Float,
                                                        TShaderParameterTypeInfo<TUniformBufferBinding<
                                                          FViewUniformShaderParameters>>::NumRows,
                                                        TShaderParameterTypeInfo<TUniformBufferBinding<
                                                          FViewUniformShaderParameters>>::NumColumns,
                                                        TShaderParameterTypeInfo<TUniformBufferBinding<
                                                          FViewUniformShaderParameters>>::NumElements,
                                                        TShaderParameterTypeInfo<TUniformBufferBinding<
                                                          FViewUniformShaderParameters>>::GetStructMetadata()));
        // 이전 멤버인 (zzMemberId + View)의 함수 포인터의 선언
            zzFuncPtr (*PrevFunc)(zzMemberIdView, TArray<FShaderParametersMetadata::FMember>*);
        PrevFunc = zzAppendMemberGetPrev;   // 이전 멤버인 (zzMemberId + View)의 zzAppendMemberGetPrev 의 함수를 함수포인터에 넣고 반환
                                                // *동일한 멤버함수 중 시그니쳐가 같은 함수자 자동으로 들어감
        return (zzFuncPtr)PrevFunc;         // 이 함수 포인터는 결국 void* 반환 시킴
      }
    
        // View의 다음 멤버는 UIMinMax 이라 (zzMemberId + UVMinMax)로 선언함.
      typedef zzNextMemberIdView
      // SHADER_PARAMETER(FVector4, UVMinMax)
        zzMemberIdUVMinMax;
    public:
        // 두번째 멤버 UVMinMax의 Aligned된 타입으로 선언
        TShaderParameterTypeInfo<FVector4>::TAlignedType UVMinMax;
        static_assert(TShaderParameterTypeInfo<FVector4>::BaseType != UBMT_INVALID, "Invalid type " "FVector4" " of member "
            "UVMinMax" ".");
    private:
        // 두번째 멤버의 zzAppendMemberGetPrev를 위한 struct 선언. 그리고 ConstantBuffer에 들어가는지 여부에 따른 HasDeclaredResource 값 결정
        struct zzNextMemberIdUVMinMax
        {
            enum
            {
                HasDeclaredResource = zzMemberIdUVMinMax::HasDeclaredResource || !TShaderParameterTypeInfo<
                    FVector4>::bIsStoredInConstantBuffer
            };
        };
    
        // 두번째 멤버의 타입정보를 Memebers에 담아주고, 이 멤버의 이전에 있는 타입정보를 얻어올 수 있는 함수의 포인터 리턴.
        static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdUVMinMax, TArray<FShaderParametersMetadata::FMember>* Members)
        {
            static_assert(TShaderParameterTypeInfo<FVector4>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
                decltype(L""), TCHAR>::Value, "No shader type for " "UVMinMax" ".");
            static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->UVMinMax))) & (
                    TShaderParameterTypeInfo<FVector4>::Alignment - 1)) == 0, "Misaligned uniform buffer struct member "
                "UVMinMax"
                ".");
            Members->Add(FShaderParametersMetadata::FMember(L"UVMinMax", L"", 134,
                                                            ((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct
                                                                *)0)->UVMinMax))),
                                                            EUniformBufferBaseType(
                                                                TShaderParameterTypeInfo<FVector4>::BaseType),
                                                            EShaderPrecisionModifier::Float,
                                                            TShaderParameterTypeInfo<FVector4>::NumRows,
                                                            TShaderParameterTypeInfo<FVector4>::NumColumns,
                                                            TShaderParameterTypeInfo<FVector4>::NumElements,
                                                            TShaderParameterTypeInfo<FVector4>::GetStructMetadata()));
            zzFuncPtr (*PrevFunc)(zzMemberIdUVMinMax, TArray<FShaderParametersMetadata::FMember>*);
            PrevFunc = zzAppendMemberGetPrev;
            return (zzFuncPtr)PrevFunc;
        }
        // UVMinMax의 다음 멤버는 (zzMemberId + AspectRatioAndInvAspecRatio)로 선언
      typedef zzNextMemberIdUVMinMax
      // SHADER_PARAMETER(FVector4, AspectRatioAndInvAspectRatio)
      zzMemberIdAspectRatioAndInvAspectRatio;
    public:
        // 이제 여기서 부터 나머지 멤버들은 위와 같은 형태로 계속해서 반복됨.
      TShaderParameterTypeInfo<FVector4>::TAlignedType AspectRatioAndInvAspectRatio;
      static_assert(TShaderParameterTypeInfo<FVector4>::BaseType != UBMT_INVALID, "Invalid type " "FVector4" " of member "
        "AspectRatioAndInvAspectRatio" ".");
    private:
      struct zzNextMemberIdAspectRatioAndInvAspectRatio
      {
        enum
        {
          HasDeclaredResource = zzMemberIdAspectRatioAndInvAspectRatio::HasDeclaredResource || !
          TShaderParameterTypeInfo<FVector4>::bIsStoredInConstantBuffer
        };
      };
    
      static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdAspectRatioAndInvAspectRatio,
                                             TArray<FShaderParametersMetadata::FMember>* Members)
      {
        static_assert(TShaderParameterTypeInfo<FVector4>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
          decltype(L""), TCHAR>::Value, "No shader type for " "AspectRatioAndInvAspectRatio" ".");
        static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->
            AspectRatioAndInvAspectRatio))) & (TShaderParameterTypeInfo<FVector4>::Alignment - 1)) == 0,
          "Misaligned uniform buffer struct member " "AspectRatioAndInvAspectRatio" ".");
        Members->Add(FShaderParametersMetadata::FMember(L"AspectRatioAndInvAspectRatio", L"", 230,
                                                        ((::size_t)&reinterpret_cast<char const volatile&>((((
                                                          zzTThisStruct*)0)->AspectRatioAndInvAspectRatio))),
                                                        EUniformBufferBaseType(
                                                          TShaderParameterTypeInfo<FVector4>::BaseType),
                                                        EShaderPrecisionModifier::Float,
                                                        TShaderParameterTypeInfo<FVector4>::NumRows,
                                                        TShaderParameterTypeInfo<FVector4>::NumColumns,
                                                        TShaderParameterTypeInfo<FVector4>::NumElements,
                                                        TShaderParameterTypeInfo<FVector4>::GetStructMetadata()));
        zzFuncPtr (*PrevFunc)(zzMemberIdAspectRatioAndInvAspectRatio, TArray<FShaderParametersMetadata::FMember>*);
        PrevFunc = zzAppendMemberGetPrev;
        return (zzFuncPtr)PrevFunc;
      }
    
      typedef zzNextMemberIdAspectRatioAndInvAspectRatio
      // SHADER_PARAMETER(FVector4, LightShaftParameters)
      zzMemberIdLightShaftParameters;
    public:
      TShaderParameterTypeInfo<FVector4>::TAlignedType LightShaftParameters;
      static_assert(TShaderParameterTypeInfo<FVector4>::BaseType != UBMT_INVALID, "Invalid type " "FVector4" " of member "
        "LightShaftParameters" ".");
    private:
      struct zzNextMemberIdLightShaftParameters
      {
        enum
        {
          HasDeclaredResource = zzMemberIdLightShaftParameters::HasDeclaredResource || !TShaderParameterTypeInfo<
            FVector4>::bIsStoredInConstantBuffer
        };
      };
    
      static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdLightShaftParameters,
                                             TArray<FShaderParametersMetadata::FMember>* Members)
      {
        static_assert(TShaderParameterTypeInfo<FVector4>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
          decltype(L""), TCHAR>::Value, "No shader type for " "LightShaftParameters" ".");
        static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->LightShaftParameters)))
            & (TShaderParameterTypeInfo<FVector4>::Alignment - 1)) == 0, "Misaligned uniform buffer struct member "
          "LightShaftParameters" ".");
        Members->Add(FShaderParametersMetadata::FMember(L"LightShaftParameters", L"", 270,
                                                        ((::size_t)&reinterpret_cast<char const volatile&>((((
                                                          zzTThisStruct*)0)->LightShaftParameters))),
                                                        EUniformBufferBaseType(
                                                          TShaderParameterTypeInfo<FVector4>::BaseType),
                                                        EShaderPrecisionModifier::Float,
                                                        TShaderParameterTypeInfo<FVector4>::NumRows,
                                                        TShaderParameterTypeInfo<FVector4>::NumColumns,
                                                        TShaderParameterTypeInfo<FVector4>::NumElements,
                                                        TShaderParameterTypeInfo<FVector4>::GetStructMetadata()));
        zzFuncPtr (*PrevFunc)(zzMemberIdLightShaftParameters, TArray<FShaderParametersMetadata::FMember>*);
        PrevFunc = zzAppendMemberGetPrev;
        return (zzFuncPtr)PrevFunc;
      }
    
      typedef zzNextMemberIdLightShaftParameters
      // SHADER_PARAMETER(FVector4, BloomTintAndThreshold)
      zzMemberIdBloomTintAndThreshold;
    public:
      TShaderParameterTypeInfo<FVector4>::TAlignedType BloomTintAndThreshold;
      static_assert(TShaderParameterTypeInfo<FVector4>::BaseType != UBMT_INVALID, "Invalid type " "FVector4" " of member "
        "BloomTintAndThreshold" ".");
    private:
      struct zzNextMemberIdBloomTintAndThreshold
      {
        enum
        {
          HasDeclaredResource = zzMemberIdBloomTintAndThreshold::HasDeclaredResource || !TShaderParameterTypeInfo<
            FVector4>::bIsStoredInConstantBuffer
        };
      };
    
      static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdBloomTintAndThreshold,
                                             TArray<FShaderParametersMetadata::FMember>* Members)
      {
        static_assert(TShaderParameterTypeInfo<FVector4>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
          decltype(L""), TCHAR>::Value, "No shader type for " "BloomTintAndThreshold" ".");
        static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->
            BloomTintAndThreshold))) & (TShaderParameterTypeInfo<FVector4>::Alignment - 1)) == 0,
          "Misaligned uniform buffer struct member " "BloomTintAndThreshold" ".");
        Members->Add(FShaderParametersMetadata::FMember(L"BloomTintAndThreshold", L"", 310,
                                                        ((::size_t)&reinterpret_cast<char const volatile&>((((
                                                          zzTThisStruct*)0)->BloomTintAndThreshold))),
                                                        EUniformBufferBaseType(
                                                          TShaderParameterTypeInfo<FVector4>::BaseType),
                                                        EShaderPrecisionModifier::Float,
                                                        TShaderParameterTypeInfo<FVector4>::NumRows,
                                                        TShaderParameterTypeInfo<FVector4>::NumColumns,
                                                        TShaderParameterTypeInfo<FVector4>::NumElements,
                                                        TShaderParameterTypeInfo<FVector4>::GetStructMetadata()));
        zzFuncPtr (*PrevFunc)(zzMemberIdBloomTintAndThreshold, TArray<FShaderParametersMetadata::FMember>*);
        PrevFunc = zzAppendMemberGetPrev;
        return (zzFuncPtr)PrevFunc;
      }
    
      typedef zzNextMemberIdBloomTintAndThreshold
      // SHADER_PARAMETER(FVector2D, TextureSpaceBlurOrigin)
      zzMemberIdTextureSpaceBlurOrigin;
    public:
      TShaderParameterTypeInfo<FVector2D>::TAlignedType TextureSpaceBlurOrigin;
      static_assert(TShaderParameterTypeInfo<FVector2D>::BaseType != UBMT_INVALID, "Invalid type " "FVector2D"
        " of member " "TextureSpaceBlurOrigin" ".");
    private:
      struct zzNextMemberIdTextureSpaceBlurOrigin
      {
        enum
        {
          HasDeclaredResource = zzMemberIdTextureSpaceBlurOrigin::HasDeclaredResource || !TShaderParameterTypeInfo<
            FVector2D>::bIsStoredInConstantBuffer
        };
      };
    
      static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdTextureSpaceBlurOrigin,
                                             TArray<FShaderParametersMetadata::FMember>* Members)
      {
        static_assert(TShaderParameterTypeInfo<FVector2D>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
          decltype(L""), TCHAR>::Value, "No shader type for " "TextureSpaceBlurOrigin" ".");
        static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->
            TextureSpaceBlurOrigin))) & (TShaderParameterTypeInfo<FVector2D>::Alignment - 1)) == 0,
          "Misaligned uniform buffer struct member " "TextureSpaceBlurOrigin" ".");
        Members->Add(FShaderParametersMetadata::FMember(L"TextureSpaceBlurOrigin", L"", 350,
                                                        ((::size_t)&reinterpret_cast<char const volatile&>((((
                                                          zzTThisStruct*)0)->TextureSpaceBlurOrigin))),
                                                        EUniformBufferBaseType(
                                                          TShaderParameterTypeInfo<FVector2D>::BaseType),
                                                        EShaderPrecisionModifier::Float,
                                                        TShaderParameterTypeInfo<FVector2D>::NumRows,
                                                        TShaderParameterTypeInfo<FVector2D>::NumColumns,
                                                        TShaderParameterTypeInfo<FVector2D>::NumElements,
                                                        TShaderParameterTypeInfo<FVector2D>::GetStructMetadata()));
        zzFuncPtr (*PrevFunc)(zzMemberIdTextureSpaceBlurOrigin, TArray<FShaderParametersMetadata::FMember>*);
        PrevFunc = zzAppendMemberGetPrev;
        return (zzFuncPtr)PrevFunc;
      }
    
      typedef zzNextMemberIdTextureSpaceBlurOrigin
      // SHADER_PARAMETER(float, BloomMaxBrightness)
      zzMemberIdBloomMaxBrightness;
    public:
      TShaderParameterTypeInfo<float>::TAlignedType BloomMaxBrightness;
      static_assert(TShaderParameterTypeInfo<float>::BaseType != UBMT_INVALID, "Invalid type " "float" " of member "
        "BloomMaxBrightness" ".");
    private:
      struct zzNextMemberIdBloomMaxBrightness
      {
        enum
        {
          HasDeclaredResource = zzMemberIdBloomMaxBrightness::HasDeclaredResource || !TShaderParameterTypeInfo<
            float>::bIsStoredInConstantBuffer
        };
      };
    
      static zzFuncPtr zzAppendMemberGetPrev(zzNextMemberIdBloomMaxBrightness,
                                             TArray<FShaderParametersMetadata::FMember>* Members)
      {
        static_assert(TShaderParameterTypeInfo<float>::bIsStoredInConstantBuffer || TIsArrayOrRefOfType<
          decltype(L""), TCHAR>::Value, "No shader type for " "BloomMaxBrightness" ".");
        static_assert( (((::size_t)&reinterpret_cast<char const volatile&>((((zzTThisStruct*)0)->BloomMaxBrightness))) &
            (TShaderParameterTypeInfo<float>::Alignment - 1)) == 0, "Misaligned uniform buffer struct member "
          "BloomMaxBrightness" ".");
        Members->Add(FShaderParametersMetadata::FMember(L"BloomMaxBrightness", L"", 390,
                                                        ((::size_t)&reinterpret_cast<char const volatile&>((((
                                                          zzTThisStruct*)0)->BloomMaxBrightness))),
                                                        EUniformBufferBaseType(
                                                          TShaderParameterTypeInfo<float>::BaseType),
                                                        EShaderPrecisionModifier::Float,
                                                        TShaderParameterTypeInfo<float>::NumRows,
                                                        TShaderParameterTypeInfo<float>::NumColumns,
                                                        TShaderParameterTypeInfo<float>::NumElements,
                                                        TShaderParameterTypeInfo<float>::GetStructMetadata()));
        zzFuncPtr (*PrevFunc)(zzMemberIdBloomMaxBrightness, TArray<FShaderParametersMetadata::FMember>*);
        PrevFunc = zzAppendMemberGetPrev;
        return (zzFuncPtr)PrevFunc;
      }
    
        // BloomMaxBrightness가 마지막 멤버이기 때문에 zzLastMemberId로 타입을 부를 수 있게 해줌.
      typedef zzNextMemberIdBloomMaxBrightness
    // END_SHADER_PARAMETER_STRUCT()
      zzLastMemberId;
    public:
        // 앞에서 선언한 zzAppendMemberGetPrev 마지막 멤버 부터 첫번째 멤버 변수 순서로 호출함.
        // 이 구조체의 모든 멤버의 타입정보를 얻어서 반환함.
      static TArray<FShaderParametersMetadata::FMember> zzGetMembers()
      {
        TArray<FShaderParametersMetadata::FMember> Members;
        zzFuncPtr (*LastFunc)(zzLastMemberId, TArray<FShaderParametersMetadata::FMember>*);     // zzAppendMemberGetPrev(zzNextMemberIdBloomMaxBrightness...) 부터 시작
        LastFunc = zzAppendMemberGetPrev;       // 타입에 맞는 zzAppendMemberGetPrev를 자동으로 맞춰주기 때문에 zzAppendMemberGetPrev(zzNextMemberIdBloomMaxBrightness..)가 LastFunc에 들어옴
        zzFuncPtr Ptr = (zzFuncPtr)LastFunc;
            // zzMemberFunc는 zzAppendMemberGetPrev들과 첫번째 파라메터인 (zzNextMemberId + 멤버변수명)를 제외하고 시그쳐가 동일함.
            // 어차피 첫번째 파라메터를 함수의 실행에 영향을 미치지 않으므로 강제로 zzMemberFunc로 형변환 시킬 수 있음.
            // zzMemberFunc로 각각의 멤버함수 zzAppendMemberGetPrev 호출을 일반화 함.
        do { Ptr = reinterpret_cast<zzMemberFunc>(Ptr)(zzFirstMemberId(), &Members); }
        while (Ptr);
        Algo::Reverse(Members);             // 마지막 멤버부터 첫번째 멤버 순으로 추가했기 때문에 역방향으로 컨테이너에 들어가 있음. 그래서 다시 뒤집어 준다.
        return Members;
      }
    };

     

    3.3. 컴파일된 쉐이더의 리플렉션 데이터를 FShader에 저장

    FShader가 생성되면 생성자로 이 쉐이더에 대한 리플렉션 정보가 넘어옵니다. 그리고 FShader는 이 리플렉션 정보를 적절히 가공하여 ParameterMapInfo(FShaderParameterMapInfo)에 저장합니다. FShaderParameterMapInfo는 각 리소스 별로 FShaderParameterInfo 배열을 가지는데, 바인딩 포인트가 되는 Index와 Size가 저장되어있습니다. 그림3를 봐주세요.

    그림3. 쉐이더 리플렉션 데이터를 저자하는 FShaderParamterInfo와 리소스 타입별로 FShaderParameterInfo를 저장하는 FShaderPrameterMapInfo

    ParameterMapInfo는 FShader의 생성자에서 FShader::BuildParameterMapInfo를 호출하여 구성됩니다.

    그림4. 리소스 바인딩 정보를 가공하여 FShaderParameterMapInfo에 저장

     

    BuildParamterMapInfo가 어떤 일을 하는지 알아봅시다.

    1. 이전 글인 쉐이더 컴파일 과정에서 얻어온 리플렉션 정보입니다.

    2. 리플렉션 정보에서 현재 처리 중인 타입이 몇 개인지 미리 계산합니다.

    3. 리소스 타입에 따라서 서로 다른 배열에 저장합니다.

    4. 리소스의 바인딩 포인트와 파라메터 크기를 저장합니다.

    그림5. FShaderParameterMapInfo 를 채우는 BuildParameterMapInfo 함수. 이 함수를 통해 FShader는 컴파일 과정에서 얻은 리플렉션 정보를 캐싱

    계속해서, UniformBuffer의 경우 FShader 생성자의 별도 코드에서 모읍니다. UniformBufferParameterStrucs에는 사용하는 UniformBuffer의 해시를 저장하고, UniformBufferParamterStructs에 대응하는 UniformBufferParameters의 인덱스 위치에 UniformBuffer의 바인딩 포인트를 저장합니다.

    그림6. UniformBuffer의 경우 UniformBufferParameterStrucs와 UniformBufferParameters에 바인딩 포인트 정보를 저장

     

    실제로 자신이 FShader를 확장하였고, 그 FShader 클래스에서 추가되는 리소스가 있는 경우도 확인해봅시다.

    1. FScreenPS 라는 FShader 입니다. 이 쉐이더는 Texture와 TextureSampler 리소스를 추가로 사용합니다.

    2. FShader가 초기화될 때, 컴파일된 쉐이더의 리플렉션 정보를 확인하여 바인딩 정보를 InTexture와 InTextureSampler에 캐싱합니다.

    3. SetParameters 함수의 파라메터로 전달받은 리소스들을 바인딩 포인트 정보가 담긴 InTexture와 InTextureSampler를 사용하여 쉐이더에 바인딩합니다. 이 함수는 뒤에서 알아볼 FMeshDrawShaderBindings 구조체를 사용하지 않고 즉시 쉐이더에 해당 리소스를 바인딩하는 함수입니다.

    4. InTexture와 InTextureSampler 멤버 변수이며, 해당 각각의 리소스들에 대한 바인딩 포인트 정보를 가집니다.

    그림6-1. 쉐이더 작성시 추가 바인딩 리소스 선언 예제

     

     

    3.4. 실제 리소스들의 쉐이더 바인딩

    지금까지는 쉐이더 코드로부터 받은 리플렉션 정보를 사용하여, UniformBuffer와 리소스의 바인딩 포인트를 FShader에 캐싱하였습니다. 이제 실제로 이 정보를 사용하여 렌더링 과정에서 UniformBuffer와 리소스의 바인딩 과정을 알아보겠습니다. 그전에 필요한 클래스 몇 개를 둘려봅시다. 그림 7의 클래스들을 알아볼 예정입니다.

    그림7. MeshDrawCommand에서 사용할 쉐이더 바인딩 정보 관리 클래스

     

    먼저 FMeshDrawShaderBindingsLayout을 봅시다. 이 클래스는 생성자에 FShader를 입력받습니다 그리고 FShader의 리플렉션 정보인 ParameterMapInfo를 가져옵니다. 이 클래스는 메모리 주소 0을 기반으로 UniformBuffer, Sampler, SRV 등의 Offset을 얻을 수 있습니다. 이 클래스의 특징은 0 메모리 주소를 기준으로 UniformBuffer, SRV, Texture 등등의 Offset 값을 얻을 수 있습니다.

    그림8. FMeshDrawShaderBindingsLayout

     

    두 번째로 FMeshDrawSingleShaderBindings 입니다. 이름 그대로 단일 쉐이더(Vertex, Pixel 등등) 마다 바인딩되는 리소스 정보들이 담긴 클래스입니다. 이 클래스는 바로 위에서 본 FMeshDrawShaderBindingsLayout에 확장입니다. 다른 점은 특정 Data 주소를 기반으로 UniformBuffer, Sampler, SRV 등의 Offset을 얻을 수 있습니다. 그리고

    특정 Data 주소를 기준으로 UniformBuffer, Sample, SRV 등의 Offset의 위치에 실제 사용할 수 있는 RHI 리소스 데이터를 기록할 수 있습니다.

    1. FMeshDrawSingleShaderBindings 의 생성자에서, RHI 리소스를 저장할 기준 메모리 주소가 되는 Data를 받습니다.

    2. 실제 외부에서 Add 함수를 호출하여 리소스를 바인딩합니다. Add 함수를 보면 아규먼트에 Prameter 정보를 같이 전달해 줍니다. Parameter 정보는 바인딩 포인트 정보까지 포함되어 있습니다. 즉, 외부에서 이미 어떻게 바인딩될지 알고 있는 상태로 이 함수가 호출된다는 의미입니다. 이러한 값은 FShader가 초기화될 때 쉐이더 컴파일 완료 후 얻을 수 있는 리플렉션 데이터인 ParameterMap 으로부터 FShader의 멤버로 선언된 Parameter들에게 설정해줍니다. 이렇게 FShader 생성자에서 사용자가 직접 추가한 RHI 리소스의 바인딩 정보를 얻는 것은 위에서 본 그림6-1 에서 확인할 수 있습니다.

    3. Data 주소를 기반으로 UniformBuffer, Sampler, SRV 등의 Offset을 얻습니다.

    4. RHI 타입의 리소스들과 바인딩 포인트를 정보를 사용하여 FMeshDrawSingleShaderBindings의 Data 메모리 주소에 저장합니다.

    그림9. FMeshDrawSingleShaderBindings

     

    FReadOnlyMehsDrawSingleShaderBindings는 FMeshDrawSingleShaderBindings에서 Data에 기록하는 기능을 제외하고, 특정 Data 주소를 기반으로 UniformBuffer, Sampler, SRV 등의 Offset을 얻을 수 있는데, 기록은 안되고 읽기만 하는 기능이 있는 클래스입니다.

    그림10. FReadOnlyMehsDrawSingleShaderBindings

     

    MeshDrawCommand에서 실제로 바인딩하는 쉐이더 별(Vertex, Pixel Shader 등등) UniformBuffer 및 리소스 정보는 FMeshDrawShaderBindings에 저장됩니다. 이 클래스 내에 TArray<FMeshDrawShaderBindingsLayout, TInlineAllocator<2>> ShaderLayouts; 에 배열 형태로 저장됩니다. 그리고 FMeshDrawShaderBindings는 실제로 UniformBuffer와 리소스들을 담을 메모리를 할당합니다. 이 메모리는 연속된 메모리 1개를 할당하여 여러 쉐이더들이 공유하여 사용합니다. 

    1. GetSingleShaderBindings는 특정 쉐이더 타입에 해당하는 FMeshDrawSingleShaderBindings를 리턴합니다. 이때, 해당 쉐이더의 바인딩 정보들을 가진 FMeshDrawShaderBindingsLayout와 FMeshDrawShaderBindings가 할당해 놓은 메모리 주소에 DataOffset를 더하여 생성합니다. DataOffset은 현재 쉐이더의 크기를 더해줘서 다음 쉐이더가 이 오프셋을 이용하여 자신의 메모리 주소를 얻을 수 있도록 해줍니다.

    2. FMeshDrawCommand 에서 렌더링 직전에 자신이 갖고 있는 모든 쉐이더의 RHI 리소스(UnfiormBuffer와 리소스들)를 실제 Grpahics API 에 바인딩해달라고 요청하는 함수입니다.

    3. 쉐이더 별 UniformBuffer와 리소스 바인딩 정보가 담긴 클래스입니다.

    4. FMeshDrawShaderBindings는 모든 쉐이더의 UniformBuffer와 리소스 정보를 담을 메모리를 할당합니다. 그 메모리를 할당하는 객체입니다.

    5. 2번에서 SetOnCommandList에서 호출하는 함수입니다. 이 함수에서 한 개의 쉐이더에 대해서 UniformBuffer와 리소스들을 Graphics API에 바인딩해달라고 요청합니다.

    그림11. FMeshDrawShaderBindings

     

    이제 FMeshDrawShaderBindings가 초기화되는 과정을 알아봅시다. FBasePassMeshProcessor 기준으로 알아봅시다.

    1. GetBasePassShaders 함수는 VertexShader와 PixelShader를 얻어 줍니다. 이 쉐이더를 BuildMeshDrawCommands 호출 시 전달합니다.

    2. BuildMeshDrawCommands에서는 1에서 구한 쉐이더를 MeshDrawCommand::SetShaders 넘겨줍니다.

    3. SetShaders 함수 내에서 FMeshDrawShaderBindings::Initialize 함수를 호출합니다. 여기서도 전달받은 쉐이더를 그대로 넘겨줍니다.

    4. FMeshDrawShaderBindingsLayout을 쉐이더 별로 추가해줍니다. 이때 생성자로 넘겨주는 FShader를 사용하여 ParameterMapInfo 정보를 FMeshDrawShaderBindingsLayout가 가지도록 합니다.

    5. FMeshDrawShaderBindings 은 자신이 가진 모든 쉐이더들의 바인딩될 리소스(RHI 리소스)를 저장하기 위해서 필요한 메모리를 할당하여 갖고 있습니다. 이 데이터는 FMeshDrawShaderBindings::FData 에 할당됩니다. 그리고 실제 바인딩할 리소스는 FRHIUniformBuffer, FRHISamplerState, FRHISahderResourceView, FRHITexture 등이며, 바인딩 포인트 정보는 ShaderLayouts의 FMeshDrawShaderBindingsLayout에 있을 것입니다.

    그림12. FMeshDrawShaderBindings가 초기화 과정

     

    계속해서 FMeshDrawShaderBindings를 Initialize 하고 나서 BuildMeshDrawCommands를 계속 진행합니다. 각 쉐이더 별로 GetShaderBindings 함수를 호출하는데, 여기서 바인딩될 RHI 리소스 형태의 UniformBuffer와 리소스를 FMeshDrawShaderBindings 의 FData에 저장합니다. MeshDrawCommand는 공유될 수 있는 내용의 SharedMeshDrawCommand를 생성한 뒤 각 MeshElement 마다 별도의 MeshDrawCommand를 생성하는 형태였습니다. (자세한 사항은 [UE5] MeshDrawCommand (1/2) 참고)

    GetShaderBindings 함수가 공유될 수 있는 바인딩될 정보들을 추가하는 함수, GetElementShaderBindings 가 Element별로 바인딩 될 정보들을 추가하는 함수입니다.

    그림13. 렌더링에 필요한 RHI 리소스를 모으는 GetShaderBindings 함수

     

    실제 GetShaderBindings가 어떻게 호출되는지 확인해봅시다.

    1. FMeshDrawSingleShaderBindings::Add 함수를 사용하는 것을 볼 수 있습니다.

    2. ShaderBindings.Add 첫 번째 파라메터는 UniformBuffer 타입을 기준으로 바인딩 인덱스를 얻어냅니다. 

    3. 2에서 얻은 바인딩 인덱스를 ShaderBindings.Add의 두 번째 파라메터인 RHI 리소스와 함께 FMeshDrawSingleShaderBindings에 저장합니다. FMeshDrawSingleShaderBindings는 FMeshDrawShaderBindings의 FData에서 자신이 사용할 일부 메모리를 uint8* Data; 형태로 전달받았습니다. 그렇기 때문에 실제로는 FMeshDrawShaderBindings 에 RHI 리소스가 저장된다고 볼 수 있습니다.

    그림14. GetShaderBindings 호출 순서

     

    3.5. 렌더링을 위해서 바인딩한 리소스들을 Graphics API 전달

    이제 UniformBuffer와 리소스 바인딩 정보는 모두 모았습니다. 렌더링 하는 과정만 확인하면 완료입니다!

    1. FMeshDarwComand는 SubmitDraw 함수에서 실제로 Graphics API 쪽으로 렌더링을 요청합니다.

    2. SubmitDraw 함수의 SubmitDrawBegin에서 렌더링에 필요한 정보를 설정하고, SubmitDrawEnd 에서 실제 렌더링 함수(ex. DrawIndexedPrimitive 등등)를 호출합니다.

    3. SubmitDrawBegin에서 RHI 리소스를(UniformBuffer와 리소스) 쉐이더에 바인딩하는 FMeshDrawShaderBindings::SetOnCommandList 함수를 호출합니다.

    4~5. SetShaderBindings 에서 각각의 RHI 리소스(UniformBuffer와 리소스)들을 바인딩해달라고 Graphics API 에 호출하는 것을 볼 수 있습니다.

    그림15. 실제 렌더링시 UniformBuffer와 리소스들을 SetShaderBindings를 사용하여 쉐이더 바인딩하는 과정

     

    3.6. 결론

    1. 언리얼의 UniformBuffer는 쉐이더 코드와 소스 코드 양쪽으로 모두 선언해줄 필요가 없습니다. 전용 매크로를 코드에 사용하여 한번 정의해두면 FShaderParametersMetadata 를 통해 쉐이더에서 사용할 코드는 자동으로 생성할 수 있습니다.

    2. FShader의 생성자에서는 쉐이더 컴파일러로부터 쉐이더 코드의 리플렉션 정보 얻을 수 있습니다. 이 정보들로부터 FShader가 가지고 있는 UniformBuffer와 리소스들의 바인딩 포인트를 얻을 수 있습니다. 이 정보는 ParameterMapInfo(FShaderParameterMapInfo)에 저장됩니다.

    3. FMeshDrawShaderBindings 클래스를 통해 FMeshDrawCommand가 렌더링에 필요한 모든 쉐이더 별 RHI 리소스(UniformBuffer와 리소스)들을 바인딩할 수 있습니다.

     

     

    4. 레퍼런스

    1. 언리얼 엔진 4 셰이더, 더 깊이 이해하기
    2. Unreal Engine 4 Rendering Part

     

     

    댓글

Designed by Tistory & scahp.