저자: 搓手苍蝇
이 문서는 UE 5.0.3 버전 기준이다. 나중에 시간이 되면 5.1 버전으로 업데이트할 예정.
개요
UE5에서 네이티브 아웃라인 렌더링을 지원하기 위해, Backface Outline 기반의 MultiPass Draw를 구현하려면 새로운 렌더링 Pass를 추가해야 한다.
이 글을 작성하기 전에 인터넷에서 여러 대단한 분들의 관련 글을 찾을 수 있었지만, 대부분 UE4 버전의 코드를 다루고 있어서 UE5에 그대로 적용하면 많은 문제가 발생한다. 그래서 이 글을 노트 삼아 남긴다. 참고한 글들의 저자분들께 감사드린다. 거인의 어깨 위에서 바라보니 정말 편하다.



구현된 아웃라인 효과의 특징
- 머티리얼 디테일 패널에서 아웃라인 켜고 끌 수 있다
- 머티리얼 디테일 패널에서 색상을 수동으로 제어할 수 있다 (모델에 버텍스 컬러로 아웃라인을 제어하면 버텍스 컬러를 사용한다)
- 머티리얼 디테일 패널에서 두께를 수동으로 조절할 수 있다 (모델에 버텍스 컬러로 두께를 제어하면 그것도 영향을 준다)
- 두께가 시야 거리에 따라 자동으로 조절된다 (먼 거리에서는 두꺼워지지만 상한선이 있고, 가까운 거리에서는 얇아진다)
- Todo: 스타일화된 브러시 스트로크 아웃라인 (필자는 스타일화를 잘 모르고 그냥 무정하게 코드만 쓰고 싶다. 아트팀 요구사항 보고 결정할 예정)
아웃라인 자체는 중요하지 않고, 중요한 것은 이 아웃라인을 위해 새로운 Pass를 만드는 코드 플로우를 통과하는 것이다.
이 글은 독자가 UE 렌더링 파이프라인에 대해 어느 정도 이해하고 있다고 가정하므로, 일부 코드 선언에 대해서는 자세한 설명을 하지 않는다.
그럼 시작해보자. 필자의 사고 흐름대로 소개하겠다.
1. 머티리얼 디테일 패널에 커스텀 변수 추가
자세한 내용은 필자의 다른 노트를 참고: UE5 Add Custom Variables in Material
2. C++ 파트
2.1 전역 Pass 열거형 값 추가
UnrealEngine\\Engine\\Source\\Runtime\\Renderer\\Public\\MeshDrawProcessor.h 파일을 열어서 커스텀 MeshDrawPass 열거형을 추가한다.
전역 Pass 열거형 선언이 모두 여기에 있는 것을 볼 수 있다.

이어서 아래의 GetMeshPassName() 함수에서도 동일한 변경을 한다. 이것은 pass에 이름을 붙이는 것이다 (RenderDoc의 디버그 정보에 표시됨).

주의: 아래쪽의 컴파일 검사도 동일하게 수정해야 한다. 그렇지 않으면 컴파일이 통과되지 않는다.
2.2 커스텀 MeshDrawCommand 생성
MeshDrawCommand는 렌더링 커맨드로, 한 번의 DrawCall에 필요한 모든 리소스(VertexBuffer, IndexBuffer, Shaders 등)를 포함한다. BuildMeshDrawCommands 함수는 FMeshPassProcessor 클래스의 공개 메서드다.

