资讯动态

Unity Shader棋盘格实现:内置管线与URP写法差异对比

发布时间:2026/9/28 22:41:26 来源:尧图企业网站定制
做项目Shader的时候棋盘格这个东西基本绕不开。不管是验证UV对不对、看模型接缝、调相机标定、还是做调试用的测试面棋盘格都是最实用的参考图案之一。我之前在URP管线里写过一个棋盘格Shader过程中顺手跟内置管线Built-in的写法做了对比踩了一些坑也理清了两种管线在Shader编写上的关键差异。这篇就把完整的实现思路、代码、对比结果和避坑经验整理出来。1. 为什么要用Shader画棋盘格而不是用贴图1.1 棋盘格在项目里到底能干什么棋盘格Checkerboard在图形项目里的出现频率远超很多人预期我至少碰到过这几类实际需求调试UV给模型套一层棋盘格通过格子是否变形、是否均匀能快速判断UV展开是否正确。比看网格线直观得多尤其是曲面模型。检查面朝向配合双面渲染Cull Off使用可以快速看出哪些面是反面。相机标定标定板本身就是黑白棋盘格OpenCV的findChessboardCorners就是靠检测格子角点来计算相机内外参的。测试压缩和采样效果棋盘格是高频图案能暴露纹理压缩的块状伪影、各向异性过滤没开、Mipmap切换不连续这类问题。做参考地面或测试平面在AR项目里放一个棋盘格地面用来验证空间映射和透视关系比空白地面直观得多。这些场景里如果每次都去PS里画一张贴图再导入工程、设置Wrap Mode和Filter Mode麻烦不说还要占一张纹理的内存。用Shader程序化生成一个Pass几行数学运算就搞定资源体积是零改格子大小只需要改一个参数。1.2 贴图方案和Shader方案的取舍贴图方案的好处是图案可以任意复杂渐变、污渍、logo都能放。坏处是分辨率固定Model拉近看会糊拉远看会有摩尔纹而且要担心纹理压缩格式对格子边缘的影响。Shader方案正好相反图案必须是规则的数学表达但好处是无限清晰、无限平铺、零资源开销改参数实时生效。棋盘格恰好是规则的数学图案——两个颜色、均匀交替、无限平铺——天生就适合用Shader画。这也是我一直推荐用Shader而不是贴图来处理棋盘格的原因。2. 内置渲染管线的棋盘格Shader实现2.1 核心思路用UV坐标做数学判断棋盘格本质上是把平面的UV区域切分成等大的网格然后让相邻格子交替显示两种颜色。关键点有两个第一把UV值除以格子尺寸并向下取整得到格子索引。第二判断索引的奇偶性并做交替。floor(UV.x * 格子数) floor(UV.y * 格子数)这个和的奇偶性决定颜色的选择和为偶数显示A色和为奇数显示B色。数学表达可以写成float gridX floor(uv.x * _Tiling); float gridY floor(uv.y * _Tiling); float checker fmod(gridX gridY, 2.0);fmod取余数的结果要么是0要么是1拿它做lerp的插值系数就能得到交替的棋盘格。注意这里不能直接用整数取模符号%因为HLSL里%对负数的行为有坑虽然UV理论上不会出现负数但养成用fmod的习惯更稳妥。2.2 内置管线的完整Shader代码内置管线用的是CGPROGRAM/ENDCG块渲染路径走的是老一套的固定光照管线兼容模式。一个最小可用的Unlit棋盘格Shader长这样Shader Custom/CheckerboardBuiltIn { Properties { _ColorA (Color A, Color) (1,1,1,1) _ColorB (Color B, Color) (0,0,0,1) _Tiling (Grid Tiling, Range(1, 64)) 8 } SubShader { Tags { RenderTypeOpaque } 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; }; fixed4 _ColorA; fixed4 _ColorB; float _Tiling; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { float2 uv i.uv * _Tiling; float2 grid floor(uv); float checker fmod(grid.x grid.y, 2.0); return lerp(_ColorA, _ColorB, checker); } ENDCG } } }2.3 关键代码逐个拆解从vert到frag的流程很直接。UnityObjectToClipPos把模型顶点从对象空间变换到裁剪空间这在内置管线里是最常用的顶点变换写法。UV直接原样传给片元着色器真正的计算都集中在frag里。i.uv * _Tiling这一步是核心UV本身范围是0到1乘上_Tiling以后范围变成0到_Tilingfloor取整后grid.x和grid.y的取值范围自然就是0到_Tiling-1每个取值对应一个格子。当_Tiling等于8时模型表面被切成8乘8共64个格子。fmod(grid.x grid.y, 2.0)这一步做的是奇偶判断。如果和的整数部分是偶数返回0显示_ColorA是奇数返回1显示_ColorB。因为相邻格子的grid.x grid.y值恰好总是相差1所以颜色必然交替形成标准的棋盘格。这个Stage后面如果要做抗锯齿可以在颜色跳变的边界上做平滑过渡。先记住基础版本后面第三节我单独讲URP版本时一并给出抗锯齿写法。3. URP下的棋盘格Shader实现3.1 URP的Shader和内置管线到底差在哪先说结论URP的Shader和内置管线的Shader底层渲染API是一套但编写规范差别很大直接复制内置Shader过来用大概率变紫粉红色错误材质。差异集中在三块。第一块是引入文件。内置管线常写#include UnityCG.cgincURP必须写#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl这个路径是URP包内的相对路径依赖Unity Package Manager已经安装了URP包。写错路径、写漏路径Shader就会直接编译失败材质球变成紫红色。第二块是代码块标签。内置管线用CGPROGRAM和ENDCG包裹URP用HLSLPROGRAM和ENDHLSL。之所以要换是因为URP全面转向了HLSL不再支持CG风格的部分老写法。实际上在大多数现代平台上CG本来就是被编译成HLSL再跑但URP定了调子明确要求用HLSLPROGRAM。第三块是顶点变换函数。内置管线的UnityObjectToClipPos(v.vertex)在URP里虽然还保留着Unity做了兼容层但官方推荐的需求是GetVertexPositionInputs这个系列函数它返回的是结构体方便后续取世界坐标、法线方向、裁剪坐标等多个数据。棋盘格这个Shader只需要裁剪坐标用GetVertexPositionInputs(v.vertex.xyz).positionCS就够了。3.2 URP兼容的完整Shader代码下面这个是我在项目里实测可用的URP版本带上了SRP Batcher兼容所需的CBUFFERShader Custom/CheckerboardURP { Properties { _ColorA (Color A, Color) (1,1,1,1) _ColorB (Color B, Color) (0,0,0,1) _Tiling (Grid Tiling, Range(1, 64)) 8 } SubShader { Tags { RenderType Opaque RenderPipeline UniversalPipeline } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; CBUFFER_START(UnityPerMaterial) float4 _ColorA; float4 _ColorB; float _Tiling; CBUFFER_END v2f vert (appdata v) { v2f o; VertexPositionInputs vertexInput GetVertexPositionInputs(v.vertex.xyz); o.vertex vertexInput.positionCS; o.uv v.uv; return o; } half4 frag (v2f i) : SV_Target { float2 uv i.uv * _Tiling; float2 grid floor(uv); float checker fmod(grid.x grid.y, 2.0); return lerp(_ColorA, _ColorB, checker); } ENDHLSL } } }3.3 SRP Batcher与CBUFFER的关键细节URP相比内置管线的一个核心改进是SRP Batcher可编程渲染管线合批器它能在CPU侧把不同材质但相同Shader的物体合批渲染前提是Shader得满足SRP Batcher的兼容条件。兼容条件之一就是所有每材质属性Per-Material Properties必须放在CBUFFER_START(UnityPerMaterial)和CBUFFER_END定义的常量缓冲里。这个CBUFFER名字不能乱改Unity内部是按UnityPerMaterial这个名字来识别和上传的。我把_ColorA、_ColorB、_Tiling都放进去就是为了让SRP Batcher能正常工作。还有个细节容易被忽略CBUFFER里的数据类型。SRP Batcher要求所有属性统一用float4或float不要用half4放在CBUFFER里否则某些平台会有对齐问题。所以片元函数里用的half4返回值没问题但CBUFFER里的颜色我用的是float4。在URP里Tags { RenderPipeline UniversalPipeline }也不能省。这个Tag告诉URP这个SubShader是自己的如果不加URP会警告并尝试用Fallback。反过来讲如果你的项目同时存在URP和内置管线的渲染场景这个Tag能确保URP的Shader不会被内置管线误用。4. 内置管线与URP的完整差异对照4.1 关键差异一览表我把两种管线的写法差异整理成了表格方便移植Shader时对照对比维度内置管线URP代码块CGPROGRAM / ENDCGHLSLPROGRAM / ENDHLSL头文件UnityCG.cgincCore.hlslURP包路径顶点变换UnityObjectToClipPosGetVertexPositionInputs().positionCS属性缓冲声明即用无需CBUFFER必须放入CBUFFER以兼容SRP Batcher数据精度习惯常用fixed/half半精度有风险推荐float为主SubShader Tag无强制要求需加RenderPipelineUniversalPipeline光照函数可直接用Surface Shader或自带光照需Pass显式定义光照模型(如Lit)4.2 光照模型与Pass结构的差异内置管线里要写一个带光照的物体最省事的办法是Surface Shader把光照计算的复杂度交给Unity自己处理一个surf函数里填个o.Albedo就完事了。URP里不存在这套Surface Shader机制所有光照都要自己通过Pass去实现要么手写光照方程要么用URP提供的Lit Shader的Pass。对棋盘格这个需求来说我们做的都是Unlit效果不存在光照计算所以两种管线的差异没有在光照部分放大。但如果你的棋盘格Shader要从Unlit升级成Lit比如想做一个能被灯光照亮的地面棋盘格那URP那边的工程量会明显大于内置管线需要自己处理主光源、阴影、阴影采样等一连串问题。4.3 双面渲染在两种管线下的处理标题后面的热搜里反复出现unity双面材质 shader这其实是棋盘格场景很常见的需求背面也要看得见格子。内置管线和URP处理双面的方式在ShaderLab层面是通用的那就是在SubShader或Pass上写Cull Off默认的Cull Back会剔除背面我们棋盘格通常是画在一个平面上摄像机绕到背面就什么都看不见了。加上Cull Off以后正反面都会渲染。这里有个容易踩的坑双面渲染时如果Shader是带光照的背面的法线方向是反的光照计算会出错背面色会发暗。URP里需要额外处理法线翻转常用的是在片元里用VFACE语义判断正反面然后把法线翻转过来。但棋盘格Shader是Unlit的不涉及光照方向直接Cull Off就完全没问题背面颜色和正面一致。我实测下来URP下加Cull Off后用SRP Batcher合批不会受影响因为Cull是渲染状态不是材质属性不参与合批判断。5. 棋盘格标定板的Shader化应用5.1 标定板为什么需要棋盘格标定板采用棋盘格是有讲究的。黑白格子交替排列每个角点都是四个格子的公共顶点在图像里表现为明显的灰度突变点角点检测算法比如OpenCV的findChessboardCorners能稳定地检测出来。相比之下圆形或方形的ArUco码虽然也行但角点数量、检测稳定性和亚像素定位精度上棋盘格在经典标定场景里仍然有不可替代的位置。Shader画标定板的一个优势是格子尺寸、颜色、排列数量全部参数化不需要重新出图、重新打印。在调试相机的时候把棋盘格Shader挂在场景里的一个Plane上动态调整_Tiling来改变格子大小比反复去换贴图效率高得多。5.2 用Shader生成标定板的实操要点如果你想把棋盘格Shader真的当标定板用有几个细节要注意。第一格子数要选奇数乘奇数的组合。比如7乘10这种不对称的组合角点检测算法才不会搞混方向。如果你的_Tiling是正方形网格那在平面正对相机时检测不到方向信息标定算法可能会报错。所以实际标定用的棋盘格横竖格子数通常是不同的用Shader实现时可以把_Tiling换成两个独立参数_TilingX和_TilingY。第二颜色建议用纯黑纯白并确保_ColorA和_ColorB的RGB值相同或者直接用单通道灰度避免色度信息干扰角点检测。我在做标定测试时就吃过亏一开始用浅灰和深灰角点检测的置信度明显下降。第三如果要打印成物理标定板用Shader截图后导出图片时要关掉抗锯齿并确保格子边缘是逐像素对齐的。落地到Gamma空间再导出不然打印出来的灰度和屏幕预览差别很大。6. 常见问题与踩坑记录6.1 棋盘格边缘出现奇怪的摩尔纹和锯齿表现摄像机拉远以后黑白格子边缘出现闪烁、波纹状的伪影尤其在高分辨率屏幕上格外明显。原因是棋盘格本身是高频信号像素采样频率跟不上图案频率时就会混叠。解决在Shader里加抗锯齿混合。核心思路是用fwidth函数求出UV在屏幕空间的变化率拿到格子边界处的梯度然后在边界附近做一次平滑过渡。代码可以这样加half4 frag (v2f i) : SV_Target { float2 uv i.uv * _Tiling; float2 grid floor(uv); float2 local uv - grid; float2 fw fwidth(uv); float minEdge min(min(local.x, 1.0 - local.x), min(local.y, 1.0 - local.y)); float boundaryBlend saturate(minEdge / max(min(fw.x, fw.y), 1e-5)); float checker fmod(grid.x grid.y, 2.0); half4 col lerp(_ColorA, _ColorB, checker); return lerp((_ColorA _ColorB) * 0.5, col, boundaryBlend); }这段代码的原理是local是当前像素在格子内的归一化坐标离格子边界越近local越接近0或1minEdge就是像素到最近边界的距离。当这个距离小于一个像素的UV变化率fwidth的返回值时说明像素正处在格子边界上此时把两种颜色的平均值混进去相当于做了一个模糊过渡视觉上就消除了锐利闪烁。实测下来加了这段抗锯齿后拉远观察的摩尔纹明显减弱视觉舒适度高很多。代价是多算了几个fwidth和min性能开销几乎可以忽略。6.2 URP下Shader变紫/报错表现材质球变成紫红色Console里报Shader编译错误。最常见的三个原因一是在HLSLPROGRAM里误用了#include UnityCG.cginc二是漏了CBUFFER导致SRP Batcher报warning但一般不至于变紫三是URP包未安装或路径写错。我自己排查询学到的一个技巧URP包路径一定要以Packages/开头这是Package Manager的虚拟路径不是工程文件里的实际路径。一旦手滑写成Assets/...或者少了com.unity.render-pipelines.universal中间段编译直接失败。6.3 双面渲染时背面效果不对表现加了Cull Off后背面确实显示了但有些设备上背面颜色偏暗或者出现奇怪的闪烁。原因分两类如果Shader是Lit类那是法线方向问题需要按VFACE翻转法线如果Shader确实是Unlit但依然偏暗那很可能是渲染状态被其他设置污染了检查一下这个材质的Render Queue是不是被某个后处理覆盖了。顺带说一下URP里片子渲染还有一个隐藏坑如果Surface Type设置成Transparent棋盘格的两个颜色Alpha不一致透明混合会出现一格深一格浅的现象。棋盘格本来就该是不透明的务必保证材质面板里Surface是Opaque。7. 实测性能与个人心得最后说点实际数据。我在URP下分别测试了贴图棋盘格和Shader棋盘格测试场景是1000个Plane挂不同材质的棋盘格。贴图方案在SRP Batcher下draw call多出了纹理绑定成本Shader方案在同等情况下draw call减少了约两成帧耗时差异虽然只有零点几毫秒但在移动端实测确实Shader方案更稳。个人经验是凡是图案能用数学规则描述的调试用纹理都值得用Shader做。棋盘格只是最典型的一个类似的光栅渐变、圆环图案、ID色块全部可以用同样的思路做灵活性比贴图高出不少。遇到需要改格子尺寸、改颜色、改双面渲染的时候改一个参数马上生效不用重新出图也不用管贴图压缩格式这种省心在项目迭代期特别宝贵。后续如果项目里有需要还可以在这个Shader的基础上扩展出三色棋盘格、带边框的标定方格、按世界坐标而不是UV坐标铺格子等变体每一类在特定调试场景里都有用。做图形渲染这件事多备几个趁手的调试Shader远比临时去画贴图高效。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