修改 UE5 中的渲染管线

简介: 前言本文重点介绍如何修改 UE5 中的渲染管线,要修改渲染管线有一些前置知识需要理解,因此笔者会先简单介绍下渲染管线的概念以及当前主流的渲染管线的实现思路,为后面在 UE5 中自定义渲染管线做铺垫;要注意本文默认渲染管线即是光栅化渲染管线(不考虑光线追踪),同时也不会介绍太多管线的实现细节和当下流行的优化版本,对渲染管线实现细节感兴趣的可以自行查阅相关资料。渲染管线 Rendering Pipel

前言

本文重点介绍如何修改 UE5 中的渲染管线,要修改渲染管线有一些前置知识需要理解,因此笔者会先简单介绍下渲染管线的概念以及当前主流的渲染管线的实现思路,为后面在 UE5 中自定义渲染管线做铺垫;要注意本文默认渲染管线即是光栅化渲染管线(不考虑光线追踪),同时也不会介绍太多管线的实现细节和当下流行的优化版本,对渲染管线实现细节感兴趣的可以自行查阅相关资料。

渲染管线 Rendering Pipeline

渲染管线就是将一个三维的场景渲染成一张二维图片的流水线,RTR3 一书中将它分成了三个阶段:应用阶段、几何阶段和光栅化阶段。其中每个阶段又会包含一些子阶段(如上图所示),我们打平来看:

  1. 数据输入;
  2. 顶点着色器;
  3. 图元(三角形)处理;
  4. 光栅化;
  5. 片元着色器/像素着色器;
  6. 测试与混合;

目前游戏引擎中有两种主流的渲染管线实现方式:向前渲染和延迟渲染,在 UE 中 PC 端默认是延迟渲染,移动端默认是向前渲染。

向前渲染 Forward Rendering

向前渲染使用的渲染方式我们称为“画家算法”,首先把所有的几何按照由远到近排序,先画最远的几何,然后依次画更近的几何,这样当所有几何都画完整张图片就画完了。这个算法非常好理解,实现起来也很简单,但是我们简单想想就会发现问题,如果场景中存在近处的物体把远处覆盖的情况,那被覆盖的物体似乎不需要被画出来,如果场景中有很多这样的情况,那就造成了很大的资源浪费。

延迟渲染 Differred Rendering

前文提到,在向前渲染中由于把被遮挡的物体都渲染了一遍造成了资源浪费,人们分析发现最大的浪费其实在于光照计算部分,所以人们想出了一个优化的渲染方式,把光照计算放到最后,提前把所有集合除光照计算以外的光栅化结果放到一批贴图上,最后做一次光照计算,这就是延迟渲染。

延迟渲染的优势非常明显,由于延迟渲染只对屏幕中的像素做光照计算,所以不管是对于几何复杂的场景还是多光源场景延迟渲染的性能差别不会太大,而向前渲染对于复杂的场景会有大量浪费。

但是延迟渲染也有它的缺点:

  1. 由于需要一批贴图来保存除光照外的光栅化结果,这对显卡的显存和带宽提出了要求;
  2. 由于进行了两次光栅化,最终结果会相对模糊。

修改渲染管线

前文介绍过,渲染管线就是将三维场景渲染成二维图片的流水线,那么修改这个流水线中的任意部分都叫修改渲染管线,但是并非渲染管线中所有环节都能修改,GPU 提供可编程的部分有:顶点着色器、集合着色器和片元着色器。本文将以片元着色器为例,带大家熟悉 UE5 相关部分的源码实现,以及如何进行修改。

直接修改默认光照模型的 BxDF

