TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] UE5 커스텀 메시패스 추가하기

jplee 2024. 7. 9. 13:17

 
저자: 张氏男子(장씨남자)
UE4에 대해 많은 큰 형님이 기사를 작성하고 프로그램의 매우 구체적인 구현을 제공하기 전에 메시 패스를 달성하는 방법에 대해, 그래서 나는 다시 한번 달성하기 위해 UE5에서 큰 형님의 기사와 아이디어를 참조, UE5 몇 가지 변경 사항,이 문서는 프로세스 및 아이디어의 구현을 기록하는 데 사용됩니다.

메시 바디 드로잉 파이프라인  언리얼 오픈데이 2019 의 새로운 드로잉 프로세스로 알려진 UE4.22 에 따르면 다음과 같습니다.

아마도 렌더링 스레드에 해당하는 프리미티브의 모든 게임 스레드는  프리미티브 씬 프록시인 경우 GetDynamicMeshElements() 함수에 의해 메시 배치로 변환된 다음 메시 패스 프로세서의 AddMeshBatch() 메서드를 호출할 것임을 의미할 것입니다.이 메서드도 커스텀으로 메시패스프로세서에서 상속된 다음 FMeshPass프로세서 클래스로 변환됩니다.그런 다음 최종적으로 AddMeshBatch () 내부에 Process () 함수(사실 자체적으로 모두 가지고 있지는 않지만 AddMeshBatch에는 있어야 함)가 있고, 이 함수는 BuildMeshDrawCommands ()를 호출하며, 이 함수는 최종적으로 SubmitMeshDarwCommands (), 즉 렌더링 명령 세트를 최종적으로 제출합니다.

메시패스를 추가하는 방법은 동적과 정적 두 가지가 있습니다.
정적인 것이 더 빠릅니다.여기서는 주로 UE5에서 스태틱을 구현하는 방법에 초점을 맞추었으며, UE5의 SkyPass 및 애니소트로피 패스(키워드: EMeshPass::애니소트로피 패스, EMeshPass::스카이패스)를 참고하시면 됩니다.
우선 메시패스를 만들려면 구현할 클래스가 3개 이상 있어야 합니다.:
1.메시 머티리얼 셰이더에서 상속된 VS
2.메시 머티리얼 셰이더에서 상속된 PS
3.메시 패스 프로세서의 커스텀 메시 패스 프로세서 이어가기

여기 세 가지 클래스를 구현하는 방법에 대해 많은 좋은 말씀이 있었습니다.

다음은 동적 및 정적 모두에 필요한 리소스이며, 정적인 경우 이 메시패스를 EMeshPass에 바인딩해야 합니다.

여기서 메시패스도 메시패스 열거형에 추가로 추가해야 합니다.

이 메시 패스를 MarkRelevant() 함수와 ComputeDynamicMeshRelevance() 에 추가하면 이제 단순히...↓ 

 

BasePassRendering.cpp 에서 메시 패스를 원하는 곳에 삽입하면 되는데, UE4 가 셰이더 빌드 함수에서 UniformBuffer 를 바인딩하던 이전과 달리 UE5 의 SetInstancedViewUniformBuffer() 는 더 이상 사용되지 않으므로 다음을 사용하여 전달해야 합니다.GetShaderParameters() 를 사용하여 전달해야 하는데, 그게 좋은지 아닌지는 잘 모르겠습니다.
여기 정적 바인딩의 과정이며, 함수의이 부분은 자체적으로 작성할 수 없으며 BasePass 또는 다른 사본을 참조하여 좋은 것을 조금 변경할 수 있습니다!


구현 참조 기사는 아래...

【UE4.26.0】나만의 메시패스 사용자 지정

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

zhuanlan.zhihu.com

UE 4.26 렌더링 파이프라인을 한동안 살펴봤습니다.확실히 이해하기가 조금 어렵습니다.따라서 메시패스를 사용자 지정하는 방법을 배워 파이프라인에 대한 이해를 심화하세요.그리고 그 과정에서 많은 것을 얻었습니다.여기서 여러분과 공유해 보겠습니다.
UE4.26에서 메시패스를 커스터마이징하려면 주로 이 두 문서를 참고하세요:

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

zhuanlan.zhihu.com

 

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

zhuanlan.zhihu.com

이전 게시물에서는 새로운 메시패스를 디퍼드 파이프라인에 넣는 방법을 설명했습니다.

지금은 여전히 스마트폰 게임이 주를 이루기 때문에 여기서는 여전히 모바일 파이프라인에 패스를 넣어 렌더링합니다.

후자의 글은 두 패스를 서로 구분하기 위해 더 많은 제네릭을 추가하여 MobileBasePassProcessor를 재사용한 것입니다.저는 이전 글과 함께 새로운 패스를 구현하기 위해 새로운 프로세서를 만들었습니다.
UE4.26에서 메시패스를 커스터마이징하는 방법에 관심이 있으시다면 계속 읽어보세요.
준비 섹션:
먼저 MeshPassProcessor.h를 찾아 파이프라인의 모든 메시패스 유형을 찾습니다.버전 4.26.0에는 기본적으로 23개의 메시패스가 있으며, 이 중 3개는 에디터용 패스입니다.
나만의 메시패스를 커스터마이징하려면 먼저 이 두 가지에 마이패스를 추가해야 합니다.

그런 다음 Engine\Source\Runtime\Renderer\Private에 MyPass.h MyPass.cpp 파일을 생성합니다.
Engine\Shader\Private 에서 MyPass.usf를 생성합니다. 여기서 ush 파일을 생성하지 않도록 주의하세요. ue는 ush 파일을 별도로 컴파일하지 않습니다.
프로젝트에서 생성하는 것을 잊지 마세요.
경험 공유:
그런 다음 코드와 셰이더를 작성했습니다.이 세 가지 파일의 코드는 이 글의 마지막에 넣겠습니다.이러한 기능의 의미는 앞의 두 글에서 더 자세히 설명했습니다.반복되는 일부 내용에 대해서는 다시 설명하지 않으므로 앞의 두 글을 읽어보시기 바랍니다.아래에서는 그 과정에서 겪었던 몇 가지 어려움과 제가 얻은 몇 가지 인사이트를 공유하고자 합니다.
Learning Down I.
코드를 작성한 후 에디터가 시작되지 않고 계속 MeterialShared.cpp가 실행되는 것을 발견했습니다.

검사 결과 여기에서 반환된 셰이더 포인터가 null임을 알 수 있습니다:

그런 다음 더 자세히 추적하면 셰이더 유형 목록에 내 커스텀 셰이더가 없습니다.
그러나 MyPass.cpp에서는 이론적으로 이러한 매크로를 사용하여 자체 셰이더를 ShaderTpye 목록에 등록할 수 있습니다.

 

최종 점검 결과 ShouldCompilePermutation에 문제가 있는 것으로 나타났습니다.

이 함수는 특정 플랫폼, 머티리얼 및 버텍스 팩토리 유형인지 감지하는 데 사용됩니다.조건이 충족되면 현재 셰이더 유형이 저장됩니다.
하지만 저는 여기 떨리는 손으로 :

return IsMetalMobilePlatform(Parameters.Platform)

이후 다음과 같이 변경되었습니다:

return IsMobilePlatform(Parameters.Platform)

여기서 문제를 해결할 수 있습니다.
Learning Down II:
ShouldCompilePermutation을 추가했을 때 다음과 같은 문제가 발생했습니다.

Parameters.MaterialParameters.bIsSpecialEngineMaterial

이 조건 이후에는 다음을 사용할 수 없습니다.

를 사용하여 자료를 가져올 수 있습니다.

그렇지 않으면 에디터를 열 때 실행됩니다.
나중에 프레임을 잘라보니 이전 프레임에서 얻은 머티리얼의 유형이 WorldGridMaterial이라는 것을 알았습니다:

 

엔진의 기본 머티리얼로, 엔진 디렉터리의 BaseEngine.ini에서 DefaultMaterialName을 확인할 수 있지만 여기서 DefaultMaterialName을 다른 머티리얼로 변경하면 여전히 문제가 발생합니다.그 이유는 기본 머티리얼 프로퍼티와 관련이 있습니다.머티리얼 에디터를 열고 다른 머티리얼과 비교할 수 있습니다.특히 엔진에 DefaultMaterial이라는 머티리얼이 있으므로(이 머티리얼은 DefaultMaterialName으로 설정된 머티리얼이 아님) 이 머티리얼과 비교하여 무엇이 다른지 알 수 있습니다.
후자의 방법으로 얻은 머티리얼은 메시 자체의 머티리얼입니다:

