为何Shader中调用UnityObjectToClipPos()会导致场景内其他Shader对象不可见?
问题分析与解决方案
首先,咱们先搞懂UnityObjectToClipPos(v.vertex)到底在做什么——这是Unity内置的顶点变换宏,核心作用是把模型空间下的顶点坐标一步步转换到裁剪空间,让GPU能正确计算顶点在屏幕上的位置。它展开后的逻辑大致是:
o.vertex = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex));
也就是依次应用模型矩阵(转世界空间)、视图矩阵(转相机空间)、投影矩阵(转裁剪空间),这是3D/2D物体正常渲染的必要步骤。
为什么它会导致其他UI元素不可见?
你的问题大概率出在渲染队列、深度测试/写入的设置冲突上:
- UI元素(尤其是
Screen Space - Overlay或Screen Space - Camera模式的Canvas)默认使用Transparent渲染队列,并且关闭ZWrite(不向深度缓冲写入数据),这样UI才能正常叠加在其他内容之上。 - 而你用Shadero Sprite生成的Shader,可能默认开启了
ZWrite On,并且渲染队列和UI重叠(比如也是Transparent)。当你的Shader执行正确的顶点变换后,物体的深度值会被写入深度缓冲,那些UI自定义Shader如果设置了严格的深度测试(比如ZTest Less),就会因为深度测试不通过被GPU直接剔除,导致完全不可见。 - 当你换成
o.vertex = v.vertex时,顶点没有经过空间转换,会被渲染到视口之外(完全不可见),自然不会写入深度缓冲,UI的深度测试也就不会被干扰,所以能正常显示。
解决方法
1. 调整你的Shader的渲染队列与深度设置
这是最直接的解决方案,让你的Shader和UI的渲染逻辑彻底隔离:
- 如果你的Shader是不透明物体用的:
SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry" } // 用默认的3D物体队列 ZWrite On ZTest LEqual // ... 其他原有代码 } - 如果是透明Sprite用的:
SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } ZWrite Off // 必须关闭深度写入,避免干扰UI Blend SrcAlpha OneMinusSrcAlpha // 开启透明混合 // ... 其他原有代码 }
2. 手动替换UnityObjectToClipPos的等效代码
如果怀疑是宏展开的潜在问题(比如Shadero Sprite生成的宏和UI的宏冲突),可以直接用手动矩阵乘法替换这个宏,逻辑完全一致,但能避免宏的不确定性:
v2f vert(appdata v) { v2f o; // 替换UnityObjectToClipPos为手动矩阵乘法 float4 worldPos = mul(unity_ObjectToWorld, v.vertex); o.vertex = mul(UNITY_MATRIX_VP, worldPos); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; }
3. 调整UI自定义Shader的设置
如果不想修改自己的Shader,也可以针对不可见的UI元素调整它们的Shader:
- 把UI Shader的
ZTest设置为ZTest Always(完全跳过深度测试),这样无论深度缓冲里有什么,UI都会被渲染。 - 或者把UI的渲染队列改得更靠后(比如
Queue="Transparent+100"),让UI在你的物体之后渲染,即使有深度值也能覆盖显示。
额外排查点
可以检查Shadero Sprite生成的Shader里的#pragma指令,有没有多编译宏(比如#pragma multi_compile)意外修改了全局状态,导致和UI Shader的编译逻辑冲突。
内容的提问来源于stack exchange,提问作者Josh1billion




