본문으로 바로가기

Unreal Compute Shader

category Technical Report/Graphics Tech Reports 2024. 10. 23. 19:26
반응형

 

원문 링크 : https://zhuanlan.zhihu.com/p/1191215272?fbclid=IwZXh0bgNhZW0CMTAAAR382J5ZCNhVxk5iNoWpfQpE7noylftcXUCSBB6TorAKEFTXUMdYO4pYD_0_aem_VSiKtufUGVmXvIaRYBLOtQ

 

https://zhuanlan.zhihu.com/p/1191215272?fbclid=IwZXh0bgNhZW0CMTAAAR382J5ZCNhVxk5iNoWpfQpE7noylftcXUCSBB6TorAKEFTXUMdYO4pYD_0_aem_VSiKtufUGVmXvIaRYBLOtQ

知乎,让每一次点击都充满意义 —— 欢迎来到知乎,发现问题背后的世界。

zhuanlan.zhihu.com

 

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

1.1 Setting

먼저 LearnShader라는 플러그인을 만듭니다.

플러그인은 LearnShader.uplugin에 있는 일부 작업을 수행해야 합니다.

이는 구성 파일이 초기화된 후 즉시 모듈을 로드하도록 PostConfigInit을 구성하고 일부 다른 구성을 구성합니다.

  • Default : 기본 로딩 단계로, 대부분의 모듈이 이 단계에서 로드됩니다.
  • EarliestPossible : 가능한 한 빨리 모듈을 로드해야 합니다. 이 단계는 가능한 한 빨리 로드해야 하는 모듈에만 사용해야 합니다.
  • PostConfigInit : 구성 파일이 초기화된 후 즉시 모듈을 로드합니다.[각주:1]
  • 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. 파이프라인 호출
    당분간 소스 코드를 변경하지 않으므로 참조하지 않습니다.
  2. 블루프린트 호출
    블루프린트 호출은 블루프린트 함수에 의존해야 하며, 수정해야 할 사항이 많고, 블루프린트를 통해 호출하는 경우 호출할 곳이 많고 관리가 문제입니다.
  3. 엔진 하위 시스템
    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 export flag
class LEARNSHADER_API ULearnShaderSubsystem : public UEngineSubsystem
{
GENERATED_BODY()

private:
TSharedPtr<FSceneViewExtensionBase, ESPMode::ThreadSafe> CustomSceneViewExtension;

public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
//Executed when removed
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 셰이더에서 FColourExtractParamsParameters 구조를 사용할 수 있도록 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);
여기서는 이전에 정의한 FParameters를 볼 수 있으며, 여기서 사용됩니다.
소스와 타겟은 렌더링에 필요합니다. 여기서 렌더링 타겟은 어떻습니까? 하지만 반환 값은 어떻습니까? 다음 매개변수를 사용하여 작동할 수 있습니다.
// 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);
}

 

 

4. Render Target

하지만 결국 우리에게 필요한 것은 컴퓨터 셰이더입니다. 여기서 화면에 렌더링하는 것은 의미가 없으므로 렌더 타겟에 렌더링하고 여기에 데이터를 수용할 수 있습니다.

대부분의 콘텐츠를 동일한 방식으로 수행할 수 있습니다. 실제로 수정해야 할 콘텐츠는 거의 없습니다. 먼저 결과가 무엇인지 살펴보겠습니다.

 

4.1 Load Texture

먼저 위 내용을 로그아웃 하시고 콜을 취소하려면 다음 줄만 수정하시면 됩니다.

폴더는 다음과 같습니다. 보시다시피 차이가 없으므로 두 내용은 여기에 함께 적지 않겠습니다.


첫번째 Subsystem 입니다.


UCLASS()
class LEARNSHADER_API URenderTargetSubsystem : public UEngineSubsystem
{
             GENERATED_BODY()

private:
             TSharedPtr<FSceneViewExtensionBase, ESPMode::ThreadSafe> CustomSceneViewExtension;

public:
             virtual void Initialize(FSubsystemCollectionBase& Collection) override;
             //Executed when removed
             virtual void Deinitialize() override;
};


그런 다음 인스턴스화 중에 텍스처를 로드합니다.
파일 위치는 Project\Content\ComputerShader\XXX입니다.
스스로 자리를 찾을 수 있어요


//RenderTargetSubsystem.cpp
#include "RenderTargetSubsystem.h"
#include "RenderTargetSceneViewExtension.h"

void URenderTargetSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{

          Super::Initialize(Collection);

          CustomSceneViewExtension = FSceneViewExtensions::NewExtension<RenderTargetSceneViewExtension>();

          if(UTextureRenderTarget2D* RenderTarget = LoadObject<UTextureRenderTarget2D>(nullptr, TEXT("/Game/ComputerShader/RT_GroupUV")))
                    {

                    TSharedPtr<RenderTargetSceneViewExtension> RenderTargetExtension = StaticCastSharedPtr<RenderTargetSceneViewExtension>(CustomSceneViewExtension);

                    if (RenderTargetExtension.IsValid())
                              {
                              RenderTargetExtension->SetRenderTarget(RenderTarget);
                               }
                    }
}


void URenderTargetSubsystem::Deinitialize()
{
          Super::Deinitialize();
          CustomSceneViewExtension.Reset();
          CustomSceneViewExtension = nullptr;
}

