You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中UnityObjectToClipPos返回Z深度值异常,如何在顶点着色器模拟SV_Position的深度计算效果?

解决Unity顶点着色器中深度计算的异常问题

你遇到的问题其实是透视插值的特性和Unity裁剪空间/深度缓冲区的规则共同导致的,咱们一步步拆解并解决:

为什么原来的方法不对?

  1. 裁剪空间z的范围:Unity的UnityObjectToClipPos返回的裁剪空间坐标,其z值范围是[-w, w](非反向Z)或[0, w](反向Z,Unity默认开启)。直接做clipPos.z / clipPos.w得到的NDC z值,在反向Z下是[0, 1],非反向Z是[-1, 1]——但你在顶点着色器计算这个值后再插值,会出问题!
  2. 透视插值的非线性:GPU的插值是基于1/w的透视校正插值,如果你在顶点计算z/w再插值,得到的结果和在片段着色器里对SV_Position做z/w的结果完全不一样,这就是你看到深度不单调、出现负值的核心原因。

正确的实现思路

要在顶点着色器计算可正确插值的深度,应该先获取视图空间的线性深度,然后在片段着色器里把这个线性深度转换成深度缓冲区的0-1值。这样插值的是线性的视图空间z,片段里的转换会保证结果和SV_Position计算的一致。

修改后的完整代码

Tags { "RenderType"="Opaque" }
LOD 200
Cull off

CGPROGRAM
#pragma target 3.0
#pragma surface surf StandardSpecular alphatest:_Cutoff addshadow vertex:vert
#pragma debug

struct Input {
    float viewZ; // 传递视图空间的z值
};

void vert(inout appdata_full v, out Input o) {
    UNITY_INITIALIZE_OUTPUT(Input, o);
    // 获取视图空间位置,Unity中视图空间z为负值(相机看向-z方向)
    float3 viewPos = UnityObjectToViewPos(v.vertex);
    o.viewZ = viewPos.z;
}

void surf(Input IN, inout SurfaceOutputStandardSpecular o) {
    // 将视图空间z转换为深度缓冲区的0-1值
    float depth;
    // _ProjectionParams.y是近裁剪面距离,_ProjectionParams.z是远裁剪面距离
    #if defined(UNITY_REVERSED_Z)
        // 反向Z:深度缓冲区0=远,1=近
        depth = (IN.viewZ + _ProjectionParams.z) / (_ProjectionParams.z - _ProjectionParams.y);
    #else
        // 非反向Z:深度缓冲区0=近,1=远
        depth = (IN.viewZ + _ProjectionParams.z) / (_ProjectionParams.z - _ProjectionParams.y);
        depth = depth * 0.5 + 0.5;
    #endif
    
    //  clamp确保值在0-1之间,赋值给Albedo的R通道可视化
    o.Albedo.x = clamp(depth, 0.0, 1.0);
    o.Alpha = 1;
}
ENDCG

关键细节解释

  1. 视图空间z的意义:UnityObjectToViewPos返回的视图空间位置,其z值是从相机位置到顶点的深度(Unity中相机看向-z方向,所以近裁剪面的z值是-_ProjectionParams.y,远裁剪面是-_ProjectionParams.z)。这个值是线性的,插值后不会出现透视校正的偏差。
  2. 深度转换公式:
    • 反向Z模式(Unity默认):(viewZ + far) / (far - near),转换后近裁剪面对应1,远裁剪面对应0。
    • 非反向Z模式:先做线性转换到[-1,1],再偏移到[0,1]。
  3. 插值效果:因为我们传递的是线性的视图空间z,GPU的透视校正插值会正确处理它,最终在片段着色器转换后的深度和直接用SV_Position计算的结果完全一致,同时你也可以在顶点着色器里基于viewZ做任何自定义计算(比如调整、偏移等)。

验证结果

现在你会看到球体的深度着色和用SV_Position计算的效果完全一致:靠近相机的部分是白色(深度1),远离的部分是黑色(深度0,反向Z下),而且深度值是单调变化的,不会出现负值或异常恒定的情况。

内容的提问来源于stack exchange,提问作者IS4

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 18:52:39