따라서 이름에서 알 수 있듯이 IsSpecialEngineMaterial은 엔진 머티리얼이 사용된 경우에만 셰이더 유형이 저장됩니다.따라서 이 조건이 켜져 있으면 이전 방법은 자연스럽게 성능이 저하됩니다.
하지만 뎁스 패스를 보면 이 특정 조건이 추가되는 것을 알 수 있습니다.이는 뎁스를 그릴 때 어떤 머티리얼을 사용하는지와는 아무런 관련이 없기 때문입니다.이 메시의 버텍스 위치 정보만 가져오면 됩니다.따라서 전자의 방법을 사용하면 뎁스 패스에서 확실히 작동합니다.
Learning down III 
사실 엔진의 기본 월드그리드 머티리얼만 가지고는 할 수 있는 일이 거의 없습니다.그래서 보통은 메시의 자체 머티리얼을 사용하여 모든 것을 조금이라도 해보고 싶어합니다.하지만 기본 버텍스 팩토리와 함께 자체 머티리얼을 사용하면 머티리얼 에디터를 열면 일반적인 위치로 충돌합니다.
다시 확인해보니 머티리얼 에디터를 열면 엔진이 셰이더를 다시 컴파일하지만 커스텀 셰이더가 셰이더 유형에서 누락되어 있어 혼란스러웠습니다.
문제는 셰이더의 캐싱 메커니즘에 있는 것으로 밝혀졌습니다.머티리얼 에디터가 열리면 일부 셰이더를 컴파일하고 캐시합니다.하지만 이 과정에서 제 커스텀 셰이더는 제외됩니다.그리고 코드에서 어떤 유형의 셰이더가 작성되었는지 컴파일하는 것도 불가능합니다.
PreviewMaterial.cpp를 열고 자신만의 셰이더를 미리보기 흐름에 넣습니다:

이렇게 하면 메시 자체에서 머티리얼을 사용할 때 머티리얼 에디터를 열어도 머티리얼이 부족하지 않습니다.
EMeshPassFeatures:

이 기능은 주로 VertexFactory와 관련이 있습니다.

여기서 PositionOnly가 켜져 있으면 셰이더의 입력은 FPositionOnlyVertexFactoryInput으로 작성되어야 하며, 다른 열거형에는 이에 대한 자체 입력 선언이 있습니다.셰이더 파일을 검색하거나 두 가지 입력이 모두 있는 제 마지막 코드를 살펴볼 수 있습니다.
물론 이는 RenderDoc에도 반영되어 있습니다:

여기에는 단 하나의 정보만 남아 있습니다.
드로우 섹션:
생성이 완료되면 새 패스를 모바일 파이프라인에 넣고 렌더링할 차례입니다.여기서의 로직은 대부분 MobileBasePass의 로직을 모델로 합니다.
먼저 SceneRendering.cpp의 이 위치에 MyPass에 대한 조건을 추가합니다.

이는 뎁스 패스 초기화 후 SetupMobileBasePassAfterShadowInit 에서 이러한 패스 생성을 완료하기 위한 것입니다.
그런 다음 MobileShadingRenderer.cppMyPass 관련 코드가 추가되는 곳에서 SetupMobileBasePassAfterShadowInit 의 정의를 찾을 수 있습니다:

SceneVisibility.cpp에서 스태틱 메시와 다이내믹 메시를 위한 MyPass 코드를 추가합니다:

MeshDrawCommands.cpp 에서 메모리를 할당하는 것을 잊지 마세요:

위와 같이 파이프라인에서 생성이 완료되었습니다.
이제 파이프라인에서 MyPass를 실제로 호출하고 렌더링할 차례입니다.
MobileShadingRenderer.h와 선언을 찾습니다:

MobileBasePassRendering.cpp 에 해당 정의를 추가합니다:

(코드를 직접 작성할 필요 없이 위의 FMobileSceneRenderer::RenderMobileBasePass 에서 직접 복사하여 약간만 수정하면 됩니다).
마지막으로 MobileShadingRenderer.cpp 로 돌아가서 모바일 파이프라인에서 호출합니다.

이로써 나만의 맞춤형 메시패스(On Demand meshpass)가 완성됩니다.
그 효과가 입증되었습니다:
RenderDoc으로 프레임을 자르면 커스텀 패스가 이미 렌더링 파이프라인에 있는 것을 확인할 수 있습니다:

커스텀 셰이더가 작동하는지 확인하기 위해 오브젝트를 녹색으로 채색하는 가장 간단한 셰이더를 작성했습니다.
원래 장면의 모습은 다음과 같습니다:

커스텀 디파인을 정의 한 후...

씬은 패스로 커스터마이즈되지 않았으며 두 개의 오브젝트는 반투명 및 마스크이며 이는 AddMeshBatch 함수의 코드에 해당합니다:

 

스카이박스도 불투명하므로 전체 화면이 녹색으로 보입니다.스카이박스를 끈 후

 

씬의 불투명도가 커스텀 패스의 영향으로 녹색으로 바뀌는 것을 볼 수 있습니다. 마지막으로 세 파일 모두에 코드를 넣습니다:
MyPass.h:

#pragma once
 
#include "MeshPassProcessor.h"
 
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FMyPassUniformParameters, )
END_GLOBAL_SHADER_PARAMETER_STRUCT()
 
typedef TUniformBufferRef<FMyPassUniformParameters> FMyPassUniformBufferRef;
 
class FPrimitiveSceneProxy;
class FScene;
class FStaticMeshBatch;
class FViewInfo;
 
class FMyPassProcessor : public FMeshPassProcessor
{
 
public:
 
    FMyPassProcessor(
        const FScene* Scene,
        const FSceneView* InViewIfDynamicMeshCommand,
        const FMeshPassProcessorRenderState& InPassDrawRenderState,
        const bool InbRespectUseAsOccluderFlag,
        const bool InbEarlyZPassMoveabe,
        FMeshPassDrawListContext* InDrawListContext
    );
 
    virtual void AddMeshBatch(
        const FMeshBatch& RESTRICT MeshBatch,
        uint64 BatchElementMask,
        const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
        int32 StaticMeshId
    )override final;
 
private:
 
    void Process(
        const FMeshBatch& MeshBatch,
        uint64 BatchElementMask,
        int32 StaticMeshId,
        const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
        const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
        const FMaterial& RESTRICT MaterialResource
    );
 
    FMeshPassProcessorRenderState PassDrawRenderState;
 
    const bool bRespectUseAsOccluderFlag;
    const bool bEarlyZPassMoveable;
 
};

MyPass.cpp:
 

#include "MyPass.h"
 
#include "ScenePrivate.h"
#include "SceneRendering.h"
 
#include "Shader.h"
#include "GlobalShader.h"
#include "MeshMaterialShader.h"
#include "MeshPassProcessor.h"
#include "MeshPassProcessor.inl"
 
bool IsSupportedVertexFactoryType(const FVertexFactoryType* VertexFactoryType);
 
IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FMyPassUniformParameters, "MyPassUniformParametersParams");
 
class FMyPassVS : public FMeshMaterialShader
{
    DECLARE_SHADER_TYPE(FMyPassVS, MeshMaterial);
 
    FMyPassVS() {}
public:
 
    FMyPassVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
        : FMeshMaterialShader(Initializer)
    {
        PassUniformBuffer.Bind(Initializer.ParameterMap, FMyPassUniformParameters::StaticStructMetadata.GetShaderVariableName());
    }
 
    static bool ShouldCompilePermutation(const FMeshMaterialShaderPermutationParameters& Parameters)
    {
        return IsMobilePlatform(Parameters.Platform) && Parameters.VertexFactoryType->SupportsPositionOnly();
    }
 
    void GetShaderBindings(const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& Material, const FMeshPassProcessorRenderState& DrawRenderState, const FMeshMaterialShaderElementData& ShaderElementData, FMeshDrawSingleShaderBindings& ShaderBindings)
    {
        FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
    }
};
 
 
class FMyPassPS : public FMeshMaterialShader
{
    DECLARE_SHADER_TYPE(FMyPassPS, MeshMaterial);
 
public:
    FMyPassPS() { }
    FMyPassPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
        : FMeshMaterialShader(Initializer)
    {
        PassUniformBuffer.Bind(Initializer.ParameterMap, FMyPassUniformParameters::StaticStructMetadata.GetShaderVariableName());
    }
 