Global Shader의 내용은 동일하며 이름만 변경되었습니다. 여기에는 두 가지 기능이 더 있습니다. 스크린샷을 찍을 때는 작성되지 않았지만 실제로 추가되었습니다.

모든 것이 기본적으로 동일합니다. Shader와 Usf를 바인딩하는 것을 잊지 마세요.

 

4.2 SceneViewExtension

RenderTargetSceneViewExtension으로 돌아가서 RenderTargetSource를 사용하여 들어오는 RnederTarget을 수락합니다.

동시에 재사용 가능한 RenderTarget을 구축하는데, 이를 주로 화면에 렌더링하고 이를 반복적으로 생성하는 것은 낭비입니다.

이 것이 여러 번 반복되는 그림에서 사용되는 것을 볼 수 있으므로 동일한 패턴을 따릅니다.


#pragma once
#include "SceneViewExtension.h"

class LEARNSHADER_API RenderTargetSceneViewExtension:public FSceneViewExtensionBase
{
private:
           TObjectPtr<UTextureRenderTarget2D> RenderTargetSource = nullptr;
           //Reuse RenderTarget
           TRefCountPtr<IPooledRenderTarget> PooledRenderTarget;

public:
           RenderTargetSceneViewExtension(const FAutoRegister& AutoRegister);
           virtual ~RenderTargetSceneViewExtension() 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 {};

           void SetRenderTarget(UTextureRenderTarget2D* RenderTarget);
private:
           //Default Requirement RenderThread
           void CreatePooledRenderTarget_RenderThread();
};

먼저 리소스를 사용할 수 있는지 확인하고 그렇지 않으면 반환하십시오. 이것은 진부한 표현입니다.


void RenderTargetSceneViewExtension::SetRenderTarget(UTextureRenderTarget2D* RenderTarget)
{
            RenderTargetSource = RenderTarget;
}


void RenderTargetSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
            FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, View, Inputs);

            if (RenderTargetSource == nullptr)
            {
                        return;
            }

            if (!PooledRenderTarget.IsValid())
            {
                              CreatePooledRenderTarget_RenderThread();
            }
}

 

4.3 CreatePool

UE에서는 UTextureRenderTarget2D 또는 다른 유형의 RenderTarget 객체 자체를 렌더링에 직접 사용할 수 없습니다.

이는 기본 렌더링 리소스를 작동하고 액세스하기 위한 몇 가지 메서드와 속성을 제공하는 컨테이너 또는 인터페이스와 비슷합니다. 따라서 여기서 처리해야 할 몇 가지 단계가 더 있습니다.


void RenderTargetSceneViewExtension::CreatePooledRenderTarget_RenderThread()
{
        //Whether in the rendering thread or RHI thread
        checkf(IsInRenderingThread() || IsInRHIThread(), TEXT("Cannot create from outside the rendering thread"));
        //Get the actual rendered Texture, and the rendering thread is required
const FTextureRenderTargetResource* RenderTargetResource = RenderTargetSource->GetRenderTargetResource();
        const FTexture2DRHIRef RenderTargetRHI = RenderTargetResource->GetRenderTargetTexture();
}
실제 렌더링된 텍스처를 가져온 다음 FPooledRenderTarget에서 사용할 FTexture2DRHIRef를 얻습니다. 그런 다음 풀링된 객체를 빌드할 수 있습니다.

Shader와 usf의 입력 및 출력과 풀링된 객체에는 생성되기 전에 수행할 수 있는 작업과 저장할 수 있는 작업을 설명하는 설명이 필요합니다.

반복 렌더링도 이와 같습니다.  한 번 렌더링할 때는 많은 것을 고려해야 합니다.


void RenderTargetSceneViewExtension::CreatePooledRenderTarget_RenderThread()
{
          //Whether in the rendering thread or RHI thread
          checkf(IsInRenderingThread() || IsInRHIThread(), TEXT("Cannot create from outside the rendering thread"));

          //Get the actual rendered Texture, and the rendering thread is required
          const FTextureRenderTargetResource* RenderTargetResource = RenderTargetSource->GetRenderTargetResource();

          if (RenderTargetResource == nullptr)
          {
                    UE_LOG(LogTemp, Warning, TEXT("Render Target Resource is null"));
          }
          const FTexture2DRHIRef RenderTargetRHI = RenderTargetResource->GetRenderTargetTexture();
          if (RenderTargetRHI.GetReference() == nullptr)
          {
                    UE_LOG(LogTemp, Warning, TEXT("Render Target RHI is null"));
          }


          //As input and output for rendering
          FSceneRenderTargetItem RenderTargetItem;
          RenderTargetItem.TargetableTexture = RenderTargetRHI;
          RenderTargetItem.ShaderResourceTexture = RenderTargetRHI;

          FPooledRenderTargetDesc RenderTargetDesc = FPooledRenderTargetDesc::Create2DDesc(
                    RenderTargetResource->GetSizeXY(), RenderTargetRHI->GetDesc().Format,
                    FClearValueBinding::Black,
                    TexCreate_RenderTargetable | TexCreate_ShaderResource | TexCreate_UAV, TexCreate_None, false);

          //Create a new pooled render target. The parameters of this method include the description object of the render target
          GRenderTargetPool.CreateUntrackedElement(RenderTargetDesc, PooledRenderTarget, RenderTargetItem);
}

