UE5 使用 custom stencil + custom depth + world normal + material id map 制作描边效果

思路:

custom stencil 处理外描边

custom depth处理内描边

WorldNormal处理两个stencil的白色在一起时

交接处不描边的问题 + material id map做衣服皮肤头发区域描边边界


打开 Material.cpp

找到:

复制代码
case MP_CustomData0:
    Active = ShadingModels.HasAnyShadingModel({
        MSM_ClearCoat,
        MSM_Hair,
        MSM_Cloth,
        MSM_Eye,
        MSM_SubsurfaceProfile
    });
    break;

在里面增加MSM_DefaultLit

让材质编辑器在选择shadingmodels为DefaultLit时,开启CustomData0为可用pin


找到MaterialAttributeDefinitionMap.cpp里面的GetAttributeOverrideForMaterial函数

找到case MP_CustomData0:

增加下列代码:

复制代码
CustomPinNames.Add({ MSM_DefaultLit, LOCTEXT("Material ID Map", "Material ID Map").ToString() });

因为我们的material id map是每个角色都有不同的material id map,所以这里是通过材质编辑器去输入我们要想的material id map,这里是把我们的custom data0修改展示名称为Material ID Map


材质编辑器的输入接口已经开启了,现在就是在材质编辑器将数据传输进行,将引擎能够接收存储

打开ShadingModelMaterial.ush

找到SetGBufferForShadingModel

复制代码
if (false)
{
}

下面增加:

复制代码
#if MATERIAL_SHADINGMODEL_DEFAULT_LIT
	else if (ShadingModel == SHADINGMODELID_DEFAULT_LIT)
	{
		GBuffer.CustomData.x =
			saturate(
				GetMaterialCustomData0(
					PixelMaterialInputs));
	}
#endif

这里将材质里面的数据通过GBuffer.CustomData.x来进行接收


打开 BasePassCommon.ush

在这里增加MATERIAL_SHADINGMODEL_DEFAULT_LIT

可以把它理解成:

复制代码
GBuffer.CustomData.x = MaterialID
        ↓
这是"准备数据"

加入 DEFAULT_LIT 到 WRITES_CUSTOMDATA_TO_GBUFFER
        ↓
这是"打开输出通道"

OutGBufferD = GBuffer.CustomData
        ↓
这是"把数据送进 GBufferD"

4. 允许 Default Lit 解码 CustomData

打开 DeferredShadingCommon.ush

复制代码
bool HasCustomGBufferData(int ShadingModelID)
{
    return ShadingModelID == SHADINGMODELID_DEFAULT_LIT
        || ShadingModelID == SHADINGMODELID_SUBSURFACE
        || ShadingModelID == SHADINGMODELID_PREINTEGRATED_SKIN
        || ShadingModelID == SHADINGMODELID_CLEAR_COAT
        || ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE
        || ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE
        || ShadingModelID == SHADINGMODELID_HAIR
        || ShadingModelID == SHADINGMODELID_CLOTH
        || ShadingModelID == SHADINGMODELID_EYE;
}

增加SHADINGMODEL_DEFAULT_LIT


引擎 Shader 中读取

任何包含:

复制代码
#include "/Engine/Private/DeferredShadingCommon.ush"

的 Deferred Shader 都可以读取:

复制代码
FGBufferData GBuffer =
    GetGBufferDataUint(PixelPosition, true);

float EncodedMaterialID =
    GBuffer.CustomData.x;

uint MaterialID =
    (uint)round(
        EncodedMaterialID * 255.0);

这里解码是我传进去r8的数据自动转成了0~1的数据所以需要解码


PostProcessToonOutline.cpp

复制代码
// Copyright Epic Games, Inc. All Rights Reserved.

#include "PostProcess/PostProcessToonOutline.h"

#include "DataDrivenShaderPlatformInfo.h"
#include "GlobalShader.h"
#include "HAL/IConsoleManager.h"
#include "PixelShaderUtils.h"
#include "RenderGraphBuilder.h"
#include "ScenePrivate.h"
#include "ShaderParameterStruct.h"

