ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity Shader Vertex TexCoord深度解析:UV原理、传递与实用避坑

Unity Shader Vertex TexCoord深度解析:UV原理、传递与实用避坑 写Unity Shader这些年要说哪个变量最不起眼却最容易出事我一定投Vertex TexCoord一票。它不像世界坐标那么醒目也不像法线那样拧一下马上看得出来但贴图歪了、法线颠倒、水面流动方向反了折腾半天查到最后往往都绕回同一个问题纹理坐标。这篇文章就把Vertex TexCoord彻底捋一遍从它到底是什么、怎么从模型一路传到片元着色器到拿它能做哪些实用效果再到最常见的坑和排查方法争取让大家少走我当年走过的那段弯路。如果你正准备写第一个自定义Shader或者已经写了几个但始终对UV这套逻辑处于“半懂”状态这篇内容应该对你有用。我尽量用大白话讲原理实操部分的代码也尽量写得可以直接抄。1. Vertex TexCoord是什么先搞清楚纹理坐标从哪来1.1 一句话理解UVVertex TexCoord在Unity Shader里最常见的形态是一个float2一般命名为uv或texcoord语义绑定是TEXCOORD0。第一个分量叫U对应纹理的水平方向第二个分量叫V对应纹理的垂直方向。常规取值落在[0,1]区间内(0,0)在纹理左下角(1,1)在纹理右上角。GPU把顶点送到屏幕之后片元着色器就是靠这个坐标到纹理里把对应位置的颜色捞出来的。我常用一个比喻解释UV把贴图想象成包装纸3D模型是一块被包装纸包住的糖。UV就是包装纸上印好的折痕和定位点每个顶点都通过这一组坐标被告知“该贴在包装纸的哪个位置”。顶点本身只有三维位置信息图案信息是UV帮它和模型建立起关联的。没了这层关联模型就是一具灰模纹理长得再漂亮也无处安放。1.2 UV是建模软件里烙下的印记很多刚接触Shader的朋友会以为UV是Unity自动生成的其实不是。UV在建模阶段就已经定下来了。你在Blender、3ds Max或者Maya里给模型展UV本质上就是在一张二维平面上把模型表面摊平记录每个顶点摊平后的坐标。这个数据会作为网格的一部分写进FBX等格式导入Unity之后挂在Mesh资产上。所以遇到“贴图怎么换都歪”的情况大概率是模型本身的UV有问题展UV的时候产生了非均匀拉伸、重叠或者共享顶点没有对齐。Shader层面对这类问题的处理能力有限我们能做的是接收UV数据、正确利用它。想从根本上解决得回到建模软件里重新展UV或者让美术重新导出。这不是Shader的锅但你在Shader里第一个背锅的往往就是它。1.3 为什么代码里见到的是TEXCOORD0而不是uvUnity Shader用语义Semantic来标识数据用途。位置用POSITION法线用NORMALUV则用TEXCOORDn。TEXCOORD后面的数字表示第几套UVTEXCOORD0对应Mesh的第一套UV也就是Unity脚本里的mesh.uvTEXCOORD1对应第二套也就是mesh.uv2以此类推。这个命名只是图形API历史习惯的延续。UV本身是Texture Coordinate的缩写而Vertex TexCoord是更严谨的全称叫法。写顶点着色器输入结构的时候你声明float2 uv : TEXCOORD0GPU就会自动把模型第一套UV数据填进来。这里有一个容易忽视的点TEXCOORDn只是语义标签变量名叫什么完全无所谓叫texcoord、叫uv、叫foo都可以真正的通道选择由冒号后面的语义决定。2. TexCoord在Shader里的完整传递路径从顶点到像素2.1 顶点着色器里拿到的UV先看一段最基础的Vertex/Fragment Shader骨架struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); return col; }注意appdata里那个float2 uv : TEXCOORD0就是网格给顶点着色器的原始UV。在顶点阶段它只存在于顶点上。一个三角面有三个顶点于是这个三角形携带三份UV。顶点着色器要做的事很直白把这份UV放进v2f结构体作为从顶点阶段流向片元阶段的中间数据传出去。如果结构体里写的是float2 uv : TEXCOORD1取到的就是第二套UV。很多需要光照贴图或额外顶点数据的自定义Shader就是靠切换这个语义来完成数据源切换的。提示这里最常见的问题是v2f里的语义和appdata里的语义互相混用。appdata决定“从网格取哪套UV”v2f决定“往片元阶段传哪套UV”两者可以不同。比如你可以把模型的第一套UV取进来做了一些 UV 运算后再用 TEXCOORD1语义传出去完全合法。但很多初学者把语义看反了结果片元里拿到了一串莫名其妙的数据排查半天才发现是语义写错了。2.2 光栅化的UV插值透视校正那件事顶点着色器输出UV之后三角形进入光栅化阶段。GPU会对三角形内部所有被覆盖到的像素做UV插值。这意味着顶点上只有三个离散的UV值但到了片元阶段每个像素都拿着一个根据像素在三角形内位置比例计算出来的、平滑过渡的UV值。早期3D硬件用一种相对简单的仿射插值没有考虑顶点深度的影响所以比较大的平面稍微侧一点看纹理就会在屏幕上扭曲抖动。玩过初代PlayStation的朋友应该对那种地板上扭来扭去的贴图有印象。现代图形API对带TEXCOORD语义的插值默认做透视校正插值插值过程中会考虑w分量于是屏幕空间里的纹理能保持稳定的透视效果。这个机制对我们写Shader来说是透明的但理解它有一个实际意义片元阶段的UV是“插值后的UV”不是某个顶点的原始精确值。你可以把它当作平滑连续的二维坐标去用比如做渐变、做边缘、做径向遮罩这些操作依赖的就是插值后的连续性。反过来如果哪天你想在片元阶段拿到某个顶点的精确原始UV光靠普通的插值数据是做不到的那就得另想方案了。2.3 Surface Shader里那套uv_前缀约定Surface Shader把大量繁琐细节藏起来了。你需要拿纹理UV时只要在Input结构体里写一个带uv_前缀的成员就行struct Input { float2 uv_MainTex; }; void surf(Input IN, inout SurfaceOutputStandard o) { o.Albedo tex2D(_MainTex, IN.uv_MainTex).rgb; }约定是成员名为“uv_”加纹理变量去掉前导下划线后的名字。比如纹理叫_DetailTex对应成员就是float2 uv_DetailTex。第二套UV写float2 uv2_XXX道理一样。这套命名由Surface Shader编译器自动识别写错拼写不会报错但那个变量会一直是0。排查时第一件事就是核对命名我因为这个吃过不止一次亏。Surface Shader和手写Vertex/Fragment Shader在这里有个重要区别编译器会在生成的顶点程序里自动把_MainTex_ST的Tiling和Offset应用到Input的uv_MainTex上。所以你直接用IN.uv_MainTex去tex2D材质面板上的Tiling、Offset是生效的。而手写的Vertex/Fragment Shader不会帮你做这一步必须手动调用TRANSFORM_TEX这个细节我放到下一节详细说。3. Vertex TexCoord的典型应用场景3.1 通用纹理采样与TRANSFORM_TEX宏如果只是原样采样UV不需要任何处理tex2D(_MainTex, i.uv)直接完事。但绝大多数材质都会用到Tiling和Offset。这两个数值在Shader侧对应一个float4参数命名规则是纹理名加_ST后缀。主纹理_MainTex对应_MainTex_STxy分量是Tiling的x和yzw分量是Offset的x和y。Unity在UnityCG.cginc里提供了TRANSFORM_TEX宏展开之后等价于这样一行o.uv i.uv * _MainTex_ST.xy _MainTex_ST.zw;实际用法是o.uv TRANSFORM_TEX(v.uv, _MainTex);这个宏做的事情是先缩放UV再加偏移。顺序很重要一定是先乘Tiling后加Offset。如果你手写时把顺序写反效果就是贴图整体跟着Tiling乱跑非常难看。另一个常见错误是采样多张纹理时全部用了_MainTex_ST导致副纹理的Tiling永远和主纹理绑在一起怎么调都无效。每张要支持Tiling/Offset的纹理都要声明自己的_ST逐一转换各自的UV。3.2 法线贴图UV和切线空间是绑定的法线贴图的采样同样依赖UV但它不是简单采样一个颜色就完事。法线贴图里存储的是切线空间下的法线偏移采样出来后还要从切线空间变换到世界空间才能参与光照计算。这个变换需要用到顶点的切线Tangent、副切线Bitangent和法线Normal而切线和副切线的计算方向跟UV的走向是强相关的。这就是为什么UV方向错误会导致法线贴图看起来左右颠倒或者光照诡异。比如一个角色的贴图在建模软件里展UV时不小心把某一侧UV水平翻转了那么在Shader里使用标准的UnpackNormal和内置的WorldNormalVector都救不回来因为切线空间本身已经歪了。遇到这类现象优先检查网格的UV方向而不是反复调Shader代码。很多情况下你把normal map采样代码删掉、换成纯色光照立刻正常那就基本可以锁定是UV/切线问题。3.3 第二套UVLightmap与自定义数据通道光照贴图Lightmap是烘焙全局光照最重要的数据载体之一。烘焙时Unity会把模型表面的UV展开成第二套UV专门用于对齐光照贴图。在自定义Shader里如果想采样光照贴图需要在顶点输入里声明TEXCOORD1语义并使用Unity提供的一系列宏比如UNITY_LIGHTMAP_COORDS和UNITY_TRANSFER_LIGHTMAP来处理不同平台的差异。第二套UV也不只用于光照贴图。很多游戏的地形、草、植被Shader会把第二套UV塞各种自定义数据密度分布、随机种子、细节贴图混合权重等等。美术侧在DCC软件里多展一套UV程序员在Shader里就多了一个自由度极高的数据通道。这也是为什么理解TEXCOORD0和TEXCOORD1的区别如此重要——它直接决定了你能不能拿到这些额外数据拿错了通道数据就全乱了。3.4 多纹理的_ST别串号一个材质只要用超过一张带Tiling/Offset的纹理就容易出现_ST串号问题。我接过一个项目主纹理正常细节纹理的Tiling死活不生效最后发现Shader里声明了_DetailTex_ST但顶点函数里写的是TRANSFORM_TEX(v.uv, _MainTex)。代码和意图不一致编译器没法报错只能人眼一行行盯。实操心得我写多纹理Shader时有个习惯变量名里带用途后缀比如_MainTex_ST、_DetailTex_ST、_NoiseTex_ST。复制粘贴改一处漏一处的概率能低不少。另外不打算支持Tiling/Offset的纹理可以不声明_ST采样时直接用原始UV减少出错面。4. 实操几个基于Vertex TexCoord的常用效果4.1 水面流动UV随时间偏移最直接好玩的应用就是UV动画。水面、岩浆、能量流这类效果本质上都是让UV随时间平移再配合噪声贴图扰动。核心代码就几行o.uv TRANSFORM_TEX(v.uv, _MainTex); o.uv.x _Time.y * _SpeedX; o.uv.y _Time.y * _SpeedY;然后片元阶段正常采样即可。这里有件事必须注意UV超出[0,1]之后会发生什么完全取决于纹理的Wrap Mode。Repeat模式会把UV折回[0,1]再采样配合可平铺纹理做无限流动没问题Clamp模式会采样边缘像素流动效果会变成贴图边缘被无限拉伸。做UV动画前先确认纹理导入设置里Wrap Mode是Repeat。如果想更自然一点可以采样两张不同速度、不同方向的噪声图再叠加或者用一张噪声图去偏移主纹理的UVfloat2 uv i.uv; uv (tex2D(_NoiseTex, i.uv * 1.5 _Time.y * 0.1).rg - 0.5) * _Distortion; fixed4 col tex2D(_MainTex, uv);这种做法在水面、热浪、能量护盾上很常见核心思想就是“用噪声去扰动UV再拿扰动后的UV去采样目标纹理”。理解这一点比记住某段特效代码有用得多。4.2 技能范围指示器纯UV数学画圆形很多游戏在施法前会在地上显示一个技能范围圈比如一个圆形区域。如果不打算用美术画好的贴图完全可以用UV数学来画。把一片Quad或者地面网格的UV当成一个以(0.5, 0.5)为中心的二维坐标系距离中心越近越亮就能得到一个圆float2 centered i.uv - 0.5; float dist length(centered); float circle 1.0 - smoothstep(_Radius - _Softness, _Radius, dist); o.Albedo _Color * circle;这里面有个细节很多人第一次会踩Quad的UV范围虽然是[0,1]但如果Quad的长宽比不是1:1在UV空间里画出来的圆在屏幕上会变成椭圆。因此实际使用时要根据地面网格的长宽比去修正float2 centered (i.uv - 0.5) * float2(_Aspect, 1.0);_Aspect由网格在世界空间的宽高比决定简单做法是直接用Plane的Scale信息或者传一个手动参数。这类“用UV做遮罩”的思路可以延伸出扇形范围、环形范围、扫描线等一大堆技能指示器效果完全不用额外贴图改起来还快。4.3 用UV驱动顶点位移UV还能作为顶点着色器的输入数据驱动模型变形。比如做一块随波动的草地或旗帜顶点着色器里可以把UV的某一分量当作相位来源float wave sin(v.uv.x * _Frequency _Time.y) * v.uv.y; v.vertex.y wave * _Amplitude;这段代码让每个顶点根据自己水平方向的UV位置做出正弦波动同时用v.uv.y做幅度衰减越靠近根部摆动越小。相比用世界坐标做驱动UV驱动的好处是变形会跟着模型的表面展开方向走模型哪怕被缩放、旋转过效果依然相对稳定。但请记住前置条件网格必须有足够密的顶点。三角形内部再怎么插值顶点数量不够什么效果都出不来。一个只有4个顶点的Plane无论如何也做不了波浪。4.4 双面材质的背面镜像问题做双面渲染时很多人会把Cull Off打开让背面也能看到。这个时候你会发现背面看到的贴图是左右镜像的圆形技能圈的渐变方向也是反的。原因是双面渲染时背面光栅化后用的是同一套UV。解决办法是在片元阶段判断面的朝向然后把U轴翻转。Built-in管线里可以这样写fixed4 frag(v2f i, fixed facing : VFACE) : SV_Target { float2 uv i.uv; uv.x lerp(uv.x, 1.0 - uv.x, facing 0); // 后续统一用 uv 采样 }Surface Shader里则在Input结构体里声明float facing : VFACEsurf函数里根据facing的正负做同样处理。这个技巧在做双面植被、透明遮罩、双面UI时非常常用属于那种“不知道的人折腾半天知道的人一行代码”的典型场景。5. 常见问题与排查实录5.1 贴图拉伸、模糊或者定位不对先分清是“UV数据错了”还是“采样代码错了”。最快的定位方法是把UV直接输出成颜色看把片元返回值设成fixed4(i.uv.x, i.uv.y, 0, 1)然后运行模型表面应该出现从左下角黑色到右上角红绿混合的平滑渐变。如果渐变分布奇怪某片区域颜色骤变或者有硬边基本可以断定是模型UV本身的问题如果渐变平滑但贴图仍然错乱问题多半出在TRANSFORM_TEX或者纹理导入设置。这个“输出UV当颜色”的技巧可以算得上Shader调试图里最便宜的一招。不需要插桩不需要调试器几秒钟就能看到UV的分布情况。很多片元Shader的疑难杂症一看UV渐变图就有思路了。我强烈建议把它刻进肌肉记忆里。5.2 UV超出[0,1]之后出现的“鬼影”UV动画做得正开心结果纹理边缘冒出一堆重复的条纹或者颜色在边缘处突然变得很奇怪。这种情况几乎都是Wrap Mode没设对。Repeat会平铺Clamp会拉伸边缘。另外如果你采样了带mipmap的纹理且屏幕上不同像素的UV分布差异很大远处会出现明显的纹理闪烁。此时要检查纹理导入设置里的mipmap选项必要时调一下Aniso Level。很多时候“远处闪得厉害”并不是Shader写错了而是导入设置没有针对场景调优。5.3 TEXCOORD1数据永远是空的自定义Shader声明了float2 uv : TEXCOORD1但采样出来的数据一直是0或者跟TEXCOORD0一模一样。原因大概率是模型压根没有第二套UV。Unity的Mesh资产默认只有uv这一套除非美术在DCC软件里展开过第二套或者你用脚本调用mesh.uv2手动赋值。这个坑特别容易出现在纯程序生成的网格上比如动态生成的地形、广告牌、粒子网格。排查方法很简单在Project窗口选中模型看Mesh的导入属性或者写一段Editor脚本打印mesh.uv2.Length是否等于顶点数。如果长度等于0那就别指望Shader能变出第二套UV来。5.4 不同渲染平台的V方向差异Unity Shader内部默认使用OpenGL风格的纹理坐标约定(0,0)在左下角。但某些平台典型的是DirectX风格在渲染到纹理或后处理时V方向可能是反的。Unity内部通过UNITY_UV_STARTS_AT_TOP宏来区分这种情况。平时写普通模型Shader不太会遇到但凡是做RenderTexture采样、图像后处理特效或者做离线渲染相关工具就要警惕画面上下颠倒的问题。处理方式一般是在片元里判断宏#if UNITY_UV_STARTS_AT_TOP uv.y 1.0 - uv.y; #endif这种平台差异不熟的话很容易瞎猜。我的建议是先在编辑器里验证通常是OpenGL风格再打包到目标平台验证一次画面朝向。凡是涉及RenderTexture的Shader都值得在真机上跑一遍再做最终交付。5.5 一张速查表症状优先怀疑对象快速验证方法贴图错乱或拉伸模型UV或Wrap Mode输出UV为颜色观察渐变法线贴图左右颠倒模型切线空间/UV翻转换一个已知正确的模型测试Tiling不生效_ST声明或TRANSFORM_TEX顺序检查Shader源码逐行比对第二套UV采样为0模型没有uv2打印mesh.uv2.LengthUV动画出现条纹Wrap Mode不是Repeat检查纹理导入设置后处理画面上下颠倒平台V方向差异用UNITY_UV_STARTS_AT_TOP处理双面渲染图案镜像背面用了同一套UVVFACE翻转U轴6. 调试习惯与几条扩展思路6.1 我常用的几条UV调试习惯第一所有用到UV的Shader提交前先输出一次UV可视化截图留档。哪怕当时没问题后面调参的时候也有个参照。第二多通道UV的命名一定带上通道号TEXCOORD0、TEXCOORD1、TEXCOORD2在代码里写清楚注释。不然三个月后回来看连自己都分不清哪套是光照、哪套是细节。第三能不用额外纹理做的东西尽量用UV数学生成比如圆环、渐变、扫描线省贴图内存不说调节还快。第四遇到UV相关的问题先在脑子里过一遍“数据从哪来、经过什么变换、最终到哪用”八成问题出在数据源和变换之间的不匹配而不是采样那行代码本身。6.2 再往深走可以研究什么如果你觉得这篇文章里这些已经不够用了下一步可以看这几个方向用脚本给程序化生成的Mesh写UV比如地形LOD、河流路径用多个UV通道做GPU驱动的自定义数据比如顶点密度、随机种子在URP的Shader Graph里对照观察UV节点的数据流还有Compute Shader配合纹理做大规模UV扰动。这些都建立在对Vertex TexCoord的扎实理解之上把这套基础打稳了后面的路会顺很多。Vertex TexCoord就是这么个东西看着不起眼却是纹理和模型之间唯一的桥梁。把它彻底搞明白Unity Shader这条路上很多弯道都能直着过。
返回列表