이 함수가 무엇을 하는지 볼 수 있습니다. 아무 것도 하지 않는 것 같습니다. 단지 복사하는 것뿐입니다.

 

4.4 Render Target

시작 위치는 기본적으로 동일하며, 뷰포트와 Sence Texture를 가져옵니다.


void RenderTargetSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
      FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, View, Inputs);
      if (RenderTargetSource == nullptr)
      {
            return;
      }
      if (!PooledRenderTarget.IsValid())
      {
            CreatePooledRenderTarget_RenderThread();
      }

      checkSlow(View.bIsViewInfo);
      const FIntRect Viewport = static_cast<const FViewInfo&>(View).ViewRect;
      const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
      const FScreenPassTexture SceneColourTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);
}

그런 다음 풀링된 객체를 UE의 RDG에 등록한 다음 viewProt를 해당 크기로 설정합니다.


FRDGTextureRef RenderTargetTexture = GraphBuilder.RegisterExternalTexture(
PooledRenderTarget, TEXT("Bound Render Target"));

//Since we are rendering to a render target, we will use the full size of the render target, not the screen
const FIntRect RenderViewport = FIntRect(0, 0, RenderTargetTexture->Desc.Extent.X,
                                         RenderTargetTexture->Desc.Extent.Y);

그리고 이전처럼 진행합니다.


FRenderTargetPS::FParameters* Parameters = GraphBuilder.AllocParameters<FRenderTargetPS::FParameters>();
      Parameters->TextureSize = RenderTargetTexture->Desc.Extent;
      Parameters->SceneColorSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
      Parameters->SceneColorTexture = SceneColourTexture.Texture;
      Parameters->RenderTargets[0] = FRenderTargetBinding(RenderTargetTexture, ERenderTargetLoadAction::EClear);

      //Get the global Shader
      TShaderMapRef<FRenderTargetPS> PixelShader(GlobalShaderMap);
      //add to RDG

      //Start Rendering
      FPixelShaderUtils::AddFullscreenPass
      (
            GraphBuilder,
            GlobalShaderMap,
            FRDGEventName(TEXT("Render Target Pass")),
            PixelShader,
            Parameters,
            RenderViewport
      );

여기를 보세요

 

1. RenderTargetTexture와 PooledRenderTarget은 서로 관련되어 있습니다. RDG가 렌더링 작업을 수행할 때 렌더링 결과를 이 RenderTargetTexture에 기록합니다.

 

2. PooledRenderTarget은 기본적으로 RenderTargetSource의 래퍼이며 RenderTargetSource의 RHI 텍스처와 연결되어 있습니다. 따라서 렌더링 결과는 결국 우리의 RT로 반환됩니다.

 

 

5. FunctionLibrary

하지만 포스트 프로세싱을 제외하면 모든 프레임을 렌더링할 필요는 없습니다. 계산은 포스트 프로세싱이며, 끌 수 있는 시간이 있습니다. 따라서 블루프린트에서 렌더링을 호출하여 RT에 렌더링하고 RenderTargetSubsystem으로 돌아가서 코드의 등록을 해제할 수 있습니다.

 

5.1 RTLibrary

RTLibrary를 만들고 그 안에 코드를 작성합니다.

여기에 원하는 기능을 작성할 수 있습니다. 여기서는 RT에서 직접 색상을 그립니다. sceneTexture를 그리는 경우 일부 구성 요소의 도움이 필요하기 때문에 단계가 매우 길어집니다.


#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "RTLibrary.generated.h"

UCLASS()
class LEARNSHADER_API URTLibrary : public UBlueprintFunctionLibrary
{
      GENERATED_BODY()
      UFUNCTION(BlueprintCallable, Category = "Render Libreay", meta = (WorldContext = "WorldContextObject"))
      static void DrawRenderTarget_Function(UTextureRenderTarget2D* OutRenderTarget, const FLinearColor& Color, const UObject* World);
};

엔터티가 필요하지 않은 정적 함수이기 때문에 정적을 추가해야 하는데, 개인적으로 정적 함수인 _Function으로 끝나는 것이 익숙합니다. UE도 함수의 목적을 식별하기 위해 _RenderThread로 끝나는 습관이 있습니다.

블루프린트에서 이 함수를 호출할 수 있다는 것을 알 수 있습니다.

함수로 돌아가서 코드 작성을 시작하면 기본적으로 동일합니다.


#include "TextureResource.h"
#include "Engine/TextureRenderTarget2D.h"

void URTLibrary::DrawRenderTarget_Function(UTextureRenderTarget2D* OutRenderTarget, const FLinearColor Color, const UObject* WorldActor)
{
      check(IsInGameThread());

      if (!OutRenderTarget)
      {
            return;
      }
      FTextureRenderTargetResource* TextureRenderTargetResource = OutRenderTarget->GameThread_GetRenderTargetResource();
      ERHIFeatureLevel::Type FeatureLevel = WorldActor->GetWorld()->Scene->GetFeatureLevel();

      if (FeatureLevel < ERHIFeatureLevel::SM5)
      {
            return;
      }

      ENQUEUE_RENDER_COMMAND(DrawGlobalShaderTestCommand)(
            [ TextureRenderTargetResource, Color, FeatureLevel ] (FRHICommandListImmediate& RHICmdList)
            {
                  DrawRenderTarget_RenderThread(RHICmdList, TextureRenderTargetResource, Color, FeatureLevel);
            }
     );
}