namespace
{
	static TAutoConsoleVariable<int32> CVarToonOutlineEnable(
		TEXT("r.ToonOutline.Enable"),
		1,
		TEXT("Enables the Toon Outline post-process pass.\n")
		TEXT(" 0: Disabled\n")
		TEXT(" 1: Enabled"),
		ECVF_RenderThreadSafe);

	static TAutoConsoleVariable<int32> CVarToonOutlineWidth(
		TEXT("r.ToonOutline.Width"),
		2,
		TEXT("Toon outline width in pixels. Range: 1-8."),
		ECVF_RenderThreadSafe);

	static TAutoConsoleVariable<float> CVarToonOutlineDepthThreshold(
		TEXT("r.ToonOutline.DepthThreshold"),
		0.01f,
		TEXT("Relative CustomDepth difference used to detect inner edges."),
		ECVF_RenderThreadSafe);

	static TAutoConsoleVariable<float> CVarToonOutlineNormalThreshold(
		TEXT("r.ToonOutline.NormalThreshold"),
		0.15f,
		TEXT("World normal difference used to detect contact edges.\n")
		TEXT("The comparison is 1 - dot(N0, N1)."),
		ECVF_RenderThreadSafe);

	static TAutoConsoleVariable<float> CVarToonOutlineOcclusionBias(
		TEXT("r.ToonOutline.OcclusionBias"),
		2.0f,
		TEXT("Depth bias in Unreal units used to reject occluded outlines."),
		ECVF_RenderThreadSafe);

	class FToonOutlinePS : public FGlobalShader
	{
	public:
		DECLARE_GLOBAL_SHADER(FToonOutlinePS);

		SHADER_USE_PARAMETER_STRUCT(
			FToonOutlinePS,
			FGlobalShader);

		BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )

			SHADER_PARAMETER_STRUCT_REF(
				FViewUniformShaderParameters,
				View)

			// 提供 SceneDepth / CustomDepth / CustomStencil / GBuffer。
			SHADER_PARAMETER_STRUCT_INCLUDE(
				FSceneTextureShaderParameters,
				SceneTextures)

			SHADER_PARAMETER_STRUCT(
				FScreenPassTextureViewportParameters,
				Input)

			SHADER_PARAMETER_STRUCT(
				FScreenPassTextureViewportParameters,
				Depth)

			SHADER_PARAMETER_RDG_TEXTURE(
				Texture2D,
				InputTexture)

			SHADER_PARAMETER_SAMPLER(
				SamplerState,
				InputSampler)

			SHADER_PARAMETER(
				FScreenTransform,
				SvPositionToViewportUVTransform)

			SHADER_PARAMETER(
				FScreenTransform,
				ViewportUVToInputUV)

			SHADER_PARAMETER(
				FScreenTransform,
				ViewportUVToSceneUV)

			SHADER_PARAMETER(FVector4f, OuterColor)
			SHADER_PARAMETER(FVector4f, InnerColor)
			SHADER_PARAMETER(FVector4f, ContactColor)
			SHADER_PARAMETER(FVector4f, MaterialColor)

			SHADER_PARAMETER(float, DepthRelativeThreshold)
			SHADER_PARAMETER(float, NormalThreshold)
			SHADER_PARAMETER(float, OcclusionBias)

			SHADER_PARAMETER(int32, OutlineWidth)

			RENDER_TARGET_BINDING_SLOTS()

		END_SHADER_PARAMETER_STRUCT()

		static bool ShouldCompilePermutation(
			const FGlobalShaderPermutationParameters& Parameters)
		{
			if (!IsPCPlatform(Parameters.Platform))
			{
				return false;
			}

			return IsFeatureLevelSupported(
				Parameters.Platform,
				ERHIFeatureLevel::SM5);
		}
	};

	IMPLEMENT_GLOBAL_SHADER(
		FToonOutlinePS,
		"/Engine/Private/PostProcessToonOutline.usf",
		"MainPS",
		SF_Pixel);
}

