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节点),将这个连续的点积值,转化为三个近似二进制的遮罩(Mask):TopMask, 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.4,Edge1为0.6。将点积的结果输入Value。这样,当法线朝上(点积>0.6)时,输出为1;完全水平(点积=0.5)时输出约为0.5;朝下(点积<0.4)时输出为0。这个输出就是我们的TopMask。为了得到更锐利的遮罩,可以后续加一个Power节点,例如Power(TopMask, 10),让中间过渡区域更窄。
- 世界空间上方向向量在UE中是
计算底部遮罩(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是线性插值。
- 计算法线向量X和Z分量的绝对值:
纹理采样:创建三个
Texture Sample节点(或者使用同一个纹理对象),分别将UV_Top,UV_Side,UV_Bottom连接到它们的UV输入口。假设我们采样同一张基础颜色贴图BaseColor,得到SampleTop,SampleSide,SampleBottom。
3.3 混合与最终输出
现在我们有三个颜色采样结果和三个遮罩权重。
初步混合:首先混合顶部和底部。
Color_TopBottom = Lerp(SampleBottom, SampleTop, TopMask)。这里用TopMask作为混合权重,因为当TopMask=1时完全显示顶部,TopMask=0时完全显示底部,中间过渡区域由两者混合。注意,在顶部和底部区域,SideMask为0,这个混合是有效的。与侧面混合:将上一步的结果与侧面采样结果混合。
FinalColor = Lerp(Color_TopBottom, SampleSide, SideMask)。这里用SideMask作为权重。当SideMask=1时(纯侧面),显示SampleSide;当SideMask=0时(顶或底),显示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节点。将点积结果输入In,Edge1和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,Edge2=0.1,输入上面的差值,得到一个混合因子BlendFactor。 - 构建侧面X投影UV(ZY平面):
Vector2(Z, Y) / _TextureScale。 - 构建侧面Z投影UV(XY平面):
Vector2(X, Y) / _TextureScale。 - 使用
Lerp节点,以BlendFactor为权重,在SideUV_Z和SideUV_X之间插值,得到最终的SideUV。
- 使用
- 顶部UV:使用
- 纹理采样:
- 创建三个
Sample Texture 2D节点。 - 将
_BaseMap属性连接到它们的Texture输入。 - 分别将
UV_Top,UV_Side,UV_Bottom连接到各自的UV输入口。 - 得到
SampleTop,SampleSide,SampleBottom(RGB输出)。
- 创建三个
4.5 混合与输出
- 第一次混合(顶与底):使用
Lerp节点,A连接SampleBottom,B连接SampleTop,T连接TopMask。输出ColorTopBottom。 - 第二次混合(与侧面):使用另一个
Lerp节点,A连接ColorTopBottom,B连接SampleSide,T连接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,其核心思想都是相通的。掌握它,你就能为你的场景创造出不受模型拓扑限制、始终保持细节的高质量材质,告别恼人的地形拉伸。