DrawRenderTarget_RenderThread도 Static으로 설정해야 합니다. 이는 CPP로 작성되지 않았습니다.

 

5.2 RenderThread

먼저 RenderTargetTexture를 가져온 다음 해당 상태를 변환합니다.


void URTLibrary::DrawRenderTarget_RenderThread(FRHICommandListImmediate& RHICmdList,FTextureRenderTargetResource* OutRenderTargetResource, const FLinearColor& InColor,ERHIFeatureLevel::Type FeatureLevel)
{
      FRHITexture2D* RenderTargetTexture = OutRenderTargetResource->GetRenderTargetTexture();
      RHICmdList.Transition(FRHITransitionInfo(RenderTargetTexture, ERHIAccess::SRVMask, ERHIAccess::RTV));
}

렌더링하려면 RenderPass를 사용해야 합니다.

Pass가 렌더링을 종료하면 렌더링 결과를 텍스처에 저장해야 합니다.

FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::DontLoad_Store);
RHICmdList.BeginRenderPass(RPInfo, TEXT("Render Target Function"));
{}b

셰이더를 Grab하고 파이프라인 상태 설정을 시작하세요.


FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel);
TShaderMapRef<FRenderTargetPS> PixelShader(GlobalShaderMap);
...
GraphicsPSOInit.BoundShaderState.VertexShaderRHI = nullptr;
GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
...

 

하지만 VertexShaderRHI가 없으면 정점 데이터가 잘못될 수 있습니다. 정점 단계가 없으면 데이터를 model에서 Projection으로 변환할 수 없습니다.

그러면 조각 단계를 실행할 수 없습니다.

따라서 이 단계에 대한 많은 레퍼런스를 찾을 수 있도록 GetVertexShader를 추가해야 합니다. 공식 소스 코드를 직접 확인해 보겠습니다.

 

5.3 GPU Data

여기서는 공식적인 지침을 따르지만 GPU Skin과 같은 GPU 스크립트에서는 BEGIN_SHADER_PARAMETER_STRUCT의 매개변수 정의 방법을 사용하지 않고 LAYOUT_FIELD를 사용하는 것을 볼 수 있습니다.

Nanite와 같은 비교적 새로운 것들은 BEGIN_SHADER_PARAMETER_STRUCT에 의해 정의된 매개변수 구조를 사용합니다.

 

5.4 new and Old

LAYOUT_FIELD

특히 플랫폼 간 데이터 정렬을 보장해야 하는 경우 C++ 클래스 또는 구조체 멤버 변수를 정의하는 데 사용되는 매크로입니다. Unreal Engine의 반사 시스템과 통합되어 USTRUCT 및 UCLASS와 같은 유형을 지원합니다.

리플렉션과 고도로 통합되고, 클래스 및 구조의 멤버를 정의하는 데 적합하며, Unreal의 리플렉션 시스템과 상호 작용할 때 사용되는 C++ 측의 매개변수 정의를 제공하는 것이 더 좋습니다.

따라서 렌더링 파이프라인 지원이 그다지 강력하지 않습니다. GPU 리소스 바인딩, 레지스터 할당, 메모리 정렬과 같은 문제를 수동으로 처리해야 합니다.

 

------------------------

 

이것이 바로 LAYOUT_FIELD와 BEGIN_SHADER_PARAMETER_STRUCT를 혼합하여 사용하는 이유입니다.

그러나 실제로 당신은 찾을 것입니다

UE4 :

LAYOUT_FIELD(FShaderParameter, )는 이전 함수 메서드에서 정의되었으며, 이 메서드는 초기 UE 셰이더 시스템, 특히 당시 UE4 렌더링 파이프라인에서는 기본적으로 사용되었습니다.

UE4 이후 및 UE5 :

BEGIN_SHADER_PARAMETER_STRUCT는 Unreal Engine의 렌더 그래프 시스템에 도입된 최신 방법이므로 Nanite와 같은 새로운 곳에서만 볼 수 있습니다.

보다 복잡한 렌더링 장면에 적합할 뿐만 아니라 매개변수 바인딩, 정렬 및 리소스 관리도 자동으로 처리합니다. 특히 현대 렌더링 작업의 대규모 병렬 처리 및 복잡한 리소스 상호 작용에 적합합니다.

따라서 최신 RDG 시스템에서는 BEGIN_SHADER_PARAMETER_STRUCT를 사용하여 매개변수를 바인딩하는 것이 좋습니다.

여기서 사용하는 것은 이전에 BEGIN_SHADER_PARAMETER_STRUCT의 매개변수 바인딩을 배웠기 때문에 여기서는 이전 메서드를 직접 사용할 수도 있습니다. 즉, 함수 정의만 변경하면 됩니다.

이때 공식 GPU 및 Computer Shader 관련 기능을 찾아보면 매개변수 정의 방식이 수정된 것을 확인할 수 있습니다.

LAYOUT_FIELD에 의해 정의된 매개변수는 생성자에 바인딩되어야 합니다.