bool IsToonOutlineEnabled()
{
	return CVarToonOutlineEnable.GetValueOnRenderThread() != 0;
}

FScreenPassTexture AddToonOutlinePass(
	FRDGBuilder& GraphBuilder,
	const FViewInfo& View,
	const FToonOutlineInputs& Inputs)
{
	check(Inputs.SceneColor.IsValid());
	check(Inputs.SceneDepth.IsValid());

	if (!IsToonOutlineEnabled())
	{
		return Inputs.SceneColor;
	}

	RDG_EVENT_SCOPE(GraphBuilder, "ToonOutline");
	RDG_GPU_STAT_SCOPE(GraphBuilder, Postprocessing);

	const FScreenPassTextureViewport InputViewport(
		Inputs.SceneColor);

	const FScreenPassTextureViewport DepthViewport(
		Inputs.SceneDepth);

	FScreenPassRenderTarget Output =
		Inputs.OverrideOutput;

	if (!Output.IsValid())
	{
		Output =
			FScreenPassRenderTarget::CreateFromInput(
				GraphBuilder,
				Inputs.SceneColor,
				View.GetOverwriteLoadAction(),
				TEXT("ToonOutlineColor"));
	}

	const FScreenPassTextureViewport OutputViewport(
		Output);

	FToonOutlinePS::FParameters* PassParameters =
		GraphBuilder.AllocParameters<
		FToonOutlinePS::FParameters>();

	PassParameters->View =
		View.ViewUniformBuffer;

	PassParameters->SceneTextures =
		Inputs.SceneTextures;

	PassParameters->Input =
		GetScreenPassTextureViewportParameters(
			InputViewport);

	PassParameters->Depth =
		GetScreenPassTextureViewportParameters(
			DepthViewport);

	PassParameters->InputTexture =
		Inputs.SceneColor.Texture;

	PassParameters->InputSampler =
		TStaticSamplerState<
		SF_Point,
		AM_Clamp,
		AM_Clamp,
		AM_Clamp>::GetRHI();

	PassParameters->SvPositionToViewportUVTransform =
		FScreenTransform::SvPositionToViewportUV(
			OutputViewport.Rect);

	PassParameters->ViewportUVToInputUV =
		FScreenTransform::ChangeTextureBasisFromTo(
			InputViewport,
			FScreenTransform::ETextureBasis::ViewportUV,
			FScreenTransform::ETextureBasis::TextureUV);

	PassParameters->ViewportUVToSceneUV =
		FScreenTransform::ChangeTextureBasisFromTo(
			DepthViewport,
			FScreenTransform::ETextureBasis::ViewportUV,
			FScreenTransform::ETextureBasis::TextureUV);

	PassParameters->OuterColor =
		FVector4f(0.0f, 0.0f, 0.0f, 1.0f);

	PassParameters->InnerColor =
		FVector4f(0.0f, 0.0f, 0.0f, 0.85f);

	PassParameters->ContactColor =
		FVector4f(0.0f, 0.0f, 0.0f, 1.0f);

	PassParameters->MaterialColor =
		FVector4f(0.0f, 0.0f, 0.0f, 1.0f);

	PassParameters->DepthRelativeThreshold =
		FMath::Max(
			CVarToonOutlineDepthThreshold
			.GetValueOnRenderThread(),
			0.0f);

	PassParameters->NormalThreshold =
		FMath::Clamp(
			CVarToonOutlineNormalThreshold
			.GetValueOnRenderThread(),
			0.0f,
			2.0f);

	PassParameters->OcclusionBias =
		FMath::Max(
			CVarToonOutlineOcclusionBias
			.GetValueOnRenderThread(),
			0.0f);

	PassParameters->OutlineWidth =
		FMath::Clamp(
			CVarToonOutlineWidth
			.GetValueOnRenderThread(),
			1,
			8);

	PassParameters->RenderTargets[0] =
		Output.GetRenderTargetBinding();

	const TShaderMapRef<FToonOutlinePS> PixelShader(
		View.ShaderMap);

	FPixelShaderUtils::AddFullscreenPass(
		GraphBuilder,
		View.ShaderMap,
		RDG_EVENT_NAME(
			"ToonOutline %dx%d",
			OutputViewport.Rect.Width(),
			OutputViewport.Rect.Height()),
		PixelShader,
		PassParameters,
		Output.ViewRect);

	return MoveTemp(Output);
}