이 함수는 모든 Pass의 MeshDrawCommands 생성 함수다. 개발자는 생성에 필요한 리소스를 채워주기만 하면 되고, 함수가 알아서 생성된 Command를 View.ParallelMeshDrawCommandPasses에 채워준다. 나중에 Render 함수에서 바로 호출하면 된다. 파라미터를 보면, Command 하나를 생성하려면 Batchmesh 데이터, 각종 렌더링 상태, PassFeature 등이 필요하다는 것을 알 수 있다. 이런 정보들이 한 번의 DrawCall에 필요한 모든 리소스를 포함한다. 이 리소스들은 여러 곳에 흩어져 있어서, 개발자가 수집해서 함께 패키징해서 이 생성 함수에 던져줘야 한다.
이것은 OpenGL 같은 그래픽 API를 사용하는 것과 유사하다. 커스텀 렌더링 특성의 요구사항을 만족시키기 위해 렌더링 커맨드를 캡슐화하는 것이다.
다음 단계는 FMeshPassProcessor의 커스텀 서브클래스인 툰 아웃라인의 FToonOutlineMeshPassProcessor 클래스를 구현하는 것이다.
먼저 엔진의 다음 디렉토리에 두 개의 파일을 새로 만든다:
- ToonOutlinePassRendering.h
- ToonOutlinePassRendering.cpp
FToonOutlineMeshPassProcessor의 선언 코드는 다음과 같다:
#pragma once
#include "MeshPassProcessor.h"
#include "MeshMaterialShader.h"
class FPrimitiveSceneProxy;
class FScene;
class FStaticMeshBatch;
class FViewInfo;
class FToonOutlineMeshPassProcessor : public FMeshPassProcessor
{
public:
FToonOutlineMeshPassProcessor(
const FScene* Scene,
const FSceneView* InViewIfDynamicMeshCommand,
const FMeshPassProcessorRenderState& InPassDrawRenderState,
FMeshPassDrawListContext* InDrawListContext
);
virtual void AddMeshBatch(
const FMeshBatch& RESTRICT MeshBatch,
uint64 BatchElementMask,
const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
int32 StaticMeshId = -1
) override final;
private:
template<bool bPositionOnly, bool bUsesMobileColorValue>
bool Process(
const FMeshBatch& MeshBatch,
uint64 BatchElementMask,
int32 StaticMeshId,
const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
const FMaterial& RESTRICT MaterialResource,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode
);
FMeshPassProcessorRenderState PassDrawRenderState;
};
위 코드에는 AddMeshBatch()라는 함수가 있는데, 이 함수는 반드시 오버라이드해야 한다. 왜냐하면 이 함수가 엔진 하위 레벨에서 BatchMesh, Material 등의 리소스를 가져와서 가시성 테스트와 배칭 작업을 수행하기 때문이다. 개발자는 이 함수 안에서 커스텀 필터링 규칙을 구현할 수 있다.
이 글의 필터링 규칙은 머티리얼이 아웃라인 속성을 켰는지 여부를 판단하는 것이다.