class FRenderTargetNewPS : public FGlobalShader
{
      DECLARE_SHADER_TYPE(FRenderTargetNewPS, Global);
public:
      FRenderTargetNewPS() {}
      FRenderTargetNewPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer)
      {
            FinalColor.Bind(Initializer.ParameterMap, TEXT("FinalColor"));
      }
}

새 렌더링과 이전 렌더링 모두 다음 함수를 사용하여 매개변수를 전달하므로 하나도 작성해야 합니다.


void SetParameters(FRHIBatchedShaderParameters& BatchedParameters, const FLinearColor& Color)
{
      SetShaderValue(BatchedParameters, FinalColor, Color);
}

대략적으로 이러한 코드만 수정하면 됩니다.

PS 아래에 직접 복사한 다음 PS를 VS로 변경합니다. 동시에 이전 하위 시스템의 렌더링에 영향을 주지 않기 위해 PS도 새 하위 시스템을 생성합니다.

 

5.5 usf

RenderTargetShader라는 usf를 직접 추가한 다음 Usf도 VS를 추가합니다. 여기서는 네 모서리 렌더링을 사용하지만 물론 큰 삼각형 렌더링으로 변경할 수도 있습니다.


#include "/Engine/Private/Common.ush"
#include "/Engine/Public/Platform.ush"


float2 TextureSize;
SamplerState SceneColorSampler;
Texture2D<float4> SceneColorTexture;
float3 TargetColor;
float4 FinalColor;

void MainVertexS(in uint VertexID : SV_VertexID, out float4 OutPosition : SV_Position)
{
      float2 positions[4] =
      {
            float2(-1.0f, -1.0f), 
            float2(-1.0f, 1.0f), 
            float2(1.0f, -1.0f),
            float2(1.0f, 1.0f)
      };

      OutPosition = float4(positions[VertexID], 0.0f, 1.0f);
}

float4 MainPixelS(float4 SvPosition : SV_POSITION) : SV_Target0
{
      return FinalColor;
}


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);
}

그런 다음 CPP의 밴딩으로 이동합니다.


//RenderTargetShader.cpp
#include "RenderTargetShader.h"
IMPLEMENT_SHADER_TYPE(, FRenderTargetNewVS, TEXT("/LearnShader/Private/RenderTargetShader.usf"), TEXT("MainVertexS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(, FRenderTargetNewPS, TEXT("/LearnShader/Private/RenderTargetShader.usf"), TEXT("MainPixelS"), SF_Pixel);

IMPLEMENT_SHADER_TYPE(, FRenderTargetPS, TEXT("/LearnShader/Private/RenderTargetShader.usf"), TEXT("MainPS"), SF_Pixel);

그런 다음 RnederPass에 바인딩하십시오.

이 부분에서는 실제로 UE의 소스 코드를 직접 복사할 수 있습니다.


        // Initialize and set the graphics pipeline state
        FGraphicsPipelineStateInitializer GraphicsPSOInit;
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
        GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
        GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
        GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
        GraphicsPSOInit.PrimitiveType = PT_TriangleStrip;
        GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
     SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);
     RHICmdList.SetViewport(0, 0, 0.f, OutRenderTargetResource->GetSizeX(), OutRenderTargetResource->GetSizeY(),
   1.f);
}

 

5.6 property

이제 매개변수를 할당한 다음, 그리고 마지막으로 매개변수 액세스를 복원할 수 있습니다.


void URTLibrary::DrawRenderTarget_RenderThread(FRHICommandListImmediate& RHICmdList, FTextureRenderTargetResource* OutRenderTargetResource, const FLinearColor& TextureColor, ERHIFeatureLevel::Type FeatureLevel)
{
    // Get the render target texture
    FRHITexture2D* RenderTargetTexture = OutRenderTargetResource->GetRenderTargetTexture();
    RHICmdList.Transition(FRHITransitionInfo(RenderTargetTexture, ERHIAccess::SRVMask, ERHIAccess::RTV));

    // Set render pass information
    FRHIRenderPassInfo RPInfo(RenderTargetTexture, ERenderTargetActions::DontLoad_Store);
    RHICmdList.BeginRenderPass(RPInfo, TEXT("Render Target Function"));
    {
        // Get the global shader map
        FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel);
        TShaderMapRef<FRenderTargetNewVS> VertexShader(GlobalShaderMap);
        TShaderMapRef<FRenderTargetNewPS> PixelShader(GlobalShaderMap);

        // Initialize and set the graphics pipeline state
        FGraphicsPipelineStateInitializer GraphicsPSOInit;
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
        GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
        GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
        GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
        GraphicsPSOInit.PrimitiveType = PT_TriangleStrip;
        GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
     SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0);

    
     RHICmdList.SetViewport(0, 0, 0.f, OutRenderTargetResource->GetSizeX(), OutRenderTargetResource->GetSizeY(),
   1.f);
        //Parameter assignment, parameter binding
     FRHIBatchedShaderParameters BatchedParameters = RHICmdList.GetScratchShaderParameters();
     PixelShader->SetParameters(BatchedParameters, TextureColor);
     RHICmdList.SetBatchedShaderParameters(PixelShader.GetPixelShader(), BatchedParameters);
    
        RHICmdList.DrawPrimitive(0, 2, 1);
    }
    RHICmdList.EndRenderPass();

    // Restore resource access permissions
    RHICmdList.Transition(FRHITransitionInfo(RenderTargetTexture, ERHIAccess::RTV, ERHIAccess::SRVMask));
}

 

 

 