SHADER_PARAMETER(

FScreenTransform,

SvPositionToViewportUV)

SHADER_PARAMETER(

FScreenTransform,

ViewportUVToInputUV)

SHADER_PARAMETER(

FScreenTransform,

ViewportUVToSceneUV)

这三个UV什么意思有什么区别?

这三个参数本质都是 FScreenTransform(Scale + Bias),区别是输入、输出坐标空间不同:

复制代码
SV_Position
    ↓ SvPositionToViewportUV
ViewportUV
    ├─ ViewportUVToInputUV → InputUV
    └─ ViewportUVToSceneUV → SceneUV
  • SvPositionToViewportUV

    • 输入:像素着色器的 SV_Position.xy,即 RenderTarget 上的绝对像素坐标。

    • 输出:当前 Viewport 内部的局部归一化 UV,左上角约为 (0,0),右下角约为 (1,1)

    • 公式近似:

      ViewportUV = (SvPosition.xy - ViewportMin) / ViewportSize;

  • ViewportUVToInputUV

    • 把当前 Viewport 的局部 UV 转换为"输入纹理"的真实采样 UV。

    • 会考虑输入纹理的尺寸、输入 ViewRect 的偏移和大小。

    • 用来采样当前 Pass 的输入纹理:

      float2 InputUV = ApplyScreenTransform(ViewportUV, ViewportUVToInputUV);
      Color = InputTexture.Sample(InputSampler, InputUV);

  • ViewportUVToSceneUV

    • 把当前 Viewport UV 转换为 Scene Texture 空间的真实 UV。

    • 通常用于采样 SceneColorSceneDepth、GBuffer 等场景纹理。

    • 会考虑场景纹理总尺寸以及当前 View 在场景纹理中的区域。

      float2 SceneUV = ApplyScreenTransform(ViewportUV, ViewportUVToSceneUV);
      Depth = SceneDepthTexture.Sample(SceneDepthSampler, SceneUV);

为什么不能直接都用 ViewportUV

假设场景纹理是 1920×1080,当前 View 只占右半边:

复制代码
SceneTexture: 1920×1080
ViewRect:     Min=(960,0), Size=(960,1080)

那么当前 View 中心:

复制代码
ViewportUV = (0.5, 0.5)
SceneUV    = (0.75, 0.5)

直接拿 (0.5,0.5) 采 SceneTexture,会采到整张纹理中心,而不是右半屏 View 的中心。

一句话记忆:

复制代码
ViewportUV:当前画面的相对位置
InputUV:输入纹理里的真实位置
SceneUV:场景纹理里的真实位置
SV_Position:RenderTarget 上的绝对像素位置

举个例子:

  • SvPosition0~1920 / 0~1080 的像素坐标。
  • ViewportUV:转换成当前 Viewport 内的 0~1
  • InputUV:映射到 Widget 框中那块输入纹理的 UV。
  • SceneUV:映射到当前 View 在整个 SceneTexture 中对应的 UV。

复制代码
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneDepthTexture)
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, GBufferATexture)
    SHADER_PARAMETER_RDG_TEXTURE(Texture2D, GBufferBTexture)
    // 还有很多......
END_SHADER_PARAMETER_STRUCT()

现在这些参数已经被 UE 打包在:

复制代码
FSceneTextureShaderParameters

所以直接写:

复制代码
SHADER_PARAMETER_STRUCT_INCLUDE(
    FSceneTextureShaderParameters,
    SceneTextures)

相当于:

复制代码
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
    // 把 FSceneTextureShaderParameters 里面的所有参数复制到这里
END_SHADER_PARAMETER_STRUCT()

假设子结构:

复制代码
BEGIN_SHADER_PARAMETER_STRUCT(FSceneParams, )
    SHADER_PARAMETER(float, Depth)