Process 함수는 가져온 리소스를 BuildMeshDrawCommands에 넣어서 MeshDrawCommand를 생성하는 구체적인 구현을 한다. 그중 모델 데이터(VB와 IB)와 뷰포트 UniformBuffer 데이터 등은 엔진이 이미 준비해뒀지만, Shader 데이터는 직접 준비해야 한다. 결국 개발자가 커스텀 렌더링 효과를 원하는 것이니까.
2.3 Shader 준비
코드 관리의 편의를 위해, Shader 클래스의 코드 구현을 ToonOutlinePassRendering.h 파일에 바로 넣기로 했다.
Shader 클래스의 선언 코드는 다음과 같다:
/** Shader Define*/
class FToonOutlinePassShaderElementData : public FMeshMaterialShaderElementData
{
public:
float ParameterValue;
};
/**
* Vertex shader for rendering a single, constant color.
*/
class FToonOutlineVS : public FMeshMaterialShader
{
DECLARE_SHADER_TYPE(FToonOutlineVS, MeshMaterial);
public:
/** Default constructor. */
FToonOutlineVS() = default;
FToonOutlineVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FMeshMaterialShader(Initializer)
{
OutLineScale.Bind(Initializer.ParameterMap, TEXT("OutLineScale"));
}
static void ModifyCompilationEnvironment(const FMaterialShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
// Set Define in Shader.
//OutEnvironment.SetDefine(TEXT("Define"), Value);
}
static bool ShouldCompilePermutation(const FMeshMaterialShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5) &&
Parameters.MaterialParameters.bUseToonOutLine &&
(Parameters.VertexFactoryType->GetFName() == FName(TEXT("FLocalVertexFactory")) ||
Parameters.VertexFactoryType->GetFName() == FName(TEXT("TGPUSkinVertexFactoryDefault")));
}
// You can call this function to bind every mesh personality data
void GetShaderBindings(
const FScene* Scene,
ERHIFeatureLevel::Type FeatureLevel,
const FPrimitiveSceneProxy* PrimitiveSceneProxy,
const FMaterialRenderProxy& MaterialRenderProxy,
const FMaterial& Material,
const FMeshPassProcessorRenderState& DrawRenderState,
const FToonOutlinePassShaderElementData& ShaderElementData,
FMeshDrawSingleShaderBindings& ShaderBindings) const
{
FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
// Get ToonOutLine Data from Material
ShaderBindings.Add(OutLineScale, Material.GetToonOutLineScale());
}
/** The parameter to use for setting the Mesh OutLine Scale. */
LAYOUT_FIELD(FShaderParameter, OutLineScale);
};
/**
* Pixel shader for rendering a single, constant color.
*/
class FToonOutlinePS : public FMeshMaterialShader
{
DECLARE_SHADER_TYPE(FToonOutlinePS, MeshMaterial);
public:
FToonOutlinePS() = default;
FToonOutlinePS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FMeshMaterialShader(Initializer)
{
OutLineColor.Bind(Initializer.ParameterMap, TEXT("OutLineColor"));
}
static void ModifyCompilationEnvironment(const FMaterialShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
// Set Define in Shader.
//OutEnvironment.SetDefine(TEXT("Define"), Value);
}
// FShader interface.
static bool ShouldCompilePermutation(const FMeshMaterialShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5) &&
Parameters.MaterialParameters.bUseToonOutLine &&
(Parameters.VertexFactoryType->GetFName() == FName(TEXT("FLocalVertexFactory")) ||
Parameters.VertexFactoryType->GetFName() == FName(TEXT("TGPUSkinVertexFactoryDefault")));
}
void GetShaderBindings(
const FScene* Scene,
ERHIFeatureLevel::Type FeatureLevel,
const FPrimitiveSceneProxy* PrimitiveSceneProxy,
const FMaterialRenderProxy& MaterialRenderProxy,
const FMaterial& Material,
const FMeshPassProcessorRenderState& DrawRenderState,
const FToonOutlinePassShaderElementData& ShaderElementData,
FMeshDrawSingleShaderBindings& ShaderBindings) const
{
FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
// Get ToonOutLine Data from Material
const FLinearColor OutLineColorFromMat = Material.GetToonOutLineColor();
FVector3f Color(OutLineColorFromMat.R, OutLineColorFromMat.G, OutLineColorFromMat.G);
// Bind to Shader
ShaderBindings.Add(OutLineColor, Color);
}
/** The parameter to use for setting the Mesh OutLine Color. */
LAYOUT_FIELD(FShaderParameter, OutLineColor);
};
Shader 클래스 구현을 완료한 후, Process() 함수의 구현을 완성한다. 이런 순서로 설명하는 이유는 Process() 안에서 Shader 인스턴스 생성이 관련되기 때문이다. 여기 모든 Cpp 코드를 공개한다:
#include "FToonOutlineMeshPassProcessor.h"
#include "ScenePrivate.h"
#include "MeshPassProcessor.inl"
IMPLEMENT_MATERIAL_SHADER_TYPE(, FToonOutlineVS, TEXT("/Engine/Private/ToonOutLine.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_MATERIAL_SHADER_TYPE(, FToonOutlinePS, TEXT("/Engine/Private/ToonOutLine.usf"), TEXT("MainPS"), SF_Pixel);
/**
* Mesh Pass Processor
*/
FToonOutlineMeshPassProcessor::FToonOutlineMeshPassProcessor(
const FScene* Scene,
const FSceneView* InViewIfDynamicMeshCommand,
const FMeshPassProcessorRenderState& InPassDrawRenderState,
FMeshPassDrawListContext* InDrawListContext)
:FMeshPassProcessor(Scene, Scene->GetFeatureLevel(), InViewIfDynamicMeshCommand, InDrawListContext),
PassDrawRenderState(InPassDrawRenderState)
{
PassDrawRenderState.SetViewUniformBuffer(Scene->UniformBuffers.ViewUniformBuffer);
if (PassDrawRenderState.GetDepthStencilState() == nullptr)
{
PassDrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_NotEqual>().GetRHI());
}
if (PassDrawRenderState.GetBlendState() == nullptr)
{
PassDrawRenderState.SetBlendState(TStaticBlendState<>().GetRHI());
}
}
void FToonOutlineMeshPassProcessor::AddMeshBatch(
const FMeshBatch& MeshBatch,
uint64 BatchElementMask,
const FPrimitiveSceneProxy* PrimitiveSceneProxy,
int32 StaticMeshId)
{
const FMaterialRenderProxy* MaterialRenderProxy = MeshBatch.MaterialRenderProxy;
const FMaterialRenderProxy* FallBackMaterialRenderProxyPtr = nullptr;
const FMaterial& Material = MaterialRenderProxy->GetMaterialWithFallback(Scene->GetFeatureLevel(), FallBackMaterialRenderProxyPtr);
// only set in Material will draw outline
if (Material.GetRenderingThreadShaderMap()
&& Material.UseToonOutLine())
{
// Determine the mesh's material and blend mode.
const EBlendMode BlendMode = Material.GetBlendMode();
bool bResult = true;
if (BlendMode == BLEND_Opaque)
{
Process<false, false>(
MeshBatch,
BatchElementMask,
StaticMeshId,
PrimitiveSceneProxy,
*MaterialRenderProxy,
Material,
FM_Solid,
CM_CCW);
}
}
}
template <bool bPositionOnly, bool bUsesMobileColorValue>
bool FToonOutlineMeshPassProcessor::Process(
const FMeshBatch& MeshBatch,
uint64 BatchElementMask,
int32 StaticMeshId,
const FPrimitiveSceneProxy* PrimitiveSceneProxy,
const FMaterialRenderProxy& MaterialRenderProxy,
const FMaterial& MaterialResource,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode)
{
const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;
TMeshProcessorShaders<
FToonOutlineVS,
FToonOutlinePS> ToonOutlineShaders;
// Try Get Shader.
{
FMaterialShaderTypes ShaderTypes;
ShaderTypes.AddShaderType<FToonOutlineVS>();
ShaderTypes.AddShaderType<FToonOutlinePS>();
const FVertexFactoryType* VertexFactoryType = VertexFactory->GetType();
FMaterialShaders Shaders;
if (!MaterialResource.TryGetShaders(ShaderTypes, VertexFactoryType, Shaders))
{
UE_LOG(LogShaders, Warning, TEXT("**********************!Shader Not Found!*************************"));
return false;
}
Shaders.TryGetVertexShader(ToonOutlineShaders.VertexShader);
Shaders.TryGetPixelShader(ToonOutlineShaders.PixelShader);
}
FToonOutlinePassShaderElementData ShaderElementData;
ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshId, false);
const FMeshDrawCommandSortKey SortKey = CalculateMeshStaticSortKey(ToonOutlineShaders.VertexShader, ToonOutlineShaders.PixelShader);
PassDrawRenderState.SetDepthStencilState(
TStaticDepthStencilState<
true, CF_GreaterEqual,// Enable DepthTest, It reverse about OpenGL(which is less)
false, CF_Never, SO_Keep, SO_Keep, SO_Keep,
false, CF_Never, SO_Keep, SO_Keep, SO_Keep,// enable stencil test when cull back
0x00,// disable stencil read
0x00>// disable stencil write
::GetRHI());
PassDrawRenderState.SetStencilRef(0);
BuildMeshDrawCommands(
MeshBatch,
BatchElementMask,
PrimitiveSceneProxy,
MaterialRenderProxy,
MaterialResource,
PassDrawRenderState,
ToonOutlineShaders,
MeshFillMode,
MeshCullMode,
SortKey,
EMeshPassFeatures::Default,
ShaderElementData
);
return true;
}
코드에서 Shader 선언 (컴파일 경로)을 주목한다.
2.4 MeshDrawCommand 소스 코드 수정
엔진에는 MeshDrawCommand를 추가하는 곳이 두 군데 있다. 하나는 static이고 하나는 Dynamic이다. 즉, MeshDrawCommand가 모델 데이터를 패키징할 때 파이프라인에 대한 필터링을 한 번 더 해서, 불필요한 MeshBatch가 속하지 않은 pass에 들어가는 것을 방지한다.
SceneVisibility의 ComputeDynamicMeshRelevance 함수를 찾아서 동적 캐시를 추가한다.
그런 다음 MarkRelevant 함수를 찾아서 static 캐시를 추가한다.