6. Computer Shader

마지막으로 Computer Shader의 차례입니다. 사실 일반적인 단계는 비슷합니다. 먼저 효과를 살펴보겠습니다.

어쩌면 Zhihu 이미지 품질을 억제하여 볼 수 없을 수도 있습니다. 단어는 선명하지만 파란색 사각형은 흐릿합니다.

먼저 Shaders 및 LearnShader 아래에 폴더를 하나, usf용으로 하나, 메인 로직용으로 하나를 만듭니다.

 

6.1 usf

Computer Shader는 고전적인 퍼지 이미지 처리이므로 자세한 설명은 생략하겠습니다. 핵심 코드는 다음과 같습니다. 작성 방법을 모르신다면 먼저 읽어보세요.

https://zhuanlan.zhihu.com/p/714767350


#include "/Engine/Public/Platform.ush"
#include "/Engine/Private/Common.ush"

float4 BoxFilter_4Tap(Texture2D<float4> tex, SamplerState samplerTex, float2 uv, float2 PixelSize)
{
      float4 d = PixelSize.xyxy * float4(-1.0, -1.0, 1.0, 1.0);

      float4 s = 0;
      s = tex.SampleLevel(samplerTex, uv + d.xy, 0) * 0.25;  
      s += tex.SampleLevel(samplerTex, uv + d.zy, 0) * 0.25; 
      s += tex.SampleLevel(samplerTex, uv + d.xw, 0) * 0.25; 
      s += tex.SampleLevel(samplerTex, uv + d.zw, 0) * 0.25; 

      return s;
}

SamplerState SceneColorSampler;
Texture2D<float4> SceneColorTexture;
RWTexture2D<float4> OutputTexture;

[numthreads(16, 16, 1)]
void MainCS(uint3 DispatchThreadID : SV_DispatchThreadID) 
{
      uint2 TextureSize;
      SceneColorTexture.GetDimensions(TextureSize.x, TextureSize.y);
      const float2 UV = DispatchThreadID.xy / float2(TextureSize);
      const float2 PixelSize = 1.0 / float2(TextureSize) * 4;

      const float4 SceneColour = BoxFilter_4Tap(SceneColorTexture, SceneColorSampler, UV, PixelSize);
      OutputTexture[DispatchThreadID.xy] = SceneColour;
}

여기에서는 전체 텍스처의 크기를 얻은 다음 UV 크기와 픽셀 크기를 얻은 다음 픽셀을 사용하여 오프셋하고 상자 필터를 수행합니다.

여기서는 Iteration을 4로 씁니다.

여기에서 퍼지 코어 알고리즘을 볼 수 있습니다. 함수가 분해되어 있으므로 빠르게 직접 수정하고 사용할 컨볼루션을 선택할 수 있습니다.

https://zhuanlan.zhihu.com/p/125744132

 

6.2 Global Shader

Global Shader의 작성 방법은 기본적으로 변경되지 않았습니다. 모두 그렇게 정의되어 있으며 이미 익숙합니다. 결과를 저장하기 위해 여기서는 outputTex를 정의합니다.


#pragma once
#include "CoreMinimal.h"
#include "DataDrivenShaderPlatformInfo.h"
#include "PostProcess/PostProcessInputs.h"

BEGIN_SHADER_PARAMETER_STRUCT(FCSBlurParams,)
SHADER_PARAMETER_SAMPLER(SamplerState, SceneColorSampler)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture)
SHADER_PARAMETER_RDG_TEXTURE_UAV(Texture2D, OutputTexture)
END_SHADER_PARAMETER_STRUCT()

class FCSBlurShader: public FGlobalShader
{
      DECLARE_EXPORTED_SHADER_TYPE(FCSBlurShader, Global, );
      using FParameters = FCSBlurParams;
      SHADER_USE_PARAMETER_STRUCT(FCSBlurShader, FGlobalShader);

      static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
      {
            return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
      }

      static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
      {
            FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
            OutEnvironment.CompilerFlags.Add(CFLAG_AllowTypedUAVLoads);
      }
};

유일한 변경 사항은 CPP가 마침내 Pixel 및 Vertex Shader 대신 컴퓨터 Shader로 선언된다는 것입니다.


#include "CSBlurShader.h"
IMPLEMENT_SHADER_TYPE(, FCSBlurShader, TEXT("/LearnShader/ComputerShader/csShader.usf"), TEXT("MainCS"), SF_Compute);

 

6.3 SceneView

UEngineSubsystem의 내용은 여기에 기록되지 않습니다. 이름은 CSBlurSubsystem으로 변경됩니다.

핵심 로직은 여전히 ​​SceneView에 있습니다.


#pragma once

#include "CoreMinimal.h"
#include "SceneViewExtension.h"
#include "PostProcess/PostProcessing.h"
#include "UObject/Object.h"

class FCSBlurSceneViewExtension: public FSceneViewExtensionBase
{

public:
      FCSBlurSceneViewExtension(const FAutoRegister& AutoRegister);
      ~FCSBlurSceneViewExtension();

      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 {};

};

첫 번째는 일반적인 판단과 데이터 수집입니다.


