
在虚幻引擎中纹理是构建视觉世界的基石。对于技术美术或图形程序员而言直接使用现成的贴图资源虽然方便但在需要动态变化、无缝平铺、风格化或性能优化时程序化生成纹理的能力就显得至关重要。HLSLHigh-Level Shading Language作为GPU的编程语言是实现这一目标的强大工具。通过编写HLSL代码我们可以在材质编辑器中直接生成复杂的图案例如一个经典的砖墙纹理这不仅能让你深入理解着色器的工作原理更能为后续实现更复杂的程序化材质打下坚实基础。本文将以“程序化生成砖墙纹理”为具体目标带你从零开始在虚幻引擎5中编写HLSL代码。你将学习到如何利用HLSL的基本函数如step、frac构建基础图案如何通过参数控制砖块的大小、颜色、灰缝等属性并最终在材质实例中动态调整这些参数。整个过程不依赖任何外部贴图完全由数学和逻辑驱动。无论你是希望深化HLSL理解的技术美术还是想探索程序化材质可能性的开发者这篇文章都将提供一条清晰的实践路径。1. 理解程序化纹理与HLSL在UE中的角色在开始敲代码之前我们需要明确两个核心概念程序化纹理是什么以及HLSL在虚幻引擎材质系统中扮演什么角色。1.1 什么是程序化纹理程序化纹理并非一张存储在磁盘上的图片文件如PNG、TGA而是一套由算法和数学公式定义的规则。当材质需要渲染某个像素时GPU会实时执行这些规则根据像素的坐标等信息计算出该点的颜色值。以砖墙为例我们不会去画每一块砖而是定义规则“在水平方向每0.2个单位重复一次砖块和灰缝在垂直方向每0.1个单位重复一次并且每隔一行偏移一半的宽度”。这种方式的优势非常明显无限分辨率与无缝平铺由于是算法生成纹理可以无限放大而不失真并且天然无缝非常适合用于地形、建筑等需要大面积平铺的表面。极小的资源占用不需要存储庞大的纹理数据仅需少量代码和参数节省内存和磁盘空间。动态与可交互性纹理的属性如颜色、尺寸、粗糙度可以通过参数实时调整甚至可以根据时间、距离或其他游戏逻辑动态变化。风格化控制可以精确控制纹理的样式生成手绘、像素化等特定艺术风格的图案。1.2 HLSL在虚幻材质编辑器中的工作流虚幻引擎的材质编辑器本质上是一个可视化编程界面你连接的每一个节点最终都会被编译成HLSL代码。当你创建一个“Custom Node”或“Material Function”并写入HLSL时你是在直接操作这个编译过程的最底层。关键机制材质坐标Texture Coordinate程序化纹理的起点通常是材质坐标即每个像素在UV空间中的位置范围通常是(0,0)到(1,1)。我们的HLSL代码将接收这个UV值作为输入并通过一系列数学变换输出最终的颜色RGB和可能的高光、法线等数据。核心函数step,frac,floor生成砖墙这类规则图案离不开几个关键的HLSL内置函数frac(x): 返回输入值x的小数部分。这是实现平铺Tiling的核心frac(UV * Tiling)可以将UV空间划分为多个重复的单元格。floor(x): 返回小于或等于x的最大整数。常用于获取当前像素所在的单元格索引。step(a, b): 一个非常重要的阈值函数。当b a时返回0否则返回1。它是生成硬边图案如砖块轮廓的利器。理解了这些基础我们就可以开始搭建环境并动手实现了。2. 环境准备与项目设置在开始编写HLSL之前确保你的开发环境已就绪并创建一个干净的项目用于实践。2.1 软件与硬件要求项目要求说明虚幻引擎版本5.0 或更高版本 (推荐 5.2)本文示例基于UE5但核心HLSL逻辑在UE4中同样适用。项目模板Blank空白或 Games First Person选择一个简单的模板避免不必要的复杂内容干扰。显卡驱动更新至最新版本确保DirectX 12功能正常以获得最好的着色器编译体验。操作系统Windows 10/11 64-bitHLSL着色器开发主要在Windows平台进行。2.2 创建测试材质与关卡新建项目启动虚幻引擎选择“Games”类别下的“Blank”项目模板设置好项目名称和路径后创建。创建测试材质在内容浏览器中右键点击选择“材质”命名为M_ProceduralBrick。创建材质实例可选但推荐右键点击M_ProceduralBrick选择“创建材质实例”命名为MI_Brick。材质实例允许我们运行时调整参数而无需重新编译着色器。搭建测试关卡打开或新建一个空白关卡。从内容浏览器拖拽一个立方体Cube或平面Plane到场景中。将刚刚创建的M_ProceduralBrick材质拖拽到该模型上。调整视图在关卡编辑器中确保“实时”Realtime模式已开启点击视口右上角的时钟图标以便实时看到材质变化。现在双击打开M_ProceduralBrick材质我们将进入材质编辑器开始编写HLSL代码。3. 使用Custom Node编写基础砖墙HLSL虚幻材质编辑器中的“Custom Node”节点是我们注入自定义HLSL代码的入口。我们将从最简单的黑白砖块图案开始。3.1 创建Custom Node并理解其接口在材质编辑器的空白处右键搜索并添加“Custom”节点其完整名称为“Custom Node”。选中Custom Node在细节面板中将“Description”改为“Brick Pattern Generator”以便识别。我们需要为其定义输入和输出引脚。输入至少需要一个UV坐标输入。点击“Inputs”旁边的“”号将名称改为UV类型保持为VMaterialFunctionInput。输出我们的目标是输出颜色。点击“Outputs”旁边的“”号将名称改为BrickMask类型选择CMOT Float 3即三维向量代表RGB。最关键的一步是在“Code”文本框中写入HLSL代码。3.2 编写生成砖块掩码Mask的HLSL代码我们的第一步是生成一个“砖块掩码”砖块区域为白色1,1,1灰缝区域为黑色0,0,0。将以下代码填入Custom Node的“Code”区域// 输入UV坐标 float2 UV Input.UV; // 参数砖块平铺度每UV单位内的砖块数量 float2 BrickTiling float2(10.0, 20.0); // 水平10块垂直20块 // 参数灰缝相对宽度 (0.0 ~ 1.0) float MortarWidth 0.1; // 1. 计算平铺后的UV和单元格索引 float2 TiledUV UV * BrickTiling; float2 CellIndex floor(TiledUV); // 当前像素属于第几个砖块单元格 float2 CellUV frac(TiledUV); // 在当前单元格内的相对位置(0~1) // 2. 生成水平方向的砖块掩码考虑错行偏移 // 判断当前行是奇数行还是偶数行用于错位 float BrickOffset (frac(CellIndex.y * 0.5) 0.0) ? 0.5 : 0.0; // 每隔一行偏移0.5个砖块宽度 float BrickMaskX step(MortarWidth, CellUV.x) * step(CellUV.x, 1.0 - MortarWidth); // 应用错位将水平坐标偏移再判断一次 float OffsetCellUVX frac(CellUV.x BrickOffset); float BrickMaskX_Offset step(MortarWidth, OffsetCellUVX) * step(OffsetCellUVX, 1.0 - MortarWidth); // 根据行号选择使用原始掩码还是偏移后的掩码 float FinalBrickMaskX (frac(CellIndex.y * 0.5) 0.0) ? BrickMaskX_Offset : BrickMaskX; // 3. 生成垂直方向的灰缝掩码 float BrickMaskY step(MortarWidth, CellUV.y) * step(CellUV.y, 1.0 - MortarWidth); // 4. 合并得到最终的砖块掩码1砖块0灰缝 float BrickMask FinalBrickMaskX * BrickMaskY; // 输出将单通道掩码扩展为RGB三通道 return float3(BrickMask, BrickMask, BrickMask);代码关键点解释floor和fracCellIndex告诉我们当前在哪块砖CellUV告诉我们在当前这块砖内的哪个位置。step(MortarWidth, CellUV.x)这是一个核心技巧。CellUV.x从0增长到1。step(0.1, CellUV.x)会在CellUV.x超过0.1时从0跳变为1。这就在x0.1处创建了一条硬边。step(CellUV.x, 0.9)则在x0.9处创建另一条硬边。两者相乘就得到了一个在x方向0.1到0.9之间为1其余为0的“条带”这就是砖块的宽度减去两侧灰缝。错行逻辑frac(CellIndex.y * 0.5) 0.0用于判断当前行是奇数行还是偶数行。在偶数行我们给水平坐标加上0.5的偏移通过frac(CellUV.x 0.5)实现再用同样的step逻辑判断这样就实现了砖墙的错缝效果。最终合并BrickMask是水平掩码和垂直掩码的乘积。只有当一个像素同时在水平方向和垂直方向都属于“砖块区域”时它才是砖块。3.3 连接节点并预览结果将材质编辑器中的“TextureCoordinate”节点设置其UTiling和VTiling为1连接到Custom Node的UV输入引脚。将Custom Node的BrickMask输出引脚连接到材质“基础颜色”Base Color输入。点击材质编辑器上方的“应用”Apply或“保存”Save。你应该能在材质预览球上看到一个黑白交错的标准砖墙图案。至此一个最基础的程序化砖墙掩码已经生成。但这只是黑白的接下来我们要为其添加颜色和更多控制。4. 实现参数化控制与高级效果将代码中的硬编码数字如BrickTiling,MortarWidth暴露为材质参数并添加颜色、粗糙度等属性是使程序化材质变得可用的关键。4.1 暴露HLSL参数到材质编辑器我们不能直接在Custom Node的代码里使用材质参数节点。需要将参数作为Custom Node的额外输入引脚传入。创建材质参数在材质编辑器中右键搜索“ScalarParameter”创建两个分别命名为BrickScaleX和BrickScaleY默认值设为10和20。再创建一个“ScalarParameter”命名为MortarWidth默认值0.1。创建两个“VectorParameter”颜色参数分别命名为BrickColor和MortarColor设置为红色和深灰色。修改Custom Node输入选中之前的Custom Node在细节面板的“Inputs”中添加新的输入BrickTiling类型VMaterialFunctionInput。MortarWidth类型VMaterialFunctionInput。BrickColor类型CMOT Float 3。MortarColor类型CMOT Float 3。更新HLSL代码 修改Custom Node的代码使用传入的参数并输出最终颜色。// 输入 float2 UV Input.UV; float2 BrickTiling Input.BrickTiling; // 现在从外部参数传入 float MortarWidth Input.MortarWidth; // 现在从外部参数传入 float3 BrickColor Input.BrickColor; float3 MortarColor Input.MortarColor; // ... (中间计算BrickMask的代码与之前完全相同) ... // 5. 根据掩码混合砖块颜色和灰缝颜色 float3 FinalColor lerp(MortarColor, BrickColor, BrickMask); return FinalColor;代码关键点解释lerp(a, b, t)线性插值函数是HLSL中最常用的混合工具。当t0时返回at1时返回bt在0到1之间时进行混合。这里我们用BrickMask非0即1作为t实现了砖块和灰缝颜色的硬切分。连接参数将BrickScaleX和BrickScaleY通过“Append”节点组合成一个二维向量连接到Custom Node的BrickTiling输入。将MortarWidth、BrickColor、MortarColor参数分别连接到对应输入。应用并测试保存材质。现在你可以通过修改BrickScaleX/Y参数来改变砖块的密度修改MortarWidth来调整灰缝粗细修改颜色参数来改变外观。在材质实例MI_Brick中调整这些参数效果会实时反映在场景中的模型上。4.2 添加表面粗糙度变化真实的砖墙砖块和灰缝的粗糙度光滑程度是不同的。我们可以很容易地扩展这个系统。添加粗糙度参数创建两个“ScalarParameter”命名为BrickRoughness和MortarRoughness默认值分别为0.7较粗糙和0.3较光滑。修改Custom Node输出我们还需要输出粗糙度。在Custom Node的“Outputs”中添加一个新输出命名为Roughness类型为CMOT Float 1单浮点数。更新HLSL代码在计算FinalColor的代码后面添加粗糙度混合逻辑。// ... (计算FinalColor的代码) ... // 6. 混合粗糙度与颜色混合逻辑一致 float BrickRoughness Input.BrickRoughness; float MortarRoughness Input.MortarRoughness; float FinalRoughness lerp(MortarRoughness, BrickRoughness, BrickMask); // 设置多输出 // 注意Custom Node的默认输出对应第一个输出引脚(BrickMask/现在叫FinalColor)。 // 我们需要使用‘Output’结构来指定多个返回值。 Output.Roughness FinalRoughness; return FinalColor; // 第一个输出引脚仍然返回颜色注意当Custom Node有多个输出时return语句返回的是第一个输出引脚的值。其他输出引脚的值需要通过Output.OutputPinName的格式来赋值。确保细节面板中输出引脚的顺序与你代码中赋值的顺序一致。连接粗糙度输出将Custom Node新增的Roughness输出引脚连接到材质“粗糙度”Roughness输入引脚。现在砖墙的砖块和灰缝部分将具有不同的高光反射特性材质看起来更具物理真实感。4.3 引入噪声制造破损与变化完全规则的砖墙看起来会非常机械。我们可以引入简单的噪声来打破这种规则性例如让部分砖块颜色变深或让灰缝宽度有细微变化。这里我们使用一个简单的伪随机函数来生成噪声。在Custom Node代码开头添加一个噪声函数// 简单的伪随机函数用于生成变化 float Random(float2 st) { return frac(sin(dot(st.xy, float2(12.9898, 78.233))) * 43758.5453123); }然后在计算BrickMask之前用噪声来扰动MortarWidth和BrickColor// 为每个砖块单元格生成一个随机值 float RandomPerBrick Random(CellIndex); // 应用随机变化到灰缝宽度例如在基础值上±0.02范围内变化 float VariedMortarWidth MortarWidth (RandomPerBrick - 0.5) * 0.04; // 确保变化后的宽度仍在合理范围 VariedMortarWidth clamp(VariedMortarWidth, 0.01, 0.5); // 在后续的step函数中使用VariedMortarWidth代替MortarWidth // 例如float BrickMaskX step(VariedMortarWidth, CellUV.x) * step(CellUV.x, 1.0 - VariedMortarWidth); // 应用随机变化到砖块颜色例如让10%的砖块变暗 float3 VariedBrickColor BrickColor; if (RandomPerBrick 0.9) { // 阈值可调 VariedBrickColor * 0.7; // 变暗 } // 在lerp混合颜色时使用VariedBrickColor通过引入这些微小的随机变化你的砖墙纹理会立刻显得生动和自然许多。5. 运行验证、性能分析与优化完成编码后我们需要验证效果、理解性能开销并考虑优化。5.1 效果验证清单在场景中检查你的程序化砖墙材质时请对照以下清单平铺正确性放大和缩小视图检查砖墙纹理是否真正无缝平铺边界处没有接缝。参数响应在材质实例MI_Brick中尝试以下操作观察实时变化大幅调整BrickScaleX和BrickScaleY砖块数量应相应变化。将MortarWidth调至接近0灰缝应几乎消失调大灰缝应变宽。修改BrickColor和MortarColor颜色应立即更新。调整粗糙度参数观察模型上的高光反射变化。动态效果如果实现了噪声旋转或移动模型由于噪声是基于世界坐标或物体坐标的纹理不应发生滑动除非你特意基于UV动画。检查随机破损效果是否自然。5.2 性能考量与优化建议程序化纹理的代价是GPU的额外计算。对于简单的砖墙开销微乎其微。但对于复杂材质需注意计算复杂度step,frac,floor,lerp都是GPU极擅长的廉价操作。我们引入的sin,dot等函数开销稍大但仍在可接受范围。避免在像素着色器中嵌套循环或使用非常复杂的噪声函数如Perlin噪声的多次采样。纹理采样替代对于超复杂的程序化图案有时预计算成一张纹理并采样性能可能更好。这是一个典型的“时间换空间”或“空间换时间”的权衡。材质实例化我们已将关键参数暴露并使用材质实例。这意味着同一个材质相同的HLSL代码可以派生出无数个不同外观的实例而不会增加Draw Call。这是最佳实践。Shader复杂度指示器在材质编辑器的“统计信息”窗口可以查看粗略的指令数。我们的砖墙材质应该属于“非常廉价”的类别。优化技巧将常量计算移至材质函数或Custom Node外部例如BrickTiling是常量参数与其在Custom Node内部用UV * BrickTiling不如在外部先用一个“Multiply”节点计算好再传入这样可能让着色器编译器有更多优化空间尽管现代编译器很智能。慎用分支GPU不喜欢if语句。我们的代码中if (RandomPerBrick 0.9)是一个潜在分支。可以考虑用lerp和smoothstep等函数来模拟平滑过渡或者确保分支条件在像素之间是连贯的。使用Material Functions封装将生成砖块掩码的核心HLSL代码封装到一个“材质函数”中。这样可以在多个材质中复用并且函数内部的代码只需编译一次有利于管理和优化。6. 常见问题排查在开发过程中你可能会遇到以下问题。这里提供排查思路。问题现象可能原因检查与解决方案材质显示为纯黑或纯白1. Custom Node代码编译错误。2. 输出值超出预期范围如返回了NaN或极大值。1. 检查材质编辑器下方的“日志”窗口是否有HLSL编译错误。错误信息通常会指向有问题的行。2. 简化代码先只返回一个固定颜色如return float3(1,0,0);测试节点是否连通。砖块图案不显示或错乱1. UV输入连接错误或Tiling为0。2.step函数参数顺序错误或阈值设置不合理。3. 错行偏移逻辑计算错误。1. 检查连接到Custom Node的UV值。可以先用一个“Constant2Vector”节点输入(0,0)和(1,1)测试。2. 记住step(a, b)是b a ? 0 : 1。检查MortarWidth是否太大如0.5导致砖块区域被完全挤没。3. 逐行调试分别输出BrickMaskX,BrickMaskY,BrickOffset到颜色通道可视化检查中间步骤。修改材质实例参数无效果1. 材质实例未成功连接到父材质。2. Custom Node的输入引脚名称与代码中Input.xxx的xxx不匹配。3. 参数未标记为“公开”。1. 确保场景中应用的是材质实例MI_Brick而不是父材质M_ProceduralBrick。2. 仔细核对Custom Node细节面板中的输入引脚名称必须与代码中Input.后面的部分完全一致大小写敏感。3. 在父材质中确保参数节点的“公开”复选框已被勾选。性能突然下降1. 在代码中意外引入了循环或极高复杂度的运算。2. 材质被应用到屏幕上大量像素如全屏后处理。1. 回顾代码移除任何for循环。对于噪声使用简化版本。2. 对于全屏效果考虑使用更优化的方案或降低采样精度。图案在特定角度闪烁摩尔纹当砖块尺寸在屏幕上接近像素大小时由step产生的硬边与屏幕像素网格对齐不良导致。使用smoothstep函数替代step。smoothstep会在阈值附近产生一个平滑过渡可以有效消除由硬边带来的锯齿和闪烁。例如float edge 0.02; float mask smoothstep(MortarWidth - edge, MortarWidth edge, CellUV.x);7. 扩展方向与最佳实践掌握了基础砖墙生成后你可以尝试以下方向来深化你的程序化材质技能。7.1 扩展方向法线贴图Normal Map为砖块和灰缝生成不同的表面法线。砖块中心可以略微凸起灰缝可以凹陷。这可以通过在HLSL中基于BrickMask混合两个法线向量或者更高级地用噪声扰动砖块表面的法线方向来实现。高度图Height Map/视差遮挡Parallax Occlusion使用类似的技术生成高度信息让砖块真正在几何感上凸出灰缝凹陷。这需要输出到材质的高度引脚并启用“视差遮挡映射”。风化与污渍引入基于世界空间坐标或物体空间坐标的噪声在砖墙的底部、边缘或特定区域叠加潮湿、脏污或苔藓的效果。这通常需要额外的遮罩和颜色混合逻辑。非矩形砖块修改算法生成菱形、六边形或其他形状的“砖块”。这需要更复杂的单元格划分和距离场函数。与纹理采样结合程序化生成遮罩Mask然后用这个遮罩去混合多张真实的纹理采样例如一张砖块Albedo贴图和一张灰缝Albedo贴图。这样既能保留程序化的灵活平铺又能拥有真实纹理的丰富细节。7.2 工程最佳实践封装为材质函数将你的砖墙生成HLSL代码最好是参数化的版本保存为一个“材质函数”。这样在任何新材质中你都可以像使用一个黑盒节点一样拖入这个函数输入参数得到输出。这极大地提高了复用性和可维护性。良好的命名与注释Custom Node、输入输出引脚、内部变量都要使用清晰易懂的名称。在复杂的HLSL代码块中用注释说明每一段代码的意图。参数范围限制对于公开的参数如MortarWidth在材质实例中可以通过“滑块范围”进行限制防止用户输入不合理的值如负数或大于1的值导致渲染异常。提供默认值确保你的材质或材质函数在没有任何参数输入时也能产生一个合理的默认外观。性能分级考虑为不同平台或质量等级创建不同复杂度的版本。例如移动端版本可以去掉噪声和法线扰动只保留基础颜色和粗糙度。通过这个从零构建程序化砖墙纹理的完整流程你不仅学会了具体的HLSL代码更重要的是掌握了在虚幻引擎中利用数学和逻辑创造无限纹理的思路。程序化生成的魅力在于一旦规则确立变化便唾手可得。尝试调整参数修改算法你将能创造出远不止于砖墙的丰富世界。