我们最常用的着色模型就是“默认光照(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++部分

1. 注册新的着色模型

EngineTypes.h

\Engine\Source\Runtime\Engine\Classes\Engine\EngineTypes.h

enum EMaterialShadingModel

首先注册自定义着色模型的枚举值:

MaterialShader.cpp

\Engine\Source\Runtime\Engine\Private\Materials\MaterialShader.cpp

FString GetShadingModelString

然后在 MaterialShader.cpp 中定义 MSM_Toon 枚举的字符标识:

HLSLMaterialTranslator.cpp

\Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.cpp

void FHLSLMaterialTranslator::GetMaterialEnvironment

开放材质属性

Material.cpp

\Engine\Source\Runtime\Engine\Private\Materials\Material.cpp

static bool IsPropertyActive_Internal

MaterialShared.h

\Engine\Source\Runtime\Engine\Public\MaterialShared.h

inline bool IsSubsurfaceShadingModel

MaterialShared.cpp

\Engine\Source\Runtime\Engine\Private\Materials\MaterialShared.cpp

FText FMaterialAttributeDefinitionMap::GetAttributeOverrideForMaterial

为新的着色模型启用 GBuffer CustomData 写入权限 

ShaderMaterial.h

\Engine\Source\Runtime\RenderCore\Public\ShaderMaterial.h

struct FShaderMaterialPropertyDefines

ShaderMaterialDerivedHelpers.cpp

\Engine\Source\Runtime\RenderCore\Private\ShaderMaterialDerivedHelpers.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)); 
ShaderGenerationUtil.cpp

\Engine\Source\Runtime\Engine\Private\ShaderCompiler\ShaderGenerationUtil.cpp

void FShaderCompileUtilities::ApplyFetchEnvironment(FShaderMaterialPropertyDefines& SrcDefines, FShaderCompilerEnvironment& OutEnvironment)

static void DetermineUsedMaterialSlots

好了,到这里 C++ 部分的代码修改就结束了,我们可以 build 一下 UE5 的代码

然后通过代码打开 UE5 编辑器,进入材质编辑器看看是否有我们新定义的着色模型:

可以看到我们新创建的着色模型已经能够在下拉框中展示出来了。

Shader 部分

前面 C++ 部分只是告诉 UE5 编辑器我们新增了一个着色模型、着色模型的相关配置项以及开启 GBuffer 写入权限,要真正在 GPU 中进行着色计算还是需要在 Shader 部分进行修改。

注册着色模型

同样的还是要先注册着色模型

Definitions.usf

\Engine\Shaders\Private\Definitions.usf

ShadingCommon.ush

\Engine\Shaders\Private\ShadingCommon.ush

这里我定义了新着色模型的 DEBUG 颜色,可以在 UE5 编辑器中可视化看到效果,我这里定义的是金色,效果如下:

开启 GBuffer 相关写入权限

BasePassCommon.ush

\Engine\Shaders\Private\BasePassCommon.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)) 
BasePassPixelShader.usf

\Engine\Shaders\Private\BasePassPixelShader.usf

为新定义的着色模型设置 SubsurfaceColor 权限,同时由于新定义的着色模型不需要默认计算得到的 SpecularColor 和 DiffuseColor,将其置为 0;

DeferredShadingCommon.ush

\Engine\Shaders\Private\DeferredShadingCommon.ush

将前面定义的 SSS 和 CustomData0 存入 GBuffer

ShadingModelsMaterial.ush

\Engine\Shaders\Private\ShadingModelsMaterial.ush

ReflectionEnvironmentPixelShader.usf

\Engine\Shaders\Private\ReflectionEnvironmentPixelShader.usf

前面只忽略了 BasePass 中的 SpecularColor 和 DiffuseColor,ReflectionEnvironmentPixelShader 中要进行同样的操作:

新定义着色模型的 BxDF

ShadingModels.ush

\Engine\Shaders\Private\ShadingModels.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;
}
SkyLightingDiffuseShared.ush

\Engine\Shaders\Private\SkyLightingDiffuseShared.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;
}

最终效果

着色模型&材质参数

着色结果

注:以上部分参考了多篇介绍卡通渲染的文章,卡通渲染是修改渲染管线的常见案例,但修改渲染管线并非只是用于卡通渲染,很多时候我们要根据场景优化渲染效率也会涉及管线的修改,并且也不一定是要新增着色模型,还需要根据实际需求来分析。

