
第1步:理解裁剪空间(Clip Space)
TransformObjectToHClip(v.positionOS) 输出一个 float4:
positionCS = (x, y, z, w)
这里的 w 不是常数,它和相机距离成正比——离相机越远,w 越大。
第2步:透视除法
GPU 自动做:
NDC = (x/w, y/w, z/w)
深度缓冲区最终存储的就是 z/w 这个值。
第3步:Reversed Z 的含义
| 近平面(相机附近) | 远平面(最远处) | |
|---|---|---|
| 非 Reversed Z | z/w = 0 | z/w = 1 |
| Reversed Z | z/w = 1 | z/w = 0 |
Unity 在 DX12/Vulkan/Metal 平台上默认开启 Reversed Z,所以两个平台的「最远」对应的数值是反的。
第4步:为什么是 z = w × factor
因为 z/w = factor,所以:
- 非 Reversed:想要
z/w ≈ 1→factor = 0.999999 - Reversed Z:想要
z/w ≈ 0→factor = 0.000001
为什么不直接写 z = 0 或 z = w?
因为 w 是动态变化的(每个顶点的 w 不一样),写固定值 z=0 会导致 z/w = 0,在非 Reversed 模式下反而是最近处,完全反了。必须用 w × factor 才能正确映射到 NDC 深度。
还有哪个环节想深入追问?比如「为什么 Reversed Z 能提高深度精度」或者「透视投影矩阵的 w 分量到底是怎么算出来的」?
裁剪空间到深度缓冲区的变换管线

关键:Clip Space 的 z 和 w 的关系
经过 MVP 变换后,裁剪空间中:
z = w × (某个与深度相关的值)
透视除法后 z/w 就是 NDC 深度,直接写入深度缓冲区。
所以我们控制 positionCS.z,就是在控制最终的深度值。

为什么用 0.000001 和 0.999999,而不是 0 和 1?
① 精度问题:z=0 或 z=w 时,浮点精度在最远处最差,容易产生 z-fighting。
② 裁剪剔除:z 恰好等于 0 或 w 可能被视锥剔除误判,导致物体消失。
③ 0.000001 和 0.999999 足够接近极值,同时避开了边界精度陷阱。
Shader "zmx/Water_1_SKY"
{
Properties
{
[MainTexture] _BaseMap("Base Map", 2D) = "white" {}
}
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 Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
CBUFFER_END
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
#if UNITY_REVERSED_Z
OUT.positionHCS.z = OUT.positionHCS.w * 0.000001f;
#else
OUT.positionHCS.z = OUT.positionHCS.w * 0.999999f;
#endif
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
float4 mainColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
return mainColor;
}
ENDHLSL
}
}
}
NDC指的是什么
NDC = Normalized Device Coordinates(归一化设备坐标)
一句话定义
NDC 是顶点在经过透视除法之后、真正变成屏幕上像素之前的坐标空间。
裁剪空间 (Clip Space) → 透视除法 → NDC → 视口变换 → 屏幕像素
(x,y,z,w) ÷w (x/w, y/w, z/w) → (px, py)
NDC 的坐标范围
| 轴 | NDC 范围 | 含义 |
|---|---|---|
| x | [-1, +1] | 屏幕左右 |
| y | [-1, +1] | 屏幕上下 |
| z | [0, 1] | 深度!直接写入深度缓冲区 |
为什么叫"归一化"
所有顶点,无论离相机多远、视野多宽,最终都会被压缩到 [-1, +1] × [-1, +1] × [0, 1] 这个立方体里。这就是"归一化"——统一到同一个标准 cube。
那段代码的本质就是:
// 在裁剪空间里动手脚
o.positionCS.z = o.positionCS.w * 0.999999f;
// ^^^^^^^^^^
// 这个比值就是 NDC 的 z 值!
// 因为 NDC.z = positionCS.z / positionCS.w
所以:
z/w = 0.999999→ NDC.z ≈ 1 → 深度缓冲区写入 ~1 → 最远处z/w = 0.000001→ NDC.z ≈ 0 → 深度缓冲区写入 ~0 → 最远处(Reversed Z)
假设某个顶点的 positionCS = (x, y, z, w) = (2.4, -0.8, 8.5, 10.0)
| 步骤 | 计算 | 结果 |
|---|---|---|
| 裁剪空间 | — | (2.4, -0.8, 8.5, 10.0) |
| 透视除法 x | 2.4 / 10.0 | 0.24 → NDC.x |
| 透视除法 y | -0.8 / 10.0 | -0.08 → NDC.y |
| 透视除法 z | 8.5 / 10.0 | 0.85 → NDC.z → 深度值 |
代码把 8.5 改成了 10.0 × 0.999999 = 9.99999,所以 NDC.z = 0.999999,深度缓冲写入 0.999999——最大值,最远处。
其中 DecodeHDREnvironment指的是什么意思?
DecodeHDREnvironment 是 Unity 内置的一个解码函数,专门用来从 HDR 格式的环境贴图(Cubemap)里采样并解码颜色值**。
它解决什么问题
环境贴图(Skybox / IBL Cubemap)在 HDR 格式下,存储的不是普通 RGB 颜色,而是经过编码的高动态范围值(因为 8-bit 存不下超亮的光)。
DecodeHDREnvironment 做的事情就是:采样 Cubemap → 解码回真实的 HDR 颜色值。
函数签名
// UnityGI_Base 或 UnityGI_Indirect 里常用
real3 DecodeHDREnvironment(float4 raw, float4 hdrDecodeParams);
| 参数 | 说明 |
|---|---|
raw | texCUBE() 采样出来的原始 float4 值 |
hdrDecodeParams | Unity 自动传给你的解码参数(在 unity_SpecCube0_HDR 里) |
unity_SpecCube0_HDR 是什么
这是 Unity 自动定义的 float4,里面存着 HDR 解码参数:
x = RGBM 的 M 系数
y = 指数
z = 保留
w = 保留
DecodeHDREnvironment 内部就是根据这个 float4 把 raw.rgb × (M ^ exp) 之类的方式解码回线性 HDR 值。
在 ASE 里
ASE 的 GI / Refletion 节点内部已经帮你调用了 DecodeHDREnvironment,你一般不直接接触它。
但如果你手写 Custom Expression 做自定义 IBL 采样,就需要手动调用它,否则采样出来的颜色会是错的(太暗或完全不正常)。

5827

被折叠的 条评论
为什么被折叠?



