- NDC空间:透视除法的结果,顶点坐标已归一化,可直接用于视口映射和裁剪
【从UnityURP开始探索游戏渲染】专栏-直达
- 在渲染管线中,归一化严格等同于透视除法,是齐次坐标到NDC空间转换的核心步骤。Unity中这步,自动执行。
- 数据归一化主要通过NDC空间(归一化设备坐标)转换实现,其核心原理是将裁剪空间坐标统一映射到标准范围([-1,1]的立方体内(OpenGL标准)或[0,1](DirectX标准))
- 可以看作是一个矩形内的坐标体系。经过转化后的坐标体系是 限制在一个立方体内的坐标体系。无论x y z轴在坐标体系内的范围都是(-1, 1)。归一化后,z轴向屏幕内。
- 归一化范围在OpenGL中范围为[-1, 1],DirectX中为[0, 1]。映射到屏幕时(0, 0)点:GpenGL是左下角,DirectX是左上角。
归一化原理
透视除法(Perspective Division)
将齐次裁剪空间坐标的(x,y,z)
分量除以w
分量,得到NDC坐标
此操作将坐标归一化至[-1,1]范围(OpenGL/Unity)或[0,1]范围(Direct3D)。
NDCExample.shader
- 1.URP标准坐标转换流程
- 2.手动NDC坐标计算
- 3.通过v2f结构传递NDC数据
glsl
// hlsl
Shader "Custom/NDCDemo"
{
SubShader
{
Pass
{
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { float4 vertex : POSITION; };
struct Varyings { float4 pos : SV_POSITION; float3 ndc : TEXCOORD0; };
Varyings vert(Attributes v)
{
Varyings o;
o.pos = TransformObjectToHClip(v.vertex.xyz);
// 手动计算NDC坐标
o.ndc = o.pos.xyz / o.pos.w;
return o;
}
ENDHLSL
}
}
}
URP中的NDC
Unity URP(Universal Render Pipeline)中,归一化的设备坐标(NDC)映射范围取决于具体的API平台:
- Direct3D风格平台 (如Windows、Xbox等):
- NDC范围是 **[-1, 1]³**(x,y,z三个维度)
- 深度值(z)映射到[0,1(通过投影矩阵转换)
- OpenGL风格平台 (如MacOS、Linux等):
- NDC范围是 **[-1, 1]³**
- 深度值(z)保持[-1,1]
URP默认使用**[-1,1]³**的NDC范围(与Built-in管线一致),但最终会适配目标平台的约定。
坐标转换示例过程
假设有一个世界空间点(2, 1, 5):
- 通过视图矩阵转换到视图空间(相机空间)
- 通过URP投影矩阵转换到裁剪空间(clip space)
- 透视除法得到NDC坐标(w分量除法)
具体数值示例(假设使用D3D风格):
世界坐标 (2, 1, 5)
↓ 视图矩阵转换
视图坐标 (1.5, 0.8, 4.2)
↓ 投影矩阵转换
裁剪坐标 (3.2, 1.6, 8.4, 4.2)
↓ 透视除法 (x/w, y/w, z/w)
NDC坐标 (0.76, 0.38, 2.0) → 超出[-1,1]会被裁剪
深度值特殊处理
在URP中,深度缓冲区的值会被重新映射:
- 原始NDC的z ∈ [-1,1](OpenGL)或 [0,1](D3D)
- 最终存储到深度纹理时统一映射到[0,1]范围
可以通过Shader验证:
glsl
hlsl
// 在Fragment Shader中:
float ndcZ = clipPos.z / clipPos.w; // 透视除法后的z值
float depth = ndcZ * 0.5 + 0.5; // D3D平台下实际存储值
URP通过_UNITY_UV_STARTS_AT_TOP等宏处理不同平台的坐标差异,保证跨平台一致性。
NDC转换在实际中的应用
虽然默认NDC计算是固定加速计算的过程,但是有时需要手动计算实现一些定制效果。
在Unity URP中,几何着色器(Geometry Shader)手动计算NDC并实现屏幕映射的典型应用场景包括:
1. 视锥裁剪
- 将世界坐标转换为NDC后判断是否在[-1,1]范围内
2. 屏幕空间特效
- 通过NDC坐标计算UV用于采样屏幕纹理
3. 几何体动态生成
- 根据NDC坐标控制顶点生成范围
计算NDC并实现屏幕空间粒子生成示例ScreenSpaceParticle.shader
- 在几何着色器中通过
clipPos.xyz / clipPos.w
完成透视除法得到NDC坐标 - 使用NDC坐标时需注意:
- D3D平台下y轴需要取反(
screenUV.y = 1 - screenUV.y
) - 深度值在D3D平台需映射到[0,1]范围
- D3D平台下y轴需要取反(
- 示例实现了屏幕空间粒子生成效果,可通过NDC坐标控制生成范围
实际应用时可结合_UNITY_MATRIX_VP矩阵进行完整坐标空间转换链验证。
glsl
Shader "Custom/NDCGeometryShader"
{
Properties { _MainTex ("Texture", 2D) = "white" {} }
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct v2g {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
struct g2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 ndc : TEXCOORD1;
};
v2g vert(appdata_base v) {
v2g o;
o.pos = TransformObjectToHClip(v.vertex);
o.uv = v.texcoord;
return o;
}
[maxvertexcount(4)]
void geom(point v2g input[1], inout TriangleStream<g2f> stream) {
// 手动计算NDC坐标
float4 clipPos = input[0].pos;
float3 ndc = clipPos.xyz / clipPos.w;
// 屏幕空间扩展(生成四边形粒子)
float size = 0.1;
g2f o;
for(int i=0; i<4; i++) {
o.pos = clipPos;
o.pos.xy += float2((i%2)*2-1, (i/2)*2-1) * size * clipPos.w;
o.uv = input[0].uv;
o.ndc = ndc;
stream.Append(o);
}
stream.RestartStrip();
}
half4 frag(g2f i) : SV_Target {
// 使用NDC坐标采样屏幕纹理
float2 screenUV = i.ndc.xy * 0.5 + 0.5;
return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, screenUV);
}
ENDHLSL
}
}
}
【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)