    static bool ShouldCompilePermutation(const FMeshMaterialShaderPermutationParameters& Parameters)
    {
        return IsMobilePlatform(Parameters.Platform) && Parameters.VertexFactoryType->SupportsPositionOnly();
    }
    void GetShaderBindings(const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& Material, const FMeshPassProcessorRenderState& DrawRenderState, const FMeshMaterialShaderElementData& ShaderElementData, FMeshDrawSingleShaderBindings& ShaderBindings)
    {
        FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
    }
};
 
IMPLEMENT_MATERIAL_SHADER_TYPE(, FMyPassVS, TEXT("/Engine/Private/MyPass.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_MATERIAL_SHADER_TYPE(, FMyPassPS, TEXT("/Engine/Private/MyPass.usf"), TEXT("MainPS"), SF_Pixel);
IMPLEMENT_SHADERPIPELINE_TYPE_VSPS(MobileShaderPipeline, FMyPassVS, FMyPassPS, true);
 
FMyPassProcessor::FMyPassProcessor(
    const FScene* Scene,
    const FSceneView* InViewIfDynamicMeshCommand,
    const FMeshPassProcessorRenderState& InPassDrawRenderState,
    const bool InbRespectUseAsOccluderFlag,
    const bool InbEarlyZPassMoveabe,
    FMeshPassDrawListContext* InDrawListContext
)
    :FMeshPassProcessor(
        Scene
        , Scene->GetFeatureLevel()
        , InViewIfDynamicMeshCommand
        , InDrawListContext
    )
    , bRespectUseAsOccluderFlag(InbRespectUseAsOccluderFlag)
    , bEarlyZPassMoveable(InbEarlyZPassMoveabe)
{
    PassDrawRenderState = InPassDrawRenderState;
    PassDrawRenderState.SetViewUniformBuffer(Scene->UniformBuffers.ViewUniformBuffer);
    PassDrawRenderState.SetInstancedViewUniformBuffer(Scene->UniformBuffers.InstancedViewUniformBuffer);
}
 
void FMyPassProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
    const FMaterialRenderProxy* FallbackMaterialRenderProxyPtr = nullptr;
    const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(Scene->GetFeatureLevel(), FallbackMaterialRenderProxyPtr);
    const FMaterialRenderProxy& MaterialRenderProxy = FallbackMaterialRenderProxyPtr ? *FallbackMaterialRenderProxyPtr : *MeshBatch.MaterialRenderProxy;
 
    const EBlendMode BlendMode = Material.GetBlendMode();
    const bool bIsTranslucent = IsTranslucentBlendMode(BlendMode);
 
    if (
        !bIsTranslucent
        && (!PrimitiveSceneProxy || PrimitiveSceneProxy->ShouldRenderInMainPass())
        && ShouldIncludeDomainInMeshPass(Material.GetMaterialDomain())
        )
    {
        if (BlendMode == BLEND_Opaque)
        {
            //const FMaterialRenderProxy& DefualtProxy = *UMaterial::GetDefaultMaterial(MD_Surface)->GetRenderProxy();
            //const FMaterial& DefaltMaterial = *DefualtProxy.GetMaterial(Scene->GetFeatureLevel());
 
            Process(
                MeshBatch,
                BatchElementMask,
                StaticMeshId,
                PrimitiveSceneProxy,
                MaterialRenderProxy,
                Material
            );
        }
    }
 
}
 
static FORCEINLINE bool UseShaderPipelines(ERHIFeatureLevel::Type InFeatureLevel)
{
    static const auto* CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.ShaderPipelines"));
    return RHISupportsShaderPipelines(GShaderPlatformForFeatureLevel[InFeatureLevel]) && CVar && CVar->GetValueOnAnyThread() != 0;
}
 
void GetMyPassShaders(
    const FMaterial& material,
    FVertexFactoryType* VertexFactoryType,
    ERHIFeatureLevel::Type FeatureLevel,
    TShaderRef<FBaseHS>& HullShader,
    TShaderRef<FBaseDS>& DomainShader,
    TShaderRef<FMyPassVS>& VertexShader,
    TShaderRef<FMyPassPS>& PixleShader,
    FShaderPipelineRef& ShaderPipeline
)
{
    ShaderPipeline = UseShaderPipelines(FeatureLevel) ? material.GetShaderPipeline(&MobileShaderPipeline, VertexFactoryType) : FShaderPipelineRef();
     
    VertexShader = ShaderPipeline.IsValid() ? ShaderPipeline.GetShader<FMyPassVS>() : material.GetShader<FMyPassVS>(VertexFactoryType);
    PixleShader = ShaderPipeline.IsValid() ? ShaderPipeline.GetShader<FMyPassPS>() : material.GetShader<FMyPassPS>(VertexFactoryType);
}
 
void FMyPassProcessor::Process(
    const FMeshBatch& MeshBatch,
    uint64 BatchElementMask,
    int32 StaticMeshId,
    const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
    const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
    const FMaterial& RESTRICT MaterialResource
)
{
    const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;
 
    TMeshProcessorShaders<
        FMyPassVS,
        FBaseHS,
        FBaseDS,
        FMyPassPS
    >MyPassShaders;
 
    FShaderPipelineRef ShaderPipeline;
 
    GetMyPassShaders(
        MaterialResource,
        VertexFactory->GetType(),
        FeatureLevel,
        MyPassShaders.HullShader,
        MyPassShaders.DomainShader,
        MyPassShaders.VertexShader,
        MyPassShaders.PixelShader,
        ShaderPipeline
    );
 
    const FMeshDrawingPolicyOverrideSettings OverrideSettings = ComputeMeshOverrideSettings(MeshBatch);
    const ERasterizerFillMode MeshFillMode = ComputeMeshFillMode(MeshBatch, MaterialResource, OverrideSettings);
    const ERasterizerCullMode MeshCullMode = ComputeMeshCullMode(MeshBatch, MaterialResource, OverrideSettings);
 
    FMeshMaterialShaderElementData ShaderElementData;
    ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshId, true);
 
    const FMeshDrawCommandSortKey SortKey = CalculateMeshStaticSortKey(MyPassShaders.VertexShader, MyPassShaders.PixelShader);
 
    BuildMeshDrawCommands(
        MeshBatch,
        BatchElementMask,
        PrimitiveSceneProxy,
        MaterialRenderProxy,
        MaterialResource,
        PassDrawRenderState,
        MyPassShaders,
        MeshFillMode,
        MeshCullMode,
        SortKey,
        EMeshPassFeatures::PositionOnly,
        ShaderElementData
    );
 
}
 
FMeshPassProcessor* CreateMyPassProcessor(
    const FScene* Scene,
    const FSceneView* InViewIfDynamicMeshCommand,
    FMeshPassDrawListContext* InDrawListContext
)
{
    FMeshPassProcessorRenderState MyPassState(Scene->UniformBuffers.ViewUniformBuffer, Scene->UniformBuffers.MobileOpaqueBasePassUniformBuffer);
    MyPassState.SetInstancedViewUniformBuffer(Scene->UniformBuffers.InstancedViewUniformBuffer);
    MyPassState.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA>::GetRHI());
    MyPassState.SetDepthStencilAccess(Scene->DefaultBasePassDepthStencilAccess);
    MyPassState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI());
 
    const FMobileBasePassMeshProcessor::EFlags Flags = FMobileBasePassMeshProcessor::EFlags::CanUseDepthStencil;
 
    return new(FMemStack::Get()) FMyPassProcessor(
        Scene,
        InViewIfDynamicMeshCommand,
        MyPassState,
        true,
        Scene->bEarlyZPassMovable,
        InDrawListContext
    );
}
 
FRegisterPassProcessorCreateFunction RegisterMyPass(
    &CreateMyPassProcessor,
    EShadingPath::Mobile,
    EMeshPass::MyPass,
    EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView
);

MyPass.usf:

#include "Common.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"
 
void MainVS(
    FPositionOnlyVertexFactoryInput Input,
    out float4 OutPosition : SV_POSITION
    )
{
    ResolvedView = ResolveView();
    float4 WorldPos = VertexFactoryGetWorldPosition(Input);
    OutPosition = INVARIANT(mul(WorldPos, ResolvedView.TranslatedWorldToClip));
}
 
 
/*void MainVS(
    FVertexFactoryInput Input,
    out float4 OutPosition : SV_POSITION
    )
{
    ResolvedView = ResolveView();
    FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
    float4 WorldPosition = VertexFactoryGetWorldPosition(Input, VFIntermediates);
    float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition);
    OutPosition = mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip);
}*/
 
