ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

UE材质进阶:Custom Expression、Custom HLSL与MPC实战解析

UE材质进阶:Custom Expression、Custom HLSL与MPC实战解析 玩UE材质编辑器玩到一定程度大家都会撞上一堵隐形的墙节点连久了很多算法用节点拼出来又乱又难看想写一段正经的Shader逻辑还得去查“这个节点能不能干这个”。这时候就绕不开标题里的三样东西——Custom Expression、Custom HLSL和MPCMaterial Parameter Collection材质参数集。这篇文章就把这三者怎么配合、怎么接入渲染管线、实际项目里怎么避开那些坑一次性梳理清楚。无论是刚入行想进阶的技术美术还是被材质问题追着跑的渲染程序这篇都值得留着当参考。先说清楚一个容易造成误解的缩写这里说的MPC是Unreal里的Material Parameter Collection和工业控制里那个Model Predictive Control模型预测控制完全不是一个东西别被热词带偏了。在Unreal里MPC是一套跨材质共享参数的机制配合Custom HLSL写出来的高级材质能做出很多普通节点根本堆不出来的效果。1. 内容整体设计与思路拆解1.1 为什么节点连久了会卡壳很多刚开始学UE的人会觉得材质编辑器里的节点已经足够强大Blend、Lerp、Dot、Noise这些基本能应对80%的需求。但项目真正做到中后期你会发现剩下的20%才是决定品质上限的部分。比如一个半色调网点过渡用节点可能要连二十多个节点中间还穿插大量Masks、Power、Clamp最后效果还不一定能调准。原因很简单节点图是二维空间里的表达方式一旦遇到循环、分支、多级函数封装它就会变得非常笨重。Custom Expression和Custom HLSL就是来解决这个问题的。前者适合处理单行或少数几行表达式后者则允许你在材质图里直接写一段完整的HLSL代码块定义函数、常量再在输出表达式里调用它们。相当于把“只能连线”变成“可以在材质里写代码”。这个转变看起来只是多了一种工具实际上是把材质的表达力从“积木拼装”提升到了“编程”级别。1.2 自定义表达式与Custom HLSL的边界很多文章喜欢把Custom Expression和Custom HLSL混着说其实两者侧重点不一样。Custom节点UE里搜索Custom出现的那个节点通常只有一张输入代码框适合写一个最终返回值表达式比如return saturate(dot(Normal, ViewDirection));。它胜在轻量、直观适合快速验证一个数学公式。Custom HLSL节点则可以理解为一个完整的“Shader代码块”你在里面可以声明函数、定义常量、写多行逻辑然后通过节点的输出表达式调用自己写的函数。这种方式更适合复用比如同一个算法要在多个材质里使用你就可以把函数写在Custom HLSL里然后其他节点通过表达式接口调用。不需要每次都重新拼接节点。从实际的工程习惯来说我会把“单行能算完”的东西丢给Custom节点把“需要封装成函数、需要传多个参数、需要跨Pass复用”的逻辑丢给Custom HLSL。这个边界一旦定清楚后期维护会省很多事。1.3 MPC全局参数的总控台MPC在材质编辑器里的地位有点像游戏里的“全局设置”面板。它的核心作用是让多个材质共享同一组参数并且在运行时能被蓝图或C统一修改。比如天气表现的雨量、风速、昼夜亮度这些参数不应该写在材质实例里然后逐个手动去同步而是应该放进一个MPC所有材质都引用同一个源。和Material Instance材质实例相比MPC的优势在于“全局性”。材质实例的每个参数是偏向单个材质资产的虽然可以用继承和Override来做层级管理但跨材质更新依然很痛苦。MPC则直接提供了“一套参数所有地方引用”的能力。而且它本身是独立资产可以在内容浏览器里创建运行时通过Blueprint的Set Scalar Parameter Value等节点修改并不需要重新编译材质。1.4 渲染管线集成从材质图到屏幕像素单纯在材质编辑器里写代码最终还是得让材质在渲染管线的某个阶段起作用。这就涉及到材质域Material Domain和Pass的选择。UE里面普通材质默认在模型渲染阶段执行而后处理材质会在全屏幕像素阶段运行Custom节点里可以通过SceneTexture节点读取SceneColor、SceneDepth、CustomDepth等数据。渲染管线集成这件事不是让你直接去改引擎的底层Shader而是通过材质节点的设置把你自己写的HLSL挂到对应管线的阶段上。比如Post Process材质对应的是全屏PassDeferred Decal对应的是贴花Pass有自定义Depth需求的材质则需要启用Custom Depth。理解了这个概念你就知道Custom HLSL不只是“写代码”还要考虑“这段代码在哪个阶段跑、能读到哪些数据”。这也是很多新手在进阶时最容易忽视的因为光是节点上的Domain、Blend Mode选项就已经能决定一大半效果形态。2. 核心细节解析与实操要点2.1 Custom节点表达式编写规范与陷阱Custom节点虽然写起来自由但有几个细节需要刻进肌肉记忆。首先在Custom节点的Code框里写的内容会被直接插入到生成的HLSL代码中。也就是说这里写的每一行都可能影响最终编译结果。最简单的方式是直接写一个表达式例如pow(saturate(1 - dot(Normal, normalize(ViewDirection))), 3)这个表达式会成为该节点的返回值。如果你写多行需要用return语句把结果显式返回float ndv dot(Normal, normalize(ViewDirection)); return pow(saturate(1 - ndv), 3);这里有一个高频踩坑点Custom节点里的变量名是直接映射到材质引脚名称的。每当你给这个节点增加一个输入引脚引脚名就是代码里可以直接引用的变量名。比如你添加两个引脚名为UVRange和Intensity代码里就能直接用return pow(UVRange, 1.0 / Intensity);引脚名不要带空格和奇怪的符号否则生成的Shader代码里会出现非法标识符编译阶段就会报错。另一个禁忌是不要在Custom节点内部使用纹理采样器除非你把这个采样器通过另一条路径传入。因为材质编辑器生成的采样器绑定和手工写的Sampler2D并不是一回事强行使用容易导致运行时材质变黑或编译失败。2.2 Custom HLSL节点的正确打开方式Custom HLSL节点比Custom节点更适合承载多行逻辑。以UE5的材质编辑器为例你在节点菜单里搜Custom HLSL后会得到一个带Code框、Output Expression框和多个输入引脚的节点。Code框用来写函数定义float3 EdgeGlow(float2 uv, float3 baseColor, float threshold) { float mip length(float2(ddx(uv.x), ddy(uv.y))); float edge smoothstep(threshold, threshold * 1.2, mip); return lerp(baseColor, baseColor * 2.0, edge); }然后在Output Expression框里填写return EdgeGlow(SceneUV, PostProcessInput0, Threshold);这里的SceneUV、PostProcessInput0、Threshold分别是节点上的输入引脚或者可以是外部节点的输出。关键在于Code框里定义的函数作用范围只在当前Custom HLSL节点内部不能跨节点直接调用。如果多个节点需要共用函数就要复制代码或考虑用Public Header但后者会让代码管理更复杂一般不建议轻易用。编写Custom HLSL时我强烈建议在每个函数前写清参数含义甚至用注释标明“这个函数是从哪个材质里复制来的”。因为引擎报编译错误时行号对应的是生成后的完整HLSL文件不是你的Code框你需要在错误信息里搜索函数名才能定位到具体节点。2.3 MPC创建、引用和运行时更新创建一个MPC的方式很简单内容浏览器右键选择“材质”类别下的“材质参数集合”命名后双击打开编辑器界面。在编辑器面板里点“”添加参数可以选标量Scalar或向量Vector。标量就是一个float向量是四通道float4但材质编辑器里常用三通道做颜色。引用MPC的材质节点叫做Material Parameter Collection。在材质编辑器里添加这个节点后左侧面板会列出所有可用的MPC资产选中你创建的MPC后节点上会生成对应的参数引脚。比如MPC里有一个Scalar参数叫ScanLinePos节点上就会出现同名的标量输入你把这个引脚连接到材质输出上即可。运行时的修改在蓝图里有两个关键节点Set Scalar Parameter Value和Set Vector Parameter Value。它们都需要先指定目标MPC资产再指定参数名。这里有个细节容易搞错如果你在材质实例里也覆盖了同名参数MPC的值可能不会生效因为材质实例参数的优先级更高。遇到“改了MPC但材质没反应”的情况先检查有没有哪个材质实例在中间横插一脚。2.4 渲染管线集成三种实战路径第一后处理材质。把材质域的选项设为Post ProcessUE会把它放到全屏后处理Pass中。此时你可以在材质图里使用SceneTexture:PostProcessInput0这类节点来读取当前屏幕颜色也可以用SceneTexture:SceneDepth读取深度。Custom HLSL在这个阶段最适合做全屏特效比如扫描线、冲击波、径向模糊。第二贴花材质。Deferred Decal材质使用在贴花Mesh上可以通过修改材质域本身或装饰插件配置来实现。贴花材质的优势是不用修改渲染Mesh可以直接把图案投射到场景表面常用来做动态标记、边缘发光等效果。第三自定义深度/模板。如果材质需要感知物体被遮挡关系或参与特殊效果就需要在Project Settings里打开Custom Depth-Stencil Pass。启用后你会在材质里的SceneTexture节点中看到Custom Depth和Custom Stencil这两个选项。这在做角色轮廓发光、遮挡透视效果时非常有用。3. 实操过程与核心环节实现3.1 示例目标一个全局扫描线加中心冲击波为了把上面的内容串起来我直接拿一个项目里做过的效果来拆解。目标是做一个后处理材质它会扫描线一样从屏幕底部向上移动同时在触发时显示一个以某点为中心向外扩散的冲击波。这个效果的参数全部放在一个MPC里蓝图可以通过按键实时改变扫描线的位置、强度、颜色和冲击波中心点。材质域选择Post Process混合模式设置为Additive这样效果是在原画面基础上叠加发光。核心思路是通过SceneTexture读取场景颜色再通过SceneUV拿到屏幕坐标然后在Custom HLSL里写两个函数一个计算扫描线强度一个计算冲击波强度最后组合输出到Emissive Color。3.2 创建MPC并定义参数布局在内容浏览器创建一个名为Globals_PP的Material Parameter Collection然后添加以下参数参数名类型用途ScanLinePositionScalar扫描线的垂直位置映射到屏幕UV的Y坐标ScanLineWidthScalar扫描线宽度ScanLineIntensityScalar扫描线发光强度常规0到2ScanLineColorVector扫描线颜色ShockwaveCenterVector冲击波圆心UV空间ShockwaveRadiusScalar冲击波半径ShockwaveIntensityScalar冲击波强度参数越清晰后期油门越大。MPC里不要塞一堆看不懂的xx、yy后期项目里你自己都认不出哪个是哪个。3.3 编写Custom HLSL实现扫描线与冲击波在材质编辑器中新建Custom HLSL节点Code框里写入以下几个函数float3 ScanLine(float2 uv, float3 sceneColor, float scanLinePos, float scanLineWidth, float intensity, float3 scanLineColor) { float dist abs(uv.y - scanLinePos); float lineMask 1.0 - smoothstep(0.0, scanLineWidth, dist); float3 finalColor lerp(sceneColor, sceneColor scanLineColor, lineMask * intensity); return finalColor; } float3 Shockwave(float2 uv, float3 sceneColor, float2 center, float radius, float intensity) { float dist distance(uv, center); float wave 1.0 - smoothstep(radius * 0.6, radius, dist); wave * sin(dist * 50.0) * 0.25 0.25; return lerp(sceneColor, sceneColor * 1.3, wave * intensity); }然后在Output Expression框里调用float3 baseColor PostProcessInput0; float2 uv SceneUV; baseColor ScanLine(uv, baseColor, ScanLinePosition, ScanLineWidth, ScanLineIntensity, ScanLineColor); baseColor Shockwave(uv, baseColor, ShockwaveCenter, ShockwaveRadius, ShockwaveIntensity); return baseColor;这里要注意我在输出表达式里使用了MPC节点提供的参数名。前面说过的Material Parameter Collection节点需要提前添加并连接好名为ScanLinePosition、ScanLineWidth、ScanLineIntensity、ScanLineColor、ShockwaveCenter、ShockwaveRadius、ShockwaveIntensity的引脚就会自动匹配到MPC参数。连接方式是在材质图里先加一个Material Parameter Collection节点选择Globals_PP资产然后把对应参数引脚拖到Custom HLSL节点的输入引脚上。你需要在Custom HLSL节点上手动添加输入引脚引脚名要和代码里的变量名一致。这里的ScanLinePosition引脚就对应代码里的ScanLinePosition参数。这个映射关系写错的话编译直接报错。3.4 将材质接入后处理渲染管线材质编辑完成后把材质域的选项改为Post Process。如果材质编辑器右侧Details面板里没有Post Process选项检查一下是否启用了“Show Advanced”或者项目版本是否支持。设置混合模式为Additive否则叠加效果会变脏。接下来把它挂到一个Post Process Volume上。最简单的做法是在关卡里放一个Post Process Volume点击它的Post Process Materials数组添加这个材质并把权重设为1。如果运行平台是移动端注意后处理材质在部分移动设备上的表现会不同需要在项目Settings中增加对应的渲染功能。默认情况下PostProcessInput0是屏幕当前颜色SceneUV是屏幕坐标。这些数据都可以通过SceneTexture节点插入也可以用一行float2 uv SceneUV;在Custom里直接引用。不过SceneUV在材质编辑器里其实是一个内置量直接写是可行的。如果你发现编译不通过就用材质图中的ScreenPosition节点连接到Custom HLSL的输入引脚代码里改用这个输入变量。3.5 效果调试与参数绑定运行后在蓝图里写一段逻辑每帧把MPC里的ScanLinePosition从0递增到1就会看到扫描线从屏幕底部移动到顶部。核心蓝图节点是Get Material Parameter Collection和Set Scalar Parameter Value参数名必须和MPC里完全一致注意大小写。我习惯在BeginPlay时获取一次MPC引用然后每帧用DeltaTime更新位置。冲击波效果则可以通过一个Input Action来触发触发时把ShockwaveCenter设置为屏幕上的某个位置比如屏幕中心同时重置ShockwaveRadius和ShockwaveIntensity然后用时间轴在几帧内让ShockwaveRadius从0.1增长到1.0。因为Custom HLSL里用了smoothstep和sin所以视觉上会有明显的边缘扩散和波纹感。实际调试时建议先只保留ScanLine不要先叠加Shockwave确认扫描线效果稳定后再把冲击波逻辑加进来。每次改完MPC里的参数值等待运行后再看效果不要指望编辑器预览能实时反馈所有变化。4. 常见问题与排查技巧实录4.1 Custom HLSL编译报错怎么看材质编辑器里的Shader编译报错是所有写Custom HLSL的人都会遇到的一道坎。错误窗口通常提示的是一大段Generated HLSL里的某一行报错行号很难直接对应到你的Code框。我的习惯做法是做三段式定位第一在Code框每个函数前加一行注释注释里写上独特的标记比如// START_SCANLINE_FUNC。当编译错误指向生成代码的某一行时可以看错误那一行附近是否有这个标记找不到就往上翻几行。这个办法非常土但真的很有效。第二把Custom HLSL节点里的代码先改成最简单的返回值比如return PostProcessInput0;复杂度降为零。如果这样能编译通过说明问题一定在代码块内部。再逐步加回函数每加一段编译一次直到问题复现。第三检查所有引脚是否都连上了。未连接的引脚在HLSL代码里是悬空的编译器经常会报变量未声明或常量有关错误。4.2 MPC参数更新不生效运行时改了MPC参数材质却没有变化这是最让人抓狂的问题之一。先排除几个常见原因确认Blueprint节点里设置的就是正确的MPC资产而不是重新创建的资产副本确认材质里引用的MPC资产没有拼错参数名确认有没有其他材质实例或Overlay覆盖了参数。还有一种情况是MPC参数驱动的是Post Process材质但Post Process Volume的权重是0或者该Volume不在Camera渲染范围内。后处理Volume启用了Unbound效果才会对全场景生效。如果只是局部区域Volume就必须确认Camera确实站在Volume内部。4.3 坐标空间造成效果偏移前不久有个朋友写扫描线发现扫描线位置总是偏的因为他把WorldPosition当成了屏幕UV来使用。这两种坐标的参考系完全不同。屏幕UV也就是SceneUV范围是0到1的屏幕坐标而WorldPosition是世界空间坐标没有归一化概念。如果你在Custom HLSL里混用两者效果自然对不齐。这里整理一张坐标选择速查表效果需求使用数据说明全屏扫描线SceneUV0到1的屏幕坐标基于屏幕边缘发光SceneUV配合distance和smoothstep与环境距离相关的效果SceneDepth深度值注意非线性物体遮挡或角色轮廓CustomDepth需要打开自定义深度Pass世界位置相关效果WorldPosition适用于贴花或模型材质4.4 性能、兼容性与Debug经验Custom HLSL给了你高度自由但同时也意味着你失去了很多节点层面的优化保障。比如在一个全屏后处理材质里写一个多层循环每个像素跑几十次帧率会直接崩掉。尽量避免在像素Shader中使用动态分支如果必须用让分支条件尽量基于低频变量比如全局时间或MPC参数而不是屏幕坐标的细节。移动端平台对Custom HLSL的支持是有限的。很多Mobile版本会忽略部分语法或因为GPU限制导致编译失败。跨平台项目建议至少用“Preview”模式切换几个主流平台测试一遍。另外用Material Instance里的参数和MPC参数混合时最好统一参数名称和命名空间不要让同一语义有两个不同名字。项目后期材质数量一多一致的命名约定能省下海量的沟通成本。最后分享一个我自己一直沿用的习惯在MPC资产里的每个参数描述栏里都写上它服务的对象或材质而不是只写一个孤零零的“Intensity”。这样几个月后重新打开项目你还能快速想起这个参数当初是给哪个效果用的。写Shader不容易但把辅助信息做好真的能让后期维护轻松很多。
返回列表