END_SHADER_PARAMETER_STRUCT()

使用普通 STRUCT

复制代码
SHADER_PARAMETER_STRUCT(FSceneParams, SceneTextures)

C++:

复制代码
Parameters->SceneTextures.Depth = 1.0f;

Shader 里带成员前缀:

复制代码
float Depth = SceneTextures_Depth;

使用 STRUCT_INCLUDE

复制代码
SHADER_PARAMETER_STRUCT_INCLUDE(FSceneParams, SceneTextures)

C++ 仍然一样:

复制代码
Parameters->SceneTextures.Depth = 1.0f;

但 Shader 里会展开,不带 SceneTextures 前缀:

复制代码
float Depth = Depth;

所以区别就是:

复制代码
SHADER_PARAMETER_STRUCT
→ Shader 参数保持嵌套,名字带 SceneTextures 前缀

SHADER_PARAMETER_STRUCT_INCLUDE
→ C++ 仍然嵌套,但 Shader 参数被平铺展开

TSR 是虚幻引擎的 Temporal Super Resolution(时间超级分辨率),是一种时序升采样技术。

为什么能提高分辨率?关键是每一帧的采样位置都故意偏一点。

假设一个高分辨率像素区域内有 4 个更细的采样位置:

复制代码
┌───────┐
│ ①  ② │
│ ③  ④ │
└───────┘

低分辨率的一帧只能采其中一个位置:

复制代码
第 1 帧采 ①
第 2 帧采 ②
第 3 帧采 ③
第 4 帧采 ④

这个偏移就是 Jitter(抖动)。虽然每一帧都是低分辨率,但多帧合起来获得了不同的亚像素信息。

TSR 的工作大致是:

复制代码
当前帧低分辨率颜色
        +
历史帧中不同位置的采样
        ↓ Motion Vector 对齐运动物体
        ↓ Depth 判断历史数据是否仍有效
        ↓ 剔除遮挡变化、残影和错误历史
在高分辨率网格上重建结果

.inl 一般是 inline implementation(内联实现文件)

它主要用来存放应该写在头文件里、但又不想让 .h 太乱的实现代码,例如:

  • inline 函数
  • 模板函数
  • 模板类成员函数
  • 很短的工具函数

典型结构:

复制代码
// ScreenPass.h
struct FScreenTransform
{
    inline FVector2f Apply(FVector2f Position) const;
};

#include "ScreenPass.inl"

// ScreenPass.inl
inline FVector2f FScreenTransform::Apply(FVector2f Position) const
{
    return Position * Scale + Bias;
}

为什么不能都放进 .cpp

模板和很多内联函数在编译调用方代码时,编译器必须能看到完整实现。如果只写在 .cpp 中,其他编译单元通常看不到,可能导致模板无法实例化或链接错误。

因此:

复制代码
.h   :声明、类型定义、对外接口
.inl :需要随头文件一起可见的函数实现
.cpp :普通实现,只编译一次

.inl 并不是 C++ 的特殊语法,编译器不会自动处理它。本质还是普通文本文件,通常由 .h 通过:

复制代码
#include "ScreenPass.inl"

包含进来。

所以 ScreenPass.inl 可以简单理解为:

ScreenPass.h 拆出去的那部分内联/模板实现。

PassParameters->InputSampler =

TStaticSamplerState<

SF_Point,

AM_Clamp,

AM_Clamp,

AM_Clamp>::GetRHI();

SF_POINT什么意思?

SF_Point 表示使用点采样(Point/Nearest Neighbor,最近邻采样)

采样一个 UV 时,直接取得距离该位置最近的那个纹素,不会和周围纹素进行插值。

例如纹理中相邻像素:

复制代码
红色 | 蓝色

采样边界附近时:

复制代码
SF_Point:直接得到红色或者蓝色
SF_Bilinear:得到红蓝混合后的紫色

特点:

复制代码
SF_Point
- 速度快
- 结果边缘硬
- 放大后有明显像素块
- 不会混合相邻像素

