坐标变换

坐标变换是图形学最基础的东西,这个搞不明白一切都是瞎搞,不自己老老实实走一遍看再多的代码,做再多的效果都是纸上谈兵。

从 vertex 到 frag

在顶点函数里面拿到的坐标就是局部坐标,如下,通过 “UnityObjectToWorld” 这个方法可以拿到世界坐标。

从模型到世界

逻辑意义,将所有模型的坐标都放在一个统一的坐标系中。

1
2
3
4
5
6
7
8
9
10
v2f vert (appdata v)
{
v2f o;
...
float4 worldPosition = UnityObjectToWorld(v.vertex);
//上面的方法等效下面的方法
//float4 worldPosition = mul(unity_ObjectToWorld, v.vertex);
...
return o;
}

从世界到视图

逻辑意义:让相机作为坐标的原点,并且固定相机的朝向,使得相机的 Y 轴朝上,看相 -Z 轴,X轴对准 X轴。

1
float4 viewPosition = mul(unity_MatrixV, worldPosition); // 从世界空间到视图空间

从视图到裁剪

逻辑意义:对坐标进行缩放,然后可以轻易判断出是否处于视锥体内,满足的条件如下:

1
2
3
-w<= x <= w
-w<= y <= w
-w<= z <= w

透视投影下的坐标此时经过变换的 W 的值为 -z,而正交投影下的坐标此时 W 的值仍然为 1。

Shader 中的变换代码如下。

1
o.vertex = mul(UNITY_MATRIX_P,viewPosition); // 从视图空间到裁剪空间

传给片元的坐标需要的是裁剪空间下的坐标,所以需要上面的转换,也就是我们常说的 MVP,不过 Unity 自带一个方法,将上面的方法全部做了

1
2
3
4
5
6
7
//一个函数就可以转为裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);

//下面的三个方法等效于上面的方法,具体的矩阵可以在 “UnityShaderVariables.cginc” 文件里面找到。
//float4 worldPosition = mul(unity_ObjectToWorld, v.vertex); // 从模型空间到世界空间
//float4 viewPosition = mul(unity_MatrixV, worldPosition); // 从世界空间到视图空间
//o.vertex = mul(UNITY_MATRIX_P,viewPosition); // 从视图空间到裁剪空间

现在如果你追到 Unity 编辑器的安装路径找到 “UnityShaderVariables.cginc” 这个文件,会发现这个方法内部是这样的。

1
2
3
4
5
6
7
8
9
10
// Tranforms position from object to homogenous space
inline float4 UnityObjectToClipPos(in float3 pos)
{
#if defined(STEREO_CUBEMAP_RENDER_ON)
return UnityObjectToClipPosODS(pos);
#else
// More efficient than computing M*VP matrix product
return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
#endif
}

注意这个方法内部矩阵的计算顺序,如果你自己想做点什么不要搞反了。

从裁剪到屏幕

  • 先进行透视除法,得到 NDC 坐标,此时无论是正交相机还是透视相机,坐标都会变换到一个相同的立方体内,且此时的xy坐标的区间是 [-1,1]。

  • 对 NDC 坐标进行映射,得到统一的屏幕坐标。

  • 映射公式如下:

  • $ \begin{array}{l}
    \text { screen }{x}=\frac{\text { clip }{x} \cdot \text { pixelWidth }}{2 \cdot \text { clip }_{w}}+\frac{\text { pixelWidth }}{2}
    \end{array}$

  • $ \begin{array}{l}
    \text { screen }{y}=\frac{\text { clip }{y} \cdot \text { pixelHeight }}{2 \cdot \text { clip }_{w}}+\frac{\text { pixelHeight }}{2}
    \end{array}$

  • NDC 坐标的 Z 值会被放入深度缓冲中,而 W 分量经过透视除法已经变成了1.

记录一下可能会用到的 Unity 已经提供的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//世界坐标到裁剪坐标
inline float4 UnityWorldToClipPos( in float3 pos )
{
return mul(UNITY_MATRIX_VP, float4(pos, 1.0));
}

// 视图坐标到裁剪坐标
inline float4 UnityViewToClipPos( in float3 pos )
{
return mul(UNITY_MATRIX_P, float4(pos, 1.0));
}

// 模型坐标到视图坐标
inline float3 UnityObjectToViewPos( in float3 pos )
{
return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
}

// 世界坐标到视图坐标
inline float3 UnityWorldToViewPos( in float3 pos )
{
return mul(UNITY_MATRIX_V, float4(pos, 1.0)).xyz;
}

// 方向向量从模型到世界,并且归一化,通常用于计算光照
inline float3 UnityObjectToWorldDir( in float3 dir )
{
return normalize(mul((float3x3)unity_ObjectToWorld, dir));
}

// 方向向量从世界到模型,并且归一化,通常用于计算光照
inline float3 UnityWorldToObjectDir( in float3 dir )
{
return normalize(mul((float3x3)unity_WorldToObject, dir));
}

// 将一个法线从模型转换到世界
inline float3 UnityObjectToWorldNormal( in float3 norm )
{
#ifdef UNITY_ASSUME_UNIFORM_SCALING
return UnityObjectToWorldDir(norm);
#else
// mul(IT_M, norm) => mul(norm, I_M) => {dot(norm, I_M.col0), dot(norm, I_M.col1), dot(norm, I_M.col2)}
return normalize(mul(norm, (float3x3)unity_WorldToObject));
#endif
}

// 计算当前世界坐标的视线变量
inline float3 UnityWorldSpaceViewDir( in float3 worldPos )
{
return _WorldSpaceCameraPos.xyz - worldPos;
}

// 返回一个范围在[0,1]的线性深度值
inline float Linear01Depth( float z )
{
return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// 深度纹理的采样结果转换到视角空间下的深度值
inline float LinearEyeDepth( float z )
{
return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

记录一些重要的已经定义的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
_WorldSpaceCameraPos        //主相机的世界坐标

UNITY_PI

struct appdata_base {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct appdata_tan {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
fixed4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

// Time (t = time since current level load) values from Unity
float4 _Time; // (t/20, t, t*2, t*3)
float4 _SinTime; // sin(t/8), sin(t/4), sin(t/2), sin(t)
float4 _CosTime; // cos(t/8), cos(t/4), cos(t/2), cos(t)
float4 unity_DeltaTime; // dt, 1/dt, smoothdt, 1/smoothdt

// x = 1 or -1 (-1 if projection is flipped)
// y = near plane
// z = far plane
// w = 1/far plane
float4 _ProjectionParams;

// x = width
// y = height
// z = 1 + 1.0/width
// w = 1 + 1.0/height
float4 _ScreenParams;

// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
// or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1)
// x = -1+far/near
// y = 1
// z = x/far
// w = 1/far
float4 _ZBufferParams;

// x = orthographic camera's width
// y = orthographic camera's height
// z = unused
// w = 1.0 if camera is ortho, 0.0 if perspective
float4 unity_OrthoParams;