UE5渲染管线简述
从学习材质系统的角度,UE5的渲染管线简单来说就是:默认使用延迟渲染 (Deferred Rendering) 管线,并为半透明等物体切换到前向渲染 (Forward Rendering),即:
- Base Pass (基础通道): 渲染不透明物体。将物体的各种表面属性(如Base Color, Roughness, Metallic, Normal等)输出到G-Buffer (几何缓冲区)**
- Lighting Pass (光照通道): 使用G-Buffer中的信息,计算场景中每个像素的光照。这是一个在屏幕空间进行的操作。最终的光照结果写入到场景颜色缓冲区。
- Forward Rendering Pass (前向渲染通道): 渲染所有半透明物体。这些物体不能使用G-Buffer,因为它们需要和背景颜色混合。它们会直接读取场景颜色缓冲区,并与自己的颜色混合后写回。
- Post Processing (后处理): 用于创建全屏的视觉效果,如滤镜、边缘检测、调色、辉光、景深、卡通效果等
因此,新建一个材质,打开材质编辑器,看到材质属性选项的最上面:

Material Domain (材质域)
第一项Material Domain (材质域) 决定了材质将被用于什么类型的物体上,参与哪个渲染阶段,有哪些可用资源,对于创建服务视觉效果的材质,前两个比较常用:
- Surface (表面): 这是最常用的选项,用于场景中几乎所有的3D物体,比如角色、建筑、道具等。使用此域的材质会参与到 Base Pass 中,将其表面属性写入G-Buffer(如果是不透明物体),或者在Forward Rendering Pass中被渲染(如果是半透明物体)
- Post Process (后处理): 用于创建全屏的视觉效果,如滤镜、边缘检测、调色、辉光、景深、卡通效果等。它在Post Processing阶段被执行。此时,这个材质可接收所有G-buffer中的图像作为输入(通过SceneTexture节点),处理后输出最终的画面颜色。它不参与任何3D空间的光照计算
- UI (用户界面): 用于UMG(Unreal Motion Graphics)中的UI元素,如按钮、血条、面板等。它在一个独立的UI渲染通道中被执行。这个通道通常在所有3D场景和后处理都完成之后,将UI元素叠加到屏幕的最上层。它的渲染非常简单,通常是无光照的,并且使用特殊的顶点信息来处理屏幕空间的布局
- Light Function (光照函数): 用来给光源(如点光源、聚光灯)增加纹理和动态效果,比如模拟穿过树叶的光斑、投影仪效果。它在Lighting Pass中被调用。当引擎计算某个光源对像素的影响时,会执行这个材质,用它的输出值(通常是灰度值)来调制(乘以)该光源的强度和颜色。它本身不渲染任何物体,而是作为光照计算的一部分
- Decal (贴花): 将一个材质投射到场景中的其他物体表面上,比如墙上的弹孔、地面的血迹。 它在一个专门的Decal Pass中被处理。在延迟渲染中,这个通道通常在Base Pass之后、Lighting Pass之前。贴花材质会修改G-Buffer中对应像素的属性(如改变Base Color、Roughness等),从而影响后续的光照计算结果。
Blend Mode (混合模式)
第二项 Blend Mode (混合模式) 应该一看就懂,透明物体不兼容延迟渲染,UE5也是单开前向渲染处理,所以透明物体需要改动这个选项,基本上也是透明物体选择Translucent即可:
- Opaque (不透明): 用于所有不透明的实体物体,如墙壁、金属、石头。这是延迟渲染管线的理想选择。在Base Pass中,它的像素着色器输出结果会直接写入G-Buffer,并完全覆盖之前该像素的任何信息。它还会写入深度缓冲区,用于遮挡判断。这是性能最高的模式
- Masked (遮罩/镂空): 用于有锐利边缘、非0即1透明效果的物体,如铁丝网、窗格、树叶(低成本做法)。同样可以高效地走延迟渲染路径。它在Opaque的基础上增加了一个Opacity Mask输入。在像素着色器中,引擎会根据这个输入值与一个阈值(通常是0.5)进行比较,来决定是否抛弃 (discard) 这个像素。如果保留,它的行为就和Opaque一样,写入G-Buffer;如果抛弃,则什么都不做。它也写入深度,可以正确遮挡。
- Translucent (半透明): 用于需要平滑过渡透明效果的物体,如玻璃、水、烟雾、幽灵。强制切换到前向渲染路径。这是它与前两者的核心区别。这类物体必须在所有不透明物体渲染完毕后,在Forward Rendering Pass中进行渲染。它的像素着色器会计算出最终颜色和透明度(Opacity),然后在GPU的Output Merger阶段,根据经典的Alpha混合公式 最终颜色 = 源颜色 * Alpha + 背景颜色 * (1 - Alpha) 与场景颜色缓冲区中的颜色进行混合。它通常不写入深度缓冲区(以避免排序问题),这也是半透明物体之间经常出现穿插错误的原因。
- Additive (叠加) & Modulate (调制): Additive用于发光特效,如火花、能量护盾(最终颜色 = 源颜色 + 背景颜色)。Modulate用于暗化背景,如阴影贴片(最终颜色 = 源颜色 * 背景颜色)。它们都是Translucent的变体,同样走前向渲染路径,只是在Output Merger阶段使用了不同的混合公式
Shading Model (着色模型)
这一项基本上可以理解为选择不同的BRDF - 双向反射分布函数,影响Lighting Pass或Forward Rendering Pass的着色原理,这里我以次表面散射相关的几个选项为例,对比着看就很清晰:
Subsurface (基础次表面散射): 使用一个简单的欺骗手段,模拟光线穿透的效果。标准的漫反射光照模型:Diffuse = max(0, dot(N, L))会在物体的背光面(dot(N, L) <= 0)产生硬朗的明暗分界线。Subsurface模型通过一个"wrap"因子来平滑这个过渡,将光照“包裹”到阴影区域。其核心公式可以简化理解为:
float NdotL = dot(N, L);
// Wrap factor allows NdotL to effectively go from [-wrap, 1] instead of [0, 1]
float wrap_factor = 0.5; // Example wrap value, usually controlled by material
float wrapped_NdotL = (NdotL + wrap_factor) / (1.0 + wrap_factor);
float lit_amount = max(0, wrapped_NdotL);
// Blend between base color and subsurface color based on how "backlit" it is
vec3 final_color = lerp(BaseColor, SubsurfaceColor, 1.0 - lit_amount);
// The actual UE implementation is more complex, but this is the core idea.简单尝试一下,将材质连接为这样(三个选项依次是Surface,Opaque,Subsurface ),创建个立方体看看:


可以看到只是简单的将背后的灯光映射到前面来了,与物体的厚度没有任何关系
只用于远景物体或不重要的有机物,如远处的植物叶片,简单的材质,如果冻、软糖等
Preintegrated Skin (预积分皮肤):预先计算好一个查找表(Lookup Table, LUT),X轴是dot(N, L),代表光线入射角的余弦值,Y轴代表表面曲率的近似值,具体原理其实不太需要管,依旧是一个近似的思想,这个材质就不展示了,其实只有漫反射的“模糊”效果,以及将阴影染色为次表面颜色,没有任何透射的感觉,只用于角色皮肤
Subsurface Profile (次表面配置文件): UE5用于实时渲染的最高质量、最物理准确的SSS方法,使用屏幕空间扩散 (Screen-Space Diffusion):
- 分离光照: 在渲染G-Buffer时,使用Subsurface Profile的材质会标记其像素。然后,在光照阶段,这些像素的漫反射光照结果被渲染到一个单独的缓冲区
- 多核模糊与加权: Subsurface Profile资产定义了一组(通常是6个)高斯核(Gaussian Kernels),每个核有自己的半径(Radius)**和**权重(Weight/Color)。这模拟了不同波长的光(如红、绿、蓝)在介质中散射的距离不同。引擎会对光照缓冲区进行多次、不同半径的高斯核模糊,然后根据Profile中定义的权重将这些模糊结果加权求和。具体来说:Scatter Radius决定了在屏幕空间中采样圆的大小,执行高斯模糊,当着色器从中心像素 C 去采样邻近像素 S 时,此时当然可以获得C 和 S的在世界空间中的位置,如果它们之间的距离太远,则拒绝该采样
- 然后将计算出的屏幕空间散射光照结果与原始场景(特别是该物体的镜面反射部分)合成