PassParameters->MaterialIdTexture =
			GSystemTextures.GetBlackDummy(GraphBuilder);

这行代码是在给 MaterialIdTexture 设置一张默认的纯黑占位纹理

复制代码
PassParameters->OcclusionBias =
        FMath::Max(
            CVarToonOutlineOcclusionBias
            .GetValueOnRenderThread(),
            0.0f);

当要绘制描边的物体其实深度不是最靠前的,这个时候判断画不画这个描边


PostProcessToonOutline.h

复制代码
#pragma once

#include "ScreenPass.h"
#include "SceneTextureParameters.h"

struct FToonOutlineInputs
{
	FScreenPassRenderTarget OverrideOutput;

	FScreenPassTexture SceneColor;
	FScreenPassTexture SceneDepth;

	// 给 USF 读取 SceneDepth / CustomDepth / CustomStencil / GBuffer。
	FSceneTextureShaderParameters SceneTextures;
};

bool IsToonOutlineEnabled();

FScreenPassTexture AddToonOutlinePass(
	FRDGBuilder& GraphBuilder,
	const FViewInfo& View,
	const FToonOutlineInputs& Inputs);

这里就没啥好说的


PostProcessToonOutline.usf:

复制代码
#include "Common.ush"
#include "ScreenPass.ush"
#include "SceneTexturesCommon.ush"
#include "DeferredShadingCommon.ush"

Texture2D InputTexture;
SamplerState InputSampler;

SCREEN_PASS_TEXTURE_VIEWPORT(Input)
SCREEN_PASS_TEXTURE_VIEWPORT(Depth)

FScreenTransform SvPositionToViewportUVTransform;
FScreenTransform ViewportUVToInputUV;
FScreenTransform ViewportUVToSceneUV;

float4 OuterColor;
float4 InnerColor;
float4 ContactColor;
float4 MaterialColor;

float DepthRelativeThreshold;
float NormalThreshold;
float OcclusionBias;
int OutlineWidth;

struct FEdgeData
{
	uint Stencil;
	uint MaterialId;

	float CustomDepth;
	float SceneDepth;

	float3 WorldNormal;

	bool HasMaterialId;
	bool Valid;
	bool Visible;
};

uint DecodeMaterialId(float EncodedMaterialId)
{
	return (uint) round(
		saturate(EncodedMaterialId) * 255.0f);
}

FEdgeData LoadEdgeData(float2 UV)
{
	FEdgeData Result;

	UV = clamp(
		UV,
		Depth_UVViewportBilinearMin,
		Depth_UVViewportBilinearMax);

	uint2 PixelPosition =
		uint2(UV * Depth_Extent);

	Result.Stencil =
		CalcSceneCustomStencil(PixelPosition);

	Result.CustomDepth =
		CalcSceneCustomDepth(UV);

	Result.SceneDepth =
		CalcSceneDepth(UV);

	FScreenSpaceData ScreenData =
		GetScreenSpaceData(UV, true);

	Result.WorldNormal =
		ScreenData.GBuffer.WorldNormal;

	Result.HasMaterialId =
		ScreenData.GBuffer.ShadingModelID ==
			SHADINGMODELID_DEFAULT_LIT;

	if (Result.HasMaterialId)
	{
		Result.MaterialId = DecodeMaterialId(
			ScreenData.GBuffer.CustomData.x);
	}
	else
	{
		Result.MaterialId = 0;
	}

	Result.Valid =
		Result.Stencil != 0;

	Result.Visible =
		Result.CustomDepth <=
			Result.SceneDepth + OcclusionBias;

	return Result;
}