DECLARE_GPU_DRAWCALL_STAT(ComputerShaderBlur);
void FCSBlurSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
      FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, View, Inputs);

      checkf(IsInRenderingThread() || IsInRHIThread(), TEXT("Cannot create from outside the rendering thread"));

      checkSlow(View.bIsViewInfo);
      const FIntRect Viewport = static_cast<const FViewInfo&>(View).ViewRect;
      const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);

      // Grab the scene texture
      const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(GraphBuilder, View, ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers);
      const FScreenPassTexture SceneColourTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);

      // RenderDoc debug information
      RDG_GPU_STAT_SCOPE(GraphBuilder, ComputerShaderBlur);
      RDG_EVENT_SCOPE(GraphBuilder, "Compute Shader Blur");
}

 

6.4 UAV

Compute Shader가 데이터를 병렬로 처리하기 때문에 쓰고 출력하는 것이 UAV(Unordered Access View)이므로 따라서 Compute Shader가 TempTexture를 직접 읽고 쓸 수 있도록 하는 UAV를 만들어야 합니다.

Compute Shader 는 런타임에 blur 효과를 생성하고 결과를 직접 작성할 수 있습니다. 그런 다음 이 blur 효과의 결과를 대상 텍스처에 복사하거나 후속 렌더링 단계에서 사용할 수 있습니다.

이제 OutputTexture가 그가 사용할 UAV를 전달합니다.


// Create a temporary texture to store the blurred result
FRDGTextureRef TempTexture = GraphBuilder.CreateTexture(SceneColourTexture.Texture->Desc, TEXT("TempTexture"));
FRDGTextureUAVRef TempUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(TempTexture));

// Set the shader parameters
FCSBlurShader::FParameters* Parameters = GraphBuilder.AllocParameters<FCSBlurShader::FParameters>();
Parameters->SceneColorSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
Parameters->SceneColorTexture = SceneColourTexture.Texture;
Parameters->OutputTexture = TempUAV; 
// The blurred result is stored in a temporary texture

 

6.5 Computer Shader

그런 다음 현재 뷰포트를 기반으로 필요한 스레드 그룹을 계산한 다음 AddPass를 호출합니다.

Compute Shader로 지정하고 스레드를 할당합니다.

그런 다음 색상을 SceneColourTexture.Texture에 다시 복사합니다.

다시 복사하고 싶지 않은 경우 bloom 작업과 같은 다른 작업을 추가할 수 있습니다.


DECLARE_GPU_DRAWCALL_STAT(ComputerShaderBlur);
void FCSBlurSceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
      FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, View, Inputs);

      checkf(IsInRenderingThread() || IsInRHIThread(), TEXT("Cannot create from outside the rendering thread"));

      checkSlow(View.bIsViewInfo);
      const FIntRect Viewport = static_cast<const FViewInfo&>(View).ViewRect;
      const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);

      // Grab the scene texture
      const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(GraphBuilder, View, ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers);
      const FScreenPassTexture SceneColourTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);

      // RenderDoc debug information
      RDG_GPU_STAT_SCOPE(GraphBuilder, ComputerShaderBlur);
      RDG_EVENT_SCOPE(GraphBuilder, "Compute Shader Blur");

      // Create a temporary texture to store the blurred result
      FRDGTextureRef TempTexture = GraphBuilder.CreateTexture(SceneColourTexture.Texture->Desc, TEXT("TempTexture"));
      FRDGTextureUAVRef TempUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(TempTexture));

      // Create a temporary texture to store the blurred result
      FCSBlurShader::FParameters* Parameters = GraphBuilder.AllocParameters<FCSBlurShader::FParameters>();
      Parameters->SceneColorSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
      Parameters->SceneColorTexture = SceneColourTexture.Texture;
      Parameters->OutputTexture = TempUAV; // The blurred result is stored in a temporary texture

      // Set the number of thread groups
      const FIntPoint ThreadCount = Viewport.Size();
      const FIntVector GroupCount = FComputeShaderUtils::GetGroupCount(ThreadCount, FIntPoint(16, 16));

      // Call the blur compute shader
      TShaderMapRef<FCSBlurShader> ComputeShader(GlobalShaderMap);
      FComputeShaderUtils::AddPass(GraphBuilder, 
        FRDGEventName(TEXT("Blur Compute Pass")),
        ERDGPassFlags::Compute, ComputeShader, 
        Parameters, GroupCount);

      // Copy the blurred result back to the scene color texture
      AddCopyTexturePass(GraphBuilder, TempTexture, SceneColourTexture.Texture);
}

 

6.7 groupshared

사실 위의 내용은 최적화가 가능한데, 실제로는 반복적으로 샘플링을 하기 때문에 굳이 할 필요가 없는 데이터가 많은데, 캐시가 가능하다면 반복적으로 샘플링할 필요가 없기 때문에 여기서는 공유 메모리를 사용합니다.

핵심 방법은 모든 픽셀을 미리 공유 메모리에 푸시한 다음 모든 데이터를 공유 메모리에서 직접 가져오는 것입니다.


#include "/Engine/Public/Platform.ush"
#include "/Engine/Private/Common.ush"

SamplerState SceneColorSampler;
Texture2D<float4> SceneColorTexture;
RWTexture2D<float4> OutputTexture;

// Define the size of the thread group
#define THREADS_X 16
#define THREADS_Y 16

// Define the shared memory size
groupshared float4 SharedColors[THREADS_X + 2][THREADS_Y + 2];

