ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Shader顶点数据解析:Vertex TexCoord与UV映射的实战排查指南

Shader顶点数据解析:Vertex TexCoord与UV映射的实战排查指南 在Unity的Shader世界里大多数人会先盯着MainTex、_Color这些看起来“一眼就知道干嘛”的属性但真正让你在材质调试时抓狂的往往是像Vertex TexCoord这种看起来不起眼、却处处决定成败的顶点数据。尤其是当你发现一个模型贴图是歪的、被拉花的、或者采样出来的图案连着反了多半问题就出在TexCoord上。这篇文章我会直接用自己实操过的项目视角聊从v.texcoord到底存了什么到它在顶点着色器里怎么流转再到多套UV、顶点动画、光照贴图这些实际场景里怎么用最后把常见的UV错乱、Tiling Offset和平台差异排查经验一并放出来。适合刚学Shader基础、正在做地表贴图或者被UV问题折磨过的Unity开发者看完之后相信你拿到任何一个模型都能快速判断它的TexCoord藏了什么问题。1. Vertex TexCoordShader 里不起眼却处处使用的“位置信息”先纠正一个容易被忽略的概念当我们说顶点信息时通常第一反应是POSITION也就是位置。但其实一个Mesh顶点数据里除了位置、法线还有一个非常核心的字段UV在Shader语义中它叫作TEXCOORD0也就是标题里的Vertex TexCoord。它存的是这个顶点在纹理平面上面对应的坐标范围一般是0到1之间普通模型但也可能是超过这个区间的平铺值。1.1 它到底是什么顶点坐标的另一个面孔可以把一个Mesh理解成一张网每个交叉点是顶点。位置坐标Position告诉你这个顶点在三维空间里放在哪而TexCoord告诉你如果我要给这张网蒙一层印花纹理那么印花的哪一格要落在你这个点上。说得再直白一点位置是给模型摆造型用的TexCoord是给模型“穿衣服”时的定位点。一张2D贴图不管画得多好最终要对应到三角形的每个顶点上靠的就是每个顶点存的两个浮点数u和v。这两个数决定纹理横向纵向的位置于是模型表面每个三角片就能裁剪出对应的一段贴图区域最后经过光栅化和插值像素级的颜色就落到了屏幕上的像素。理解它的第一层关键是TexCoord是一种数值属性不像POSITION那样被所有人关注但它直接影响你是否能正确地对模型采样纹理。一个模型如果有10000个顶点那正常来说它也得有10000个TexCoord值如果有多个UV则可能是数万。如果一个Mesh没有TexCoord在Standard Shader里跑起来往往表现为材质完全显示为纯色或者发黑而不是显示一张图。1.2 为什么需要单独传一套坐标纹理映射的意义为什么不直接用Unity世界坐标或者模型局部坐标来采样贴图想一想不同模型形状差异极大同一个平面坐标(0, 0)到(1, 1)映射到不同模型表面形状千差万别。如果你用局部坐标直接采样当一个模型被拉伸缩放贴图也会跟着扭曲。TexCoord的本质相当于把“贴图的印版”和“模型的位置”解耦开让美术可以在建模软件里自由地规划一张图该怎么覆盖模型就像做衣服前先画好裁片版再缝到衣服上。也就是说TexCoord是“独立于模型的拓扑结构”却与“模型的表面分割方式”强相关。我认为理解这一点比背一堆API名称更重要。这也是为什么同一个模型哪怕改个局部坐标最后UV可能完全不变因为UV是预先烘焙在Mesh数据里的。实际项目里最常见的例子地形的草和岩石贴图。一套地形Mesh本身顶点很少比如几百个顶点但依赖TexCoord在平面上仔细分布才能让整片地形看起来像不同材质混合的结果。而如果你把Mesh的TexCoord丢失了那材质就像一块布没有裁片完全没法定制和拼接。2. 从顶点数据到片元TexCoord 是怎么被传递和使用的Shader里接收TexCoord按我自己的习惯八成是体现在顶点着色器的输入结构和输出结构里。Unity的Surface Shader虽然自动处理了很多东西但你一旦开始写顶点片元Shader就必须自己把v.texcoord从顶点阶段传到片元阶段。整个过程并不复杂但这一步颠倒或者漏掉后果非常严重。2.1 固定语义TEXCOORD0 与相关语义在HLSL/CG的语义体系里TEXCOORD0不是随便取的变量名而是绑定到Mesh第一套UV的语义。如果你写float2 uv : TEXCOORD0那么顶点着色器就能自动读取该模型mesh.uv对应的数据。相应地如果要读第二套UV就要用float4 uv2 : TEXCOORD1通常存的是lightmap UV或第二套可定制UV。这里有个容易被初学者误会的点TEXCOORDn在顶点着色器的输入里是“读取第n1套UV”的意思但在顶点着色器的输出里也就是v2f结构里它只是普通的“插值寄存器”标记你完全可以拿它来传递任何你想传到片元阶段的值比如颜色、法线、世界坐标方向等只要你保证类型匹配。很多人把TEXCOORDn单纯理解成“UV专用通道”其实不完全准确。我自己写Shader时已经习惯把v2f结构里的那些变量都尽量明确语义该当UV用就当UV用该当法线传递就当法线传递。这其实是Unity Shader中的一个核心玩法顶点着色器负责把需要插值的数据打包扔给片元至于这些数据是什么语义取决于你自己代码里怎么给它赋值。2.2 写入 vs. 读取为什么要用 2D 变量大部分情况下UV就是二维坐标所以用float2就够了。但有些时候你会看到float4 uv : TEXCOORD0这是怎么回事很简单我们还可以在同一通道里塞下更多信息。比如Unity内置的appdata_full里float4 texcoord的z和w在特定管线里可能被用于深度或者辅助数据但在常规使用中只记得uv.x和uv.y。关键点是你从v.vertex拿位置从v.texcoord拿UV坐标然后自己赋值到输出结构里的float2 uv : TEXCOORD0。写Shader时最常犯的一个错误就是只写顶点位置却不把o.uv v.texcoord.xy传到片元里结果就是片元着色器的i.uv是undefined或者全0采样出来永远是贴图的左下角那一个像素看起来像一块纯色。2.3 多套UVTEXCOORD1... 和 Mesh.DataUnity Mesh里可以同时存放多套UV比如uv、uv2、uv3、uv4在Shader端对应TEXCOORD0到TEXCOORD3。美术常用的做法是第一套UV存纹理贴图坐标第二套UV存光照贴图坐标Lightmap UV此外还有第三套可能存地形混合用的权重、网格细节ID等。实际项目里我接手过一个地编项目地形表面需要四张贴图混合草、岩石、泥土、雪地光靠法线判断会出现边缘锯齿最后我们就用了第二套UV按顶点位置预烘焙了一张权重图在Shader里给定不同贴图的采样坐标。绕开这套UV整个融合效果没法实现。另外如果要实现“大世界纹理平铺”往往会用大量重复的Noise采样而重复采样的坐标也必须依赖一套连续UV否则不同mesh之间会接缝明显。至于怎么从脚本层面传递这多套UV其实也不难只需要给Mesh.uv、Mesh.uv2赋值即可。但这个操作和Shader语义是绑定的如果在Shader里声明了TEXCOORD1采样但Mesh压根没有uv2结果就会出错。所以你的Shader不仅要写对语义还要保证输入的Mesh有对应的UV数据存在。3. 我常用的几种 TexCoord 实战场景与代码拆解讲完原理下面直接上些能跑通的例子。我挑了自己在项目里验证过的三种场景基础贴图采样、UV动画、以及用UV来做顶点位置的微调。3.1 基础纹理采样让模型“穿衣服”最简单但最关键的Shader代码我可以直接给一段CG形式的Shader Custom/SimpleTexture { Properties { _MainTex (Texture, 2D) white {} } SubShader { Tags { RenderTypeOpaque } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); return col; } ENDCG } } }这里最值得说的是TRANSFORM_TEX宏。它做的事很简单先把_MainTex_ST.xy也就是Tiling乘到uv上再把_MainTex_ST.zwOffset加到结果上。很多人不理解为什么不能直接用模型uv偏要用宏处理一下原因很简单Unity材质面板上Inspector里的Tiling和OffsetShader里如果不读取这两组数值材质面板改了也不会生效。而_MainTex_ST这个变量就是Unity自动注入的专门用来把面板上的Tiling和Offset传给Shader。所以这不仅是“拿到UV”这么简单更要考虑“面板控制”。在写自己的工具Shader时如果没引入_MainTex_ST上头美术调平铺参数完全无效这就非常容易造成误会。3.2 用TexCoord做顶点动画UV不只用来采样纹理它本身和顶点位置是平行的属性所以还可以拿来驱动顶点位置。我做过一个“水面波动”效果平面网格的顶点高度完全靠v.uv.x * v.uv.y结合正弦函数来扰动。代码片段float4 vert (appdata_full v) : SV_POSITION { // 把uv的x或者y当做一个输入参数参与顶点位移计算 float wave sin(v.uv.x * 50.0 _Time.y * 3.0) * 0.1; v.vertex.y wave; return UnityObjectToClipPos(v.vertex); }用UV驱动顶点动画的好处是每个顶点的uv是固定的所以即使模型旋转、移动uv仍保持顶点自身的分布在做布料飘动时比较可控缺点是uv坐标和模型的空间位置并不总有直观对应关系如果美术建模时uv是散乱的用它做动画会得到一种不规则的抖动视觉上反而很有意思比如做波动旗帜可以故意乱烘培uv。这种用法算是对“顶点数据并行”思路的延展Shader不能只盯着位置这一路数据法线、切线、UV、颜色都能作为输入参与计算。只要你想明白每个顶点信息表达的是什么几乎都能组合出不同的表现。3.3 世界空间还是局部空间TexCoord 和世界坐标怎么配合很多效果需要在片元里拿世界坐标做计算这时常会绕开TexCoord。但如果想制作“近处清晰远处模糊”的“距离混合”就得同时采样世界坐标和顶点世界位置而UV往往只是辅助。最常见的组合是主贴图用第一套UV采样细节贴图用第二套UV采样最后混合权重参考顶点世界坐标的Y值或法线。我在地形混合里一个常用的做法float2 worldUv posXZ.xy * 0.02; // 世界坐标转平面UV float2 detailUv i.uv2; // 模型自带的第二套UV half4 detailCol tex2D(_DetailTex, worldUv * _DetailTiling);这种方案的几个优势是模型移动时世界坐标的UV是连续对接的不同mesh之间不会出现错位接缝而如果用模型自带UV跨mesh时接缝就会明显。当然缺点也有如果模型重叠放置就会出现花纹重复紧密这时还得依靠材质实例的Scale、Offset个性化调整。所以TexCoord不是唯一选择但它在单个mesh内部的“保真度”和操控性是任何世界坐标方案替代不了的。两者的配合会成为你做地表或大墙体贴图最有效的组合拳。4. 万字级排查UV错乱、平铺镜像、不同平台差异在实际项目里我看到很多初学Shader的同学因为UV问题反复改代码最后发现根本不是代码的事而是模型本身或者导入设置的问题。这里分享我踩过最多的一批坑按优先级排序。4.1 UV错乱/拧麻花常见现象模型渲染出来贴图扭曲、撕裂像是贴图被揉成一团。大部分情况下不是Shader写错而是模型本身在第一套UV上就丢数据了。可以先检查Mesh导入面板的Generate Lightmap UVs以及原始FBX里的UV通道数量。另一个典型原因是“顶点合并”导致UV拉伸特别在边缘转折的硬交界处建模软件如果没正确split edges就会让UV跨越一个巨大的角度到另一个面片上。我自己的排查路径是编辑器里用MeshUtility工具读取mesh.uv的边界值如果uv范围超出了0~1一大截不是平铺就是导入设置有问题要么就是确实用了大范围连续UV直接在Asset Store或自己写个小调试Shader把uv的x、y分别映射成红、绿颜色输出一眼就能看出UV分布是否均匀。把uv转成颜色输出的调试Shader我强烈建议每个Shader开发都存一份。它比肉眼看贴图快得多能直达病灶。4.2 平铺和镜像还有一个高频坑材质面板设置的Tiling对不上模型表面的实际像素密度。比如砖墙的砖块尺寸在模型里是0.5m但Tiling设的是1结果一块砖变成了4米长看起来像怪物。这里的“反转”背后其实就是每套UV数值应该对应模型表面的物理尺寸。你得先搞清楚模型建模时一单位对应多少米美术如果没有严格按照真实世界尺寸后续用Tiling计算就全乱了。镜像问题也很经典贴图呈现左右反了这通常意味着UV坐标在水平方向是1-u而不是u。原因是美术在建模时或导入时把UV旋转了180度。如果Shader里想强行修可以对uv再做一次映射但最正规的做法还是回建模软件里整理UV。临时改Shader修UV映射往往会在后续其他模型上引入新的问题。另外Unity里texture wrap mode这个Texture Asset设置不多提了之前我遇到过把一个纹理调成了Clamp模式表面硬生生露出一条边缘色带。当时我花了很长时间怀疑是uv精度问题其实只是Wrap Mode设置错了。这个经验很肉痛。4.3 平台差异与精度为什么同样一段ShaderPC上正常手机上一片花很多会影响片元的浮点精度。Unity Shader里half2 uv在移动端可能只有16位浮点的精度当uv数值比较大比如超过1024的平铺纹理就会出现锯齿或颜色抖动。解决办法顶点着色器里使用float2存储uv必要时甚至用float4片元里如果要频繁计算插值记得保留float的精度不要盲目都用half对于超大范围贴图考虑把uv先做一个中心偏移缩小整体幅值比如uv - 0.5这样浮点误差对相对值的影响就变低了。平台的Shader编译优化还可能改变浮点行为这本身不算Shader逻辑错误但排查很痛苦。我一般会先在编辑器里跑一遍再在真机对比同一模型和材质的输出确认是精度影响还是代码逻辑影响。4.4 常见考证/踩坑记录这里整理几个我在团队里被问过、也被面试官问过的问题当做速查问题原因/现象解决方案为什么我Shader里没有texcoord材质全黑模型Mesh缺少UV数据检查Mesh导入设置或模型是否有UV或者改用不依赖UV的Shader为什么第二套UV采出来是黑的mesh.uv2没有被赋值检查Mesh赋值的第二套UV或者地形上LightmapShadow插件不支持为什么一个模型用了多个材质但贴图全是同一个位置模型的Material引用同一个meshUV却一样检查Mesh是否有独立的多通道UV或群里脚本动态SetUV为什么Tiling调了没反应在Shader里没有使用_MainTex_ST宏强制在顶点着色器用TRANSFORM_TEX结合属性计算为什么uv转颜色后显示一片白uv数值普遍大于1在移动端half精度下归一化过度手动除以平铺倍数或只显示小数部分frac来做可视化有一条通用的铁律不管问题看似多复杂先把uv可视化再看顶点结构最后才怀疑代码。三个步骤能消灭80%的问题。比如你在场景里看到一片黄的贴图很可能是uv范围整体过大导致纹理缩小成一团你直接看uv的红色通道就能明白。5. 我对TexCoord这个统一入口的实操体会聊到这我猜你已经大致清楚Vertex TexCoord在Shader里不是“多此一举”的细节了。我个人在实际项目里的体会是当你把Shader定位成“给模型穿衣服、给现象描边、给后期上色”时TexCoord就是连接纹理世界和模型世界的桥梁。它既简单又极具欺骗性很多人都只顾着写采样函数却没有想过UV怎么来的、怎么流的。到现在我接手别人的Shader第一个动作永远是看他的顶点输入结构里有没有TEXCOORD第二个动作是看输出的v2f里怎么处理这部分数据。如果一帧画面出错了我不会急着改纹理、调Alpha而是先在脑子里把模型、UV、Shader三者串成一条线很快就能定位到是模型的UV本身不对还是Shader直接忽略了它。最后再分享一个小技巧如果你需要给一个低模写一个“脚本化”的贴图效果不必纠结模型本身有没有UV可以直接用脚本临时生成一套Spherical UV或者Planar UV赋值给Mesh.uv。这会让你绕开一堆美术建模的心智负担快速验证自己写出来的效果。哪怕未来重建模型只要通道对上了Shader表现也基本能保持一致。总之UV这套“隐藏坐标”值得你多花点时间琢磨。
返回列表