void MainPS(
	float4 SvPosition : SV_POSITION,
	out float4 OutColor : SV_Target0)
{
	float2 ViewportUV =
		ApplyScreenTransform(
			SvPosition.xy,
			SvPositionToViewportUVTransform);

	float2 InputUV =
		ApplyScreenTransform(
			ViewportUV,
			ViewportUVToInputUV);

	float2 SceneUV =
		ApplyScreenTransform(
			ViewportUV,
			ViewportUVToSceneUV);

	OutColor =
		Texture2DSampleLevel(
			InputTexture,
			InputSampler,
			InputUV,
			0);

	FEdgeData Center =
		LoadEdgeData(SceneUV);

	static const int2 Directions[8] =
	{
		int2(-1, 0),
		int2(1, 0),
		int2(0, -1),
		int2(0, 1),

		int2(-1, -1),
		int2(1, -1),
		int2(-1, 1),
		int2(1, 1)
	};

	float OuterMask = 0.0f;
	float InnerMask = 0.0f;
	float ContactMask = 0.0f;
	float MaterialMask = 0.0f;

	[loop]
	for (int Radius = 1;
		Radius <= OutlineWidth;
		++Radius)
	{
		[unroll]
		for (int Index = 0;
			Index < 8;
			++Index)
		{
			float2 Offset =
				float2(Directions[Index]) *
				Depth_ExtentInverse *
				Radius;

			FEdgeData Neighbor =
				LoadEdgeData(SceneUV + Offset);

			if (!Center.Valid &&
				Neighbor.Valid &&
				Neighbor.CustomDepth <=
					Center.SceneDepth +
					OcclusionBias)
			{
				OuterMask = 1.0f;
			}

			if (Center.Valid &&
				Neighbor.Valid &&
				Center.Visible &&
				Neighbor.Visible)
			{
				float MinimumDepth =
					max(
						min(
							Center.CustomDepth,
							Neighbor.CustomDepth),
						1.0f);

				float RelativeDepthDifference =
					abs(
						Center.CustomDepth -
						Neighbor.CustomDepth) /
					MinimumDepth;

				if (RelativeDepthDifference >
					DepthRelativeThreshold)
				{
					InnerMask = 1.0f;
				}

				bool DifferentObject =
					Center.Stencil !=
						Neighbor.Stencil;

				float NormalDifference =
					1.0f -
					saturate(
						dot(
							Center.WorldNormal,
							Neighbor.WorldNormal));

				if (DifferentObject ||
					NormalDifference >
						NormalThreshold)
				{
					ContactMask = 1.0f;
				}

				if (Center.HasMaterialId &&
					Neighbor.HasMaterialId &&
					Center.MaterialId !=
						Neighbor.MaterialId)
				{
					MaterialMask = 1.0f;
				}
			}
		}
	}

	float4 EdgeColor = 0.0f;

	if (OuterMask > 0.0f)
	{
		EdgeColor = OuterColor;
	}

	if (InnerMask > 0.0f)
	{
		EdgeColor = InnerColor;
	}

	if (ContactMask > 0.0f)
	{
		EdgeColor = ContactColor;
	}

	if (MaterialMask > 0.0f)
	{
		EdgeColor = MaterialColor;
	}

	OutColor.rgb =
		lerp(
			OutColor.rgb,
			EdgeColor.rgb,
			EdgeColor.a);
}

之前有讲过,这里再写一次吧:

复制代码
SCREEN_PASS_TEXTURE_VIEWPORT(Input)
SCREEN_PASS_TEXTURE_VIEWPORT(Depth)

第一行展开后类似:

复制代码
float2 Input_Extent;
float2 Input_ExtentInverse;
uint2  Input_ViewportMin;
uint2  Input_ViewportMax;
float2 Input_ViewportSize;
float2 Input_ViewportSizeInverse;
float2 Input_UVViewportMin;
float2 Input_UVViewportMax;
// 还有其他参数

第二行则展开为:

复制代码
float2 Depth_Extent;
float2 Depth_ExtentInverse;
uint2  Depth_ViewportMin;
uint2  Depth_ViewportMax;
float2 Depth_ViewportSize;
float2 Depth_ViewportSizeInverse;
float2 Depth_UVViewportMin;
float2 Depth_UVViewportMax;
// ...

Depth_UVViewportBilinearMinDepth_UVViewportBilinearMax 是当前 Depth Viewport 中,专门为双线性采样准备的安全 UV 边界

它们其实是两个变量:

复制代码
Depth_UVViewportBilinearMin
Depth_UVViewportBilinearMax