[numthreads(THREADS_X, THREADS_Y, 1)]
void MainCS(uint3 DispatchThreadID : SV_DispatchThreadID, uint3 GroupThreadID : SV_GroupThreadID, uint3 GroupID : SV_GroupID)
{
    uint2 TextureSize;
    SceneColorTexture.GetDimensions(TextureSize.x, TextureSize.y);
    float2 PixelSize = 1.0 / float2(TextureSize);

    // Load the center pixel into shared memory
    SharedColors[GroupThreadID.x + 1][GroupThreadID.y + 1] = SceneColorTexture.SampleLevel(SceneColorSampler, DispatchThreadID.xy * PixelSize, 0);

     // Load border pixels into shared memory
    if (GroupThreadID.x == 0) {
        SharedColors[0][GroupThreadID.y + 1] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(-1, 0)) * PixelSize, 0);
    }
    if (GroupThreadID.x == THREADS_X - 1) {
        SharedColors[THREADS_X + 1][GroupThreadID.y + 1] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(1, 0)) * PixelSize, 0);
    }
    if (GroupThreadID.y == 0) {
        SharedColors[GroupThreadID.x + 1][0] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(0, -1)) * PixelSize, 0);
    }
    if (GroupThreadID.y == THREADS_Y - 1) {
        SharedColors[GroupThreadID.x + 1][THREADS_Y + 1] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(0, 1)) * PixelSize, 0);
    }

    // Corner pixels
    if (GroupThreadID.x == 0 && GroupThreadID.y == 0) {
        SharedColors[0][0] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(-1, -1)) * PixelSize, 0);
    }
    if (GroupThreadID.x == THREADS_X - 1 && GroupThreadID.y == 0) {
        SharedColors[THREADS_X + 1][0] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(1, -1)) * PixelSize, 0);
    }
    if (GroupThreadID.x == 0 && GroupThreadID.y == THREADS_Y - 1) {
        SharedColors[0][THREADS_Y + 1] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(-1, 1)) * PixelSize, 0);
    }
    if (GroupThreadID.x == THREADS_X - 1 && GroupThreadID.y == THREADS_Y - 1) {
        SharedColors[THREADS_X + 1][THREADS_Y + 1] = SceneColorTexture.SampleLevel(SceneColorSampler, (DispatchThreadID.xy + uint2(1, 1)) * PixelSize, 0);
    }

    // Make sure all threads have finished loading the data in shared memory
    GroupMemoryBarrierWithGroupSync();

    // Perform BoxFilter operation
    float4 SceneColour = 0.25f * (
        SharedColors[GroupThreadID.x][GroupThreadID.y] +
        SharedColors[GroupThreadID.x + 2][GroupThreadID.y] +
        SharedColors[GroupThreadID.x][GroupThreadID.y + 2] +
        SharedColors[GroupThreadID.x + 2][GroupThreadID.y + 2]
    );

    // Write the result to the output texture
    OutputTexture[DispatchThreadID.xy] = SceneColour;
}

 

수학을 해보세요

최적화 전 : 총 1920×1080×4=8294400 전역 메모리 액세스가 필요합니다.

공유 메모리 : 1920×1080=2073600 전역 메모리 액세스 약 75%의 성능이 최적화됩니다.

컨볼루션 커널이 클수록 성능 향상도 커집니다.

그러니 얘야, 공유 메모리를 사용해 여기서는 풀링된 개체가 아직 사용되지 않습니다.

풀링된 객체를 사용하는 경우 최적화 및 하드웨어 가속 최적화가 있습니다.

동시에 두 개의 컴퓨터 셰이더가 하나의 usf에 사용되면 샘플링 없이 두 번째 컴퓨터 셰이더를 사용할 수 있어 직접적으로 75%를 절약할 수 있습니다.

 

이것이 바로 많은 샘플이 일반 셰이더보다는 Compuer 셰이더에 배치되는 경향이 있는 이유입니다.

 

 

7. StructureBuffer

컴퓨터 셰이더를 사용할 때는 반드시 렌더링에만 사용되는 것이 아니라 파괴 및 클러스터링과 같은 다른 목적에도 사용됩니다.

컴퓨터 셰이더를 사용하여 단일 객체를 계산한 다음 풀을 사용하여 객체를 재활용하고 생성할 수 있습니다.

빈번한 객체 생성 및 파괴, 빈번한 순회 추적 및 순회 설정이 빈번한 재활용 및 표시, 단일 추적 및 순회 설정으로 변경되었습니다.

이는 컴퓨터 셰이더와 풀의 장점을 더욱 반영합니다.

 

 

8. Math

빈번한 길 찾기 알고리즘에서 AI의 경우 컴퓨터 셰이더를 사용하여 A* 알고리즘 자체의 핵심을 실행한 다음 점을 무작위로 분산시키는 데 의존하는 대신 자유롭게 이동할 수 있습니다.

 

Ref :

Mao Xingyun: 고품질 후처리: 10가지 이미지 흐림 알고리즘 요약 및 구현

YivanLee: Unreal 4 렌더링 프로그래밍(셰이더) [2권: Unreal 4 셰이더 파이프라인 없이 자신만의 셰이더 사용]

  1. 이 설정이 없을경우 셰이더 초기화 시 충돌이 발생할 수 있다 [본문으로]
반응형