언리얼 엔진 4에서 글로벌 셰이더는 C++ 면에서 포스트 프로세싱 이펙트를 렌더링하고, 계산 셰이더를 디스패치하고, 화면을 깔끔하게 치우는 등에 사용할 수 있는 셰이더입니다(즉 머티리얼이나 메시에서는 작동하지 않는 셰이더입니다). 가끔씩 원하는 그래픽을 내기 위해서는 좀 더 고급 기능이 필수적이며, 커스텀 셰이더 패스가 필요합니다. 이 작업은 비교적 간단하며 여기에서 설명해 보겠습니다.
언리얼 셰이더 파일과 그 사용법
언리얼 엔진은 Engine/Shader 폴더에서 .usf(Unreal Shader Files - 언리얼 셰이더 파일) 파일을 읽어들입니다. 모든 새 셰이더는 소스 파일을 여기에 두어야 합니다. 또한 4.17부터 플러그인(Plugin/Shaders)에서도 셰이더를 읽어들일 수 있습니다. 저는 개발의 편의를 위해 ConsoleVariables.ini 파일에서 r.ShaderDevelopmentMode=1를 활성화 할 것을 추천합니다. 이 문서를 확인해 더 많은 정보를 얻어보시기 바랍니다.
여러분의 Engine/Shaders 폴더에 새 .usf 파일을 추가하는 것으로 시작해 보겠습니다. 이 파일은 MyTest.usf라고 이름을 지어 주겠습니다. 그런 다음 단순 패스-스루(pass-through) 버텍스 셰이더(Vertex Shader)와 커스텀 컬러를 반환하는 픽셀 셰이더를 추가합니다:
// MyTest.usf // 단순 패스-스루 버텍스 셰이더 void MainVS( in float4 InPosition : ATTRIBUTE0, out float4 Output : SV_POSITION ) { Output = InPosition; } // Simple solid color pixel shader float4 MyColor; float4 MainPS() : SV_Target0 { return MyColor; }
이제 언리얼 엔진 4가 셰이더 컴파일을 시작하게 하려면, C++ 클래스를 선언해야 합니다. 버텍스 셰이더를 가지고 시작해 보겠습니다:
// 이것은 헤더나 cpp 파일로 갈 수 있음 class FMyTestVS : public FGlobalShader { DECLARE_EXPORTED_SHADER_TYPE(FMyTestVS, Global, /*MYMODULE_API*/); FMyTestVS() { } FMyTestVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { } static bool ShouldCache(EShaderPlatform Platform) { return true; } };
여기에는 몇 가지 요구사항이 있습니다:
- 이것은 FGlobalShader의 서브클래스입니다. 따라서 Global Shader Map에서 끝날 것입니다(즉 이것을 찾을 머티리얼이 필요하지 않다는 뜻입니다).
- DECLARE_EXPORTED_SHADER_TYPE() 매크로 사용은 셰이더 유형 등의 직렬화에 필요한 익스포트를 생성할 것입니다. 세번째 파라미터는 필요할 경우 셰이더 모듈이 라이브될 코드 모듈로의 외부 연결을 위한 유형입니다(예를 들어 렌더러 모듈 내에서 라이브 되어 있지 않은 C++ 코드).
- 생성자 두 개, 둘 다 기본 설정 및 직렬화
- 이 셰이더가 특정 상황에서 컴파일 되어야 할지 결정하는 데 필요한 ShouldCache() 함수(예를 들면 비-계산 셰이더 가능 RHI(non-compute shader capable RHI)에는 계산 셰이더(compute shader)를 컴파일하고 싶지 않을 수도 있습니다.
클래스를 선언했으니, 이제 언리얼 엔진 4의 리스트에 셰이더 유형을 등록할 수 있습니다:
// 이것은 cpp 파일로 가야 함 IMPLEMENT_SHADER_TYPE(, FMyTestVS, TEXT("MyTest"), TEXT("MainVS"), SF_Vertex);
이 매크로는 .usf 파일(MyTest.usf), 셰이더 입력 포인트(MainVS), 그리고 빈도/셰이더 단계(SF_Vertex)에 유형(FMyTestVS)을 매핑합니다. 또한 셰이더의 ShouldCache() 메소드가 true를 반환하는 한 이 셰이더가 컴파일 리스트에 추가되도록 하기도 합니다.
참고: 어떤 모듈에 FGlobalShader를 추가하든 반드시 엔진이 실제로 켜지기 전에 불러와야 하며, 그렇지 않으면 “Shader type was loaded after engine init, use ELoadingPhase::PostConfigInit on your module to cause it to load earlier(셰이더 유형이 초기 엔진 이후에 로드되었습니다. ELoadingPhase::PostConfigInit를 사용해 귀하의 모듈이 보다 빨리 로드되도록 하십시오).” 같은 안내를 보게 됩니다. 우리는 현재 게임이나 에디터가 실행된 후에 동적 모듈을 불러와 자신의 셰이더 유형을 추가하는 것을 허용하고 있지 않습니다.
이제 더 흥미로운 픽셀 셰이더를 선언해 봅시다:
class FMyTestPS : public FGlobalShader { DECLARE_EXPORTED_SHADER_TYPE(FMyTestPS, Global, /*MYMODULE_API*/); FShaderParameter MyColorParameter; FMyTestPS() { } FMyTestPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { MyColorParameter.Bind(Initializer.ParameterMap, TEXT("MyColor"), SPF_Mandatory); } static void ModifyCompilationEnvironment(EShaderPlatform Platform, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Platform, OutEnvironment); // 셰이더 코드에 자신의 정의를 추가하세요 OutEnvironment.SetDefine(TEXT("MY_DEFINE"), 1); } static bool ShouldCache(EShaderPlatform Platform) { // 플랫폼 컴파일을 생략할 수 있습니다 == 예를 들면 SP_METAL return true; } // FShader 인터페이스 virtual bool Serialize(FArchive& Ar) override { bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar); Ar << MyColorParameter; return bShaderHasOutdatedParameters; } void SetColor(FRHICommandList& RHICmdList, const FLinearColor& Color) { SetShaderValue(RHICmdList, GetPixelShader(), MyColorParameter, Color); } }; // 예전과 같은 소스 파일이지만, 입력 포인트가 다릅니다 IMPLEMENT_SHADER_TYPE(, FMyTestPS, TEXT("MyTest"), TEXT("MainPS"), SF_Pixel);
이 클래스에서는 이제 .usf 파일에서 셰이더 파라미터MyColor를 노출시키고 있습니다:
- 런타임에 바인딩을 찾을 수 있게 하는 정보를 가진 FShaderParameter MyColorParameter 멤버가 클래스에 추가되어, 파라미터의 값이 런타임에 설정될 수 있게 합니다.
- 우리는 직렬화 생성자에서 파라미터를 ParameterMap에 이름으로 Bind() 했으며, 이것은 .usf 파일의 이름과 일치해야 합니다.
- 새 ModifyCompilationEnvironment() 함수는 같은 C++ 클래스가 다른 비헤이비어를 정의하고 셰이더 내 #define 값을 구성할 수 있을 때 사용됩니다.
- Serialize() 메소드가 필요합니다. 이것은 (직렬화 생성자에서 맞춰진)셰이더의 바인딩에서 시간 정보를 컴파일/쿠킹해 로드한 다음 런타임에 저장한 것입니다.
- 마침내, MyColor 파라미터를 런타임에서 지정값과 함께 어떻게 설정할지 보여주는 커스텀 SetColor() 메소드가 생겼습니다.
이제 이 셰이더 유형을 사용해 전체 화면 쿼드를 그릴 간단한 함수를 작성합시다:
void RenderMyTest(FRHICommandList& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, const FLinearColor& Color) { // 글로벌 셰이더 콜렉션을 구함 auto ShaderMap = GetGlobalShaderMap(FeatureLevel); // 셰이더맵에서 실제 셰이더 인스턴스를 없앰 TShaderMapRefMyVS(ShaderMap); TShaderMapRef MyPS(ShaderMap); // 해당 셰이더를 사용하는 바운드 셰이더 상태를 선언하고 명령 리스트에 적용함 static FGlobalBoundShaderState MyTestBoundShaderState; SetGlobalBoundShaderState(RHICmdList, FeatureLevel, MyTestBoundShaderState, GetVertexDeclarationFVector4(), *MyVS, *MyPS); // 파라미터를 구성하도록 우리 함수를 호출 MyPS->SetColor(RHICmdList, Color); // 입체 쿼드를 그리기 위해 GPU를 프렙에 구성 RHICmdList.SetRasterizerState(TStaticRasterizerState ::GetRHI()); RHICmdList.SetBlendState(TStaticBlendState<>::GetRHI()); RHICmdList.SetDepthStencilState(TStaticDepthStencilState ::GetRHI(), 0); // 버텍스 구성 FVector4 Vertices[4]; Vertices[0].Set(-1.0f, 1.0f, 0, 1.0f); Vertices[1].Set(1.0f, 1.0f, 0, 1.0f); Vertices[2].Set(-1.0f, -1.0f, 0, 1.0f); Vertices[3].Set(1.0f, -1.0f, 0, 1.0f); // 쿼드를 그림 DrawPrimitiveUP(RHICmdList, PT_TriangleStrip, 2, Vertices, sizeof(Vertices[0])); }
이것을 여러분의 코드 베이스에서 테스트해보고 싶다면, 콘솔 변수가 런타임에서 토글될 수 있도록 다음과 같이 선언해볼 수 있습니다:
static TAutoConsoleVariableCVarMyTest( TEXT("r.MyTest"), 0, TEXT("Test My Global Shader, set it to 0 to disable, or to 1, 2 or 3 for fun!"), ECVF_RenderThreadSafe ); void FDeferredShadingSceneRenderer::RenderFinish(FRHICommandListImmediate& RHICmdList) { [...] // *** // 렌더링을 끝내기 직전에 삽입된 코드로, 화면의 콘텐츠를 덮어쓸 수 있음! int32 MyTestValue = CVarMyTest.GetValueOnAnyThread(); if (MyTestValue != 0) { FLinearColor Color(MyTestValue == 1, MyTestValue == 2, MyTestValue == 3, 1); RenderMyTest(RHICmdList, FeatureLevel, Color); } // 삽입된 코드 끝 // *** FSceneRenderer::RenderFinish(RHICmdList); [...] }
이 시점에서 여러분은 새 글로벌 셰이더를 테스트해볼 수 있을 것입니다! 프로젝트를 실행하고, ~(물결표) 키를 눌러 콘솔을 불러낸 다음 r.MyTest 1를 입력하세요. 그 다음 r.MyTest 2을 입력하거나 r.MyTest 3을 입력해 색깔을 바꿔 보세요. 패스를 비활성화 하려면 r.MyTest 0를 사용하세요.
생성된 소스 디버깅
usf 파일 컴필레이션 디버그 및/혹은 프로세스된 파일 확인이 가능하게 만들고 싶다면, 쉐이더 컴파일 프로세스 디버깅 하기 블로그를 확인하시기 바랍니다.
추가!
여러분은 쿠킹되지 않은 게임/에디터가 실행중인 동안 .usf 파일을 수정할 수 있으며, 그런 다음 Ctrl+Shift+. (마침표)을 누르거나 recompileshaders changed를 콘솔에 입력해 빠른 반복 작업을 위해 셰이더를 다시 빌드할 수 있습니다!
즐겨 보세요!
전문 용어 설명
글로벌 셰이더(Global Shaders) - 머티리얼 에디터를 사용해 만들어지지 않은 셰이더를 말하며, 대표적으로 컴퓨트 셰이더, 포스트 프로세싱 셰이더 등이 있습니다. 이것은 메타 유형 중 하나(예: 글로벌, 머티리얼, 메시)로 남은 메타 유형은 나중에 다루어질 것입니다.
FShaderType - 셰이더 코드로 지정된 ‘템플릿’이나 ‘클래스’로, 코드에서 지정된 피지컬 C++ 클래스에 매핑됩니다.
FShaderResource - 컴파일된 셰이더 마이크로 코드와 그 런타임 RHI 리소스입니다.
FShader - FShaderType 컴파일된 셰이더 인스턴스로, FShaderResource의 정보로부터 빌드됩니다.
TShaderMap - 다양한 FShaderType의 셰이더를 같은 메타 유형(Global)끼리 모아둔 콜렉션입니다.