void MainPS(
    out float4 OutColor : SV_Target0
)
{
    OutColor = float4(0, 1, 0, 1);
}

덧붙은 참조 문서:

언리얼4 쉐이더편(Shader편)[13권: 맞춤 제작 MeshDrawPass]

저자 : YivanLee
INTRODUCTION:
언리얼 엔진은 고도로 캡슐화되어 있어 두 번의 패스를 정의하고 모델이 두 번 그려지도록 제어할 수 있는 유니티와 달리 모델의 드로잉을 원하는 대로 제어할 수 없습니다. TA로서 엔진의 드로잉을 원하는 대로 제어할 수 없다는 것은 분명 좋지 않은 일이기 때문에 이 섹션에서는 4.22의 새로운 렌더링 아키텍처인 MeshDrawPipeLine으로 엔진을 개선할 예정입니다.
메시드로우파이프라인에 대해 잘 모르신다면 이전 포스팅을 참조하세요.  ↓↓

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

zhuanlan.zhihu.com

 
先上效果吧:

저는 프리패스 후에 커스텀 MobileVolumeLightZ의 메시드로우패스 렌더링을 추가했습니다. 구현 방법은 다음과 같습니다.

먼저 우리가 해야 할 핵심 작업은 다음과 같습니다:
【1】필요한 MeshDrawCommand를 생성합니다.
【2】메시 드로우 커맨드를 호출합니다.


MAIN CONTENT:
【메시 드로우 커맨드 구성하기】
메시드로우패스 커스터마이징의 첫 번째 단계는 메시드로우패스에 채널을 추가하고, MeshDrawProcessor.h에 자체 메시드로우패스를 추가하는 것입니다.

그런 다음 GetMeshPassName으로 이동하여 해당 코드를 추가합니다:

메시드로우커맨드는 단일 드로우콜, 버텍스버퍼, 인덱스버퍼, 셰이더 등에 필요한 모든 리소스를 포함하는 렌더링 명령입니다. 먼저

FMeshPassProcessor::빌드메시드로우커맨드

이 함수는 모든 패스의 메시드로우커맨드에 대한 생성 함수입니다. 생성에 필요한 리소스를 채우기만 하면 View.ParallelMeshDrawCommandPass를 채울 좋은 명령을 생성하는 데 도움이 되며, 바로 Render 함수에서 이를 호출할 수 있습니다. 매개변수에서 볼 수 있듯이 명령을 생성하려면 배치 메시 데이터, 다양한 렌더링 상태, PassFeature 등이 필요합니다. 이 정보에는 DrawCall에 필요한 모든 리소스가 포함되어 있습니다. 이러한 리소스는 여기저기 흩어져 있으므로 이를 한데 모아 제너레이터 함수에 패키징해야 합니다. 이 컬렉션을 관리할 객체를 찾아야 하는데, 바로 FMeshPassProcessor입니다.

따라서 첫 번째 단계는 엔진의 다음 디렉토리에 두 개의 파일을 만드는 것입니다.
MobileVolumeLightPassRendering.h
MobileVolumeLightPassRendering.cpp

그런 다음 FMeshPassProcessor의 클래스를 선언합니다.

#pragma once

#include "MeshPassProcessor.h"

class FPrimitiveSceneProxy;
class FScene;
class FStaticMeshBatch;
class FViewInfo;

class FMobileVolumeLightPassProcessor : public FMeshPassProcessor
{

public:

	FMobileVolumeLightPassProcessor(
		const FScene* Scene,
		const FSceneView* InViewIfDynamicMeshCommand,
		const FMeshPassProcessorRenderState& InPassDrawRenderState,
		const bool InbRespectUseAsOccluderFlag,
		const bool InbEarlyZPassMoveabe,
		FMeshPassDrawListContext* InDrawListContext
	);

	virtual void AddMeshBatch(
		const FMeshBatch& RESTRICT MeshBatch,
		uint64 BatchElementMask,
		const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
		int32 StaticMeshId /* = -1 */
	)override final;

private:

	void Process(
		const FMeshBatch& MeshBatch,
		uint64 BatchElementMask,
		uint32 StaticMeshID,
		const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
		const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
		const FMaterial& RESTRICT MaterialResource,
		ERasterizerFillMode MeshFillMode,
		ERasterizerCullMode MeshCullMode
	);

	FMeshPassProcessorRenderState PassDrawRenderState;

	const bool bRespectUseAsOccluderFlag;
	const bool bEarlyZPassMoveable;

};

이 함수는 엔진 하단에서 BatchMesh, Material 및 기타 리소스를 가져오기 때문에 이 함수는 과부하가 걸려야 하며, Process 함수는 BuildMeshDrawCommands로 리소스를 가져와 MeshDrawCommand를 생성하는 일을 담당합니다. FMobileVolumeLightPassProcessor는 리소스 전달을 담당하고, 모델 데이터(VB 및 IB)와 뷰포트 UniformBuffer 데이터는 준비되어 있지만 셰이더 데이터는 우리가 직접 준비해야 합니다. 결국 렌더링 효과를 커스터마이징하고 싶기 때문입니다. 새 usf 파일을 만들어 보겠습니다.
 

#include "Common.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"

void MainVS(
	FPositionOnlyVertexFactoryInput Input,
	out float4 OutPosition : SV_POSITION
#if USE_GLOBAL_CLIP_PLANE
	, out float OutGlobalClipPlaneDistance : SV_ClipDistance
#endif
#if INSTANCED_STEREO
	, uint InstanceId : SV_InstanceID
#if !MULTI_VIEW
		, out float OutClipDistance : SV_ClipDistance1
#else
		, out uint ViewportIndex : SV_ViewPortArrayIndex
#endif
#endif
	)
{
#if INSTANCED_STEREO
	uint EyeIndex = GetEyeIndex(InstanceId);
	ResolvedView = ResolveView(EyeIndex);

#if !MULTI_VIEW
		OutClipDistance = 0.0;
#else
		ViewportIndex = EyeIndex;
#endif
#else
    ResolvedView = ResolveView();
#endif

    float4 WorldPos = VertexFactoryGetWorldPosition(Input);

	ISOLATE
	{
#if ODS_CAPTURE
		float3 ODS = OffsetODS(WorldPos.xyz, ResolvedView.TranslatedWorldCameraOrigin.xyz, ResolvedView.StereoIPD);
		OutPosition = INVARIANT(mul(float4(WorldPos.xyz + ODS, 1.0), ResolvedView.TranslatedWorldToClip));
#else
        OutPosition = INVARIANT(mul(WorldPos, ResolvedView.TranslatedWorldToClip));
#endif
    }

#if INSTANCED_STEREO && !MULTI_VIEW
	BRANCH 
	if (IsInstancedStereo())  
	{
		// Clip at the center of the screen
		OutClipDistance = dot(OutPosition, EyeClipEdge[EyeIndex]);

		// Scale to the width of a single eye viewport
		OutPosition.x *= 0.5 * ResolvedView.HMDEyePaddingOffset;

		// Shift to the eye viewport
		OutPosition.x += (EyeOffsetScale[EyeIndex] * OutPosition.w) * (1.0f - 0.5 * ResolvedView.HMDEyePaddingOffset);
	}
#endif
#if USE_GLOBAL_CLIP_PLANE
	OutGlobalClipPlaneDistance = dot(ResolvedView.GlobalClippingPlane, float4(WorldPos.xyz - ResolvedView.PreViewTranslation.xyz, 1));
#endif
}


void MainPS(
	out float4 OutColor : SV_Target0
)
{
    OutColor = float4(1, 1, 1, 1);
}

셰이더 파일을 준비한 후에는 셰이더를 사용 가능한 바이너리로 컴파일하고 렌더링 파이프라인에 설정해야 하므로 C++ 레이어에서 해당 셰이더 클래스를 구현해야 합니다.

MobileVolumeLightPassRendering.cpp에서 셰이더 클래스를 구현합니다.

#include "MobileVolumeLightPassRendering.h"
#include "MeshPassProcessor.inl"

#include "ScenePrivate.h"
#include "SceneRendering.h"
#include "Shader.h"
#include "GlobalShader.h"

/**
 * Vertex shader for rendering a single, constant color.
 */
