本文重点介绍如何修改 UE5 中的渲染管线,要修改渲染管线有一些前置知识需要理解,因此笔者会先简单介绍下渲染管线的概念以及当前主流的渲染管线的实现思路,为后面在 UE5 中自定义渲染管线做铺垫;要注意本文默认渲染管线即是光栅化渲染管线(不考虑光线追踪),同时也不会介绍太多管线的实现细节和当下流行的优化版本,对渲染管线实现细节感兴趣的可以自行查阅相关资料。
渲染管线就是将一个三维的场景渲染成一张二维图片的流水线,RTR3 一书中将它分成了三个阶段:应用阶段、几何阶段和光栅化阶段。其中每个阶段又会包含一些子阶段(如上图所示),我们打平来看:
数据输入;
顶点着色器;
图元(三角形)处理;
光栅化;
片元着色器/像素着色器;
测试与混合;
目前游戏引擎中有两种主流的渲染管线实现方式:向前渲染和延迟渲染,在 UE 中 PC 端默认是延迟渲染,移动端默认是向前渲染。
向前渲染使用的渲染方式我们称为“画家算法”,首先把所有的几何按照由远到近排序,先画最远的几何,然后依次画更近的几何,这样当所有几何都画完整张图片就画完了。这个算法非常好理解,实现起来也很简单,但是我们简单想想就会发现问题,如果场景中存在近处的物体把远处覆盖的情况,那被覆盖的物体似乎不需要被画出来,如果场景中有很多这样的情况,那就造成了很大的资源浪费。
前文提到,在向前渲染中由于把被遮挡的物体都渲染了一遍造成了资源浪费,人们分析发现最大的浪费其实在于光照计算部分,所以人们想出了一个优化的渲染方式,把光照计算放到最后,提前把所有集合除光照计算以外的光栅化结果放到一批贴图上,最后做一次光照计算,这就是延迟渲染。
延迟渲染的优势非常明显,由于延迟渲染只对屏幕中的像素做光照计算,所以不管是对于几何复杂的场景还是多光源场景延迟渲染的性能差别不会太大,而向前渲染对于复杂的场景会有大量浪费。
但是延迟渲染也有它的缺点:
由于需要一批贴图来保存除光照外的光栅化结果,这对显卡的显存和带宽提出了要求;
由于进行了两次光栅化,最终结果会相对模糊。
前文介绍过,渲染管线就是将三维场景渲染成二维图片的流水线,那么修改这个流水线中的任意部分都叫修改渲染管线,但是并非渲染管线中所有环节都能修改,GPU 提供可编程的部分有:顶点着色器、集合着色器和片元着色器。本文将以片元着色器为例,带大家熟悉 UE5 相关部分的源码实现,以及如何进行修改。
我们最常用的着色模型就是“默认光照(DefaultLit)”模型,要快速看到效果,可以直接改这个模型对应的 BxDF 函数:
FDirectLighting DefaultLitBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
BxDFContext Context;
FDirectLighting Lighting;
#if SUPPORTS_ANISOTROPIC_MATERIALS
bool bHasAnisotropy = HasAnisotropy(GBuffer.SelectiveOutputMask);
#else
bool bHasAnisotropy = false;
#endif
float NoV, VoH, NoH;
BRANCH
if (bHasAnisotropy)
{
half3 X = GBuffer.WorldTangent;
half3 Y = normalize(cross(N, X));
Init(Context, N, X, Y, V, L);
NoV = Context.NoV;
VoH = Context.VoH;
NoH = Context.NoH;
}
else
{
#if SHADING_PATH_MOBILE
InitMobile(Context, N, V, L, NoL);
#else
Init(Context, N, V, L);
#endif
NoV = Context.NoV;
VoH = Context.VoH;
NoH = Context.NoH;
SphereMaxNoH(Context, AreaLight.SphereSinAlpha, true);
}
Context.NoV = saturate(abs( Context.NoV ) + 1e-5);
#if MATERIAL_ROUGHDIFFUSE
// Chan diffuse model with roughness == specular roughness. This is not necessarily a good modelisation of reality because when the mean free path is super small, the diffuse can in fact looks rougher. But this is a start.
// Also we cannot use the morphed context maximising NoH as this is causing visual artefact when interpolating rough/smooth diffuse response.
Lighting.Diffuse = Diffuse_Chan(GBuffer.DiffuseColor, Pow4(GBuffer.Roughness), NoV, NoL, VoH, NoH, GetAreaLightDiffuseMicroReflWeight(AreaLight));
#else
Lighting.Diffuse = Diffuse_Lambert(GBuffer.DiffuseColor);
#endif
Lighting.Diffuse *= AreaLight.FalloffColor * (Falloff * NoL);
BRANCH
if (bHasAnisotropy)
{
//Lighting.Specular = GBuffer.WorldTangent * .5f + .5f;
Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * SpecularGGX(GBuffer.Roughness, GBuffer.Anisotropy, GBuffer.SpecularColor, Context, NoL, AreaLight);
}
else
{
if( IsRectLight(AreaLight) )
{
Lighting.Specular = RectGGXApproxLTC(GBuffer.Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture);
}
else
{
Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * SpecularGGX(GBuffer.Roughness, GBuffer.SpecularColor, Context, NoL, AreaLight);
}
}
FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(GBuffer.Roughness, Context.NoV, GBuffer.SpecularColor);
// Add energy presevation (i.e. attenuation of the specular layer onto the diffuse component
Lighting.Diffuse *= ComputeEnergyPreservation(EnergyTerms);
// Add specular microfacet multiple scattering term (energy-conservation)
Lighting.Specular *= ComputeEnergyConservation(EnergyTerms);
Lighting.Transmission = 0;
return Lighting;
}
BxDF 函数返回的是 FDirectLighting 的实例,其中主要包含 Diffuse 和 Specular 两部分,分别表示漫反射和镜面反射的颜色,我们把其中的镜面反射改为固定红颜色效果如下:
由于大部分材质都是 DefaultLit ,看起来被我改坏了,赶紧改回来。
C++部分
EngineTypes.h
Engine/Source/Runtime/Engine/Classes/Engine/EngineTypes.h
enum EMaterialShadingModel
首先注册自定义着色模型的枚举值:
Engine/Source/Runtime/Engine/Private/Materials/MaterialShader.cpp
FString GetShadingModelString
然后在 MaterialShader.cpp 中定义 MSM_Toon 枚举的字符标识:
Engine/Source/Runtime/Engine/Private/MaterialsHLSLMaterialTranslator.cpp
void FHLSLMaterialTranslator::GetMaterialEnvironment
Engine/Source/Runtime/Engine/PrivateMaterialsMaterial.cpp
static bool IsPropertyActive_Internal
Engine/Source/Runtime/Engine/Public/MaterialShared.h
inline bool IsSubsurfaceShadingModel
Engine/Source/Runtime/Engine/Private/Materials/MaterialShared.cpp
FText FMaterialAttributeDefinitionMap::GetAttributeOverrideForMaterial
Engine/Source/Runtime/Render/Core/Public/ShaderMaterial.h
struct FShaderMaterialPropertyDefines
EngineSourceRuntimeRenderCorePrivateShaderMaterialDerivedHelpers.cpp
FShaderMaterialDerivedDefines RENDERCORE_API CalculateDerivedMaterialParameters
Dst.WRITES_CUSTOMDATA_TO_GBUFFER = (Dst.USES_GBUFFER && (Mat.MATERIAL_SHADINGMODEL_SUBSURFACE || Mat.MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || Mat.MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || Mat.MATERIAL_SHADINGMODEL_CLEAR_COAT || Mat.MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || Mat.MATERIAL_SHADINGMODEL_HAIR || Mat.MATERIAL_SHADINGMODEL_CLOTH || Mat.MATERIAL_SHADINGMODEL_EYE || Mat.MATERIAL_SHADINGMODEL_TOON));
Engine/Source/Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp
void FShaderCompileUtilities::ApplyFetchEnvironment(FShaderMaterialPropertyDefines& SrcDefines, FShaderCompilerEnvironment& OutEnvironment)
static void DetermineUsedMaterialSlots
好了,到这里 C++ 部分的代码修改就结束了,我们可以 build 一下 UE5 的代码
然后通过代码打开 UE5 编辑器,进入材质编辑器看看是否有我们新定义的着色模型:
可以看到我们新创建的着色模型已经能够在下拉框中展示出来了。
前面 C++ 部分只是告诉 UE5 编辑器我们新增了一个着色模型、着色模型的相关配置项以及开启 GBuffer 写入权限,要真正在 GPU 中进行着色计算还是需要在 Shader 部分进行修改。
同样的还是要先注册着色模型
Definitions.usf
EngineShadersPrivateDefinitions.usf
ShadingCommon.ush
EngineShadersPrivateShadingCommon.ush
这里我定义了新着色模型的 DEBUG 颜色,可以在 UE5 编辑器中可视化看到效果,我这里定义的是金色,效果如下:
EngineShadersPrivateBasePassCommon.ush
#define WRITES_CUSTOMDATA_TO_FBUFFER (USES_GBUFFER && (MATERIAL_SHADINGMODEL_SUBSURFACE || MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || MATERIAL_SHADINGMODEL_CLEAR_COAT || MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || MATERIAL_SHADINGMODEL_HAIR || MATERIAL_SHADINGMODEL_CLOTH || MATERIAL_SHADINGMODEL_EYE || MATERIAL_SHADINGMODEL_TOON))
EngineShadersPrivateBasePassPixelShader.usf
为新定义的着色模型设置 SubsurfaceColor 权限,同时由于新定义的着色模型不需要默认计算得到的 SpecularColor 和 DiffuseColor,将其置为 0;
EngineShadersPrivateDeferredShadingCommon.ush
ShadingModelsMaterial.ush
EngineShadersPrivateShadingModelsMaterial.ush
EngineShadersPrivateReflectionEnvironmentPixelShader.usf
前面只忽略了 BasePass 中的 SpecularColor 和 DiffuseColor,ReflectionEnvironmentPixelShader 中要进行同样的操作:
ShadingModels.ush
EngineShadersPrivateShadingModels.ush
全部代码如下:
FDirectLighting ToonBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
//并不是真正光源颜色
//真正的光源颜色在DeferredLightingCommon.ush中通过LightAccumulator_AddSplit()叠加
float3 LightColor = AreaLight.FalloffColor * Falloff;
//解GBuffer
float SpecularRange = GBuffer.Metallic;
float SpecularIntensity = GBuffer.Specular;
float ShadowThreshold = GBuffer.Roughness;
float InnerLine = GBuffer.CustomData.a;
float3 SSSColor = ExtractSubsurfaceColor(GBuffer); //解码SSS
//明暗颜色
float3 BrightColor = GBuffer.BaseColor;
float3 ShadowColor = GBuffer.BaseColor * SSSColor;
//加粗内描边
if (InnerLine < 0.8f)
{
InnerLine *= 0.5f;
}
float3 InnerLineColor = float3(InnerLine, InnerLine, InnerLine);
half3 H = normalize(V + L);
float NoH = saturate(dot(N, H));
//阴影区计算
float IsShadow = step(ShadowThreshold, NoL * Shadow.SurfaceShadow);
//光照计算
FDirectLighting Lighting;
Lighting.Diffuse = InnerLineColor * LightColor * Diffuse_Lambert(lerp(ShadowColor, BrightColor, IsShadow));
Lighting.Specular = LightColor * BrightColor * IsShadow * InnerLineColor * step(0.2f, SpecularRange * pow(NoH, SpecularIntensity));
Lighting.Transmission = 0;
return Lighting;
}
EngineShadersPrivateSkyLightingDiffuseShared.ush
由于当前示例是要实现卡通渲染的效果,还需要减少天光对着色的影响:
BRANCH
if(GBuffer.ShadingModelID == SHADINGMODELID_TOON)
{
float InnerLine = GBuffer.CustomData.a;
if (InnerLine < 0.8f)
{
InnerLine *= 0.5f;
}
float3 InnerLineColor = float3(InnerLine, InnerLine, InnerLine);
Lighting = InnerLineColor * GBuffer.BaseColor * View.SkyLightColor.rgb * 0.05f; //削弱天光影响
return Lighting;
}
注:以上部分参考了多篇介绍卡通渲染的文章,卡通渲染是修改渲染管线的常见案例,但修改渲染管线并非只是用于卡通渲染,很多时候我们要根据场景优化渲染效率也会涉及管线的修改,并且也不一定是要新增着色模型,还需要根据实际需求来分析。
参考
UE自定义渲染
剖析虚幻渲染体系(04)- 延迟渲染管线
Forward Rendering vs. Deferred Rendering
从零开始的UE5卡通渲染【二】:自定义着色模型