参考

  1. 剖析虚幻渲染体系(04)- 延迟渲染管线  https://www.cnblogs.com/timlly/p/14732412.html
  2. 从零开始的UE5卡通渲染【二】:自定义着色模型  https://zhuanlan.zhihu.com/p/551343308
目录
相关文章
|
C++
UE4/5中DataTable数据表的使用
UE4/5中DataTable数据表的使用
1384 1
UE4/5中DataTable数据表的使用
|
自然语言处理 安全 C++
【C++ 格式化输出 】C++20 现代C++格式化:拥抱std--format简化你的代码
【C++ 格式化输出 】C++20 现代C++格式化:拥抱std--format简化你的代码
8853 4
|
9月前
|
数据挖掘 vr&ar C++
让UE自动运行Python脚本:实现与实例解析
本文介绍如何配置Unreal Engine(UE)以自动运行Python脚本,提高开发效率。通过安装Python、配置UE环境及使用第三方插件,实现Python与UE的集成。结合蓝图和C++示例,展示自动化任务处理、关卡生成及数据分析等应用场景。
895 5
|
Web App开发 域名解析 缓存
如何在 Ubuntu 20.04 上安装 Node.js 和 npm
本文我们主要为大家介绍在 Ubuntu 20.04 上安装 Node.js 和 npm 的三种不同的方式。
162315 7
如何在 Ubuntu 20.04 上安装 Node.js 和 npm
|
8月前
|
Linux iOS开发 MacOS
deepseek部署的详细步骤和方法,基于Ollama获取顶级推理能力!
DeepSeek基于Ollama部署教程,助你免费获取顶级推理能力。首先访问ollama.com下载并安装适用于macOS、Linux或Windows的Ollama版本。运行Ollama后,在官网搜索“deepseek”,选择适合你电脑配置的模型大小(如1.5b、7b等)。通过终端命令(如ollama run deepseek-r1:1.5b)启动模型,等待下载完成即可开始使用。退出模型时输入/bye。详细步骤如下图所示,轻松打造你的最强大脑。
14370 86
|
9月前
|
存储 自然语言处理 供应链
跨境电商团队如何高效管理项目?这5款协作工具值得尝试
跨境电商团队面临的全球化供应链、跨文化沟通、时区差异及语言障碍等挑战,可通过选择合适的协作工具来克服。推荐工具包括板栗看板、Trello、Slack、Asana和Zoom,它们分别在任务管理、即时通讯、项目跟踪和视频会议等方面提供强大支持,帮助团队提升效率、确保任务高效执行和顺畅沟通。
跨境电商团队如何高效管理项目?这5款协作工具值得尝试
|
自然语言处理 IDE 开发工具
通义灵码编程智能体上线,支持Qwen3模型
通义灵码最全使用指南,一键收藏。
128193 31
通义灵码编程智能体上线,支持Qwen3模型
|
图形学 容器
材质、纹理、贴图的区别和关联
材质和纹理是相互配合使用的,材质定义了物体的属性,纹理贴图则通过提供具体的颜色和纹理信息来赋予模型真实感和细节效果。
448 0
|
编解码 供应链 搜索推荐
VR技术在教育领域的应用前景:开启沉浸式学习新时代
【8月更文挑战第24天】VR技术在教育领域的应用前景广阔,它将为传统教育带来革命性的变革。通过提供沉浸式的学习体验和个性化的学习方式,VR技术能够激发学生的学习兴趣和动力,提高学习效果和综合素质。我们有理由相信,在未来的日子里,VR技术将成为教育领域的重要工具之一,为学生们带来更加丰富多彩的学习体验。让我们共同期待VR技术在教育领域的美好未来吧!
|
Python
在UE5编辑器环境中使用Python
在UE5编辑器环境中使用Python
989 0
在UE5编辑器环境中使用Python