可以看到准确模拟出了光线透过材质的感觉,需要更细致的调节Subsurface Profile的参数来缓解噪点
举这个例子只是为了对Shading Model这个选项有个大概的感觉,大部分时候,选择Default Lit即可
类似的选项还有Translucent->Lighting Mode,Post Process Material->Blend Location等,后面涉及到再说
熟悉材质节点编辑器
创建一个基础材质,来快速认识一下UE5的材质编辑器,打开内容浏览器,右键创建一个材质,双击进入编辑:

默认创建的材质是"Surface+Opaque+Default Lit",即最常用的不透明默认光照材质,右键搜索“TextureSample”创建纹理采样节点,或者直接从内容浏览器拖入纹理,连接一些常用的材质通道,应该是比较直观的

接着可以尝试给每个通道加入一些调整,以基础颜色为例,如下图:

所有节点都可以右键搜索找到,其中的"Param"节点为常量参数节点,搜索"constant"以添加,并在节点上“右键->Convert to Parameter”转换为参数,然后这个参数就可以在窗口中看到,并在材质实例中调节(在内容浏览器对材质“右键->Create Material Instance”创建此材质的实例):

这些参数节点可以添加名字、分组和设置排序优先级等,不多赘述。贴一些快捷键,加速常用节点创建速度:
数字键 (用于创建常量和向量):
- 1 + 左键: 创建 常量 (Constant) - 单个浮点数,常用于控制粗糙度、金属度等
- 2 + 左键: 创建 二维常量 (Constant2Vector) - 两个浮点数,常用于控制UV坐标
- 3 + 左键: 创建 三维常量 (Constant3Vector) - 三个浮点数,最常用于表示RGB颜色
- 4 + 左键: 创建 四维常量 (Constant4Vector) - 四个浮点数,常用于表示带Alpha通道的RGBA颜色
- 字母键 (用于创建常用函数和节点):
- A + 左键: 创建 加法 (Add) 节点
- M + 左键: 创建 乘法 (Multiply) 节点。
- D + 左键: 创建 除法 (Divide) 节点
- E + 左键: 创建 **幂节点 (Power)**节点
- S + 左键: 创建 标量参数 (ScalarParameter) - 一个可命名的、可在材质实例中修改的浮点数。
- V + 左键: 创建 向量参数 (VectorParameter) - 一个可命名的、可在材质实例中修改的颜色值
- T + 左键: 创建 纹理取样 (TextureSample) - 用于对纹理进行采样,是最核心的节点之一
- U + 左键: 创建 纹理坐标 (TextureCoordinate) - 即UV坐标,用于控制纹理的平铺和偏移
- L + 左键: 创建 线性插值 (LinearInterpolate / Lerp) - 根据Alpha值在A和B之间进行混合
- P + 左键: 创建 平移 (Panner) - 使UV坐标随时间移动,制作流动效果
- N + 左键: 创建 归一化 (Normalize) - 用于将向量的长度变为1,在处理法线等数据时非常重要
- O + 左键: 创建 1-x (OneMinus) - 计算 1 - 输入值,常用于反转遮罩
深入材质系统
这一节中,用一些小例子,来简单熟悉一下材质系统,并探索原理
雪地
首先是一个闪光的雪地材质,先准备一张工具纹理:

使用PS可以轻松创建这种纹理,大概是“填充->添加模糊->色阶”。然后是材质:

将最后的节点接入Base Color看看效果,调整视角,可以看到朝向阳光的闪光的白点:

