SungJin Kang

SungJin Kang

hour30000@gmail.com

© 2024

Dark Mode

언리얼 엔진 쉐이더 변환 도구 ( hlslcc, ShaderConductor )

언리얼 엔진은 기본적으로 hlsl ( D3D의 쉐이더 언어 )로 작성된 우버 쉐이더 코드를 필요에 따라 여러 전처리 매크로들을 On, Off하여 사용한다. 여기서 우버 쉐이더란 필요한 기능이 전부 들어 있는 하나의 쉐이더 코드로 필요한 기능을 하나의 쉐이더 코드에 다 때려 넣고 전처리 매크로로 필요한 기능한 포함시켜 컴파일하는 것을 의미한다. ( 자세한건 여기를 참고하시길 바랍니다… )

쉐이더 클래스를 보면 ModifyCompilationEnvironment 함수를 통해서 앞서 말한 전처리 매크로를 ON/OFF하는 것을 볼 수 있다.
FSR
FSR2

그리고 우버 쉐이더 코드에서 나오는 수 많은 Permutation ( 전처리 매크로에 따라 분기되는 수 많은 쉐이더 코드 )들 중 어떤 분기를 실제로 컴파일해서 사용할지를 결정하는 코드도 볼 수 있다.
Shader
Shader2
Shader3


언리얼 엔진 매터리얼 에디터로 붙인 여러 매터리얼 노드들을 HLSLMaterialTranslator를 통해 위에서 말한 템플릿 코드와 조합하여 최종적은 HLSL 코드를 만들어낸다.
이렇게 만들어진 쉐이더 코드는 매터리얼 에디터에서 확인해볼 수 있다.

이렇게 만들어진 최종적인 hlsl 쉐이더 코드를 이제는 목표로 하는 타깃 그래픽 API의 쉐이더 코드로 변환하여야 한다.
언리얼 엔진에서는 크게 두 가지 방법을 사용한다. hlslcc를 사용하는 방법과 ShaderConductor를 사용하는 방법이다.
hlslcc를 사용하는 방법은 현재 UE5에서는 대부분의 그래픽 API에서 deprecated되었고 UE5에서는 대부분 ShaderConductor를 활용하는 방법을 사용한다.

아래의 사진이 hlslcc를 통한 방법, ShaderConductor를 통한 방법에서 어떻게 hlsl로 작성된 쉐이더 코드가 타깃 쉐이더 언어로 변환되는지 잘 보여준다.
hlslcc,dxc

아래 사진은 ShaderConductor의 구조를 보여준다.
ShaderConductor

hlslcc를 이용한 경우에는 hlsl로 작성된 쉐이더 코드를 hlslcc를 통해 Mesa IR(중간 언어)로 변환하고 이 변환된 IR를 최적화하는 과정을 거쳐서 최종적으로 타깃하는 쉐이더 언어로 변환된다.

ShaderConductor를 활용하는 방법의 경우 언리얼 엔진에서는 이 방법은 Shader Conductor라는 모듈로 관리하는데 Shader Conductor에서 DXC 컴파일러를 사용하여 hlsl 쉐이더 코드를 SPIRV라는 중간 언어로 변환한 후 SPIRV에 대해 SPIRV-Tools를 활용해 SPIRV를 최적화한 후 SPIRV-Cross를 사용하여 목표로 하는 쉐이더 언어로 변환한다.

쉐이더 코드를 모든 쉐이더 언어별로 전부 작성할 필요 없이 hlsl 하나로만 작성하면 타깃하는 쉐이더 언어로 자동으로 변환되기 때문에 매우 편리하다.
또한 중간 언어에서 타깃하는 쉐이더 언어로 변환 전 쉐이더 코드에 대한 최적화를 하기 때문에 성능적인 부분에서도 유리할 것이라고 생각된다.

정확히 해야하는 것이 hlslcc, ShaderConductor는 쉐이더를 컴파일하는 도구가 아닌 hlsl로 작성된 쉐이더 코드를 타깃하는 쉐이더 언어로 변환하는 도구이다.
이렇게 타깃 쉐이더 언어로 변환된 쉐이더 코드는 기기에서 컴파일된다. ( Metal의 경우 오프라인 컴파일러로 저수준 코드로 변환되기 전 중간 코드로 변환되어 빌드에 포함되고, OpenGL의 경우 고수준 glsl 코드가 필드에 포함되어 유저의 기기에서 컴파일된다. )


hlslcc는 거의 deprecated 되었으니 ShaderConductor쪽 코드를 살펴보겠다.
OpenGL을 타깃으로 빌드한다고 가정하고 쭉 살펴보자…

쿠킹시 우버 쉐이더에서 특정 Permutation을 기준으로 쉐이더 컴파일 Job을 추가한다.
1

타깃으로 하는 쉐이더 언어, 그래픽 API, 환경, 사용될 컴파일러 등등에 대한 전처리 매크로를 정의하는 동작들을 수행한다.
2

이제 ShaderCompilerWorker에서 실제 쉐이더 컴파일 동작 ( 정확히는 HLSL 코드를 타깃으로 하는 쉐이더 언어로 변환하는 작업 )을 수행한다.
13

앞서 말했듯이 여기서는 HLSL 코드를 GLSL ( OpenGL의 쉐이더 언어 )로 변환하는 과정을 살펴볼 것이다.
14

16

HLSL 쉐이더 코드에 대한 전처리 동작을 수행하고 ( 우버 쉐이더 코드에서 실제 사용될 코드들만 남은 HLSL 코드로 변환된다. ), ShaderConductor를 통해 실제 쉐이더 코드 변환 작업을 수행한다.
16_2

ShaderConductor에서는 HLSL 코드에 대해 SPIRV(중간 코드)로 변환하는 과정을 수행 ( HLSL 코드가 SPIRV로 변환되는 동작의 동작 원리는 여기서 볼 수 있다. )하고 변환된 SPIRV에 대한 리플랙션 데이터(버퍼 바인딩 등 다양한 용도로 사용된다)를 만들어낸다.
그리고 SPIRV 코드를 최종적으로 GLSL 코드로 변환한다. ( SPIRV-CROSS를 사용하여 GLSL 코드로 변환한다. )
HLSL 코드를 SPIRV로 변환하는 과정에는 변환된 SPIRV에 대한 최적화도 수행한다. ( SPIRV-TOOL을 사용하여 최적화 수행 )
17

references : https://github.com/EpicGames/UnrealEngine/tree/ue5-main/Engine/Source/ThirdParty/hlslcc/hlslcc, https://github.com/microsoft/DirectXShaderCompiler, https://www.slideshare.net/cagetu/gdc-14-bringing-unreal-engine-4-to-opengl, https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Rendering/ShaderDevelopment/HLSLCrossCompiler/, https://github.com/KhronosGroup/SPIRV-Cross , https://therealmjp.github.io/posts/shader-fp16/, https://chowdera.com/2021/08/20210802230911045t.html