2.5 렌더링 Pass 입구 추가
이 작업을 완료한 후, 이제 엔진에 우리의 렌더링 Pass 입구를 추가할 수 있다. UE4.27에 비해 상대적으로 간단해졌다. 왜냐하면 UE4의 RDG 코드 스타일은, 개인적으로 보기에 UE5만큼 성숙하지 않아서 가끔 RHI와 RDG 스타일이 공존하는 흔적을 볼 수 있기 때문이다.
추가 설명을 하자면, FSceneRenderer 클래스는 렌더러가 호출하는 베이스 클래스이고, 렌더링 입구라고 할 수도 있다. 예를 들어 DeferredRenderer 클래스는 FSceneRenderer 클래스의 서브클래스인데, 그것의 멤버 함수에서 RenderBasePass와 기타 드로우 함수를 구현했다.


본론으로 돌아가자. 하지만 이번 실험의 아웃라인 효과는 BasePass에서 실행되지 않으므로, 새로운 Pass를 FSceneRenderer의 멤버 함수로 만들 수 있다 (좀 더 하위 레벨이라 호출하기 편하다).
Engine\\Source\\Runtime\\Renderer\\Private\\SceneRendering.h의 FSceneRenderer 클래스 멤버에 커스텀 Pass 멤버 함수를 추가한다. 그 선언 규칙은 위아래에 이미 있는 pass 함수를 참고할 수 있다.