这部分材质最后其实是接入Emissive Color的,但一个最重要的技巧是,将结果接入Base Color以阶段性的观察结果。用这个技巧分解上面的节点组,将标记为"# 1", "# 2", "# 3"的节点接入Base Color,结合节点的名字(或按住Ctrl+Alt查看节点说明),原理应该比较容易了,由于是第一个例子,这里详细说明一下:
- (# 1):将太阳光和物体顶点法线点乘,这样朝向阳光的地方为正值,背向阳光为负值,"Saturat"节点将结果钳制到[0, 1](大于1为1,小于0为0),然后通过"Power"调整对比度
- (# 2)::"TextureSample"这个节点,默认不连接UVs引脚时即通过物体顶点UV采样,可以通过"TextureCoord[0]"显式获取这个UV,乘以一个值即可缩放纹理
- (# 3):通过屏幕UV采样纹理,即可将纹理“固定”在屏幕上,实现特殊的视觉效果,这里除以"700"的节点可以升级为参数,来调整效果
然后依次相乘这些结果,# 2 × # 3即闪光效果,# 3的纹理是固定在屏幕上的,而# 2随物体或摄像机的变化而变化,只有它们俩正好重合结果才会为“1”。# 1则是为了保证闪光点出现在向阳面
最后随便找一个雪地的图片纹理,有基础颜色和法线即可,这里就不展示最终效果了
透明材质(冰块)
原理
接下来是Blend Mode中的Translucent材质,看了开头的管线说明,就知道虚幻5默认的管线是没有任何“真实”光追效果的,包括折射、镜面反射、自发光等等,所有的这些都还是传统管线的近似模拟,只不过虚幻5属于这些方法的集大成者,有时候效果都让人误以为是光追。所以制作材质时要注意这一点,很多时候与其他3D软件里常用的离线渲染器不同,例如这里的冰块材质,一般使用一些小技巧来模拟折射
首先是对“延迟渲染+前向渲染”的透明物体渲染方法有个概念,即只渲染物体的表面,并没有光线在物体内部的穿行、衰减和散射。通过一系列方法对“体积感”进行近似,你可以想象通过各种技巧,计算出一个“透明度”,然后将其叠加在原始图像中(原始图像即:当渲染半透明物体时,引擎已经有了一个渲染好不透明物体的背景图像)对于不同的效果,有如下一些方法:
折射 (Refraction):扭曲背景
半透明材质的Refraction输入(通常是一个IOR值,Index of Refraction)与物体的表面法线(Normal)结合,计算出一个UV偏移量。着色器使用这个偏移后的UV坐标去采样原始图像。结果就是,看到的“穿过”物体的背景被扭曲了,看起来就像光线发生了折射
吸收 (Absorption) 和 透射 (Transmission):为光线上色
基于比尔-朗伯定律 (Beer-Lambert Law) 的简化。定律指出,光的强度随穿透介质的距离呈指数衰减。 * FinalColor = TransmittanceColor * exp(-AbsorptionCoefficient * Distance) * 在UE中,通常用更简单的方式来模拟。
如何知道穿透介质的距离?这个话题先省略,后面用到了再提
散射 (Scattering):让体积变得浑浊
开头管线简述中详细提到过,虚幻实际上将其分类到了"Opaque",即不透明物体,可见延迟管线还是比较讨厌“透明”的东西
光照
开始具体的材质之前,最后通过一个选项来宏观的看看Translucent材质的光照特性,找到材质细节中的"Translucency->Lighting Mode",
其中有两大分类:体积光照(Volumetric) 和 表面光照(Surface)
体积光照(Volumetric)
虽然上面说了,没有光线在物体内部的穿行的部分,但这是针对”画面颜色“而言的,对于光照,虚幻5还是使用了一些技巧,用于一类特殊的透明物体,即烟雾
其原理为一个放置在场景中的稀疏3D体素网格,每个体素(或叫样本点)存储了该空间位置的光照信息,可以通过Show > Visualize > Volume Lighting Samples来查看这些样本点,这些信息用球谐函数(Spherical Harmonics, SH)来编码,学过图像学的各位应该对这个不陌生。具体来说,一个二阶球谐的L0 (零阶)代表该点的平均环境光(Non-Directional),是一个单一的颜色值,L1 (一阶)包含4个值,可以重建出一个主导光方向和颜色,而几个选项的区别如下:
- Volumetric NonDirectional (体积非定向):物体的每个像素(或顶点)会找到其在世界空间中位置周围的几个最近的体积光照样本点。它只采样这些样本点的 L0 (零阶) SH系数,并进行三线性插值。得到一个单一的、没有方向性的颜色值,乘以材质的基色
- Volumetric Directional (体积定向):同时采样L0和L1的SH系数。使用L1系数重建出一个近似的主光源方向向量(LightVector)和颜色。然后进行一个简单的兰伯特光照计算:
FinalColor = AmbientColor(L0) + DirectionalColor(L1) * saturate(dot(Normal, LightVector)) - Volumetric PerVertex (体积逐顶点):光照计算从像素着色器(Pixel Shader)移到了顶点着色器(Vertex Shader)中,适用于顶点密度非常高的物体
表面光照(Surface)
- Surface ForwardShading (表面正向渲染):这个没什么特殊的,理解为常规的正向渲染实现就行,最高质量,最高开销,支持完整的PBR光照模型(类似GGX BRDF),其间接光从Image-Based Lighting (IBL) 和光照探针采样,质量很高
- Surface TranslucencyVolume (表面半透明体积) :这个是默认模式,直接光和表面正向渲染类似,但间接光用体积光照实现,开销更低
材质
原理部分就这样了,创建一个冰块材质来轻松一下吧:

同样将最后的节点接入Base Color,将材质预览替换为立方体,转动观察,由于是动态效果,就不上图了,能看到材质好像在物体内部一样,模拟了冰块的内部细节。这就是一个典型的对”体积感“的模拟,通过一些巧妙的向量运算,形成视觉上的错觉。在上图最左边,在切线空间中计算视线的反射方向(切线空间可以理解为,在z轴与物体法线对齐的局部坐标系中运算),想象当视角掠过物体表面时(视线与物体法线夹角接近90度),此时的反射向量z轴很小,# 1节点值比较大,此时其xy分量对最后纹理的UV产生较大偏移
然后是模拟折射的部分:

最简单的模拟方法,调整几个数值以控制折射程度

最后混合一下上面的两个部分,有点麻烦就不上图了,需要的可以下载自取
更多例子WIP...
后处理材质
后处理材质就是”屏幕空间效果“,如果不太熟悉图形学,可以理解为滤镜,但我们能获取的不止有原始图像,还有每个像素点处物体的位置、法线、颜色、深度等等,获取到这些图像后,根据这些信息计算想要的效果。虚幻5主线就是延迟渲染,所以G-buffer可以获取的东西相当多,你能想到的能写入G-buffer的信息都有
创建一个材质,选择Material Domain为Post Process,下拉看到”Post Process Material->Blendable Location“,这是相当重要的一个选项,决定了你在此材质中定义的屏幕空间效果在哪一阶段被应用,而在每个阶段获取的图像是不一样的,这个其实相当玄学,建议是对于每个后处理材质,都一个个试过去,或者这样:

搜索"SceneTexture"节点,这个即是从G-buffer获取图像的节点,选择每一个"Scene Texture Id"观察是什么,而这些图像在不同的”Post Process Material->Blendable Location“定义的阶段中又不一定一样,请务必注意这一点
具体遵循的规则相当复杂,这里简单列举,仅供参考:
Scene Color Before DOF (景深前): 在完成了基本的光照、反射、环境光遮蔽之后,但在计算景深(Depth of Field, DOF)效果之前,整个画面都是清晰的。因为景深效果还没被应用,所以没有模糊的区域
Scene Color After DOF (景深后):在景深效果计算完成之后,但在运动模糊(Motion Blur)之前。画面中已经有了模糊和清晰的区域。根据摄像机的焦点设置,焦外的物体已经被模糊处理
Translucency After DOF (景深后的半透明): 这是一个比较特殊的注入点。它作用于半透明物体被渲染并叠加到场景上的时候。此时,不透明场景已经完成了景深计算。专门处理半透明物体,可以获取半透明物体本身的颜色,以及它背后的、已经经过景深处理的场景颜色。改变半透明物体(如玻璃、水、鬼魂)与背景混合的方式。例如,让透过玻璃看到的模糊背景产生特殊的色散或扭曲效果
SSR Input (屏幕空间反射输入):这不是一个直接修改最终画面的阶段。它修改的是用于计算屏幕空间反射(Screen Space Reflections, SSR)的输入源。可以在此(计算反射前),手动移除某些物体(如玩家角色)对反射的贡献,或者降低输入图像的复杂度来优化性能。也可以对反射源增加噪点或扭曲,创造特殊效果
Scene Color Before Bloom (辉光前):在景深之后,但在计算辉光(Bloom)效果之前,辉光是基于图像中高亮度区域产生的。通过在这个阶段修改图像,可以增强或减弱某些区域的亮度,从而控制它们是否产生辉光以及辉光的强度。例如,让一把魔法剑发出强烈辉光,同时阻止旁边一面同样很亮的白墙产生辉光
Replacing the Tonemapper (替换色调映射器):取代UE默认的色调映射器(Tonemapper)。此时的图像是已经完成了所有效果(景深、运动模糊、辉光等)叠加后的最终HDR线性空间图像。你的材质必须负责将这个HDR图像转换成显示器能够显示的LDR(低动态范围)、sRGB空间的图像。实现完全自定义的电影调色、LUT(颜色查找表)应用
Scene Color After Tonemapping (色调映射后):后处理链的最后一个阶段,在色调映射完成之后,但在渲染UI之前。添加暗角(Vignette)、胶片颗粒(Film Grain)、屏幕划痕等。这些效果在LDR空间下操作更简单,也更符合直觉。对最终画面进行一些简单的亮度/对比度调整。因为HDR信息已经丢失,所以不适合做大的色彩改动,很适合做最终的“覆盖式”效果
| 阶段名称 | 位置 | 图像特征 (颜色/动态范围) | 关键点/用途 |
|---|---|---|---|
| Scene Color Before DOF | 景深前 | 线性 / HDR | 画面全清晰,适合做描边、边缘检测 |
| Scene Color After DOF | 景深后 | 线性 / HDR | 画面带景深模糊,适合做镜头污渍等前景效果 |
| Translucency After DOF | 半透明混合时 | 线性 / HDR | 专门处理半透明物体与背景的混合 |
| SSR Input | SSR计算前 | 线性 / HDR | 修改SSR的反射源,而非最终画面 |
| Scene Color Before Bloom | 辉光计算前 | 线性 / HDR | 控制哪些区域产生辉光 |
| Replacing the Tonemapper | 色调映射阶段 | 输入: 线性/HDR -> 输出: sRGB/LDR | 完全自定义颜色分级和画面风格,最强大 |
| Scene Color After Tonemapping | 色调映射后 (最后) | sRGB / LDR | LDR图像,适合做暗角、胶片颗粒等最终屏幕效果 |
总的来说,首先明确效果要在HDR还是LDR空间,然后是景深/透明物体/反射/辉光这几个特殊需求,剩下不清楚的就一个个试过去就行
物体描边
物体描边是后处理的一个经典应用,就用这个例子来熟悉一下后处理材质的创建方法吧
原理很简单,检测屏幕深度/法线的边缘,即值发生剧烈变化的地方,这里以深度为例
还是创建一个新的材质,在材质细节窗格,选择"Material Domain->Post Process"。首先来获取深度缓冲,右键添加"SceneTexture"节点,在这个节点当中,我们有两种选择,"SceneDepth"和"CustomDepth",SceneDepth即全场景所有物体的深度,而CustomDepth则是仅对我们选择的物体渲染深度,两种有不同的适用场景,对于物体描边,建议使用CustomDepth(Custom Depth可以将描边效果精确地限定在特定物体上,避免对整个场景(如远处的背景)都产生不必要的边缘计算,性能更好,控制更精确)
要使用CustomDepth,先在"Project Settings->Engine->Rendering->PostProcessing->Custom Depth-Stencil Pass"选择启用,然后对于想渲染深度的物体,注意选中其"StaticMeshComponent"而不是Actor的根节点(这里一定注意),搜索"Render CustomDepth Pass"并启用。随后可以在视图中"Lit->Buffer Visualization->CustomDepth"查看
随后转到材质:

这里的"SceneTexture::PostProcessInput0"是渲染好的场景结果,一般都会将其他效果叠加在上面。然后"DrawOutline"是一个自定义节点,右键搜索"Custom"以添加,这个节点允许添加HLSL代码,一般来说后处理效果逻辑是比较复杂的,用代码会更简单清晰,我们添加下面的代码:
- 点击跳转或手动查看:代码附录
注释的非常清楚了,就不多做解释,注意在这里获取的深度是单位为cm的距离摄像机的距离,随后定义好custom节点的输入和输出:

材质就创建完成了,随后在场景中使用它,在快速添加中找到"Volumes->Post Process Volume",修改其属性:

选择创建好的材质,"Infinite Extent (Unbound)"意思是作用于全屏,请根据需求开启。看看效果(这个效果没法在材质编译器预览,因为编译器中不存在自定义深度):

最后再提一下"SceneTexture"这个节点以及其对应代码中的"SceneTextureLookup"函数,在后处理材质中,至少拖入一个SceneTexture节点以防止编译报错(即使你不用):
[SM6] /Engine/Generated/Material.ush:3374:27: error: use of undeclared identifier 'SceneTextureLookup'
float NeighborDepth = SceneTextureLookup(SampleUV, 13, false).r;
^大概原理是有这个节点才会包含这个函数声明,所以这个编译报错其实也可以不用管,最终的材质会用于正确的上下文
风格化(卡通)
同样的,了解完上面的基础操作后,来一个复杂点的例子,首先将上面的描边算法再简化一下,并添加法线边缘检测:

注意图中有两个边缘检测的自定义节点,DetectOutline_Depth和DetectOutline_Normal,分别是检测深度的边缘和法线的边缘,深度边缘一般是物体的外轮廓,而法线边缘则是物体的所有拐角。这里为深度检测添加了一个根据距离控制描边粗细的逻辑,不然远处的描边会看上去比较凌乱
代码如下:
- 点击跳转或手动查看:代码附录
随后对于法线轮廓,同样剔除超过某个深度的部分,不然视角效果会比较乱:

接着处理原始画面,添加一些卡通的效果,例如混合漫反射颜色,制造分层的阴影:

其中的ShadowStep为一个"CurveAtlasRowParameter"节点,即一个类似渐变映射的节点,依赖于两个外部资产:曲线 (Curve) 和 曲线图谱 (Curve Atlas),首先在内容浏览器创建一个曲线(曲线线性颜色 (CurveLinearColor)),拉成阶梯状就行:

然后创建曲线图谱 (Curve Atlas Asset),在"Gradient Curves"中添加上面的曲线就行,其会自动生成一个纹理,其中每一行像素就代表一个添加的曲线,虽然这里只有一个,但还是要操作一下
随后回到材质编辑器,选择CurveAtlasRowParameter节点,添加刚刚的两个资产就行:

最后,连接所有的节点:

看看效果,上图是默认光照,下图是添加了卡通风格的效果:


最后,创建一个新的后处理材质,添加一个艺术效果,即漫画排线,其中的纹理同样可以在PS中绘制,就是黑色的平行线:

要注意一点,虚幻5的Lumen模式默认没有环境光遮蔽,要额外开启:
单次临时开启,在控制台输入:
r.Lumen.DiffuseIndirect.SSAO 1
r.Lumen.ScreenProbeGather.ShortRangeAO 0
或者针对项目开启,找到项目的 Config 目录,然后打开 DefaultEngine.ini 文件,在文件中找到 [SystemSettings] 这个区段并添加:
[SystemSettings]
; Lumen下用SSAO替代ScreenProbeGather的AO
r.Lumen.DiffuseIndirect.SSAO=1
r.Lumen.ScreenProbeGather.ShortRangeAO=0看看效果:

虚幻的环境光遮蔽计算效果一般般,可能是因为默认都不启用,所以没怎么优化,后面可能会作为例子,来讲讲怎么手动计算场景AO
附录
代码
后处理材质-物体描边:
// 参数,这些将在Custom节点的输入引脚上定义
// float3 SceneColor: 原始场景颜色
// float3 OutlineColor: 描边的颜色
// float OutlineThickness: 描边的粗细
// float EdgeThreshold: 判断边缘的深度阈值
// 获取单个像素在UV空间的大小
// View.ViewSizeAndInvSize.zw 包含了 (1/ViewWidth, 1/ViewHeight)
float2 TexelSize = View.ViewSizeAndInvSize.zw;
// 获取UV
float2 UVs = GetSceneTextureUV(Parameters);
// 定义采样偏移量,乘以Thickness可以控制描边粗细
float2 Offsets[4] =
{
float2(0, 1), // 上
float2(0, -1), // 下
float2(-1, 0), // 左
float2(1, 0) // 右
};
// 采样中心点的自定义深度
// 第三个参数是是否开启过滤,对于深度这类精确数据,我们设为 false
float CenterDepth = SceneTextureLookup(UVs, PPI_CustomDepth, false).r;
float maxDepthDiff = 0;
// 循环检查周围的像素
for(int i = 0; i < 4; i++)
{
// 计算邻居像素的UV
float2 SampleUV = UVs + Offsets[i] * OutlineThickness * TexelSize;
// 采样邻居像素的深度
float NeighborDepth = SceneTextureLookup(SampleUV, PPI_CustomDepth, false).r;
// 计算深度差的绝对值
float depthDiff = abs(CenterDepth - NeighborDepth);
// 找出最大的深度差
maxDepthDiff = max(maxDepthDiff, depthDiff);
}
// 大于阈值,设定为物体边缘
float edge = 0;
if(maxDepthDiff>EdgeThreshold){
edge = 1;
}
// Lerp函数:在场景颜色和描边颜色之间插值
// edge为0时,返回SceneColor;edge为1时,返回OutlineColor
return lerp(SceneColor, OutlineColor, edge);后处理材质-风格化(卡通):
// DetectOutline_Depth 节点
// 参数,这些将在Custom节点的输入引脚上定义
// float OutlineThickness: 描边的粗细
// float EdgeThreshold: 判断边缘的深度阈值
// 获取单个像素在UV空间的大小
// View.ViewSizeAndInvSize.zw 包含了 (1/ViewWidth, 1/ViewHeight)
float2 TexelSize = View.ViewSizeAndInvSize.zw;
// 获取UV
float2 UVs = GetSceneTextureUV(Parameters);
// 定义采样偏移量,乘以Thickness可以控制描边粗细
float2 Offsets[4] =
{
float2(0, 1), // 上
float2(0, -1), // 下
float2(-1, 0), // 左
float2(1, 0) // 右
};
// 采样中心点的自定义深度
// 第三个参数是是否开启过滤,对于深度这类精确数据,我们设为 false
float CenterDepth = SceneTextureLookup(UVs, PPI_CustomDepth, false).r;
float maxDepthDiff = 0;
// 循环检查周围的像素
for(int i = 0; i < 4; i++)
{
// 计算邻居像素的UV
float2 SampleUV = UVs + Offsets[i] * OutlineThickness * TexelSize;
// 采样邻居像素的深度
float NeighborDepth = SceneTextureLookup(SampleUV, PPI_CustomDepth, false).r;
// 计算深度差的绝对值
float depthDiff = abs(CenterDepth - NeighborDepth);
// 找出最大的深度差
maxDepthDiff = max(maxDepthDiff, depthDiff);
}
float edge = 0;
if(maxDepthDiff>EdgeThreshold){
edge = 1;
}
return edge;// DetectOutline_Normal 节点
// 参数,这些将在Custom节点的输入引脚上定义
// float OutlineThickness: 描边的粗细
// float NormalThreshold: 判断边缘的法线差异阈值 (建议值 0.1 - 0.5)
// 获取单个像素在UV空间的大小
float2 TexelSize = View.ViewSizeAndInvSize.zw;
// 获取当前像素的UV
float2 UVs = GetSceneTextureUV(Parameters);
// 定义采样偏移量
float2 Offsets[4] =
{
float2(0, 1), // 上
float2(0, -1), // 下
float2(-1, 0), // 左
float2(1, 0) // 右
};
// 采样中心点的世界空间法线
// PPI_WorldNormal (ID: 2) 存储了法线信息。
// 法线数据存储在[0,1]范围,需要解压到[-1,1]
float3 CenterNormal = SceneTextureLookup(UVs, PPI_WorldNormal, false).rgb;
CenterNormal = normalize(CenterNormal * 2.0 - 1.0);
// 用于记录与周围法线点积的最小值,初始值为1(表示完全相同)
float minDotProduct = 1.0;
// 循环检查周围的像素
for(int i = 0; i < 4; i++)
{
// 计算邻居像素的UV
float2 SampleUV = UVs + Offsets[i] * OutlineThickness * TexelSize;
// 采样邻居像素的法线并解压
float3 NeighborNormal = SceneTextureLookup(SampleUV, PPI_WorldNormal, false).rgb;
NeighborNormal = normalize(NeighborNormal * 2.0 - 1.0);
// 计算中心法线和邻居法线的点积(Dot Product)
// 点积结果越接近1,表示法线方向越一致;越小则差异越大。
float currentDot = dot(CenterNormal, NeighborNormal);
// 找出最小的点积值,代表最大的角度差
minDotProduct = min(minDotProduct, currentDot);
}
// 将点积值转换为一个0-1的差异度。
// 如果法线完全相同,dot=1, normalDiff=0。如果法线垂直,dot=0, normalDiff=1。
float normalDiff = 1.0 - minDotProduct;
// 如果法线差异大于阈值,则判定为边缘
float edge = normalDiff > NormalThreshold ? 1.0 : 0.0;
return edge;