
1. 项目概述为什么要在材质节点里“写循环”在UE5的材质编辑器中鼓捣过一阵子的朋友大概率都遇到过一种“有力使不出”的憋屈感。你想实现一个稍微复杂点的图像处理效果比如今天要聊的高斯模糊按照常规思路是不是得拖一堆Texture Sample、Multiply、Add节点然后为了模拟采样半径还得把这些节点复制粘贴个七八遍连线连到眼花缭乱这还只是基础的一维模糊要是想做性能尚可的二维模糊那个节点网络简直能织成一张“蜘蛛网”后期维护和调试更是噩梦。这时候“在材质节点里写循环”这个念头就会冒出来。要是能像写代码一样用个for循环几行指令就搞定多次采样和加权计算那该多清爽。但UE的材质蓝图本身并不直接提供循环节点它的设计哲学是可视化的、无状态的函数式连接。那么这个想法是不是就泡汤了当然不是。UE为我们留了一扇“后门”这就是Custom自定义节点。你可以把它理解为一个嵌入到材质蓝图中的微型HLSLHigh-Level Shading Language代码片段编译器。通过它我们就能突破可视化节点的限制直接编写着色器代码实现包括循环在内的各种复杂逻辑。所以这个项目的核心价值在于掌握一种“降维打击”式的材质制作方法。当常规节点变得笨重、低效时Custom节点配合HLSL能让你重获精确控制与高性能。高斯模糊是一个绝佳的教学案例因为它逻辑清晰权重计算、循环采样效果直观且对性能敏感能充分展示这种方法的优势。无论你是想优化后期处理材质还是创造独特的视觉特效这套技能都能让你在UE5材质开发中更加游刃有余。2. 核心原理拆解高斯模糊与HLSL在UE5中的交汇在动手写代码之前我们必须把两件事吃透一是高斯模糊的数学原理在着色器中如何表达二是UE5的Custom节点如何与渲染管线交互。2.1 高斯模糊的数学本质与采样策略高斯模糊简单说就是用一个符合高斯函数正态分布的权重矩阵对图像中每个像素及其周围的像素进行加权平均。这个“周围”的范围就是模糊半径Radius。权重由高斯函数决定离中心越远的像素权重越低。在着色器中实现关键在于两点权重计算我们需要根据采样点距离中心的偏移量以像素为单位实时计算或查表获取高斯权重。标准二维高斯函数是G(x,y) (1/(2*π*σ²)) * exp(-(x²y²)/(2*σ²))。其中σ标准差决定了模糊的“扩散”程度。在实际应用中为了简化我们常使用两个一维高斯核先水平模糊再垂直模糊来代替一个二维核这能将采样次数从 Radius² 量级降低到 2*Radius 量级是极大的性能优化。循环采样对于中心像素的每一个偏移方向例如水平方向从 -Radius 到 Radius我们需要采样纹理并乘以对应的权重最后将所有结果累加。这个“从 -R 到 R”的过程就是循环。在HLSL中这个循环看起来会是这样一种结构概念代码float4 BlurColor float4(0, 0, 0, 0); float WeightSum 0.0; for (int i -Radius; i Radius; i) { float2 OffsetUV UV float2(i * PixelWidth, 0); // 计算偏移UV float Weight CalculateGaussianWeight(i, Sigma); // 计算权重 BlurColor Texture2DSample(Tex, TexSampler, OffsetUV) * Weight; WeightSum Weight; } BlurColor / WeightSum; // 归一化2.2 UE5 Custom节点的运行机制与数据接口Custom节点不是一个“黑盒”。当你把它拖进材质图表时它本质上是在生成着色器代码时将你写的HLSL代码片段直接插入到对应的着色器阶段通常是像素着色器中。有几个关键特性必须牢记输入引脚即函数参数你在Custom节点左侧创建的每个输入引脚都会成为你HLSL代码中的一个输入变量。你需要在自己的代码中声明并使用它们名称必须严格匹配。输出类型你需要根据输出结果选择输出类型如float3用于颜色float用于灰度值。代码中必须对return语句返回对应类型的值。代码环境Custom节点内的代码并非独立的着色器它运行在UE为你构建的材质着色器上下文中。这意味着你可以直接使用一些预定义的变量如Texture2DSample函数来采样纹理但像Texture2D类型的变量名需要你通过输入引脚传递进来。没有隐式循环这是最重要的一点。Custom节点本身不提供循环结构但你在其HLSL代码里写的for循环会被直接编译进着色器。着色器中的循环通常是“展开”的即如果循环次数是常数如我们定义的模糊半径编译器会在编译时直接展开成多条指令而不是动态循环判断这并不会造成额外的运行时开销。理解了这两点我们就知道我们的任务就是在Custom节点的代码框里用HLSL语言正确地实现上述高斯模糊的采样循环与权重计算逻辑并将纹理、UV、模糊强度等参数通过引脚连接进来。3. 完整实现步骤从节点搭建到HLSL编码接下来我们一步步构建这个高斯模糊材质函数。我会假设你已经在UE5中创建了一个新的材质或材质函数。3.1 创建与配置Custom节点创建材质函数在内容浏览器右键选择“材质与纹理” - “材质函数”。命名为MF_GaussianBlur是个好习惯。打开它。拖入Custom节点在材质函数图表中右键搜索“Custom”将“Custom”节点拖入。设置输入输出首先我们需要几个输入TextureObject类型为“Texture Object”用于传入需要模糊的纹理。UVs类型为“Function Input - Vector2”用于传入纹理坐标。Radius类型为“Function Input - Scalar”表示模糊半径。这里有个关键技巧为了着色器循环优化我们应该将其设置为一个编译时常量。在材质函数中可以将其“默认值”设为5并勾选“Use Preview Value as Default”。在HLSL代码中我们将直接使用这个常数值而不是一个变量。Sigma类型为“Function Input - Scalar”高斯函数的标准差控制权重分布。通常与Radius关联如 Sigma Radius / 2.0。TextureSize类型为“Function Input - Vector2”纹理的尺寸宽高。用于计算一个像素的UV偏移量PixelSize 1.0 / TextureSize。输出保持默认的“输出类型”为float3如果我们输出RGB颜色。如果你的纹理包含Alpha通道且需要模糊可以设为float4。3.2 HLSL代码实现详解双击Custom节点打开代码编辑器。我们将代码分为两部分一维模糊函数和主执行逻辑。// 定义高斯权重计算函数 float GaussianWeight(float x, float sigma) { float variance sigma * sigma; return exp(-(x * x) / (2.0 * variance)) / sqrt(2.0 * PI * variance); } // 主函数 - 执行可分离的一维高斯模糊水平方向 // 注意输入引脚名称必须与你在节点上创建的名称完全一致 float3 Main( Texture2D TextureObject, SamplerState TextureSampler, // UE会自动提供采样器 float2 UVs, float Radius, float Sigma, float2 TextureSize ) { // 计算一个像素对应的UV偏移量 float2 PixelSize 1.0 / TextureSize; // 初始化累加变量 float4 TotalColor float4(0, 0, 0, 0); float TotalWeight 0.0; // 预计算PI值 const float PI 3.14159265359; // 水平模糊循环 // 注意这里将Radius作为常数直接使用。在实际着色器中循环是展开的。 for (int x -Radius; x Radius; x) { // 计算当前采样点的UV偏移 float2 SampleUV UVs float2(x * PixelSize.x, 0); // 计算高斯权重使用距离x float weight GaussianWeight(float(x), Sigma); // 采样纹理并加权累加。使用Texture2DSample函数这是UE HLSL环境中的标准函数。 float4 SampledColor Texture2DSample(TextureObject, TextureSampler, SampleUV); TotalColor SampledColor * weight; TotalWeight weight; } // 归一化确保权重和为1避免亮度变化 float3 FinalColor TotalColor.rgb / TotalWeight; // 返回RGB颜色。如果需要Alpha可以返回TotalColor.a / TotalWeight。 return FinalColor; }代码要点解析TextureSampler这个参数你不需要手动创建输入引脚UE的Custom节点在编译时如果第一个输入是Texture2D它会自动将第二个参数映射为对应的采样器状态。这是一个需要记住的约定。Radius在循环中使用尽管Radius作为输入传入但在HLSL循环中如果循环边界是常数表达式着色器编译器会进行循环展开。为了确保这一点我们在材质函数调用时应尽量给Radius传入一个字面量常数比如通过材质参数集的默认值而不是一个每帧变化的动态参数。Texture2DSample这是UE提供的内部函数用于在着色器中采样纹理。它比通用的tex2D函数能更好地兼容UE的渲染管线。分离模糊以上代码只实现了水平方向的一维模糊。要完成完整的二维高斯模糊标准做法是创建两个这样的材质函数一个水平模糊MF_GaussianBlur_Horizontal一个垂直模糊MF_GaussianBlur_Vertical。然后在主材质中将原始纹理先经过水平模糊函数得到的渲染目标Render Target再作为输入传入垂直模糊函数。这就是“可分离滤波”的核心实践。3.3 构建完整的可分离高斯模糊网络创建水平模糊函数将上述代码保存为材质函数MF_GaussianBlur_Horizontal。在循环中偏移量是float2(x * PixelSize.x, 0)。创建垂直模糊函数复制一份重命名为MF_GaussianBlur_Vertical。修改循环内的偏移量为float2(0, y * PixelSize.y)同时循环变量改为y。在主材质中应用通常高斯模糊用于后期处理Post Process Material或对某个渲染纹理进行处理。你需要两个SceneTexture节点或TextureObject参数来串联这个过程。但在实际中更高效的做法是使用渲染目标Render Target。标准流程 a. 在蓝图中创建两个渲染目标RT_A, RT_B。 b. 使用一个绘制材质Material that Renders to RT该材质调用MF_GaussianBlur_Horizontal对原始场景纹理进行采样并处理将结果输出到RT_A。 c. 使用另一个绘制材质调用MF_GaussianBlur_Vertical对RT_A进行采样处理将最终结果输出到RT_B或直接用于显示。在材质函数内模拟为了在单次材质编辑中预览效果你可以采用“链式调用”。但这会显著增加着色器指令数仅用于预览。将水平模糊函数的输出直接作为垂直模糊函数TextureObject的输入。但请注意这并非运行时的高效做法。4. 关键参数调优与性能考量实现功能只是第一步做出高效、好看的效果需要调优。4.1 半径Radius与标准差Sigma的黄金搭配Radius和Sigma共同决定了模糊的强度和质感。Radius半径直观控制模糊的视觉范围。值越大参与采样的像素越多模糊越强烈性能开销也线性增长因为循环次数是2*Radius1。Sigma标准差控制权重分布的集中程度。Sigma越小权重越集中在中心点模糊效果越“硬”边缘可能存在色块Sigma越大权重分布越平缓模糊效果越“柔”和自然。经验公式一个常见的起始点是设置Sigma Radius / 2.0。这个比例通常能产生视觉上平滑的结果。你可以在此基础上微调想要更柔和、弥散感强的模糊适当增大Sigma如Sigma Radius / 1.5。想要在较大半径下保持一些结构适当减小Sigma如Sigma Radius / 3.0。重要提示在Custom节点的循环中Radius必须是编译时常数即材质实例中设置的默认值或通过StaticSwitch控制的常数。如果传入一个动态变化的参数可能导致着色器编译失败或性能急剧下降因为动态循环在着色器中是非常昂贵的。4.2 性能优化实战技巧坚持可分离滤波这是最重要的优化。将O(R²)的复杂度降为O(2R)。我们的代码本身就是为可分离滤波设计的。控制采样次数模糊半径是性能的最大杀手。在移动平台或需要实时运行的场合半径 rarely 超过7或9。通常半径5共采样11次已经能产生不错的模糊效果。使用降采样Downsample这是专业后期处理中大幅提升模糊性能的“杀手锏”。原理是先将原始图像渲染到一个长宽各一半或1/4的渲染目标上在这个低分辨率纹理上进行高斯模糊计算最后再将结果上采样回原尺寸。因为模糊本身就会丢失高频细节所以降采样带来的信息损失在视觉上几乎不可察觉但性能却能提升数倍像素处理量变为1/4或1/16。实现方法在第一步水平模糊前先通过一个简单的材质或使用SceneTexture的缩小版本将场景渲染到一半大小的RT中。后续的模糊操作都基于这个低分辨率RT进行。权衡权重计算我们的代码中每轮循环都计算一次exp函数这有一定开销。对于固定半径和Sigma的情况可以预先计算好权重数组以常量的形式硬编码在HLSL中消除动态计算开销。例如static const float Weights[11] {0.05, 0.09, 0.12, 0.15, 0.16, 0.15, 0.12, 0.09, 0.05}; // 举例Radius4然后在循环中直接按索引取值。这需要你根据公式预先算好权重。5. 常见问题排查与调试心得即使代码逻辑正确在UE里也可能遇到各种稀奇古怪的问题。这里分享几个我踩过的坑和解决方法。5.1 编译错误与代码语法错误“未识别的标识符 ‘Texture2DSample’”确保你的第一个输入引脚类型是Texture Object并且参数列表里包含了SamplerState TextureSampler。UE的Custom节点对函数签名有特定要求。错误“循环变量必须为常量表达式”这是最常遇到的问题。你的Radius输入在材质实例中可能被连接了一个动态参数。解决方案在材质函数中将Radius输入引脚的“默认值”设为一个固定数如5并在调用该函数的材质中确保传入的是一个“常量”参数例如材质参数集的默认值而不是一个随时间变化的动态值。你也可以使用StaticSwitch参数来控制不同的固定半径版本。效果全黑或全白首先检查权重总和TotalWeight。如果权重没有正确归一化即总和不为1会导致颜色值被异常缩放。在代码末尾加一句// return float3(TotalWeight, TotalWeight, TotalWeight);临时输出权重总和来调试理论上它应该非常接近1.0。5.2 视觉瑕疵与解决方案边缘出现硬边或色块这是因为在图像边缘采样时UV坐标可能超出[0,1]范围。默认的纹理寻址模式Wrap, Clamp等会影响边缘像素的获取。在模糊这种需要邻域信息的操作中Clamp模式重复边缘像素通常比Wrap模式更合适。你可以在采样纹理的Texture Sample节点如果你用节点给Custom传纹理上设置或者如果直接用SceneTexture其默认就是Clamp。在Custom代码中我们使用的是传入的采样器状态其寻址模式由纹理资源本身或上层节点决定。模糊效果有方向性“拖影”检查你的偏移量计算PixelSize是否正确。确保TextureSize传入的是正确的纹理宽高通常可以通过SceneTexture节点的“纹理尺寸”输出引脚获取。一个像素的UV偏移量绝对是1.0 / TextureSize。性能热点使用UE5的GPU Visualizer在编辑器偏好设置中启用来查看你的材质在屏幕不同区域消耗的GPU时间。如果高斯模糊材质显示为深红色热点说明开销过大。此时必须考虑采用降采样优化这是让实时高斯模糊变得可行的最关键一步。5.3 关于“循环”的终极理解最后再强调一下这个项目的灵魂——“在材质节点里写循环”。我们写的for循环在最终的着色器代码中如果循环边界是常数会被完全展开。例如for(int i-5; i5; i)会展开成11条独立的采样和计算指令。它并不是一个在GPU上动态判断跳转的“循环”因此没有传统意义上的循环开销。这也解释了为什么动态的Radius不行——编译器在编译时无法知道要展开多少次。所以我们实现的是一种“编译时循环”它给予了我们节点编辑器无法提供的灵活性同时又通过编译优化保障了运行效率。掌握了Custom节点和HLSL你就相当于拥有了在UE5材质系统中直接编写着色器代码的能力。高斯模糊只是一个起点你可以将这套方法应用于任何需要复杂计算、条件判断或迭代的图像处理效果中比如自定义的屏幕空间反射SSR、复杂噪声生成、或者基于物理的磨损效果计算。这扇门后的世界广阔得很。