思路:
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。
-
通常用于采样
SceneColor、SceneDepth、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 上的绝对像素位置
举个例子:
SvPosition:0~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_UVViewportBilinearMin 和 Depth_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和其他描边重合的问题,我们之后一个一个解决
这一期就暂时在这里,还会继续多思考