class FMobileVolumeLightVS : public FMeshMaterialShader
{
	DECLARE_SHADER_TYPE(FMobileVolumeLightVS, MeshMaterial);

	/** Default constructor. */
	FMobileVolumeLightVS() {}

public:

	FMobileVolumeLightVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		: FMeshMaterialShader(Initializer)
	{
		//DepthParameter.Bind(Initializer.ParameterMap, TEXT("InputDepth"), SPF_Mandatory);
		BindSceneTextureUniformBufferDependentOnShadingPath(Initializer, PassUniformBuffer, PassUniformBuffer);
	}

	//static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	//{
		//FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
		//OutEnvironment.SetDefine(TEXT("USING_NDC_POSITIONS"), (uint32)(bUsingNDCPositions ? 1 : 0));
		//OutEnvironment.SetDefine(TEXT("USING_LAYERS"), (uint32)(bUsingVertexLayers ? 1 : 0));
	//}

	//void SetDepthParameter(FRHICommandList& RHICmdList, float Depth)
	//{
	//	SetShaderValue(RHICmdList, GetVertexShader(), DepthParameter, Depth);
	//}

	//// FShader interface.
	//virtual bool Serialize(FArchive& Ar) override
	//{
	//	bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
	//	//Ar << DepthParameter;
	//	return bShaderHasOutdatedParameters;
	//}

	static bool ShouldCompilePermutation(EShaderPlatform Platform, const FMaterial* Material, const FVertexFactoryType* VertexFactoryType)
	{
		return VertexFactoryType->SupportsPositionOnly() && Material->IsSpecialEngineMaterial();
	}

	void GetShaderBindings(const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& Material, const FMeshPassProcessorRenderState& DrawRenderState, const FMeshMaterialShaderElementData& ShaderElementData, FMeshDrawSingleShaderBindings& ShaderBindings)
	{
		FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
	}

	//static const TCHAR* GetSourceFilename()
	//{
	//	return TEXT("/Engine/Private/OneColorShader.usf");
	//}

	//static const TCHAR* GetFunctionName()
	//{
	//	return TEXT("MainVertexShader");
	//}

private:
	//FShaderParameter DepthParameter;
};


class FMobileVolumeLightPS : public FMeshMaterialShader
{
	DECLARE_SHADER_TYPE(FMobileVolumeLightPS, MeshMaterial);
public:

	FMobileVolumeLightPS() { }
	FMobileVolumeLightPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		: FMeshMaterialShader(Initializer)
	{
		//ColorParameter.Bind( Initializer.ParameterMap, TEXT("DrawColorMRT"), SPF_Mandatory);
	}

	//virtual void SetColors(FRHICommandList& RHICmdList, const FLinearColor* Colors, int32 NumColors);

	// FShader interface.
	static bool ShouldCompilePermutation(EShaderPlatform Platform, const FMaterial* Material, const FVertexFactoryType* VertexFactoryType)
	{
		return VertexFactoryType->SupportsPositionOnly() && Material->IsSpecialEngineMaterial();
	}
	void GetShaderBindings(const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& Material, const FMeshPassProcessorRenderState& DrawRenderState, const FMeshMaterialShaderElementData& ShaderElementData, FMeshDrawSingleShaderBindings& ShaderBindings)
	{
		FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
	}
	/** The parameter to use for setting the draw Color. */
	//FShaderParameter ColorParameter;
};

IMPLEMENT_MATERIAL_SHADER_TYPE(, FMobileVolumeLightVS, TEXT("/Engine/Private/MobileVolumeLight.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_MATERIAL_SHADER_TYPE(, FMobileVolumeLightPS, TEXT("/Engine/Private/MobileVolumeLight.usf"), TEXT("MainPS"), SF_Pixel);
IMPLEMENT_SHADERPIPELINE_TYPE_VS(MobileShaderPipeline, FMobileVolumeLightVS, true);

그런 다음 MobileVolumeLightPassRendering.cpp에서 FMobileVolumeLightPassProcessor가 수행하지 않은 구현을 수행합니다. 이제 셰이더가 준비되었고 AddMeshBatch를 통해 모델 머티리얼과 기타 리소스를 가져올 수 있으므로 다음 단계는 MeshDrawCommand에 채우는 것입니다.

먼저 FMobileVolumeLightPassProcessor의 생성자에서 준비해야 하는 UniformBuffer가 있습니다.

그런 다음 AddMeshBatch 구현과 Process 구현이 있습니다.

void FMobileVolumeLightPassProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
	const FMaterialRenderProxy* FallBackMaterialRenderProxyPtr = nullptr;
	const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(Scene->GetFeatureLevel(), FallBackMaterialRenderProxyPtr);

	const EBlendMode BlendMode = Material.GetBlendMode();
	const ERasterizerFillMode MeshFillMode = ComputeMeshFillMode(MeshBatch, Material);
	const ERasterizerCullMode MeshCullMode = ComputeMeshCullMode(MeshBatch, Material);
	const bool bIsTranslucent = IsTranslucentBlendMode(BlendMode);

	if (
		!bIsTranslucent
		&& (!PrimitiveSceneProxy || PrimitiveSceneProxy->ShouldRenderInMainPass())
		&& ShouldIncludeDomainInMeshPass(Material.GetMaterialDomain())
		)
	{
		if (
			BlendMode == BLEND_Opaque
			&& MeshBatch.VertexFactory->SupportsPositionOnlyStream()
			)
		{
			const FMaterialRenderProxy& DefualtProxy = *UMaterial::GetDefaultMaterial(MD_Surface)->GetRenderProxy();
			const FMaterial& DefaltMaterial = *DefualtProxy.GetMaterial(Scene->GetFeatureLevel());

			Process(
				MeshBatch,
				BatchElementMask,
				StaticMeshId,
				PrimitiveSceneProxy,
				DefualtProxy,
				DefaltMaterial,
				MeshFillMode,
				MeshCullMode
			);
		}
	}

}

bool _UseShaderPipelines()
{
	static const auto* CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.ShaderPipelines"));
	return CVar && CVar->GetValueOnAnyThread() != 0;
}

void GetMobileVolumeLightShaders(
	const FMaterial& material,
	FVertexFactoryType* VertexFactoryType,
	ERHIFeatureLevel::Type FeatureLevel,
	FBaseHS*& HullShader,
	FBaseDS*& DomainShader,
	FMobileVolumeLightVS*& VertexShader,
	FMobileVolumeLightPS*& PixleShader,
	FShaderPipeline*& ShaderPipeline
)
{
	ShaderPipeline = _UseShaderPipelines() ? material.GetShaderPipeline(&MobileShaderPipeline, VertexFactoryType) : nullptr;
	VertexShader = ShaderPipeline ? ShaderPipeline->GetShader<FMobileVolumeLightVS>() : material.GetShader<FMobileVolumeLightVS>(VertexFactoryType);
}

void FMobileVolumeLightPassProcessor::Process(
	const FMeshBatch& MeshBatch,
	uint64 BatchElementMask,
	uint32 StaticMeshID,
	const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
	const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
	const FMaterial& RESTRICT MaterialResource,
	ERasterizerFillMode MeshFillMode,
	ERasterizerCullMode MeshCullMode
)
{
	const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;

	TMeshProcessorShaders<
		FMobileVolumeLightVS,
		FBaseHS,
		FBaseDS,
		FMobileVolumeLightPS
	>MobileVolumeLightShaders;

	FShaderPipeline* ShaderPipline = nullptr;
	GetMobileVolumeLightShaders(
		MaterialResource,
		VertexFactory->GetType(),
		FeatureLevel,
		MobileVolumeLightShaders.HullShader,
		MobileVolumeLightShaders.DomainShader,
		MobileVolumeLightShaders.VertexShader,
		MobileVolumeLightShaders.PixelShader,
		ShaderPipline
	);

	FMeshMaterialShaderElementData ShaderElementData;
	ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshID, true);

	const FMeshDrawCommandSortKey SortKey = CalculateMeshStaticSortKey(MobileVolumeLightShaders.VertexShader, MobileVolumeLightShaders.PixelShader);

	BuildMeshDrawCommands(
		MeshBatch,
		BatchElementMask,
		PrimitiveSceneProxy,
		MaterialRenderProxy,
		MaterialResource,
		PassDrawRenderState,
		MobileVolumeLightShaders,
		MeshFillMode,
		MeshCullMode,
		SortKey,
		EMeshPassFeatures::PositionOnly,
		ShaderElementData
	);
	
}

