Unreal Compute Shader(작성중)
Compute Shader 란? : https://zhuanlan.zhihu.com/p/714767350
UE5 Plug-in 제작 방법 : https://zhuanlan.zhihu.com/p/898184891
UE5 Render Dependency Graph: https://dev.epicgames.com/documentation/en-us/unreal-engine/render-dependency-graph-in-unreal-engine?application_version=5.4
Github : https://github.com/onlyyz/UE5.4-Computer-Shader
1. Plugin
먼저 LearnShader라는 플러그인을 만듭니다.
플러그인은 LearnShader.uplugin에 있는 일부 작업을 수행해야 합니다.
이는 구성 파일이 초기화된 후 즉시 모듈을 로드하도록 PostConfigInit을 구성하고 일부 다른 구성을 구성합니다.
- Default : 기본 로딩 단계로, 대부분의 모듈이 이 단계에서 로드됩니다.
- EarliestPossible : 가능한 한 빨리 모듈을 로드해야 합니다. 이 단계는 가능한 한 빨리 로드해야 하는 모듈에만 사용해야 합니다.
- PostConfigInit : 구성 파일이 초기화된 후 즉시 모듈을 로드합니다.
- PostSplashScreen : 엔진의 스플래시 화면을 표시한 후 모듈을 로드합니다.
- PreLoadingScreen : 게임 로딩 화면을 표시하기 전에 모듈을 로드합니다.
- PostEngineInit : 엔진 초기화가 완료된 후 모듈을 로드합니다.
- PreDefault : 기본 단계 이전에 모듈을 로드합니다.
LearnShader.Build.cs를 이어 제작합니다
1.2 Module or Plugin
가상 경로를 정의하고 여기에 실제 셰이더 파일을 로드합니다.
#include "LearnShader.h" #include "Interfaces/IPluginManager.h" #define LOCTEXT_NAMESPACE "FLearnShaderModule" void FLearnShaderModule::StartupModule() { const FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("LearnShader"))->GetBaseDir(), TEXT("Shaders")); if(!AllShaderSourceDirectoryMappings().Contains(TEXT("/LearnShader"))) { AddShaderSourceDirectoryMapping(TEXT("/LearnShader"), PluginShaderDir); } } void FLearnShaderModule::ShutdownModule() { } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FLearnShaderModule, LearnShader) |
IMPLEMENT_MODULE
모듈 인스턴스를 구현하는 데 사용됩니다. 이 매크로는 글로벌 모듈 인스턴스를 생성하고 모듈 초기화 및 종료와 같은 모듈의 일부 기본 작업을 정의합니다.
마지막 코드 줄은 FLearnShaderModuleA 유형의 모듈 인스턴스를 생성하고 LearnShader로 이름을 지정합니다. 이 모듈 인스턴스는 엔진이 시작될 때 생성되고 엔진이 종료될 때 파괴됩니다.
그런 다음 Shaders 폴더를 만듭니다.
2. Execute
자신의 셰이더를 사용하는 전제는 Shader를 C++와 연관시켜야 한다는 것입니다. 따라서 FGlobalShader가 필요합니다.
이를 UE5와 HLSL 간의 바인딩 브리지로 생각해보세요.
하지만 렌더링은 반드시 보고 확인하고, 볼 수 없다면 실수가 있는지 알 수 없는 듯 합니다.
2.1 SceneView 확장
여기서는 SceneViewExtension을 사용합니다.
UE의 SceneViewExtension은 어느 정도 Unity의 RenderFeature와 유사합니다. 렌더링 파이프라인에 커스텀 렌더링 작업을 삽입하는 데에는 소스 코드를 수정할 필요가 없습니다.
소스 코드를 수정해야 하는 경우 파이프라인 소스 코드에 SceneViewExtension을 작성할 수 있습니다.
다른 모듈이 우리 코드를 사용할 수 있도록 하려면 클래스나 함수가 내보내질 컴파일러를 제어해야 합니다. 그렇지 않으면 우리가 쓴 플러그인이 닫히고 의미가 없어 보입니다.
따라서 ID가 필요합니다. 보통 [Plugin]_API 입니다
동시에 FSceneViewExtensionBase를 상속하게 하세요.
#pragma once #include "SceneViewExtension.h" class LEARNSHADER_API LearnShaderSceneViewExtension:public FSceneViewExtensionBase {}; |
오류가 있지만 문제는 크지 않습니다. 코드를 참조하고 FSceneViewExtensionBase의 함수를 살펴볼 수 있습니다. 무엇을 써야 할지 여기를 참조할 수 있습니다.
우리는 아마도 보완이 필요한 코드를 알고 있을 것입니다.
하지만 생성자만으로는 충분하지 않으므로 렌더링 스레드를 호출하는 함수가 있습니다.
FSceneViewExtensionBase에서 호출 전후에 함수가 있고, 각 렌더링 단계에 스레드 함수가 있는 것을 볼 수 있습니다. 더 많은 것이 더 나은 원칙이며, 모두 복사하여 어떤 함수를 사용할지 살펴볼 수 있습니다.
#pragma once #include "SceneViewExtension.h" class LEARNSHADER_API LearnShaderSceneViewExtension:public FSceneViewExtensionBase { public: LearnShaderSceneViewExtension(const FAutoRegister& AutoRegister); virtual ~LearnShaderSceneViewExtension() override; virtual void SetupViewFamily(FSceneViewFamily& InViewFamily) override; virtual void SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView) override; virtual void BeginRenderViewFamily(FSceneViewFamily& InViewFamily) override; virtual void PostRenderBasePassDeferred_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView, const FRenderTargetBindingSlots& RenderTargets, TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures) override {}; virtual void PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) override {}; virtual void PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView) override {}; virtual void PostRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView) override {}; virtual void PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs) override; virtual void PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) override {}; }; |
Followed by the CPP file
#include "LearnShaderSceneViewExtension.h" LearnShaderSceneViewExtension::LearnShaderSceneViewExtension(const FAutoRegister& AutoRegister): FSceneViewExtensionBase(AutoRegister){} LearnShaderSceneViewExtension::~LearnShaderSceneViewExtension(){} void LearnShaderSceneViewExtension::SetupViewFamily(FSceneViewFamily& InViewFamily){} void LearnShaderSceneViewExtension::SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView){} void LearnShaderSceneViewExtension::BeginRenderViewFamily(FSceneViewFamily& InViewFamily){} void LearnShaderSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs) { FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, View, Inputs); } |
렌더링 호출은 완료했지만, 호출에는 모두 SceneViewExtension을 사용합니다. 참조할 수 있는 세 가지 위치가 있습니다.
- 1. 파이프라인 호출
당분간 소스 코드를 변경하지 않으므로 참조하지 않습니다. - 블루프린트 호출
블루프린트 호출은 블루프린트 함수에 의존해야 하며, 수정해야 할 사항이 많고, 블루프린트를 통해 호출하는 경우 호출할 곳이 많고 관리가 문제입니다. - 엔진 하위 시스템
4.22 버전에 도입되었으며, 4.24에서는 완전한 모듈인 UEngineSubsystem 특수 하위 시스템 유형으로, 전역적이며 인스턴스가 하나뿐이며 엔진이 시작될 때 생성됩니다. 따라서 USubsystem은 게임이 시작될 때 자동으로 생성되고 초기화됩니다.
그 중 UEngineSubsystem은 UE에서 싱글톤으로 사용됩니다.
여기서 저는 Subsystem을 사용하기로 했습니다. 결국 싱글톤의 이점에 대해 더 이상 말할 필요가 없고, 별도의 플러그인과 모듈이 스케줄링에서 데이터와 기능을 관리하는 주요 서브시스템을 갖는 것이 합리적입니다.
이렇게 함으로써 플러그인의 모든 기능을 UEngineSubsystem에 통합하고, 플러그인과 모듈 간의 관계를 분리하고, 더 효율적으로 개발할 수 있게 됩니다.
기능을 제거하려면 해당 플러그인을 로드하고 해당 UEngineSubsystem을 수정하거나 삭제하기만 하면 됩니다.
여기서는 SceneViewExtension을 관리하기 위해 엔진 하위 시스템을 선택합니다.
엔진 하위 시스템에 대한 자세한 내용은 여기에서 기사를 참조하세요 : https://zhuanlan.zhihu.com/p/158717151
결국 모든 기능의 본질은 게임 서비스를 위한 것입니다. 일방적인 호출과 코드를 마음대로 고려하지 마세요. 결국 게임은 실행하자마자 전반적으로 충돌합니다.
2.2 Engine Subitem
엔진 하위 시스템 생성
#pragma once #include "CoreMinimal.h" #include "Subsystems/EngineSubsystem.h" #include "LearnShaderSubsystem.generated.h" class FSceneViewExtensionBase; UCLASS() //DLL导出标志 class LEARNSHADER_API ULearnShaderSubsystem : public UEngineSubsystem { GENERATED_BODY() private: TSharedPtr<FSceneViewExtensionBase, ESPMode::ThreadSafe> CustomSceneViewExtension; public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; //被移除时执行 virtual void Deinitialize() override; }; |
UEngineSubsystem 관리가 매우 간단합니다. 초기화될 때 생성되고, 제거될 때 비어 있을 때 파괴됩니다.
3. Global Shader
이제 입구를 완료했으니 Shader와 관련된 콘텐츠를 실제로 시작할 수 있습니다.
3.1 usf
usf를 작성하여 Shaders 폴더에 넣습니다.
#include "/Engine/Private/Common.ush" #include "/Engine/Public/Platform.ush" float3 TargetColour; Texture2D<float4> SceneColorTexture; float4 MainPS(float4 SvPosition : SV_POSITION) : SV_Target0 { const float4 SceneColour = SceneColorTexture.Load(int3(SvPosition.xy, 0)); const float3 MainColor = SceneColour.rgb * TargetColour; return float4(MainColor, 1.0); } |
3.2 Property
앞서 언급했듯이 Global Shader는 usf와 UE 사이의 브리지이므로 간단한 데이터 전송을 담당하고 사용할 usf를 지정해야 합니다.
Learn Shader라는 이름의 글로벌 셰이더를 만들어 Rendering 폴더 아래에 넣습니다. 앞으로는 다양한 기능을 가진 글로벌 셰이더도 해당 폴더에 넣을 것입니다.
usf에 변수를 전달해야 하므로 매개변수 구조를 정의해야 합니다.
BEGIN_SHADER_PARAMETER_STRUCT매크로는 구조를 정의하기 시작합니다.
END_SHADER_PARAMETER_STRUCT매크로 정의를 끝냅니다.
이 두 매크로 사이에서 다양한 SHADER_PARAMETER매크로를 사용하여 셰이더 매개변수를 정의할 수 있습니다.
이 구조는 주로 CPU와 GPU 간에 셰이더 매개변수를 전달하는 데 사용됩니다.
#pragma once #include "CoreMinimal.h" #include "DataDrivenShaderPlatformInfo.h" #include "SceneTexturesConfig.h" #include "PostProcess/PostProcessInputs.h" //Shader Property Struct BEGIN_SHADER_PARAMETER_STRUCT(FColourExtractParams,) // define color and texutre parameter SHADER_PARAMETER(FVector3f, TargetColour) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture) SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures) //Binding render targets at runtime RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() class FLearnShaderPS : public FGlobalShader { public: }; |
동시에, 이 구조는 여러 글로벌 셰이더에서 사용할 수 있다는 장점이 있습니다.
이제 Global Shader 작성을 시작할 수 있습니다.
3.3 Global Shader
글로벌 셰이더의 전체 코드는 다음과 같습니다.
//LearnShader.h #pragma once #include "CoreMinimal.h" #include "DataDrivenShaderPlatformInfo.h" #include "SceneTexturesConfig.h" #include "PostProcess/PostProcessInputs.h" //Shader Property Struct BEGIN_SHADER_PARAMETER_STRUCT(FColourExtractParams,) // define color and texutre parameter SHADER_PARAMETER(FVector3f, TargetColour) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture) SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures) //Binding render targets at runtime RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() class FLearnShaderPS : public FGlobalShader { public: //Generate shader type and serialize DECLARE_EXPORTED_SHADER_TYPE(FLearnShaderPS,Global,) using FParameters = FColourExtractParams; SHADER_USE_PARAMETER_STRUCT(FLearnShaderPS, FGlobalShader); //Determine whether the shader should be compiled in some cases static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5); } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); //SET_SHADER_DEFINE(OutEnvironment, YOUR_SHADER_MARCO, 0); } }; |
그중에서 DECLARE_EXPORTED_SHADER_TYPE의 정의를 볼 수 있습니다. 이 매크로는 셰이더 유형을 선언하는 데 사용되며, 여기서는 Global Shader로 선언됩니다.
동시에 GLobal 생성자, 소멸자, 직렬화 함수, 역직렬화 함수 등과 같은 일부 필수 코드가 생성됩니다. 이 예에서 LearnShaderPS는 선언된 셰이더 유형이고 Global은 셰이더의 분류입니다.
이 Global 셰이더에서 FColourExtractParams의 Parameters 구조를 사용할 수 있도록 FColourExtractParams 유형을 가리키는 유형 별칭 FParameters를 정의하고 FParameters를 통해 값을 할당할 수도 있습니다.
두 개의 헬퍼 함수를 정의합니다. 하나는 이 셰이더를 컴파일하기 위해 DX11 이상을 지원하는 것입니다.
하나는 전처리 명령을 처리하는 것입니다. SET_SHADER_DEFINE은 이 글로벌 셰이더에서 사용하는 usf의 매크로를 설정합니다.
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5); } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); //SET_SHADER_DEFINE(OutEnvironment, YOUR_SHADER_MARCO, 0); } |
다음과 같은 ModifyCompilationEnvironment 사용 :
하지만 여기서는 사용하지 않을 거예요
하지만 여기서는 Global Shader의 매개변수 구조, 매크로, 셰이더 유형만 정의했을 뿐 UE5와 usf 사이의 가교 역할을 하지는 않았습니다.
따라서 코드를 추가해야 합니다.
//LearnShaderPS.cpp #include "LearnShaderPS.h" //Register the shader type to the Unreal Engine's type list: IMPLEMENT_SHADER_TYPE(, FLearnShaderPS, TEXT("/LearnShader/Private/LShader.usf"), TEXT("MainPS"), SF_Pixel); |
IMPLEMENT_SHADER_TYPE
이 매크로는 다음 유형을 선언합니다.
매개변수는 선택적 템플릿 매개변수, 셰이더 클래스, 셰이더 파일, 입력 함수, 셰이더 유형입니다. Unreal Engine은 컴파일 타임에 이 Shader 유형을 Shader 시스템에 등록합니다. 런타임에 이 Shader를 사용해야 하는 경우 Unreal Engine은 이 Shader를 찾아 로드할 수 있습니다.
경로는 앞서 정의한 가상 경로를 사용하는 것입니다.
3.4 ViewExtension
이제 Global Shader를 호출하여 usf를 호출한 다음 렌더링할 수 있습니다. 따라서 SenceViewExtension을 반환해야 합니다.
DECLARE_GPU_DRAWCALL_STAT(ColourMix); |
GPU 그리기 호출의 통계를 처리하는 매크로를 정의합니다. Unreal Engine에서 통계는 CPU 및 GPU 사용, 메모리 사용 등을 포함하여 게임이 실행될 때의 성능에 대한 정보를 수집하고 보고하는 데 사용됩니다.
뷰포트를 가져옵니다. 그런데 이때 오류가 발생했다.
변환할 수 없음을 보여주지만 bool은 안전한 변환을 정의합니다. checkSlow 어설션이 성공하면 그런 문제는 없어야 합니다.
FivewInfo의 문제인지 참고할 수 있습니다.
FivewInfo가 Runtime\Renderer\Private 파일 아래에 위치해 있어서 접근할 수 없습니다.
3.5 Combine
"Renderer" 모듈의 "private" 하위 디렉토리를 가리키는 경로를 빌드할 수 있습니다. 이런 방식으로 액세스하려면 리플렉션을 사용하는 것이 너무 번거롭습니다.
Path.Combine(GetModuleDirectory("Renderer"), "private"), |
Ofrivate 파일을 포함할 수 있고 동시에 변환이 성공적으로 이루어지는 것을 볼 수 있습니다.
3.6 Get Shader
우선, 모든 글로벌 셰이더의 매핑 테이블을 잡아야 하고, 그를 통해 글로벌 셰이더를 잡아야 합니다.
void LearnShaderSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs) { FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, View, Inputs); checkSlow(View.bIsViewInfo); const FIntRect Viewport = static_cast<const FViewInfo&>(View).ViewRect; const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel); //Unreal Insights RDG_GPU_STAT_SCOPE(GraphBuilder,ColourMix); // RenderDoc RDG_EVENT_SCOPE(GraphBuilder,"Colour Mix"); } |
두 개의 매크로가 동시에 정의됨
RDG_GPU_STAT_SCOPE
렌더링 중 GPU 성능 통계 삽입
DECLARE_GPU_DRAWCALL_STAT
렌더링 중 디버그 이벤트 삽입. 여기서는 GPU 디버거(예: NVIDIA Nsight 또는 RenderDoc)에서 볼 수 있는 "Color Mix"라는 이벤트를 삽입합니다.
3.7 Shader Bind
먼저 장면의 색상을 잡으세요. 색상은 다음과 같습니다.
// Grab the scene texture const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(GraphBuilder, View, ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers); // This is the color that actually has the shadow and shading const FScreenPassTexture SceneColourTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport); |
그런 다음 Bridge Global Shader를 통해 usf로 전달합니다.
//Set Global Shader data and allocate memory FLearnShaderPS::FParameters* Parameters = GraphBuilder.AllocParameters<FLearnShaderPS::FParameters>(); Parameters->SceneColorTexture = SceneColourTexture.Texture; Parameters->SceneTextures = SceneTextures; Parameters->TargetColour = FVector3f(1.0f, 0.0f, 0.0f); |
// Set RenderTarget and Return texture Parameters->RenderTargets[0] = FRenderTargetBinding((*Inputs.SceneTextures)->SceneColorTexture, ERenderTargetLoadAction::ELoad); |
그런 다음 렌더링을 시작합니다. 여기서 우리는 Shader를 참조해야 하며, 이는 앞서 정의한 GlobalShaderMap을 통해 우리에게 변환될 수 있습니다.
FLearnShaderPS
TShaderMapRef<FLearnShaderPS> PixelShader(GlobalShaderMap); FPixelShaderUtils::AddFullscreenPass(GraphBuilder, GlobalShaderMap, FRDGEventName(TEXT("Colour Mix Pass")), PixelShader, Parameters, Viewport); |
우리는 usf를 수정하여 효과를 볼 수 있습니다
float3 TargetColour; Texture2D<float4> SceneColorTexture; float4 MainPS(float4 SvPosition : SV_POSITION) : SV_Target0 { const float4 SceneColour = SceneColorTexture.Load(int3(SvPosition.xy, 0)); float3 FinalColor = SceneColour.rgb * TargetColour; uint width, height; SceneColorTexture.GetDimensions(width, height); float2 quarterSize = float2(width, height) / 2.0; float mask1 = step(quarterSize.x, SvPosition.x) * step(quarterSize.y, SvPosition.y); float mask2 = step(SvPosition.x, quarterSize.x) * step(SvPosition.y, quarterSize.y); float mask = mask1 + mask2; return lerp(float4(SceneColour), float4(FinalColor, 1.0), mask); } |