三方向映射技术详解:解决UV拉伸,实现高质量纹理投影

三方向映射技术详解:解决UV拉伸,实现高质量纹理投影 1. 项目概述三方向映射到底是什么以及为什么你需要它在三维图形渲染里纹理映射是个基础得不能再基础的操作。我们通常把一张二维的图片“贴”到三维模型表面这个过程就叫UV映射。但这里有个经典难题当模型表面在某个方向上被极度拉伸时比如一个细长的山坡、一根管道的内壁或者一个被严重挤压的平面传统的UV映射就会出问题。贴图会跟着模型表面一起被拉伸导致纹理变得模糊、像素化或者出现难看的重复图案。这就是所谓的“地形拉伸”问题在做地形、建筑墙面、管道内衬时尤其常见。三方向映射就是来解决这个问题的“特效药”。它的核心思想非常直观既然一个方向的UV会拉伸那我干脆不用一个固定的UV了。我根据模型表面在世界空间中的朝向把模型表面分成三大类朝上的面比如地面、屋顶、朝侧面的面比如墙壁、朝下的面比如天花板内壁、管道底部。然后我分别给这三类面投影三套独立的、不受模型变形影响的UV坐标。对于朝上的面我使用世界XZ平面作为UV对于朝侧面的面我根据法线是偏向世界X轴还是Z轴选择世界XY或ZY平面作为UV朝下的面则类似朝上的面做镜像或另一套处理。这样做的好处是立竿见影的。无论你的地形网格怎么起伏、怎么拉伸贴图永远基于世界坐标对齐。地面上的一块砖纹无论在多陡的坡上它的大小和比例都是恒定的墙面的壁纸无论墙面是倾斜还是扭曲图案都不会被拉扁。这对于追求写实细节的材质表现比如砖墙、地板、瓦片、岩石等是质的提升。它让材质摆脱了模型拓扑的束缚实现了“所见即所得”的纹理表现。这个技术并非UE或Unity的专利它是计算机图形学中一种经典的投影技术。但在游戏引擎的材质编辑器中实现它需要一些节点技巧和对空间变换的理解。本教程将手把手带你分别在UE4/UE5的材质蓝图和Unity URP的Shader Graph中从零构建一个完整可用的三方向映射材质并深入讲解每一步背后的数学原理和优化技巧。2. 核心原理拆解世界空间、法线与投影要理解三方向映射必须吃透三个核心概念世界空间、顶点法线、和投影遮罩。这是所有后续操作的基石。2.1 世界空间坐标我们的绝对标尺在着色器里一个点的位置可以用多种坐标空间来描述模型空间相对于模型原点、世界空间相对于游戏世界原点、观察空间相对于摄像机。三方向映射的“方向”是基于世界空间来定义的。因为世界空间是全局的、不变的。我们说“朝上”指的是世界空间的Y轴正方向在UE里是Z轴正方向这点需要注意引擎不同轴向不同。使用世界坐标作为投影基准确保了无论物体如何移动、旋转纹理相对于世界的关系是固定的。这是解决拉伸问题的关键——纹理不再附着于变形的模型表面而是“铺”在了整个游戏世界上。在UE材质中我们可以通过“Absolute World Position”节点获取像素的世界坐标。在Unity Shader Graph中使用“Position”节点并设置为“World”空间。得到的世界坐标是一个三维向量X, Y, Z我们将用它来生成UV。2.2 顶点法线判断朝向的唯一依据一个表面是朝上、朝侧面还是朝下由该点的顶点法线Vertex Normal决定。法线是一个单位向量垂直于模型表面。通过计算法线向量与世界空间上方向向量如(0, 1, 0)的点积Dot Product我们可以得到一个标量值。这个值在1到-1之间。当点积结果接近1例如 0.5说明法线方向与上方向基本一致表面朝上。当点积结果接近-1例如 -0.5说明法线方向与上方向相反表面朝下。当点积结果的绝对值较小例如介于-0.5到0.5之间说明法线更偏向水平方向表面是侧面。这个点积运算就是我们生成“遮罩”的基础。我们将通过一个平滑的过渡函数通常是SmoothStep或Power节点将这个连续的点积值转化为三个近似二进制的遮罩MaskTopMask, SideMask, BottomMask。理想情况下对于任意一个像素点这三个遮罩中只有一个为1白色另外两个为0黑色代表它属于哪一个朝向类别。2.3 投影与遮罩混合有了朝向遮罩和世界坐标接下来就是投影。对于Top面我们忽略世界Y坐标使用(X, Z)作为UV。对于Side面我们需要再区分是朝向X轴方向还是Z轴方向的侧面通常的做法是取法线向量的X和Z分量的绝对值哪个更大就认为主要朝向那个轴然后使用对应的世界坐标平面如主要朝X则用(Z, Y)作为UV主要朝Z则用(X, Y)作为UV。对于Bottom面处理方式与Top面类似有时会对UV进行翻转以避免接缝。最后我们分别用三套UV去采样同一张或不同的纹理然后根据各自的遮罩权重将三个采样结果混合起来。这个混合必须是平滑的即在不同朝向的交界处比如墙面与地面的拐角遮罩之间要有柔和的过渡否则会出现生硬的接缝。通常使用遮罩直接作为权重进行线性插值Lerp就能得到不错的效果更高级的实现会用到Triplanar Mapping节点UE内置或自己构造更复杂的混合函数。3. UE4/UE5材质蓝图实现全流程下面我们进入实战环节在Unreal Engine中构建这个材质。我将假设你有一张想要平铺的纹理例如一套石板或砖墙材质。3.1 基础节点搭建获取数据与计算遮罩首先创建一个新的材质命名为M_Triplanar。获取基础数据拉出节点搜索Absolute World Position获取世界空间坐标。我们将其Break为X, Y, Z三个分量。拉出节点搜索Vertex Normal WS世界空间法线。这是关键输入。计算顶部遮罩Top Mask世界空间上方向向量在UE中是(0, 0, 1)。使用Dot Product节点连接Vertex Normal WS和常量(0, 0, 1)。点积结果是一个标量。我们使用一个SmoothStep节点来创建一个平滑的遮罩。SmoothStep需要三个输入Edge0, Edge1, 和Value。它的效果是当Value小于Edge0时返回0大于Edge1时返回1在中间则平滑过渡。设置SmoothStep的Edge0为0.4Edge1为0.6。将点积的结果输入Value。这样当法线朝上点积0.6时输出为1完全水平点积0.5时输出约为0.5朝下点积0.4时输出为0。这个输出就是我们的TopMask。为了得到更锐利的遮罩可以后续加一个Power节点例如Power(TopMask, 10)让中间过渡区域更窄。计算底部遮罩Bottom Mask原理相同但方向相反。计算Vertex Normal WS与(0, 0, -1)的点积。同样使用SmoothStep但注意阈值。因为点积结果为负我们可以设置Edge0为-0.6,Edge1为-0.4。或者更简单的方法对计算TopMask的点积结果取反乘以-1然后使用相同的SmoothStep(0.4, 0.6)。这样得到BottomMask。计算侧面遮罩Side Mask侧面遮罩就是“既不是顶也不是底”的部分。最简单的方式是SideMask 1 - (TopMask BottomMask)。但注意由于TopMask和BottomMask在过渡区不是严格的0或1直接相加可能导致某些区域权重和大于1。更稳健的做法是SideMask 1 - Saturate(TopMask BottomMask)。Saturate函数将值钳制在0到1之间。为了更好的侧面效果我们还可以进一步区分X轴侧面和Z轴侧面。通过比较世界法线的X和Z分量的绝对值来决定使用哪组世界坐标做UV。这里我们先实现基础的侧面混合。注意遮罩的计算是艺术性的。SmoothStep的阈值和Power的指数需要根据你的模型和需求微调。阈值越接近过渡越锐利Power指数越大遮罩越“硬”。对于岩石等粗糙表面过渡可以宽一些对于人造物如砖墙过渡需要非常锐利。3.2 构建三套UV并采样纹理现在我们为三个朝向生成UV。顶部UV顶部是XZ平面。将Absolute World Position的X和Z分量提取出来除以一个纹理缩放系数TextureScale这是一个标量参数得到最终的UV。UV_Top (WorldPos.X, WorldPos.Z) / TextureScale。底部UV底部同样可以看作是XZ平面但有时为了纹理方向正确需要对其中一个轴进行翻转例如UV_Bottom (WorldPos.X, -WorldPos.Z) / TextureScale。是否翻转取决于你的纹理内容。侧面UV这是最复杂的一步。我们需要根据法线是偏向X轴还是Z轴来智能选择投影平面。计算法线向量X和Z分量的绝对值AbsNormalX Abs(Normal.X),AbsNormalZ Abs(Normal.Z)。比较两者大小。我们可以用一个巧妙的技巧计算AbsNormalX减去AbsNormalZ然后通过一个SmoothStep(-0.1, 0.1, Difference)来生成一个混合权重BlendXZ。当X分量远大于Z时BlendXZ接近1表示使用X轴为主的投影即ZY平面当Z分量远大于X时BlendXZ接近0表示使用Z轴为主的投影即XY平面在中间地带平滑过渡。因此侧面UV由两组UV混合而成UV_Side_X(使用ZY平面):(WorldPos.Z, WorldPos.Y) / TextureScaleUV_Side_Z(使用XY平面):(WorldPos.X, WorldPos.Y) / TextureScale最终UV_Side Lerp(UV_Side_Z, UV_Side_X, BlendXZ)。这里的Lerp是线性插值。纹理采样创建三个Texture Sample节点或者使用同一个纹理对象分别将UV_Top,UV_Side,UV_Bottom连接到它们的UV输入口。假设我们采样同一张基础颜色贴图BaseColor得到SampleTop,SampleSide,SampleBottom。3.3 混合与最终输出现在我们有三个颜色采样结果和三个遮罩权重。初步混合首先混合顶部和底部。Color_TopBottom Lerp(SampleBottom, SampleTop, TopMask)。这里用TopMask作为混合权重因为当TopMask1时完全显示顶部TopMask0时完全显示底部中间过渡区域由两者混合。注意在顶部和底部区域SideMask为0这个混合是有效的。与侧面混合将上一步的结果与侧面采样结果混合。FinalColor Lerp(Color_TopBottom, SampleSide, SideMask)。这里用SideMask作为权重。当SideMask1时纯侧面显示SampleSide当SideMask0时顶或底显示Color_TopBottom。权重归一化检查高级理论上TopMask SideMask BottomMask应该处处等于1。但由于我们计算SideMask的方式在三个遮罩的过渡三角区总和可能略小于1。更精确的混合是使用“加权平均”FinalColor (SampleTop * TopMask SampleSide * SideMask SampleBottom * BottomMask) / (TopMask SideMask BottomMask 0.001)。分母加一个极小值防止除零。这种方法混合更精确但节点更多。对于大多数情况两步Lerp已经足够好。连接材质输出将计算出的FinalColor连接到材质节点的Base Color引脚。别忘了你还可以用同样的三套UV和遮罩逻辑去采样法线贴图、粗糙度贴图、环境光遮蔽贴图等并分别进行混合实现完整的PBR材质。3.4 UE内置捷径Triplanar节点如果你觉得上面太复杂UE其实提供了内置的Triplanar节点在材质面板搜索。这个节点封装了大部分逻辑。你只需要提供Texture需要投影的纹理。Position世界位置通常连Absolute World Position。Normal世界法线连Vertex Normal WS。Tile纹理平铺度相当于我们的TextureScale。Blend混合锐度值越大不同朝向间的过渡越硬。内置节点非常方便但它是“黑箱”。自定义程度低例如无法为顶、侧、底分别指定不同的纹理或缩放。我们手动构建的意义在于完全的控制权和深入的理解。你可以轻松修改为顶部用草地纹理侧面用岩石纹理底部用泥土纹理。4. Unity URP Shader Graph实现详解在Unity的URP管线中我们通过Shader Graph可视化工具来实现同样的功能。逻辑完全相通只是节点名称和获取数据的方式略有不同。4.1 创建Graph与属性设置在Project窗口右键Create - Shader - Universal Render Pipeline - Shader Graph。将其命名为SG_Triplanar并打开。我们需要几个属性Property_BaseMap(Texture2D): 基础贴图。_TextureScale(Float): 纹理缩放默认值设为5。_BlendSharpness(Float): 混合锐度用于控制遮罩过渡默认值10。4.2 获取世界空间数据世界位置添加一个Position节点将其Space设置为World。这个节点输出一个三维向量。世界法线添加一个Normal Vector节点将其Space设置为World。这是计算遮罩的关键。上方向向量在Unity的世界空间中上方向是(0, 1, 0)。我们可以用一个Vector3节点手动输入或者使用Transform Direction节点将物体空间的(0,1,0)转换到世界空间。简单起见直接用Vector3节点输入(0,1,0)。4.3 计算三向遮罩顶部遮罩使用Dot Product节点连接World Normal和上方向向量(0,1,0)。使用Smoothstep节点。将点积结果输入InEdge1和Edge2分别设为0.4和0.6。输出即为初始的顶部遮罩。为了增加锐利度使用Power节点。将Smoothstep的输出连接到Power的In将_BlendSharpness属性连接到Power的Exp。输出为最终的TopMask。底部遮罩复制顶部遮罩的计算分支。在点积之后插入一个Multiply节点乘以-1将点积结果反转。后续的Smoothstep和Power参数与顶部遮罩保持一致。输出为BottomMask。侧面遮罩使用Add节点将TopMask和BottomMask相加。使用Saturate节点钳制其值在0-1。使用One Minus节点即1 - x计算SideMask 1 - Saturate(TopMask BottomMask)。4.4 生成UV与纹理采样分解世界坐标使用Split节点将World Position的X, Y, Z分量拆开。构建UV顶部UV使用Vector2节点输入(X, Z)。连接一个Divide节点除以_TextureScale。底部UV使用Vector2节点输入(X, Z)。可以通过一个Multiply节点对其中一个分量乘以-1来实现翻转再除以_TextureScale。侧面UV区分X/Z使用Absolute节点分别获取世界法线的X和Z分量的绝对值。使用Subtract节点计算Abs(Normal.X) - Abs(Normal.Z)。使用Smoothstep节点Edge1-0.1,Edge20.1输入上面的差值得到一个混合因子BlendFactor。构建侧面X投影UVZY平面Vector2(Z, Y) / _TextureScale。构建侧面Z投影UVXY平面Vector2(X, Y) / _TextureScale。使用Lerp节点以BlendFactor为权重在SideUV_Z和SideUV_X之间插值得到最终的SideUV。纹理采样创建三个Sample Texture 2D节点。将_BaseMap属性连接到它们的Texture输入。分别将UV_Top,UV_Side,UV_Bottom连接到各自的UV输入口。得到SampleTop,SampleSide,SampleBottomRGB输出。4.5 混合与输出第一次混合顶与底使用Lerp节点A连接SampleBottomB连接SampleTopT连接TopMask。输出ColorTopBottom。第二次混合与侧面使用另一个Lerp节点A连接ColorTopBottomB连接SampleSideT连接SideMask。输出FinalColor。连接到主节点将FinalColor连接到Master Stack或Fragment中的Base Color输入。扩展PBR属性用完全相同的三套UV和遮罩逻辑可以采样法线贴图记得在Sample Texture 2D节点上将Type改为Normal、粗糙度贴图等并分别混合然后将结果连接到Normal、Smoothness等相应的输入口。5. 性能优化、常见问题与进阶技巧实现功能只是第一步要让这个材质在实际项目中可用还需要考虑性能和效果优化。5.1 性能考量与优化策略三方向映射材质比标准UV映射消耗更大因为它至少需要三次纹理采样顶、侧、底加上复杂的遮罩计算。在移动端或大场景中需谨慎使用。减少纹理采样次数这是最大的开销。如果顶、侧、底使用同一张纹理可以尝试只采样一次然后通过不同的UV计算来复用。但在Shader Graph或材质蓝图中每个Sample Texture 2D节点通常就是一次采样。无法避免。使用纹理数组或纹理图集如果三个面必须用不同的纹理考虑使用纹理数组。这样可以在一个采样指令中通过索引读取不同纹理效率远高于三次独立采样。但这需要编写自定义HLSL代码在Shader Graph中可以通过Custom Function节点实现。控制使用范围不要在所有物体上都用。仅在地形、大型岩石、建筑基底等明显会受拉伸影响的静态物体上使用。对于小道具、角色等传统UV足矣。简化遮罩计算Smoothstep和Power是消耗。如果视觉上可以接受可以使用更简单的Step函数非0即1来生成硬遮罩或者降低Power的指数。LOD优化为材质设置合理的LOD细节层次在远距离使用简化版本比如只采样一次或降低混合复杂度。5.2 常见问题与排查清单问题现象可能原因排查与解决方案接缝处出现闪烁或Z-fighting不同朝向的面在接缝处深度值计算有微小差异。1. 确保所有投影计算使用的世界位置一致。2. 稍微增加遮罩的过渡区域宽度调大Smoothstep的阈值差。3. 在Unity中可以尝试微调材质的Depth Offset。纹理在斜面上扭曲或不对齐侧面UV的X/Z投影选择逻辑在斜面处混合不当。检查BlendFactor的计算。确保在法线X和Z分量接近时45度斜面混合是平滑的。可以调整Smoothstep的阈值如-0.3, 0.3让过渡更平缓。性能开销巨大在过多物体或高分辨率纹理上使用。应用5.1节的优化策略。使用性能分析工具如UE的Unreal Insights Unity的Profiler定位瓶颈确认是纹理采样还是ALU计算开销大。底部纹理是倒置的底部UV没有进行翻转。在构建UV_Bottom时尝试对其中一个坐标轴取反例如(WorldPos.X, -WorldPos.Z)。哪个轴需要翻转取决于你的纹理内容和美术需求。移动设备上效果很差或崩溃超出了移动端片元着色器的指令数或纹理采样限制。1. 大幅降低纹理分辨率。2. 考虑使用烘焙的Vertex Color或Lightmap来模拟细节替代复杂的实时三向映射。3. 仅在高端机型上开启此特性。与光影、法线贴图结合后效果奇怪法线信息没有进行正确的三向混合。务必对法线贴图也进行完全相同的三向投影和混合。直接混合法线贴图的RGB值是错误的必须将法线从切线空间转换到世界空间混合后再转回切线空间或者直接使用世界空间法线。这是一个高级话题通常使用引擎内置的Triplanar节点会帮你处理好。手动实现时建议查阅“世界空间法线混合”相关文献。5.3 进阶技巧与扩展思路分通道控制缩放为什么不给顶部、侧面、底部设置不同的纹理缩放Tiling呢你可以创建三个TextureScale参数_ScaleTop,_ScaleSide,_ScaleBottom。在生成UV时分别除以不同的缩放值。这样可以让地面砖块大一些墙面砖块小一些更符合视觉比例。基于高度或斜率混合纹理结合顶点世界坐标的Y值高度或法线与上方向的点积斜率可以动态混合不同的纹理。例如在山体上高处混合雪纹理低处混合岩石纹理中间平滑过渡。这可以与三向映射结合实现极其复杂和自然的地表材质。细节纹理叠加在三向映射的基础上可以再叠加一层高频的细节噪声纹理Detail Texture用于打破重复感增加表面丰富度。这层细节纹理可以使用另一套独立缩放的三向映射UV或者简单的屏幕空间UV。面向对象的三向映射有时我们不希望纹理基于世界坐标而是基于物体自身的坐标模型空间。这适用于可旋转、可移动的单个物体如一个巨大的岩石道具。只需将所有的“世界位置”和“世界法线”替换为“模型位置”和“模型法线”即可。这样纹理会随着物体一起旋转适用于道具而非地形。实现一个鲁棒的三方向映射材质是理解着色器中空间变换和纹理投影的绝佳练习。它从“为什么”出发解决了美术制作中的一个实际痛点并将图形学理论转化为了可视化的节点连线。无论是UE还是Unity其核心思想都是相通的。掌握它你就能为你的场景创造出不受模型拓扑限制、始终保持细节的高质量材质告别恼人的地形拉伸。