이 작업이 완료되면 렌더링 리소스를 BuildMeshDrawCommands 함수에 성공적으로 채우고 명령의 최종 생성을 수행하여 View.ParallelMeshDrawCommandPasses에 채웁니다.

이 작업이 완료되면 마지막 단계는 엔진의 글로벌 매니저에 추가된 메시드로우프로세서를 등록하는 것입니다.

void SetupMobileVolumeLightPassState(FMeshPassProcessorRenderState& DrawRenderState)
{
	DrawRenderState.SetBlendState(TStaticBlendState<CW_NONE>::GetRHI());
	DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI());
}

FMeshPassProcessor* CreateMobileVolumeLightPassProcessor(
	const FScene* Scene,
	const FSceneView* InViewIfDynamicMeshCommand,
	FMeshPassDrawListContext* InDrawListContext
)
{
	FMeshPassProcessorRenderState MobileVolumeLightRenderPassState;
	SetupMobileVolumeLightPassState(MobileVolumeLightRenderPassState);

	return new(FMemStack::Get()) FMobileVolumeLightPassProcessor(
		Scene,
		InViewIfDynamicMeshCommand,
		MobileVolumeLightRenderPassState,
		true,
		Scene->bEarlyZPassMovable,
		InDrawListContext
	);
}

FRegisterPassProcessorCreateFunction RegisteMobileVolumeLightMeshPass(
	&CreateMobileVolumeLightPassProcessor,
	EShadingPath::Deferred,
	EMeshPass::MobileVolumeLight,
	EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView
);

셰이더와 MeshDrawCommand 생성을 마쳤으니 이제 MeshDrawCommand를 추가해야 합니다. 엔진이 MeshDrawCommand를 추가하는 위치는 정적과 동적 두 곳입니다.

DynamicMesh용 MeshDrawCommand는 매 프레임마다 생성되어야 합니다. 현재는 다른 VertexFactory는 셰이더 바인딩을 설정하기 위해 뷰에 의존해야 하므로 FLocalVertexFactoy, 즉 (UStaticComponent) 만 캐시할 수 있습니다.

스태틱 캐시 및 다이내믹 캐시 생성에 대해서는 이전 게시물을 참조하세요.

동적 캐시를 추가하려면 SceneVisibility의 ComputeDynamicMeshRelevance 함수를 찾습니다. 다음과 같이 수정합니다.

그런 다음 정적 캐시 추가를 수행할 MarkRelevant 함수를 찾아 다음과 같이 수정합니다:

명령 캐시가 완료되면 다음 단계는 최종 드로잉을 준비하는 것입니다. 무언가를 그리려면 렌더타깃이 필요합니다.

렌더타깃을 SceneRenderTargets에 추가합니다.

그런 다음 두 가지 함수를 추가하여 렌더 타겟을 설정하여 그리기를 준비합니다.

이 함수는 다음과 같이 cpp에서 구현됩니다.

void FSceneRenderTargets::BeginRenderMobileVolumeLightPass(FRHICommandList& RHICmdList, bool bPerformClear)
{
	FTexture2DRHIRef MVLZTarget = (const FTexture2DRHIRef&)MobileVolumeLightZ->GetRenderTargetItem().TargetableTexture;

	FRHIRenderPassInfo RPInfo;
	RPInfo.DepthStencilRenderTarget.DepthStencilTarget = MVLZTarget;
	RPInfo.DepthStencilRenderTarget.ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;

	if (bPerformClear)
	{
		RPInfo.DepthStencilRenderTarget.Action = MakeDepthStencilTargetActions(ERenderTargetActions::Clear_Store, ERenderTargetActions::Clear_Store);
	}
	else
	{
		RPInfo.DepthStencilRenderTarget.Action = MakeDepthStencilTargetActions(ERenderTargetActions::Load_Store, ERenderTargetActions::Load_Store);
	}

	RHICmdList.BeginRenderPass(RPInfo, TEXT("BeginRenderingMobileVolumeLightPass"));

	if (!bPerformClear)
	{
		RHICmdList.BindClearMRTValues(false, true, true);
	}
}

void FSceneRenderTargets::FinishRenderMobileVolueLightPass(FRHICommandList& RHICmdList)
{
	check(RHICmdList.IsInsideRenderPass());

	SCOPED_DRAW_EVENT(RHICmdList, FinishRenderingPrePass);

	RHICmdList.EndRenderPass();

	GVisualizeTexture.SetCheckPoint(RHICmdList, MobileVolumeLightZ);
}

또한 렌더타깃을 생성하는 함수와 소멸하는 함수에서 렌더타깃을 만들어야 합니다.

 

이 작업이 완료되면 마지막 단계로 드로우 함수를 만들어서 실제로 DrawCall을 호출해야 합니다.
디퍼드쉐이딩 렌더러에 드로잉 함수 추가하기

그런 다음 cpp에서 구현합니다.

static void SetupMobileVolumeLightPassView(
	FRHICommandList& RHIComList,
	const FViewInfo& View,
	const FSceneRenderer* SceneRenderer,
	const bool bIsEditorPrimitivePass = false
)
{
	RHIComList.SetScissorRect(false, 0, 0, 0, 0);

	if (View.IsInstancedStereoPass() || bIsEditorPrimitivePass)
	{
		RHIComList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0.0f, View.ViewRect.Max.X, View.ViewRect.Max.Y, 1.0f);
	}
	else
	{
		if (View.bIsMultiViewEnabled)
		{
			const uint32 LeftMinX = SceneRenderer->Views[0].ViewRect.Min.X;
			const uint32 LeftMaxX = SceneRenderer->Views[0].ViewRect.Max.X;
			const uint32 RightMinX = SceneRenderer->Views[1].ViewRect.Min.X;
			const uint32 RightMaxX = SceneRenderer->Views[1].ViewRect.Max.X;

			const uint32 LeftMaxY = SceneRenderer->Views[0].ViewRect.Max.Y;
			const uint32 RightMaxY = SceneRenderer->Views[1].ViewRect.Max.Y;

			RHIComList.SetStereoViewport(LeftMinX, RightMinX, 0.0f, 0.0f, 0.0f, LeftMaxX, RightMaxX, LeftMaxY, RightMaxY, 1.0f);
		}
		else
		{
			RHIComList.SetViewport(0, 0, 0.0f, SceneRenderer->InstancedStereoWidth, View.ViewRect.Max.Y, 1.0f);
		}
	}
}

void FDeferredShadingSceneRenderer::RenderMobileVolumeLight(FRHICommandListImmediate& RHICmdList)
{
	check(RHICmdList.IsOutsideRenderPass());
	//SCOPED_DRAW_EVENT(FDeferredShadingSceneRenderer_RenderMobileVolumeLight, FColor::Emerald);

	FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

	SceneContext.BeginRenderMobileVolumeLightPass(RHICmdList, true);
	RHICmdList.EndRenderPass();

	SceneContext.BeginRenderMobileVolumeLightPass(RHICmdList, false);
	FScopedCommandListWaitForTasks Flusher(false, RHICmdList);

	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
	{
		//SCOPED_CONDITIONAL_DRAW_EVENT(RHICmdList, EventView, Views.Num() > 1, TEXT("View%d"), ViewIndex);
		const FViewInfo& View = Views[ViewIndex];
		SCOPED_GPU_MASK(RHICmdList, !View.IsInstancedStereoPass() ? View.GPUMask : (Views[0].GPUMask | Views[1].GPUMask));

		FSceneTexturesUniformParameters SCeneTextureParameters;
		SetupSceneTextureUniformParameters(SceneContext, View.FeatureLevel, ESceneTextureSetupMode::None, SCeneTextureParameters);
		FScene* Scene = View.Family->Scene->GetRenderScene();
		if (Scene)
		{
			
		}

		if (View.ShouldRenderView())
		{
			Scene->UniformBuffers.UpdateViewUniformBuffer(View);
			SetupMobileVolumeLightPassView(RHICmdList, View, this);
			View.ParallelMeshDrawCommandPasses[EMeshPass::MobileVolumeLight].DispatchDraw(nullptr, RHICmdList);
		}
	}

	SceneContext.FinishRenderMobileVolueLightPass(RHICmdList);

}

마지막으로 렌더링 함수에서 호출하여 그리기만 하면 됩니다.

 

마지막으로 최종 결과물을 확인하기 위해 RenderDoc에서 프레임을 자릅니다.

메시드로우를 성공적으로 완료했습니다. 코드 다운로드