그런 다음 RenderToonOutlinePass() 아웃라인 pass 함수를 이전에 만든 ToonOutlinePassRendering.cpp 파일의 아래쪽에 구현한다:
FInt32Range GetDynamicMeshElementRange(const FViewInfo& View, uint32 PrimitiveIndex)
{
int32 Start = 0; // inclusive
int32 AfterEnd = 0; // exclusive
// DynamicMeshEndIndices contains valid values only for visible primitives with bDynamicRelevance.
if (View.PrimitiveVisibilityMap[PrimitiveIndex])
{
const FPrimitiveViewRelevance& ViewRelevance = View.PrimitiveViewRelevanceMap[PrimitiveIndex];
if (ViewRelevance.bDynamicRelevance)
{
Start = (PrimitiveIndex == 0) ? 0 : View.DynamicMeshEndIndices[PrimitiveIndex - 1];
AfterEnd = View.DynamicMeshEndIndices[PrimitiveIndex];
}
}
return FInt32Range(Start, AfterEnd);
}
BEGIN_SHADER_PARAMETER_STRUCT(FToonOutlineMeshPassParameters, )
SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
SHADER_PARAMETER_STRUCT_INCLUDE(FInstanceCullingDrawParams, InstanceCullingDrawParams)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
/**
* Render()
*/
void FSceneRenderer::RenderToonOutlinePass(
FRDGBuilder& GraphBuilder,
FRDGTextureRef SceneColorTexture)
{
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
{
FViewInfo& View = Views[ViewIndex];
if (View.Family->Scene == nullptr)
{
UE_LOG(LogShaders, Log, TEXT("View.Family->Scene is NULL! GettingNextNow... - RenderToonOutlinePass()"));
continue;
}
FSimpleMeshDrawCommandPass* SimpleMeshPass = GraphBuilder.AllocObject<FSimpleMeshDrawCommandPass>(View, nullptr);
FMeshPassProcessorRenderState DrawRenderState;
DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_LessEqual>().GetRHI());
FToonOutlineMeshPassProcessor MeshProcessor(
Scene,
&View,
DrawRenderState,
SimpleMeshPass->GetDynamicPassMeshDrawListContext());
// Gather & Filter MeshBatch from Scene->Primitives.
for (int32 PrimitiveIndex = 0; PrimitiveIndex < Scene->Primitives.Num(); PrimitiveIndex++)
{
const FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->Primitives[PrimitiveIndex];
if (View.PrimitiveVisibilityMap[PrimitiveSceneInfo->GetIndex()])
{
const FPrimitiveViewRelevance& ViewRelevance = View.PrimitiveViewRelevanceMap[PrimitiveSceneInfo->GetIndex()];
if (ViewRelevance.bRenderInMainPass && ViewRelevance.bStaticRelevance)
{
for (int32 StaticMeshIdx = 0; StaticMeshIdx < PrimitiveSceneInfo->StaticMeshes.Num(); StaticMeshIdx++)
{
const FStaticMeshBatch& StaticMesh = PrimitiveSceneInfo->StaticMeshes[StaticMeshIdx];
if (View.StaticMeshVisibilityMap[StaticMesh.Id])
{
constexpr uint64 DefaultBatchElementMask = ~0ul;
MeshProcessor.AddMeshBatch(StaticMesh, DefaultBatchElementMask, StaticMesh.PrimitiveSceneInfo->Proxy);
}
}
}
if (ViewRelevance.bRenderInMainPass && ViewRelevance.bDynamicRelevance)
{
const FInt32Range MeshBatchRange = GetDynamicMeshElementRange(View, PrimitiveSceneInfo->GetIndex());
for (int32 MeshBatchIndex = MeshBatchRange.GetLowerBoundValue(); MeshBatchIndex < MeshBatchRange.GetUpperBoundValue(); ++MeshBatchIndex)
{
const FMeshBatchAndRelevance& MeshAndRelevance = View.DynamicMeshElements[MeshBatchIndex];
constexpr uint64 BatchElementMask = ~0ull;
MeshProcessor.AddMeshBatch(*MeshAndRelevance.Mesh, BatchElementMask, MeshAndRelevance.PrimitiveSceneProxy);
}
}
}
}//for PrimitiveIndex
const FSceneTextures& SceneTextures = FSceneTextures::Get(GraphBuilder);
FToonOutlineMeshPassParameters* PassParameters = GraphBuilder.AllocParameters<FToonOutlineMeshPassParameters>();
PassParameters->View = View.ViewUniformBuffer;
PassParameters->RenderTargets[0] = FRenderTargetBinding(SceneTextures.Color.Target, ERenderTargetLoadAction::ELoad);
PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
SceneTextures.Depth.Target,
ERenderTargetLoadAction::ENoAction,
ERenderTargetLoadAction::ELoad,
FExclusiveDepthStencil::DepthWrite_StencilNop);
SimpleMeshPass->BuildRenderingCommands(GraphBuilder, View, Scene->GPUScene, PassParameters->InstanceCullingDrawParams);
FIntRect ViewportRect = View.ViewRect;
FIntRect ScissorRect = FIntRect(FIntPoint(EForceInit::ForceInitToZero), SceneColorTexture->Desc.Extent);
GraphBuilder.AddPass(
RDG_EVENT_NAME("ToonOutlinePass"),
PassParameters,
ERDGPassFlags::Raster,
[this, ViewportRect, ScissorRect, SimpleMeshPass, PassParameters](FRHICommandList& RHICmdList)
{
RHICmdList.SetViewport(ViewportRect.Min.X, ViewportRect.Min.Y, 0.0f, ViewportRect.Max.X, ViewportRect.Max.Y, 1.0f);
RHICmdList.SetScissorRect(
true,
ScissorRect.Min.X >= ViewportRect.Min.X ? ScissorRect.Min.X : ViewportRect.Min.X,
ScissorRect.Min.Y >= ViewportRect.Min.Y ? ScissorRect.Min.Y : ViewportRect.Min.Y,
ScissorRect.Max.X <= ViewportRect.Max.X ? ScissorRect.Max.X : ViewportRect.Max.X,
ScissorRect.Max.Y <= ViewportRect.Max.Y ? ScissorRect.Max.Y : ViewportRect.Max.Y);
SimpleMeshPass->SubmitDraw(RHICmdList, PassParameters->InstanceCullingDrawParams);
});
}//for View
}
위 코드 구현에서, Scene 아래의 View 순회부터 시작해서 MeshProcessor를 생성한다.
그런 다음 View 아래의 많은 PrimitiveSceneProxy에 대해 가시성 테스트를 한 번 수행한다.
씬의 SceneTextures를 가져와서 (여기서 RDG 관련 내용을 이해할 필요가 있다), pass의 렌더링 RT, 깊이 RT, 그리고 Shader에 어떤 파라미터를 전달할지 설정한다.
함수 마지막에 AddPass() 메서드를 호출한다. 이 함수를 관찰하면, 예전 UE4의 많은 RHI 호출이 모두 이 함수 안에 통합될 수 있다는 것을 발견할 수 있다.
최종적으로, 이미 구현 완료된 RenderToonOutlinePass()를 Render() 함수의 어딘가에서 호출한다.
필자는 아웃라인 pass를 LightingPass (조명 계산) 이후, TranslucentPass (반투명 렌더링) 이전에 추가하기로 선택했다.

