ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정
    UE4 & UE5/Rendering 2021. 9. 10. 07:00

    [UE5] Shader Compile [2/3] - 쉐이더 컴파일 과정

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

     

    목차

    1. 환경
    2. 목표
    3. 내용
      3.1. 쉐이더의 확장
        3.1.1. PixelShader의 확장
          3.1.1.1. Material Editor를 사용한 확장
          3.1.1.2. PixelShader에서 Material Editor으로부터 작성한 노드의 연산 얻기
          3.1.1.3. Material Detail 패널을 통한 확장
        3.1.2. VertexShader의 확장
          3.1.2.1. Vertex Input의 정의
          3.1.2.2. VertexInput을 사용하는 약속된 함수
      3.2. 컴파일에 사용할 템플릿 쉐이더
        3.2.1. Material Shader
        3.2.2. VertexFactory Shader
        3.2.3. Vertex/Pixel Shader
      3.3. Material/MeshMaterial Shader 컴파일
        3.3.1. 쉐이더 컴파일 시작
        3.3.2. Material Editor의 노드를 쉐이더 코드로 변환
        3.3.3. MaterialTemplate.ush를 사용하여 Material.ush 생성
        3.3.4. MaterialShaderMap을 사용한 쉐이더 컴파일 시작
        3.3.5. 머터리얼과 조합될 수 있는 FMaterialShaderType, FMeshMaterialShaderType 수집과 조합
        3.3.6. 수집한 FMaterialShaderType과 FMeshMaterialShaderType(FShaderType + FVertexFactoryType)를 사용한 컴파일 환경 설정 캡슐화
        3.3.7. 쉐이더 컴파일과 컴파일 완료된 쉐이더로 부터 FShader 생성
      3.4. 상수 컬러 벡터와 파라메터 컬러 벡터의 곱셈을 평가하는 방식

      3.5. 결론
    4. 레퍼런스

     

    1. 환경

    Unreal Engine 5 (ue5-main branch acc8c5f399ca01f6f549108be1fb75381fecbca8)

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

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

    쉐이더 컴파일 과정은 핵심 개념보다는 코드를 훑어보면서 실제 컴파일되는 부분을 확인하는 부분이 많습니다. 그러다 보니 훑어보는 코드의 양이 상당히 많습니다. 혹시 코드의 설명과 코드의 본문을 서로 교차해서 보기에 부담스러울 만큼 긴 경우 창을 두 개 띄워서 하나는 설명 하나는 코드를 보는 것을 추천해드립니다.

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

     

    2. 목표

    1. MaterialShader와 MeshMaterialShader가 컴파일되는 과정을 알아봅시다.
    2. BasePass의 VertexShader와 PixelShader를 각각 어떻게 확장하는지 알아봅시다.
    3. MaterialEditor에서 확장되는 노드 기반 쉐이더가 어떻게 코드로 변환되고 PixelShader에 반영되는지 알아봅시다.
    4. VertexFactory가 어떻게 VertexShader의 기능을 확장하는지 알아봅시다.

    이전 글 "[UE5] Shader [1/3] - 주요 클래스 파악" 에서는 아래의 과정을 알아봤습니다.
    UE5 Shader 컴파일되는 과정에 필요한 기본 클래스들과 쉐이더가 컴파일되면 어떻게 캐싱되는지를 알아봤습니다. 이전 글을 먼저 보시고 이 글을 보는 것을 추천합니다.

    마지막 글 "[UE5] Shader ResourceBinding [3/3] - UniformBuffer와 리소스 바인딩" 에서는 아래의 과정을 알아봅니다.
    UE5 Shader가 UniformBuffer, Texture, SRV, UAV 등과 같은 리소스를 SourceCode와 Shader 간에 어떻게 정의하고 바인딩하는지 알아봅시다.

     

     

    3. 내용

    이전 글에서 언리얼의 쉐이더는 총 3가지 타입 Global/Material/MeshMaterial 쉐이더가 있었습니다. 아마 대부분의 경우는 Mesh에 사용하는 Material을 정의하고 사용할 것입니다. 그렇다면 가장 중요한 쉐이더는 MeshMaterialShader라고 생각되기 때문에 오늘은 Material과 MeshMaterialShader의 컴파일 과정을 볼 것입니다. MeshMaterialShader는 MaterialShader의 확장이므로 이 부분을 보다 보면 자연스레 같이 보게 될 것입니다.

    3.1. 쉐이더의 확장

    3.1.1. PixelShader의 확장
    3.1.1.1. Material Editor를 사용한 확장
    먼저 오늘 알아볼 MaterialShader는 Material Editor와 연관되어 있기 때문에 Material Editor에서 노드가 어떻게 변환되는지 생각해봅시다.

    Material Editor에서 노드 기반 쉐이더를 작성하면 해당 노드들은 C++ 코드에서 Shader 코드로 변환시켜줍니다. 그리고 특수한 노드가 있는데 바로 파라메터 노드입니다. 이 노드를 추가하면 머터리얼 인스턴스에서 해당 파라메터를 동적으로 조정 가능합니다.
    아래 그림1의 노드 옆에 적혀있는 이름은 Material Editor의 노드들이 코드로 변환될 때, 붙여진 이름을 적은 것입니다.

    그림1. 머터리얼 에디터의 노드기반 쉐이더

    Material Editor에서 작성된 노드는 MaterialTemplate.ush 라는 쉐이더 코드에 삽입됩니다. MaterialTemplate 이름 그대로 모든 Material Shader에 사용될 약속된 함수들과 변수들을 제공합니다. 삽입하는 방식은 생각 외로 단순합니다. 그냥 %s 쪽 부분이 우리가 만든 쉐이더 코드(Material Editor에서 노드로 작성하여 쉐이더 코드로 변환된)로 변경됩니다.

    실제로 Material Editor에서 작성한 노드가 MaterialTemplate.ush에 어디에 들어가고 어떻게 PixelShader에서 가져다 쓰는지 알아봅시다. 노드들이 추가되는 곳은 MaterialTemplate.ush의 CalcPixelMaterialInputs 입니다.
    먼저 MaterialTemplate.ush 의 CalcPixelMaterialInputs의 원본 템플릿과 위의 그림1의 노드로 채워진 결과를 차례로 봅시다.

    그림2. MaterialTemplate.ush 에서 노드가 쉐이더 코드로 변하는 CalcPixelMaterialInputs

    아래의 빨간색 네모 부분이 BaseColor 부분을 위해 노드로부터 쉐이더 코드로 생성된 부분입니다. 그중 초록색 네모 부분이 마지막으로 Color와 Texture를 Multiply 하는 부분입니다. Color들끼리의 Multiply 부분은 없는데요. 이 부분은 Material.VectorExpressions[4].rgb로 대체되었네요. 이 부분은 이후에 컴파일 과정을 보면서 어떻게 생성되었는지 알아봅시다.

    그림3. 노드가 쉐이더 코드로 변환될 결과를 보여주는 CalcPixelMaterialInputs, BaseColor Pin만 연결됨..

    이 머터리얼 쉐이더에서 사용하는 UniformBuffer 에 대한 쉐이더 코드입니다. 위 그림4의 두 벡터 파라메터의 곱을 나타내는 Material.VectorExpressions[4]이 UniformBuffer로 사용된다는 것을 알 수 있습니다.

    그림4. Material Editor 노드에서 생성된 파레메터 값, UniformBuffer(ConstantBuffer)로 생성됨.


    3.1.1.2. PixelShader에서 Material Editor으로부터 작성한 노드의 연산 얻기
    MaterialTemplate.ush의 CalcPixelMaterialInputs에 Material Editor의 노드의 실제 연산이 들어간 것을 확인했습니다. 이제 PixelShader에서 이것을 어떻게 가져오는지 봅시다. BasePassPixelShader.usf 부터 시작합니다.

    그림5. 픽셀쉐이더에서 CalcPixelMaterialInputs를 호출하여 Material Editor에서 생성한 노드를 결과를 얻어오는 쉐이더 코드


    3.1.1.3. Material Detail 패널을 통한 확장
    머터리얼 에디터의 디테일 패널에 있는 내용은 UMaterial 클래스에 설정됩니다. 이 값들은 쉐이더 컴파일 시 적절하게 사용됩니다. 디테일 패널에 있는 내용 중 일부 값을 모아 FMaterialShaderParameters 를 생성합니다. FMaterialShaderParameters는 아래와 같은 것을 하는 데 사용합니다.
    1). 이 쉐이더를 컴파일 가능 여부를 확인하는 데 사용 (ShouldCompilePermutation, ShouldCompileVertexFactoryPermutation 등등)
    2). 이 쉐이더의 컴파일 환경 설정을 하는데 사용 (ModifyCompilationEnvironment)

    그림6. 머터리얼 쉐이더의 옵션들이 나열된 FMaterialShaderParameters


    3.1.2. VertexShader의 확장
    VertexShader는 VertexFactory를 통해 확장됩니다. 가장 기본적인 LocalVertexFactory.ush와 BasePassVertexShader 기준으로 알아봅시다.
    VertexFactory에는 Vertex Input의 레이아웃이 정의되어있을 것이고, BasePassVertexShader는 어떤 Vertex Input이 들어오더라도 잘 처리될 수 있게 일반화하는 것이 목표일 것입니다. 그렇다면 VertexFactory와 VertexShader 간에 서로 약속된 구조체와 그 약속된 구조체를 사용하는 약속된 함수들이 있으면 가능할 것입니다.

    3.1.2.1. Vertex Input의 정의
    VertexFactory에 Vertex Input이 정의되고, BasePassVertexShader는 모든 VertxFactory가 동일한 이름의 구조체(FVertexFactoryInput)를 사용하도록 약속하면 이 문제를 해결할 수 있을 것입니다. 추가적으로 FVertexFactoryIntermediates를 두어 계속해서 계산되는 부분을 캐싱하는 데 사용합니다.

    3.1.2.2. VertexInput을 사용하는 약속된 함수
    Vertex Input의 Vertex Position 정보를 로컬에서 월드로 변환하는 처리 역시 FVertexFactoryInput의 상세 정보를 알고 있는 VertexFactory가 알고 있으면 되고 어떤 함수가 이 일을 할지 BasePassVertexShader와 VertexFactory 간에 약속을 해두면 될 것입니다. BasePassVertexShader.usf 를 봅시다.
    아래 부분처럼 Vertex Input 관련된 구조체는 FVertexFactoryInput 변수로 그리고 이 변수를 조정하는 약속된 함수들 VertexFactoryGetWorldPosition, VertexFactoryGetTangentToLocal 등등을 볼 수 있습니다.

    그림7. BasePassVertexShader.usf 와 VertexFactory 간의 약속된 구조체와 인터페이스


    아래 그림8에서는 FVertexFactoryInput, 그림9에서는 중간 계산을 캐싱하는 FVertexFactoryIntermediates는 보여줍니다. 이 둘은 모두 LocalVertexFactory에 정의됩니다.
    VertexShader는 주요 목적은 버택스의 변환을 로컬에서 월드로 변환하는 것입니다. 거기서 가장 중요한 Position 을 아래 그림에서 찾을 수 있습니다. 그 외에 Tangent, Color, TexureUV 등등을 확인할 수 있습니다.

    그림8. LocalVertexFactory의 Vertex Input


    그림9를 보면 FVertexFactoryIntermediates와 GetVertexFactoryIntermediates가 정의되어 있습니다. GetVertexFactoryIntermediates FVertexFactoryIntermediates 함수를 채웁니다.
    1. GetVertexFactoryIntermediates 의 TangentToLocal을 보면 LocalVertexFactory.usf의 VertexFactoryGetTangentToLocal에서 사용됩니다. 이 함수는 BasePassVertexShader.usf에서 그대로 사용되어집니다(그림7 참고). 즉, FVertexFactoryIntermediates에서 한번 계산해둔 TangentToLocal을 재활용하는 것을 볼 수 있습니다.

    그림9. LocalVertexFactory의 중간 결과 캐싱 구조체


    그림10는 LocalVertexFactoryush에 정의된 월드좌표를 얻어오는 함수 VertexFactoryGetWorldPosition입니다. 이 함수는 그림7에서 보듯 BasePassVertexShader.usf에서 사용됩니다.

    그림10. BasePassVertexShader와 VertexFactory의 약속된 인터페이이 중 Local에서 World로 버택스를 Transform 시키는 함수

     

    3.2. 컴파일에 사용할 템플릿 쉐이더

     

    3.2.1. Material Shader
    Material Editor에서 사용될 MaterialTemplate.ush 은 고정입니다. 모든 Material이 이 Template을 사용합니다.

    3.2.2. VertexFactory Shader
    VertexFactory는 저마다의 FVertexFactory와 매칭 되는 쉐이더 코드를 따로 구현하여 추가해주어야 합니다.

    3.2.3. Vertex/Pixel Shader
    이전 글에서 확인 한것과 같이 FShaderType을 생성할 때 함께 넘겨줍니다. 아래 그림11를 봅시다. 실제 이 매크로가 어떻게 확장되는지는 이전 글을 참고하면 볼 수 있습니다. 여기에 넘겨진 Uber Shader를 사용하여 3.1에서 알아본 쉐이더 확장 부분을 적용하여 쉐이더 컴파일을 수행합니다.

    그림11. 버택스와 픽셀 쉐이더의 템플릿 선택


    3.3. Material/MeshMaterial Shader 컴파일

    컴파일 과정의 디버깅을 쉽게 하기 위해서 엔진에 아래와 같은 설정을 하고 시작하였습니다.

    병렬 컴파일을 옵션을 껐습니다. BaseEngine.ini의 아래 항목 2개를 아래와 같이 설정해주면 됩니다. 
    [DevOptions.Shaders]
    ; See FShaderCompilingManager for documentation on what these do 
    bAllowCompilingThroughWorkers=False 
    bAllowAsynchronousShaderCompiling=False


    3.3.1. 쉐이더 컴파일 시작
    쉐이더 컴파일은 FShaderCompileJob의 Input에 컴파일에 필요한 정보를 채우고 컴파일합니다. 그 뒤에 컴파일한 결과를 Output에 담는 형태입니다. 쉐이더 컴파일은 이런 것을 Job 구조에 캡슐화시켜서 각각을 병렬로 컴파일시킵니다.
    1. 컴파일할 Job의 Key를 FShaderCompileJobKey로 두고 ShaderType, VertexFactoryType, PermutationId 조합하여 생성합니다.
    2. FShaderCompileJob은 FShaderCompileJobKey을 가집니다.
    3. 그리고 FShaderCompilerInput 에 컴파일에 필요한 것을 모두 채우고, 컴파일 결과를 FShaderCopilerOuput에 컴파일 결과를 저장합니다.

    그림12. FShaderCompileJobKey 쉐이더 컴파일을 위해 필요한 입력과 컴파일 결과를 캡슐화 함


    1. 쉐이더 컴파일에 필요한 기본정보들이 선언되어 있습니다.
    2. FShaderCompilerEnvironment는 쉐이더에 컴파일에 필요한 Preprocessor나 include 되는 쉐이더 코드 등등, 쉐이더의 컴파일에 필요한 정보를 갖고 있습니다. FSharedShaderCompilerEnvironment는 Material 로부터 만들어진 정보들로 Material, VertexFactory, Permutation이 바뀌어도 동일하게 사용되는 항목들을 가집니다. Environment는 이 쉐이더 컴파일 Job에만 해당하는 항목들을 가집니다.

    그림13. FShaderCompilerInput 쉐이더 컴파일에 필요한 입력


    1. 쉐이더에서 사용하는 파라메터들(UniformBuffer, SRV, UAV 등)의 바인딩 인덱스 정보를 가집니다. 추후 파라메터들을 쉐이더에 바인딩할 때 참고하여 바인딩합니다.
    2. 컴파일된 쉐이더의 바이트 코드가 들어있습니다. 이 쉐이더 코드로부터 FRHIShader를 생성할 수 있을 것입니다.

    그림14. FShaderCompilerOutput 쉐이더 컴파일 후 결과 출력


    쉐이더 컴파일 과정은 CacheResourceShadersForRendering 를 호출하여 컴파일이 진행됩니다.
    코드 흐름을 따라 가봅시다. Material이 처음 생성되어 MaterialShaderMap이 없는 상태라고 가정합니다. 그리고 처음은 아래와 같은 순서로 함수가 호출됩니다. 빠르게 넘어가는 부분은 코드 흐름만 보고 넘어가겠습니다.
    UMeterial::PostLoad -> CacheResourceShadersForRendering -> CacheShadersForResources -> CacheShaders ->CacheShaders -> BeginCompileShaderMap 순서로 차례대로 호출됩니다.

    그림15. 머터리얼에서 컴파일의 시작점


    여기서부터가 중요한 부분입니다. 아래의 그림16 코드에 나오는 설명을 마치면 모든 컴파일 과정을 확인한 것입니다.
    1. 새로운 FMaterialShaderMap을 생성합니다.
    2. Material 과 관련된 컴파일에 필요한 여러 환경 설정 정보들을 채웁니다.
    3. 실제 컴파일에 필요한 모든 정보가 있는 클래스입니다. Material Editor 노드에서 생성한 /Engine/Generated/Material.ush 역시 이 클래스의 가상 경로(IncludeVirtualPathToContentsMap)에 추가합니다. 그 외에 여러 Preprocessor 역시 여기에 추가됩니다.
    4. Material Editor에서 작성한 노드들를 해석하여 쉐이더 코드를 생성합니다.
    5. MaterialTemplate.ush에 4에서 만든 노드들부터 만든 쉐이더 코드와 기타 환경설정을 반영하여 /Engine/Generated/Material.ush(MaterialTemplate.ush에서 필요한 부분이 채워진 쉐이더 코드)를 생성합니다. 이 파일은 가상 주소에 저장되며 실제로 파일로 저장되진 않습니다. 또한 4번 과정에서 Material Editor의 노드를 분석해 봤을 때, UniformBuffer로 표현될 수 있는 SRV, UAV, UniformBuffer 가 있다면 그것 또한 생성하여 /Engine/Generated/UniformBuffers/Material.ush 에 추가해줍니다. 이 파일 또한 실제로 파일에 저장되진 않고 메모리상에 가상 경로로 관리됩니다.
    6. 쉐이더 컴파일을 수행합니다. 현재는 병렬로 컴파일하지 않으므로 이 함수가 수행되고 나면 모든 쉐이더 컴파일 과정을 마친 상태가 됩니다.
    7. 컴파일이 완료된 FMaterialShaderMap을 이 FMaterial의 ShaderMap으로 설정합니다. 현재는 병렬로 컴파일 하지 않으므로 여기서 ShaderMap을 설정합니다.

    그림16. MaterialShaderMap을 컴파일하고 FShader를 생성하여 캐싱하는 모든 것이 담긴 함수

     

    3.3.2. Material Editor의 노드를 쉐이더 코드로 변환

    먼저 Translate_Legacy 함수입니다. 이 부분이 상당히 호흡이 길 것입니다. 하지만 한 번은 봐 두는 것이 이후에 코드를 보는데 도움이 될 것입니다. 그림1의 머터리얼 노드를 평가하는 것을 따라가 볼텐데요. 고정상수 타입의 컬러벡터와 Parameter 타입 컬러벡터를 곱하는 부분에 집중하여 분석해보겠습니다. Texture Parameter의 경우 컬러벡터간의 연산을 평가하는 방식과 유사하므로 추후 디버깅해보는 것으로 하겠습니다. 그림1 머터리얼이 컴파일되면 그림3와 같은 코드가 생성됩니다.
    1. 머터리얼 노드를 평가합니다. Material Editor의 최종 노드로부터 연결된 노드를 역으로 따라가며 평가합니다. 그림18를 봐주세요. 그림18의 번호가 노드가 평가 완료되는 순서입니다.
    2. 평가한 머터리얼 노드를 쉐이더 코드로 생성합니다. 그리고 FSharedShaderCompilerEnvironment의 IncludeVirtualPathToContentsMap에 /Engine/Generated/Material.ush 이름으로 생성된 코드를 추가합니다.

    그림17. Material Editor의 노드를 분석하는 Translate 함수
    그림18. 오늘 분석해볼 간단한 쉐이더 노드



    계속해서 Translate 함수를 보겠습니다. 함수가 길어서 필요한 부분을 잘라냈습니다. 여기서는 BaseColor 부분이 어떻게 평가되는지만 따라가 볼 예정입니다.
    Translate -> Material-> CompilePropertyAndSetMaterialProperty -> CompileProperty -> CompilePropertyEx -> CompileWithDefault 까지는 흐름만 따라오면 됩니다. FExpressionInput::Compile 함수에서 평가(Evaluation)가 시작됩니다.
    1. BaseColor 의 평가(Evaluation)가 시작되는 지점입니다. 나머지 Pin들 또한 CompilePropertyAndSetMaterialProperty 함수를 통해 호출됩니다.

    그림19. 머터리얼 노드의 분석 시작, 각 Pin을 출력에서 부터 입력 쪽으로 연결선을 따라가면서 평가(Evaludate)


    Compile 함수는 BaseColor Pin에 가장 가까이 있는 Multiply 부터 먼저 Compile을 시작합니다. 소스코드에서는 이 노드들을 MaterialExpressionXX 형태로 불립니다. 각 노드들의 이름을 그림1에서 확인해봅시다.

    그림20. MaterialExpressionMultiply_1 노드 컴파일 도입



    아래 그림21 MaterialExpressionMultiply_1을 컴파일하는 것을 보여줍니다.
    1. 첫 번째 Input에는 MaterialExpressionMultiply_0이 연결되어 있습니다. 우리는 먼저 이 부분을 따라가 볼 것입니다.
    2. 두 번째 Input에는 Texture Sampling 하는 노드가 연결되어 있습니다. 이 Texture 노드는 Parameter로 선언되었기 때문에 머터리얼 인스턴스를 만들면 인스턴스에서 수정 가능합니다.

    그림21. MaterialExpressionMultiply_1 노드 컴파일

    아래 그림22 MaterialExpressionMultiply_0을 컴파일하는 것을 보여줍니다.
    1. 첫 번째 Input에는 MaterialExpressionConstant3Vector_0이 연결되어 있습니다. 계속해서 이 부분을 따라가 보겠습니다.
    2. 두 번째 Input에는 MaterialExpressionVectorParameter_0이 연결되어 있습니다. 이 값은 MaterialExpressionConstant3Vector_0와 같이 Vecto3 노드이지만 다른 점은 Parameter로 선언되었다는 것입니다. 머터리얼 인스턴스로 생성하면 인스턴스에서 수정 가능합니다.

    그림22. MaterialExpressionConstant3Vector_0 노드를 컴파일


    아래 그림23 MaterialExpressionContant3Vector_0을 컴파일하는 것을 보여줍니다.
    1. 머터리얼에 있는 녹색 컬러가 생성된 것을 볼 수 있습니다. MaterialFloat3 이라는 타입으로 되어있는데 이 값은 추후에 float3이나 half3 등으로 대체됩니다.

    그림23. MaterialExpressionConstant3Vector_0 노드 컴파일하여 상수 벡터를 얻음


    다시 뒤로 돌아가서 MaterialExpressionMultiply_0으로 와서 두 번째 Input Pin을 컴파일합니다. 위와 같은 형태로 계속 진행됩니다. 이 부분은 차례대로 쭉 따라보면 됩니다.
    1. 첫 번째 Input Pin과 다른 점이 MaterialExpressionContant3Vector가 아니라 MaterialExpressionVectorParameter로 선언된 것을 보실 수 있습니다. 이 부분이 어떻게 다르게 평가되는지 알아봅시다.

    그림24. MaterialExpressionVectorParameter_0 노드 컴파일

    아래 그림25는 MaterialExpressionVectorParameter_0 입니다.
    1. Parameter 타입의 경우는 Material용으로 별도로 관리되는 UniformBuffer에 추가됩니다. UniformBuffer는 이렇게 코드에서 동적으로 생성되는 경우와 코드에 정의되어있어서 컴파일 타임에 결정되는 타입 등이 있습니다. 그림16에서 봤던 MaterialCompilationOutput 항목에 UniformExpressionSet->UniformVectorParameter 에 VectorParameter를 ColorParam 이름(Material Editor에서 정한 이름)으로 추가해줍니다. 여기에 추가된 UniformBuffer에 대한 정보는 Material Editor의 노드 평가를 마친 다음에 이 UniformBuffer를 ush로 만들어 추가해줍니다. 만약 이전에 다른 노드가 평가되면서 만들어둔 같은 이름의 UniformVectorParameter가 이미 있다면 그것을 사용할 것입니다. 하지만 지금은 그냥 새로 생성된다고 하고 보면 됩니다.
    2. 1에서 추가한 VectorParameter를 코드화 시켜야 하는데, 이미 이전에 만들어둔 코드가 있다면 그것을 재활용합니다.
    3. 그렇지 않다면 해당 코드를 새로 생성합니다.

    그림25. MaterialExpressionVectorParameter_0 노드 컴파일, Parameter 노드의 경우 UniformBuffer로 만들기 위하여 UniformExpression에 추가해줌

    이제 MaterialExpressionMultiply_0 의 두 Input이 모두 평가되었으므로 Mul 연산을 수행합니다.
    1. 이 둘의 곱을 쉐이더 코드로 잘 변환시킨 것을 볼 수 있습니다.
    2. MaterialExpressionMultiply_0 의 두 입력값은 UniformBufferExpression의 FMaterialUniformExpressionFoldedMath 항목 하위에 잘 들어가 있는 것을 확인할 수 있습니다.

    그림26. MaterialExpressionMultiply_0 의 양쪽 노드를 모두 컴파일하고 양쪽노드를 곱함. 그런데 곱셈을 하지 않고 새로운 UniformBuffer 변수를 추가하고 FMaterialUniformExpressionFoldedMath 노드하위에 양쪽 노드의 결과가 들어감. 이부분은 추후에 UniformBuffer CPU 측에 채울때 평가됨.

    텍스쳐 샘플링도 Vector 연산과 비슷한 형태로 진행됩니다. 그래서 그 부분은 생략하고 Material Editor의 노드가 모두 쉐이더 코드로 변환되었고 여기에 필요한 UniformBuffer 정보들도 모두 모았다고 가정하고 계속해서 코드를 보겠습니다.

    이제 MaterialTemplate.ush 에 모든 값이 다 채워졌을 것입니다. 그중 Material Editor의 노드들은 CalcPixelMaterialInputs 함수 내에 생성되며, 아래 그림27 와 같이 변환됩니다. 조금 특이한 점은 Local3.rgb는 Texture Sample 정보인 것을 알겠는데, 상수 컬러 벡터와 파라메터 컬러 벡터를 곱한 값은 Material.VectorExpressions[4].rgb가 되었습니다. 이 부분은 쉐이더가 아닌 CPU에서 연산한 다음 연산 결과를 Material의 Uniformbuffer에 업데이트시켜줍니다. 바로 FMaterialUniformExpressionFoldedMath 부분입니다. 이 과정은 상당히 길기 때문에 이글의 맨 마지막 목차의 3.4에서 살펴보도록 합시다. 일단은 Material Editor의 노드가 쉐이더 코드로 잘 변환되었다고 하고 계속 진행해보겠습니다.

    그림27. Material Editor의 노드가 쉐이더 코드로 평가된 결과

     

    3.3.3. MaterialTemplate.ush를 사용하여 Material.ush 생성
    다시 BeginCompileShaderMap 입니다.
    1. 지금까지 Material Editor의 노드를 쉐이더 코드화 시킨 함수가 Translate_Legacy입니다.
    2. NewCompilationOutput.UniformExpressionSet.CreateBufferStruct() 함수를 통하여 FShaderParametersMetaData를 얻어냅니다. 다음 글에서 보겠지만 이 내용은 UniformBuffer에 대한 리플렉션 정보입니다. FShaderParametersMetaData만 있으면 우리는 UniformBuffer에 대응하는 쉐이더 코드를 생성해낼 수 있습니다.
    3. 머터리얼 쉐이더를 컴파일하기 위해서 필요한 환경설정을 하는 부분입니다.
    4. 2에서 생성한 FShaderParametersMetaData로 부터 UniformBuffer에 대응하는 쉐이더 코드를 생성하는 함수입니다. 이 함수 호출 후에 Material의 설정에 따라 필요한 Preprocessor 등을 추가해줍니다.
    5. FShaderParametersMetaData로 부터 생성한 UniformBuffer 쉐이더 코드를 만들어 /Engine/Generated/UniformBuffers/Material.ush 라 이름 붙여 IncludeVirtualPathToContentMap에 저장합니다. 그리고 /Engine/Generated/GeneratedUniformBuffers.ush에 #include "/Engine/Generated/UniformBuffers/Material.ush"를 넣어줍니다. 최종적으로 컴파일 전 /Engine/Generated/UniformBuffers/GeneratedUniformBuffers.ush만 include 하면 모든 필요한 UniformBuffer가 inlcude 되도록 하기 위함입니다.

    그림28. Material Editor의 노드를 평가하고, 노드 평가중 생성이 필요하다고 판단된 UniformBuffer 를 쉐이더 코드로 만들어서 컴파일 할 수 있게 준비함

     

    3.3.4. MaterialShaderMap을 사용한 쉐이더 컴파일 시작

    1. 다시 BeginCompilationShaderMap와서 돌아와서 이제 ShaderMap의 Compile을 호출합니다.
    2. 이 ShaderMap에서 사용할 FMaterialShaderMapContent를 생성하여 ShaderMap에 추가합니다. FMaterialShaderMapContent는 캐싱된 FShader를 관리하는 객체입니다.
    3. 컴파일을 진행하기 위해 준비하는 단계입니다. Job을 만들어서 컴파일러 매니져에 추가해주는 역할을 합니다.
    4. 컴파일을 완료하고 FShader를 생성하여 캐싱하는 단계입니다.

    그림29. MaterialSahderMap을 생성하고 컴파일할 Job을 준비하여 컴파일한 뒤, FShader를 생성하는 함수

     

    3.3.5. 머터리얼과 조합될 수 있는 FMaterialShaderType, FMeshMaterialShaderType 수집과 조합
    컴파일을 진행하기 위한 과정인 SubmitCompileJobs를 봅시다. 이 과정에서 가장 중요한 것은 이 Material 과 호환되는 FShaderType을 모읍니다. 그리고 FMeshMaterialShader를 만들 수 있는 FShaderType과 FVertexFactoryType을 모읍니다. 그리고 Jobs 구조체로 캡슐화하여 각 워커스레드가 쉐이더를 컴파일할 수 있도록 준비해주는 것입니다. 코드에서 ShaderPipeline이라는 구조도 있는데 지금은 그 부분을 무시하고 기본 쉐이더에 집중하도록 하겠습니다.
    1. FMaterialShaderMap의 AcquireMaterialShaderMapLayout 함수는 MaterialParameters(FMaterialShaderParameters)를 기반으로 컴파일하여 캐싱 가능한 모든 FShaderType, FVertexFactoryType를 모아줍니다. AcquireMaterialShaderMapLayout 는 SubmitCompileJobs의 핵심이 되는 부분입니다. MaterialParameters를 기반으로 FShaderType과 FVertexFactoryType의 호환성을 체크하므로, MaterialParameters 자체가 HashKey가 될 수 있습니다.
    2. AcquireMaterialShaderMapLayout->AcquireLayout 함수에서는 MaterialParameters를 기반으로 기존에 캐싱된 호환되는 FShaderType, FVertexFactoryType이 있는지 먼저 확인합니다. 이미 만들어 둔 것이 있으면 그것을 사용합니다.
    3. CreateLayout 함수는 실제 MaterialParameters를 사용하여 호환되는 FShaderType, FVertexFactoryType을 모읍니다.
    4. 모든 ShaderType 중 Material과 MeshMaterial 타입의 ShaderType을 모읍니다.
    5. MaterialShaderType에 대해서 컴파일 가능 여부를 비교합니다.
    6. ShouldCompilePermutation 함수로 해당 MaterialShader의 Permutation 까지 모두 포함하여 컴파일해야 할 쉐이더로 모읍니다. 이런 FShaderType들은 Layout(FMaterialShaderMapLayout)의 Shaders에 저장됩니다.
    7. 이제 FVertexFactoryType에 대해서 컴파일 가능 여부를 비교합니다.
    8. FVertexFactoryType과 호환되는 MeshShaderType에 대해서 컴파일 가능 여부를 비교합니다.
    9. ShouldCompileVertexFactoryPermutation를 사용하여 현재의 FVertexFactoryType과 FShaderType이 조합되어 컴파일 가능한지 여부를 실제 판단합니다.
    10. 컴파일 가능한 경우 모든 Permutation에 대해서 쉐이더 컴파일 가능한지 확인하고 가능하면 FVertexFactory, FShaderType을 모아서 FMeshMaterialShaderMapLayout 클래스에 캡슐화하여 Layout의 MeshShaderMap에 보관합니다.

    그림30. 현재의 머터리얼과 같이 컴파일 될 수 있는 FMaterialShaderType과 FMeshMaterialShaderType을 찾는 함수


    이제 현재 Material과 호환되는 모든 쉐이더 조합들을 모았습니다. 이제 다시 SubmitCompileJobs 으로 돌아와서 계속 진행합니다.
    1. MeshMaterialShader의 경우 Layout.MeshShaderMap에 저장했습니다. 먼저 MeshMaterialShader에 대해서 컴파일을 수행합니다. FMeshMaterialShaderMap은 FMaterialShaderMap에 있으며, FVertexFactoryType을 Key로하여 FMaterialShaderMap로 부터 FMeshMaterialShaderMap을 얻을 수 있습니다.
    2. FVertexFactoryType과 조합될 수 있는 모든 FShaderType(FShaderType의 Permutation 포함)에 대해서 컴파일 가능 여부를 ShouldCache로 판단합니다.
    3. 컴파일 가능하고, ShaderMap에 캐싱되어있지 않다면 ShaderType->BeginCompileShader 를 수행합니다. 이 함수는 이름과는 다르게 컴파일을 수행하는 건 아니고 컴파일 환경을 준비하여 하나의 FShaderCompileJob으로 만들어줍니다.
    4. 계속해서 MaterialShader에 대해서 컴파일을 수행합니다. MaterialShader는 Layout.Shaders 에 저장했었습니다. 이 FShaderType 역시 ShouldCache로 컴파일 가능 여부를 판단하고 ShaderType->BeginCompileShader 로 컴파일할 쉐이더를 FShaderCompileJob에 캡슐화합니다.

    그림31. 이 머터리얼과 호환되는 FMaterialShaderType과 FMeshMaterialShaderType의 컴파일 가능 여부를 판단하고 쉐이더 컴파일 Job 준비

     

    3.3.6. 수집한 FMaterialShaderType과 FMeshMaterialShaderType(FShaderType + FVertexFactoryType)를 사용한 컴파일 환경 설정 캡슐화
    이제 ShaderType->BeginCompileShader 를 계속해서 봅시다. BeginCompileShader는 모든 쉐이더들을 개개별로 FShaderCompileJob을 만들어 캡슐화합니다. 이렇게 하여 이 Job을 워커스레드들이 가져가 컴파일하게 됩니다.
    1. PrepareMeshMaterialShaderCompileJob 이름 그대로 ShaderCompileJob을 만들어주는 함수입니다.
    2. FVertexFactoryType의 컴파일에 필요한 환경설정을 수행합니다. VertexFactory는 각각 자기만의 ush를 손수 구현해야 한다고 하였습니다. 여기서는 LocalVertexFactory를 사용하였기 때문에 #include "/Engine/Private/LocalVertexFactory.ush"를 include 하도록 합니다. 이 include는 /Engine/Generated/VertexFactory.ush 로 지정합니다. 그리고 계속해서 ModifyCompilationEnvironmentRef 콜백 함수를 호출하는데, 이것은 이전 글에서 봤었던, FShader에 구현된 스태틱 함수인 ModifyCompilationEnvironment에 해당합니다.
    3. ShaderType->SetupCompileEnvironment의 경우 ShaderType의 ModifyCompilationEnvironment를 호출하여 ShaderType의 컴파일 환경설정을 해줍니다.
    4. ::GlobalBeginCompileShader 에서는 쉐이더에서 사용하는 UniformBuffer들을 모두 찾아서 쉐이더 코드로 추가해주고, 최종적으로 컴파일해야 할 FShaderCompileJobs을 전역 변수인 FShaderCompilingManager에 추가합니다.

    그림32. ShaderType과 VertexFactoryType에 있는 컴파일 환경설정을 반영


    계속해서 ::GlobalBeginCompileShader를 봅시다. FShaderCompilerInput에 필요한 정보를 채워 컴파일을 할 수 있게 준비하는 것을 볼 수 있습니다. GlobalBeginCompileShader를 호출하고 나면 GShaderCompilerManager에 FShaderCompileJobs가 추가됩니다.
    1. ShaderType->AddReferencedUniformBufferIncludes 는 ShaderType이 참조하는 모든 UniformBuffers의 쉐이더 코드를 모으는 함수입니다.
    2. VFType->AddReferencedUniformBufferIncludes 는 VertexFactory가 참조하는 모든 UniformBuffers의 쉐이더 코드를 모으는 함수입니다.
    3. 이 ShaderType이 사용하는 모든 UniformBuffer의 정보는 ReferencedUniformBufferStructsCache에 저장되어있습니다. ReferencedUniformBufferStructsCache는 엔진이 초기화되는 시점에 설정됩니다. 그림33를 봐주세요. 이 UniformBuffer들을 include 할 수 있는 쉐이더 코드 형태로 변환합니다. 만약 이미 변환해둔 게 있다면 그것을 사용하고 아니면 CacheUniformBufferIncludes 함수를 호출해 ReferencedUniformBufferStructsCache 의 각 UniformBuffer의 Value(FString)에 UniformBuffer에 구조체에 대한 쉐이더 코드를 채워줍니다.
    4. 현재 보유하고 있는 ReferencedUniformBuffersStructsCache의 Key는 UniformBuffer의 이름입니다. 이것과 존재하는 모든 UniformBuffer들을 순회하면서 일치하는 이름을 찾습니다. 그리고 일치하는 것을 찾은 경우 CreateUniformBufferShaderDeclaration를 호출합니다. 이 함수가 바로 UniformBuffer의 구조체에 대한 쉐이더 코드를 생성해줍니다.
    5. 생성된 쉐이더 코드는 "/Engine/Generated/UniformBuffers/[UniformBufferName].ush"로 추가되며, "/Engine/Generated/GeneratedUniformBuffers.usf"에 추가됩니다. 추후에 GeneratedUniformBuffers.usf 만 include 하면 컴파일에 필요한 모든 UniformBuffer가 포함되어 있을 것입니다.
    6. FShaderParametersMetadata->AddResourceTableEntries를 호출합니다. FShaderParametersMetaData는 UniformBuffer의 리플렉션 타입입니다. 이 FShaderType이 소유하는 모든 UniformBuffer에 대해서 ResourceIndex를 붙여줍니다. UniformBuffer 에 다른 UniformBuffer 가 포함되어 있으면 그것들을 또한 모두 ResourceIndex 를 붙여줍니다. UniformBuffer 가 가진 모든 ResourceIndex를 0~N 까지 모두 붙여 ResoureceTable에 저장합니다. 그리고 개개별의 UniformBuffer는 UniformBufferMap에 FUniformBufferEntry로 추가해줍니다. 그림35 에 AddResourceTableEntries 의 구현을 볼 수 있습니다.

    그림33. ShaderType과 VertexFactoryType가 참조하고 있는 UniformBuffer의 쉐이더 코드 생성하고 컴파일할 파일로 include


    ShaderType, VertexFactoryType의 ReferencedUniformBufferStructsCache를 채우는 과정을 보여줍니다. 엔진이 초기화되는 시점에서 InitializeShaderTypes를 호출하여 모든 ShaderType, VertexFactoryType에 대해서 ReferecedUniformBufferStructsCache를 채웁니다.
    1. 쉐이더 파일 별로 참조하고 있는 Uniformbuffer를 모읍니다.
    2. ShaderType, VertexFactoryType 에 대해서 자신이 캐싱하고 있는 Uniformbuffer를 ReferencedUniformBufferStructsCache에 담아줍니다.

    그림34. 각각의 ShaderType과 VertexFactoryType이 갖고 있는 UniformBuffer들을 모으는 함수. 언리얼 엔진 구동시 호출됨.


    1. UniformBuffer가 가지고 있는 모든 UniformBuffer에 ResourceIndex를 붙여줍니다.
    2. 현재 UniformBuffer(FShaderParametersMetadata)의 정보를 UniformBufferMap에 추가합니다.

    그림35. UniformBuffer의 선언 순서에 따라서 바인딩 될 인덱스를 할당함

     

    3.3.7. 쉐이더 컴파일과 컴파일 완료된 쉐이더로부터 FShader 생성
    이제 FMeterialShaderMap::Compile 과정의 마지막인 GShaderCompilingManager->FinishCompilation 입니다. 여기서 컴파일 완료 및 FShader 인스턴스 생성합니다.
    1. 실제 그래픽스 API에 쉐이더 컴파일을 요청합니다.
    2. 컴파일된 쉐이더를 통해 FShader를 생성합니다.

    그림36. 만들어진 Job들을 통해 쉐이더를 컴파일하고 FShader를 생성하는 함수


    BlockOnShaderMapCompiletion 에서 실제 모든 컴파일 과정이 진행됩니다. 이 부분을 먼저 봅시다. 그래픽스 API 단까지 모든 호출 과정을 한 번에 봅시다. 호흡이 좀 깁니다.
    1. 쉐이더 컴파일을 수행합니다. 현재는 동기방식의 쉐이더 컴파일을 하고 있으므로 스레드 개수만큼 호출되는 것은 무시할 수 있습니다.
    2. 컴파일이 완료된 ShaderMap을 추가해줍니다.
    3. CompileDirectlyThroughDll -> FShaderCompileUtiles::ExecuteCompileJob 를 호출하여 한 개의 FShaderCompileJob을 컴파일하도록 합니다.
    4. 필요한 컴파일 환경을 모두 모읍니다.
    5. FShaderCompileJob의 Input과 Output 정보를 전달하여 쉐이더 컴파일을 실제로 수행시킵니다.
    6. 설정한 쉐이딩 모델과 플랫폼에 맞는 컴파일 함수가 호출됩니다. CompileShader_Windows->CompileD3DShader->CompileAndProcessD3DShaderDXC 까지 차례대로 호출합니다.
    7. DirectX 쉐이더 컴파일과 관련된 함수 포인터를 얻습니다. 여기서 중요한 부분은 쉐이더 컴파일을 통해 쉐이더 바이트 코드를 생성하는 부분과 쉐이더 코드로부터 리플렉션 정보를 얻는 부분입니다. D3DCompileWrapper 함수 호출을 통해 쉐이더 바이트 코드가 TRefCountPtr<ID3DBlob> Shader; 에 들어갑니다.
    8. 쉐이더 리플렉션 정보를 얻어내는 부분입니다.
    9. ExtractParameterMapFromD3DShader 함수를 통해 리플렉션 정보에서 필요한 정보를 얻습니다. Output.ParameterMap이 여기서 중요한 정보입니다. 다음 글에서 UniformBuffer와 리소스 바인딩에 대해서 알아볼 때 중요한 컨테이너입니다.
    10. 컴파일된 쉐이더 바이트 코드를 CompressedData에 담아서 GenerateFinalOutput 함수로 넘겨줍니다. GenerateFinalOutput 함수에서 Output에 필요한 모든 데이터를 담아줍니다.
    12. TRefCountPtr<ID3DBlob> CompressedData를 TArray<uint8> 형태로 Output.ShaderCode에 저장합니다.

    그림37. 쉐이더 컴파일 과정


    다시 GShaderCompilingManager->FinishCompilation으로 돌아가서, ProcessCompiledShaderMap을 호출합니다. 이제 컴파일 완료된 FShader 인스턴스를 생성합니다.
    1. 이제 컴파일을 완료한 ShaderMap 를 인자로 넘겨주면 이 ShaderMap 가 가진 ShaderCode를 기반으로 FShader 를 생성하여 캐싱합니다.
    2. 컴파일된 모든 ShaderMap 에 대하여 ProcessCompilationResults를 호출합니다.
    3. ProcessCompilationResults는 ProcessCompilationResultsForSingleJob를 통해 FShader 를 생성합니다. 계속해서 함수 안으로 들어가 봅시다.
    4. FMaterialShaderMap의 GetResourceCode 함수는 이전 글에서 봤던 FShaderMapResourceCode 객체를 돌려줍니다. 이 객체는 컴파일된 쉐이더 바이트 코드를 가지고 있다고 했었는데요. 여기서 컴파일된 쉐이더 바이트 코드가 추가되는 것을 볼 수 있습니다.
    5. VertexFactory가 있는 경우 MeshMaterialShader, 없는 경우는 MaterialShader가 되므로, VFType의 여부에 따라 적절한 함수를 호출합니다.
    6. ShaderType->FinishCompileShader를 호출합니다. 이 함수에서는 이제 실제로 FShader를 생성할 수 있도록 CompiledShaderInitializerType을 생성하여 컴파일 결과에 관한 정보를 만듭니다. 이 정보를 FShader의 생성자에 넘겨서 최종적으로 FShader 가 생성되도록 합니다.
    7. ConstructCompiled 함수는 FShader로 부터 전달 받은 FShader 생성에 관한 콜백 함수를 그대로 호출해줍니다.
    8. FShader의 생성자에서는 CompiledShaderInitializerType이 호출되는 것을 볼 수 있습니다.
    9. ParameterMap으로 이 쉐이더에 리소스 바인딩에 대한 바인딩 인덱스 정보가 담겨있습니다. 다음 글에서 자세히 볼 부분입니다.
    10. 모든 UniformBuffer 를 순회하면서 쉐이더 코드의 리플렉션 정보(ParameterMap)와 일치하는 UniformBuffer가 있는지 확인합니다. 리플렉션 정보와 일치하는 UniformBuffer를 찾는다면, 해당 UniformBuffer가 쉐이더 몇 번 인덱스에 바인딩될지 정보를 저장합니다. 이 부분은 다음 글에서 자세히 볼 것입니다.
    11. 만약 이 쉐이더가 FMaterialShader라면 추가적으로 FMaterialShader 생성자를 호출합니다.
    12. MaterialShader가 가지는 UniformBuffer를 쉐이더 코드의 리플렉션 정보를 참고해 적절한 바인드 인덱스를 설정합니다.
    13. 만약 이 쉐이더 FMeshMaterialShader라면 FShader와 FMaterialShader를 호출한 다음 최종적으로 FMeshMaterialShader를 호출할 것입니다. CreateParameters 함수는 FVertexFactory의 콜백 함수를 통해 쉐이더 코드의 리플렉션 정보를 참고하여 UniformBuffer등의 리소스에 바인딩 인덱스를 설정해줍니다.

    그림38. 컴파일된 결과로 부터 FShader 생성하여 캐싱

     

    여기까지가 쉐이더 컴파일 과정의 끝입니다. 마지막으로 쉐이더 컴파일 과정 설명중 추가로 알아보기로 한 CPU에서 계산하는 Material Editor 노드 연산을 알아봅시다.

     


    3.4. 상수 컬러 벡터와 파라메터 컬러 벡터의 곱셈을 평가하는 방식

    먼저 FHLSLMaterialTranslate::Translate 에서 BaseColor Pin을 평가하는 부분을 봅시다.
    1. 현재 Multiply 노드를 컴파일 중에 있습니다. 상수 컬러 벡터와 파라메터 컬러 벡터를 곱한 결과가 A에 있고 이것이 FMaterialUniformExpressionFoldedMath 입니다. 이 FoldedMath에 A, B에 들어가 있습니다. B에는 Texture Sampling 한 것이 있습니다.
    2. A와 B 두 개를 코드로 변환시키는 부분입니다. 우리는 FMaterialUniformExpressionFoldedMath 가 코드로 변화하는 과정만 보려고 합니다.
    3. FMaterialUniformExpressionFoldedMath가 Material.VectorExpressions[4].rgb로 변환됩니다. 이 VectorExpression[4]는 Uniformbuffer를 통해 쉐이더 외부에서 설정해줍니다. 그렇다면 노드의 일부 평가는 CPU에서 된다는 뜻입니다.

    그림39. 상수 벡터와 파라메터 벡터간의 곱을 컴파일 한 결과 새로운 파라메터 벡터를 대신 생성함

    계속해서 BaseColor Pin을 평가한 다음에 나오는 코드입니다. 평가 중에 Uniformbuffer로 사용될 벡터 파라메터들은 UniformVectorExpressions에 들어가 있습니다.
    1. MaterialCompilationOuput.UniformExpressionSet.UniformVectorPreshaders에 UniformVectorExpressions 정보를 넣습니다. 여기 들어간 것은 CPU 에서 평가합니다.

    그림40. Material Editor의 노드를 모두 쉐이더 코드로 바꾸고 그 과정에서 생성이 필요한 UniformVector들을 UniformBuffer에 추가함. UniformVectorPreshaders 컨테이너 들어간 노드는 추후 CPU에서 UniformBuffer을 채울때 평가함


    1. 우리가 위에서 봤던 쉐이더 컴파일 과정을 수행하는 함수입니다.
    2. 컴파일이 마쳐지면 Material 노드를 평가하면서 생성된 Uniformbuffer를 생성합니다.
    3. 렌더스레드 쪽에 있는 FMaterialRenderProxy에 UniformBuffer를 만들도록 CacheUniformExpressions 함수를 호출합니다.
    4. Uniformbuffer 생성을 예약합니다.
    5. 적당한 순간 UpdateDeferredCachedUniformExpressions 함수를 호출하여 생성합니다.
    6. 아직 평가전인 Preshaders에 들어가 있는 항목을 모두 평가한 뒤, FRHIUniformBuffer를 FMaterialProxy의 UniformExpressionCache->UniformBuffer에 생성합니다.
    7. FUniformExpressionSet은 위에서 본 것처럼 머터리얼 쉐이더 노드를 평가하면서 생성된 Uniformbuffer 버퍼 정보입니다. 이 구조를 기반으로 FUniformExpressionSet->FillUniformBuffer를 호출하여 TempBuffer에 Uniformbuffer의 데이터를 채웁니다.
    8. 벡터 파라메터 중 평가하지 않은 것들의 평가를 바로 여기서 수행합니다.
    9. 스칼라 파라메터 중 평가하지 않은 것들의 바로 평가를 여기서 수행합니다.
    10. Uniformbuffer의 값이 모두 채워지면 이제 Uniformbuffer를 생성 또는 업데이트합니다.

    그림41. Material에서 쉐이더를 모두 생성한 뒤 UniformBuffer를 채우는 과정


    EvaluatePreshader 함수에서 평가하지 않았던 노드들을 평가합니다. 우리가 쉐이더에서 평가하지 않았던 부분은 Material.VectorExpression[4].rgb 이었습니다. 그리고 이것은 상수 컬러 벡터와 파라메터 컬러 벡터의 곱입니다.
    1. 상수 벡터의 경우 여기서 값을 읽어냅니다. 상수 컬러는 녹색(0, 1, 0)이 었습니다.
    2. 파라메터 벡터의 경우 여기서 값을 읽어냅니다.
    3. 파라메터에 바인딩된 값이 없다면, 기본 값을 설정해줍니다. 기본값은 녹색(0, 1, 0)이 었습니다.
    4. 두 파라메터를 가지고 와서 Add, Sub, Mul 등의 연산을 수행합니다.

    그림42. UniformVectorPreshaders에서 두 피연산자(상수 벡터와 파라메터 벡터)를 얻고, 연산자를 얻어낸 후 연산을 수행함

     

    3.5. 결론

    1. Material Editor 에서 만들어진 쉐이더 노드들은 쉐이더 코드로 변환된 후 MaterialTemplate.ush 에 채워진다.

    2. MaterialTemplate.ush는 PixelShader 쪽에서 사용할 수 있게 서로 약속된 인터페이스와 구조체를 제공한다.

    3. VertexFactory는 VertexShader 쪽에서 사용할 수 있게 서로 약속된 인터페이스와 구조체를 제공한다.

    4. Material Editor 의 노드 들은 Parameter 형식의 타입들을 UniformBuffer의 변수들로 구성시켜준다.

    5. Material Editor 의 상수 노드와 Parameter 노드는 쉐이더 연산을 줄이기 위한 최적화를 위해서 CPU에서 채워지는 형태로 확장된다.

    6. 쉐이더의 컴파일은 FShaderCompileJob 이라는 구조에 쉐이더 필요한 Input과 컴파일 결과인 Output을 모두 담을 수 있게 캡슐화한다. 그리고 이 Job는 각각의 워커스레드가 하나씩 컴파일 한다.

    7. Material(Material.ush)과 같이 컴파일할 수 있는 모든 FMaterialShaderType, 그리고 FMeshMaterialShaderType(VertexFactoryType + MaterialType)와 조합하여 쉐이더를 컴파일 한다.

    8. 쉐이더 컴파일을 마치면 쉐이더 코드에 대한 리플렉션 정보 또한 얻을 수 있다.

    9. FShader를 생성하는 과정에는 UniformBuffer나 SRV, UAV, Texture 등과 같은 리소스들을 실제 쉐이더 코드의 바인딩 포인트로 매핑시켜 주는 과정이 포함된다.

     

    4. 레퍼런스

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

     

     

    댓글

Designed by Tistory & scahp.