4.20定制自己的DrawPass代码
0.00MB

전체 코드는 아래와 같습니다:
 
MobileVolumeLight.usf

#include "Common.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"

void MainVS(
	FPositionOnlyVertexFactoryInput Input,
	out float4 OutPosition : SV_POSITION
#if USE_GLOBAL_CLIP_PLANE
	, out float OutGlobalClipPlaneDistance : SV_ClipDistance
#endif
#if INSTANCED_STEREO
	, uint InstanceId : SV_InstanceID
#if !MULTI_VIEW
		, out float OutClipDistance : SV_ClipDistance1
#else
		, out uint ViewportIndex : SV_ViewPortArrayIndex
#endif
#endif
	)
{
#if INSTANCED_STEREO
	uint EyeIndex = GetEyeIndex(InstanceId);
	ResolvedView = ResolveView(EyeIndex);

#if !MULTI_VIEW
		OutClipDistance = 0.0;
#else
		ViewportIndex = EyeIndex;
#endif
#else
    ResolvedView = ResolveView();
#endif

    float4 WorldPos = VertexFactoryGetWorldPosition(Input);

	ISOLATE
	{
#if ODS_CAPTURE
		float3 ODS = OffsetODS(WorldPos.xyz, ResolvedView.TranslatedWorldCameraOrigin.xyz, ResolvedView.StereoIPD);
		OutPosition = INVARIANT(mul(float4(WorldPos.xyz + ODS, 1.0), ResolvedView.TranslatedWorldToClip));
#else
        OutPosition = INVARIANT(mul(WorldPos, ResolvedView.TranslatedWorldToClip));
#endif
    }

#if INSTANCED_STEREO && !MULTI_VIEW
	BRANCH 
	if (IsInstancedStereo())  
	{
		// Clip at the center of the screen
		OutClipDistance = dot(OutPosition, EyeClipEdge[EyeIndex]);

		// Scale to the width of a single eye viewport
		OutPosition.x *= 0.5 * ResolvedView.HMDEyePaddingOffset;

		// Shift to the eye viewport
		OutPosition.x += (EyeOffsetScale[EyeIndex] * OutPosition.w) * (1.0f - 0.5 * ResolvedView.HMDEyePaddingOffset);
	}
#endif
#if USE_GLOBAL_CLIP_PLANE
	OutGlobalClipPlaneDistance = dot(ResolvedView.GlobalClippingPlane, float4(WorldPos.xyz - ResolvedView.PreViewTranslation.xyz, 1));
#endif
}


void MainPS(
	out float4 OutColor : SV_Target0
)
{
    OutColor = float4(1, 1, 1, 1);
}

MobileVolumeLightPassRendering.h

#pragma once

#include "MeshPassProcessor.h"

class FPrimitiveSceneProxy;
class FScene;
class FStaticMeshBatch;
class FViewInfo;

class FMobileVolumeLightPassProcessor : public FMeshPassProcessor
{

public:

	FMobileVolumeLightPassProcessor(
		const FScene* Scene,
		const FSceneView* InViewIfDynamicMeshCommand,
		const FMeshPassProcessorRenderState& InPassDrawRenderState,
		const bool InbRespectUseAsOccluderFlag,
		const bool InbEarlyZPassMoveabe,
		FMeshPassDrawListContext* InDrawListContext
	);

	virtual void AddMeshBatch(
		const FMeshBatch& RESTRICT MeshBatch,
		uint64 BatchElementMask,
		const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
		int32 StaticMeshId /* = -1 */
	)override final;

private:

	void Process(
		const FMeshBatch& MeshBatch,
		uint64 BatchElementMask,
		uint32 StaticMeshID,
		const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
		const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
		const FMaterial& RESTRICT MaterialResource,
		ERasterizerFillMode MeshFillMode,
		ERasterizerCullMode MeshCullMode
	);

	FMeshPassProcessorRenderState PassDrawRenderState;

	const bool bRespectUseAsOccluderFlag;
	const bool bEarlyZPassMoveable;

};

MobileVolumeLightPassRendering.cpp

#include "MobileVolumeLightPassRendering.h"
#include "MeshPassProcessor.inl"

#include "ScenePrivate.h"
#include "SceneRendering.h"
#include "Shader.h"
#include "GlobalShader.h"

/**
 * Vertex shader for rendering a single, constant color.
 */
class FMobileVolumeLightVS : public FMeshMaterialShader
{
	DECLARE_SHADER_TYPE(FMobileVolumeLightVS, MeshMaterial);

	/** Default constructor. */
	FMobileVolumeLightVS() {}

public:

	FMobileVolumeLightVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		: FMeshMaterialShader(Initializer)
	{
		//DepthParameter.Bind(Initializer.ParameterMap, TEXT("InputDepth"), SPF_Mandatory);
		BindSceneTextureUniformBufferDependentOnShadingPath(Initializer, PassUniformBuffer, PassUniformBuffer);
	}

	//static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	//{
		//FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
		//OutEnvironment.SetDefine(TEXT("USING_NDC_POSITIONS"), (uint32)(bUsingNDCPositions ? 1 : 0));
		//OutEnvironment.SetDefine(TEXT("USING_LAYERS"), (uint32)(bUsingVertexLayers ? 1 : 0));
	//}

	//void SetDepthParameter(FRHICommandList& RHICmdList, float Depth)
	//{
	//	SetShaderValue(RHICmdList, GetVertexShader(), DepthParameter, Depth);
	//}

	//// FShader interface.
	//virtual bool Serialize(FArchive& Ar) override
	//{
	//	bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
	//	//Ar << DepthParameter;
	//	return bShaderHasOutdatedParameters;
	//}

	static bool ShouldCompilePermutation(EShaderPlatform Platform, const FMaterial* Material, const FVertexFactoryType* VertexFactoryType)
	{
		return VertexFactoryType->SupportsPositionOnly() && Material->IsSpecialEngineMaterial();
	}

	void GetShaderBindings(const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& Material, const FMeshPassProcessorRenderState& DrawRenderState, const FMeshMaterialShaderElementData& ShaderElementData, FMeshDrawSingleShaderBindings& ShaderBindings)
	{
		FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
	}

	//static const TCHAR* GetSourceFilename()
	//{
	//	return TEXT("/Engine/Private/OneColorShader.usf");
	//}

	//static const TCHAR* GetFunctionName()
	//{
	//	return TEXT("MainVertexShader");
	//}

private:
	//FShaderParameter DepthParameter;
};


class FMobileVolumeLightPS : public FMeshMaterialShader
{
	DECLARE_SHADER_TYPE(FMobileVolumeLightPS, MeshMaterial);
public:

	FMobileVolumeLightPS() { }
	FMobileVolumeLightPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		: FMeshMaterialShader(Initializer)
	{
		//ColorParameter.Bind( Initializer.ParameterMap, TEXT("DrawColorMRT"), SPF_Mandatory);
	}

	//virtual void SetColors(FRHICommandList& RHICmdList, const FLinearColor* Colors, int32 NumColors);

	// FShader interface.
	static bool ShouldCompilePermutation(EShaderPlatform Platform, const FMaterial* Material, const FVertexFactoryType* VertexFactoryType)
	{
		return VertexFactoryType->SupportsPositionOnly() && Material->IsSpecialEngineMaterial();
	}
	void GetShaderBindings(const FScene* Scene, ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneProxy* PrimitiveSceneProxy, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& Material, const FMeshPassProcessorRenderState& DrawRenderState, const FMeshMaterialShaderElementData& ShaderElementData, FMeshDrawSingleShaderBindings& ShaderBindings)
	{
		FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
	}
	/** The parameter to use for setting the draw Color. */
	//FShaderParameter ColorParameter;
};