3. Shader 파트
C++에서 정의한 Shader 클래스, 이제 Shader 파일도 구현해보자.
Engine/Shaders/Private 폴더에 ToonOutline.usf 파일을 새로 만들고, 구현 코드는 다음과 같다:
#include "Common.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"
struct FSimpleMeshPassVSToPS
{
FVertexFactoryInterpolantsVSToPS FactoryInterpolants;
float4 Position : SV_POSITION;
};
float OutLineScale;// from cpp
float3 OutLineColor;
#if VERTEXSHADER
void MainVS(
FVertexFactoryInput Input,
out FSimpleMeshPassVSToPS Output)
{
ResolvedView = ResolveView();// view
FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
float4 WorldPos = VertexFactoryGetWorldPosition(Input, VFIntermediates);
float3 WorldNormal = VertexFactoryGetWorldNormal(Input, VFIntermediates);
float3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);
FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPos.xyz, TangentToLocal);
WorldPos.xyz += GetMaterialWorldPositionOffset(VertexParameters);
float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPos);
Output.FactoryInterpolants = VertexFactoryGetInterpolantsVSToPS(Input, VFIntermediates, VertexParameters);
Output.Position = mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip);
float2 ExtentDir = normalize(mul(float4(WorldNormal, 1.0f), ResolvedView.TranslatedWorldToClip).xy);
float Scale = clamp(0.0f, 0.5f, Output.Position.w * OutLineScale * 0.1f);
Output.Position.xy += ExtentDir * Scale;
}
#endif // VERTEXSHADER
void MainPS(
FSimpleMeshPassVSToPS Input,
out float4 OutColor : SV_Target0)
{
OutColor = float4(OutLineColor, 1.0);
}
컴파일하고 실행하면 끝. 즐기면 된다!
원문
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] UE5 시스템 솔루션: 고품질 식생 시스템 (0) | 2025.10.29 |
|---|---|
| [번역] SLG 게임에서의 나무 컬링 최적화 - GPU 기반 접근법 (0) | 2025.10.25 |
| [번역] Unity - PBR과 PBR+NPR 캐릭터 렌더링 연구 (0) | 2025.10.25 |
| [번역] UE5. 앰비언트 라이트 및 GI 2: 노멀 스무딩하기 (0) | 2025.10.23 |
| [번역] UE5. 카툰 렌더링 셰이딩 파트 4: 앰비언트 라이트와 GI (0) | 2025.10.23 |