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 TAutoConsoleVariableint32 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 TAutoConsoleVariableint32 CVarToonOutlineWidth( TEXT(r.ToonOutline.Width), 2, TEXT(Toon outline width in pixels. Range: 1-8.), ECVF_RenderThreadSafe); static TAutoConsoleVariablefloat CVarToonOutlineDepthThreshold( TEXT(r.ToonOutline.DepthThreshold), 0.01f, TEXT(Relative CustomDepth difference used to detect inner edges.), ECVF_RenderThreadSafe); static TAutoConsoleVariablefloat 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 TAutoConsoleVariablefloat 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 TShaderMapRefFToonOutlinePS 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什么意思有什么区别这三个参数本质都是FScreenTransformScale Bias区别是输入、输出坐标空间不同SV_Position ↓ SvPositionToViewportUV ViewportUV ├─ ViewportUVToInputUV → InputUV └─ ViewportUVToSceneUV → SceneUVSvPositionToViewportUV输入像素着色器的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_PositionRenderTarget 上的绝对像素位置举个例子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()使用普通STRUCTSHADER_PARAMETER_STRUCT(FSceneParams, SceneTextures)CParameters-SceneTextures.Depth 1.0f;Shader 里带成员前缀float Depth SceneTextures_Depth;使用STRUCT_INCLUDESHADER_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 TStaticSamplerStateSF_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所以双线性安全范围不是01而是0.1250.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各个通道的精度不一样会产生映射的错误导致精度误差越来越大第三视角由近既远的过程中单个像素占世界的范围也越来越大那么两个相邻像素去法线和深度去找插值也在不断变化所以会产生闪烁为什么GBuffer的法线的精度要比金属度 高光 粗糙度 basecolor的精度要高啊11:14因为 GBuffer 里的法线不是“一个材质颜色参数”它是后面几乎所有光照计算的方向基础。它一点点错后面会被放大。可以先这样记BaseColor / Metallic / Specular / Roughness 材质属性 Normal 光照方向计算的几何依据光照里最常见的计算是Diffuse ≈ BaseColor * saturate(dot(Normal, LightDir))这里Normal只要偏一点dot(N, L)就变了。尤其在掠射角、边缘、高光区域这个误差会很明显。更麻烦的是高光Specular Highlight / Reflection 强依赖 Normal法线偏一点反射方向也会偏正常法线 高光在正确位置 量化误差大的法线 高光位置抖动 / 断层 / 一块一块 banding问题三、会有material id map和其他描边重合的问题我们之后一个一个解决这一期就暂时在这里还会继续多思考