IMPLEMENT_MATERIAL_SHADER_TYPE(, FMobileVolumeLightVS, TEXT("/Engine/Private/MobileVolumeLight.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_MATERIAL_SHADER_TYPE(, FMobileVolumeLightPS, TEXT("/Engine/Private/MobileVolumeLight.usf"), TEXT("MainPS"), SF_Pixel);
IMPLEMENT_SHADERPIPELINE_TYPE_VS(MobileShaderPipeline, FMobileVolumeLightVS, true);

FMobileVolumeLightPassProcessor::FMobileVolumeLightPassProcessor(
	const FScene* Scene,
	const FSceneView* InViewIfDynamicMeshCommand,
	const FMeshPassProcessorRenderState& InPassDrawRenderState,
	const bool InbRespectUseAsOccluderFlag,
	const bool InbEarlyZPassMoveabe,
	FMeshPassDrawListContext* InDrawListContext
)
	:FMeshPassProcessor(
		Scene
		, Scene->GetFeatureLevel()
		, InViewIfDynamicMeshCommand
		, InDrawListContext
	)
		, bRespectUseAsOccluderFlag(InbRespectUseAsOccluderFlag)
		, bEarlyZPassMoveable(InbEarlyZPassMoveabe)
{
	PassDrawRenderState = InPassDrawRenderState;
	PassDrawRenderState.SetViewUniformBuffer(Scene->UniformBuffers.ViewUniformBuffer);
	PassDrawRenderState.SetInstancedViewUniformBuffer(Scene->UniformBuffers.InstancedViewUniformBuffer);
}

void FMobileVolumeLightPassProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
	const FMaterialRenderProxy* FallBackMaterialRenderProxyPtr = nullptr;
	const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(Scene->GetFeatureLevel(), FallBackMaterialRenderProxyPtr);

	const EBlendMode BlendMode = Material.GetBlendMode();
	const ERasterizerFillMode MeshFillMode = ComputeMeshFillMode(MeshBatch, Material);
	const ERasterizerCullMode MeshCullMode = ComputeMeshCullMode(MeshBatch, Material);
	const bool bIsTranslucent = IsTranslucentBlendMode(BlendMode);

	if (
		!bIsTranslucent
		&& (!PrimitiveSceneProxy || PrimitiveSceneProxy->ShouldRenderInMainPass())
		&& ShouldIncludeDomainInMeshPass(Material.GetMaterialDomain())
		)
	{
		if (
			BlendMode == BLEND_Opaque
			&& MeshBatch.VertexFactory->SupportsPositionOnlyStream()
			)
		{
			const FMaterialRenderProxy& DefualtProxy = *UMaterial::GetDefaultMaterial(MD_Surface)->GetRenderProxy();
			const FMaterial& DefaltMaterial = *DefualtProxy.GetMaterial(Scene->GetFeatureLevel());

			Process(
				MeshBatch,
				BatchElementMask,
				StaticMeshId,
				PrimitiveSceneProxy,
				DefualtProxy,
				DefaltMaterial,
				MeshFillMode,
				MeshCullMode
			);
		}
	}

}

bool _UseShaderPipelines()
{
	static const auto* CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.ShaderPipelines"));
	return CVar && CVar->GetValueOnAnyThread() != 0;
}

void GetMobileVolumeLightShaders(
	const FMaterial& material,
	FVertexFactoryType* VertexFactoryType,
	ERHIFeatureLevel::Type FeatureLevel,
	FBaseHS*& HullShader,
	FBaseDS*& DomainShader,
	FMobileVolumeLightVS*& VertexShader,
	FMobileVolumeLightPS*& PixleShader,
	FShaderPipeline*& ShaderPipeline
)
{
	ShaderPipeline = _UseShaderPipelines() ? material.GetShaderPipeline(&MobileShaderPipeline, VertexFactoryType) : nullptr;
	VertexShader = ShaderPipeline ? ShaderPipeline->GetShader<FMobileVolumeLightVS>() : material.GetShader<FMobileVolumeLightVS>(VertexFactoryType);
}

void FMobileVolumeLightPassProcessor::Process(
	const FMeshBatch& MeshBatch,
	uint64 BatchElementMask,
	uint32 StaticMeshID,
	const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
	const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
	const FMaterial& RESTRICT MaterialResource,
	ERasterizerFillMode MeshFillMode,
	ERasterizerCullMode MeshCullMode
)
{
	const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;

	TMeshProcessorShaders<
		FMobileVolumeLightVS,
		FBaseHS,
		FBaseDS,
		FMobileVolumeLightPS
	>MobileVolumeLightShaders;

	FShaderPipeline* ShaderPipline = nullptr;
	GetMobileVolumeLightShaders(
		MaterialResource,
		VertexFactory->GetType(),
		FeatureLevel,
		MobileVolumeLightShaders.HullShader,
		MobileVolumeLightShaders.DomainShader,
		MobileVolumeLightShaders.VertexShader,
		MobileVolumeLightShaders.PixelShader,
		ShaderPipline
	);

	FMeshMaterialShaderElementData ShaderElementData;
	ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshID, true);

	const FMeshDrawCommandSortKey SortKey = CalculateMeshStaticSortKey(MobileVolumeLightShaders.VertexShader, MobileVolumeLightShaders.PixelShader);

	BuildMeshDrawCommands(
		MeshBatch,
		BatchElementMask,
		PrimitiveSceneProxy,
		MaterialRenderProxy,
		MaterialResource,
		PassDrawRenderState,
		MobileVolumeLightShaders,
		MeshFillMode,
		MeshCullMode,
		SortKey,
		EMeshPassFeatures::PositionOnly,
		ShaderElementData
	);
	
}

void SetupMobileVolumeLightPassState(FMeshPassProcessorRenderState& DrawRenderState)
{
	DrawRenderState.SetBlendState(TStaticBlendState<CW_NONE>::GetRHI());
	DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI());
}

FMeshPassProcessor* CreateMobileVolumeLightPassProcessor(
	const FScene* Scene,
	const FSceneView* InViewIfDynamicMeshCommand,
	FMeshPassDrawListContext* InDrawListContext
)
{
	FMeshPassProcessorRenderState MobileVolumeLightRenderPassState;
	SetupMobileVolumeLightPassState(MobileVolumeLightRenderPassState);

	return new(FMemStack::Get()) FMobileVolumeLightPassProcessor(
		Scene,
		InViewIfDynamicMeshCommand,
		MobileVolumeLightRenderPassState,
		true,
		Scene->bEarlyZPassMovable,
		InDrawListContext
	);
}

FRegisterPassProcessorCreateFunction RegisteMobileVolumeLightMeshPass(
	&CreateMobileVolumeLightPassProcessor,
	EShadingPath::Deferred,
	EMeshPass::MobileVolumeLight,
	EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView
);

Render:

void FDeferredShadingSceneRenderer::RenderMobileVolumeLight(FRHICommandListImmediate& RHICmdList)
{
	check(RHICmdList.IsOutsideRenderPass());
	//SCOPED_DRAW_EVENT(FDeferredShadingSceneRenderer_RenderMobileVolumeLight, FColor::Emerald);

	FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

	SceneContext.BeginRenderMobileVolumeLightPass(RHICmdList, true);
	RHICmdList.EndRenderPass();

	SceneContext.BeginRenderMobileVolumeLightPass(RHICmdList, false);
	FScopedCommandListWaitForTasks Flusher(false, RHICmdList);

	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
	{
		//SCOPED_CONDITIONAL_DRAW_EVENT(RHICmdList, EventView, Views.Num() > 1, TEXT("View%d"), ViewIndex);
		const FViewInfo& View = Views[ViewIndex];
		SCOPED_GPU_MASK(RHICmdList, !View.IsInstancedStereoPass() ? View.GPUMask : (Views[0].GPUMask | Views[1].GPUMask));

		FSceneTexturesUniformParameters SCeneTextureParameters;
		SetupSceneTextureUniformParameters(SceneContext, View.FeatureLevel, ESceneTextureSetupMode::None, SCeneTextureParameters);
		FScene* Scene = View.Family->Scene->GetRenderScene();
		if (Scene)
		{
			
		}

		if (View.ShouldRenderView())
		{
			Scene->UniformBuffers.UpdateViewUniformBuffer(View);
			SetupMobileVolumeLightPassView(RHICmdList, View, this);
			View.ParallelMeshDrawCommandPasses[EMeshPass::MobileVolumeLight].DispatchDraw(nullptr, RHICmdList);
		}
	}

	SceneContext.FinishRenderMobileVolueLightPass(RHICmdList);

}

SUMMARY AND OUTLOOK:
섹션 1부터 이번 섹션인 섹션 13까지 언리얼 렌더링에 대한 기본적인 연구는 대부분 마쳤으며, 이제 기본적으로 언리얼에서 렌더링하고 싶은 것을 제한 없이 렌더링할 수 있습니다.



2. 狗哥老司机:Unreal添加自定义Pass
3. 原亮:如何在UE4.22中实现多Pass渲染

封面来自twitter@Lucinotion

원문

https://zhuanlan.zhihu.com/p/547444128?utm_psn=1793959163199643649

zhuanlan.zhihu.com