普通 Viewport 边界:

复制代码
Depth_UVViewportMin
Depth_UVViewportMax

表示区域的几何边缘。但纹理像素的采样点位于每个像素的中心,不在边缘。

例如一张宽度为 4 的纹理:

复制代码
纹理边界:0 ------------------------- 1
像素中心: 0.125  0.375  0.625  0.875

所以双线性安全范围不是 0~1,而是:

复制代码
0.125~0.875

通用计算方式:

复制代码
BilinearMin = UVViewportMin + 0.5 / TextureExtent;
BilinearMax = UVViewportMax - 0.5 / TextureExtent;

两个view的时候,viewrect裁剪了左边的,右边的进行正常显示,然后UV还是正常的左上角00,右下角11,然后这个时候到右边显示的边界的时候,怕采样到了左边被viewrect裁剪的像素,所以保留一个安全UV边界

这里采样的是 Depth,一点点混入可能影响很大。例如:

复制代码
当前 View 深度:100
旁边 View 深度:1

双线性混入一点后:

复制代码
Depth = 100 × 0.9 + 1 × 0.1 = 90.1

描边通常根据深度差判断边缘。这个错误值可能导致:

  • 屏幕边缘突然出现描边
  • 深度遮挡判断错误
  • 分屏交界处出现黑线
  • 镜头移动时边缘闪烁

DecodeMaterialId() 本质就是:

复制代码
(uint)round(saturate(Value) * 255.0f)

CalcSceneCustomStencil() 是虚幻引擎自带的 Shader 函数。

定义在:

SceneTexturesCommon.ush

实现大致是:

复制代码
uint CalcSceneCustomStencil(uint2 PixelPos)
{
    return SceneTexturesStruct.CustomStencilTexture.Load(
        uint3(PixelPos, 0)
    ) STENCIL_COMPONENT_SWIZZLE;
}

GetScreenSpaceData() 也是虚幻引擎自带的 Shader 函数。

定义在:

DeferredShadingCommon.ush

函数声明:

复制代码
FScreenSpaceData GetScreenSpaceData(
    float2 UV,
    bool bGetNormalizedNormal = true,
    bool bForceUnlitOrDefaultLit = false)

FScreenSpaceData能拿到GBuffer数据

可以直接使用 GetGBufferData(),不一定要用 GetScreenSpaceData()

实际上 GetScreenSpaceData() 内部就是这样写的:

复制代码
FScreenSpaceData GetScreenSpaceData(float2 UV, bool bGetNormalizedNormal)
{
    FScreenSpaceData Out;

    Out.GBuffer = GetGBufferData(UV, bGetNormalizedNormal);

    float4 ScreenSpaceAO = Texture2DSampleLevel(
        SceneTexturesStruct.ScreenSpaceAOTexture,
        SceneTexturesStruct_ScreenSpaceAOTextureSampler,
        UV,
        0);

    Out.AmbientOcclusion = ScreenSpaceAO.r;

    return Out;
}

GetGBufferData() 是 UE Shader 文件中的 HLSL 函数。

它定义在:

复制代码
/Engine/Private/DeferredShadingCommon.ush

这是一个初版效果:

这里面有很多问题:

1、距离离远离近,像素粗度都是一致

2、距离移动会产生闪烁

3、会有material id map和其他描边重合的问题


问题一、距离离远离近像素粗细都是一致的

用距离去做线与场景颜色的alpha混合,并且,根据距离远近让线的粗细也产生变化

问题二:距离移动会产生闪烁

首先得把执行pass的位置放到taa和tsr之前

第二:有精度损失问题:

GBufferA的精度是R10G10B10A2 GBufferD精度是R8G8B8A8,各个通道的精度不一样,会产生映射的错误,导致精度误差越来越大

第三视角由近既远的过程中,单个像素占世界的范围也越来越大,那么两个相邻像素去法线和深度去找插值也在不断变化,所以会产生闪烁

问题三、会有material id map和其他描边重合的问题,我们之后一个一个解决

这一期就暂时在这里,还会继续多思考