一個關於渲染管線中座標變換的簡單例項
前言
關於渲染管線大家一定都不陌生,那麼關於渲染管線中的座標變換大家到底是陌生還是不陌生呢?我曾經有段時間覺得:emmm,我好像懂了。但是多問幾遍自己,就開始懷疑人生了!!所以我決定寫這麼一篇東西來解決一些朋友們的遇到的和我曾經相似的疑惑。
這篇文章結合了Unity,所有的東西是在Unity中實現的。
注意,下面的例子我只做了位移變換
。在開始之前,我先給出兩個Unity的API:
Camera。WorldToScreenPoint
Camera。WorldToViewportPoint
在這裡我就有一個問題了:這兩個API中到底執行了什麼呢?怎麼就從world到viewport或screen了呢?那麼帶著這兩個問題開始吧!!
無論是Camera。WorldToScreenPoint還是Camera。WorldToViewportPoint,它們的背後就是我們經常提到的座標變換。
座標變換的根源就是 — 矩陣
!!這裡我不會涉及到任何關於數學的講解,這講下去估計我自己都能把自己說糊塗!
先給出一張珍藏多年的圖(原來買了個老外的教程):
可以看到,座標空間的變化大致會經過
Object Space - World Space
World Space - View Space
View Space - Projection Space
Projection Space - NDC
NDC - Texture Space
Texture Space - Screen Space
所有靠後的空間,可以看作是之前一個空間的父空間。對於頂點在座標空間的變換就是用父空間的矩陣來對子空間做變換,從而得到在父空間的位置。所以重點就出來了:
如何構建父空間的矩陣
!
Object Space - World Space
我們知道,對於一個模型而言,他的每個頂點位置是基於自身座標空間的 — 通常我們叫做模型空間(Object Space)。
將模型丟到世界座標中之後,對於整個模型而言,它有一個相對於世界空間的座標值。我們這時候能說這個模型是位於世界空間的。
因此要讓模型中的頂點變換到對應的世界空間下,就是讓這些頂點的自身座標變換到模型對應的世界座標下。我們就可以利用模型在世界空間下的座標值構建矩陣。
// 模型空間到世界空間
Vector4
O2W
(){
Vector3
parentPoint
=
Parent
。
position
;
Vector3
childPoint
=
Child
。
localPosition
;
Matrix4x4
worldMat
=
new
Matrix4x4
();
worldMat
。
SetRow
(
0
,
new
Vector4
(
1
,
0
,
0
,
parentPoint
。
x
));
worldMat
。
SetRow
(
1
,
new
Vector4
(
0
,
1
,
0
,
parentPoint
。
y
));
worldMat
。
SetRow
(
2
,
new
Vector4
(
0
,
0
,
1
,
parentPoint
。
z
));
worldMat
。
SetRow
(
3
,
new
Vector4
(
0
,
0
,
0
,
1
));
Vector4
worldPos
=
worldMat
*
new
Vector4
(
childPoint
。
x
,
childPoint
。
y
,
childPoint
。
z
,
1
);
Debug
。
Log
(
“WorldPosition: ”
+
worldPos
);
return
worldPos
;
}
World Space - View Space
經過了O2W的變換之後,下一步就是W2V的變換。其實就是將世界空間下的頂點變換到相機空間下。這個變換兩點比較特殊的:
1、因為相機和世界座標原點可能不重合,我們要做的是讓他們重合。這裡的做法是移動相機的位置讓他與世界座標重合。
2、因為在Unity中相機空間使用的是右手座標系,那麼它的正方向是朝向螢幕外面的,所以在矩陣中要對Z值取反。
//世界空間到檢視空間(相機空間)
Vector4
W2V
(){
Vector3
cameraPoint
=
Camera
。
main
。
transform
。
position
;
// 讓相機座標和世界座標對齊,
Vector3
c2m
=
-
cameraPoint
;
Matrix4x4
viewMat
=
new
Matrix4x4
();
viewMat
。
SetRow
(
0
,
new
Vector4
(
1
,
0
,
0
,
c2m
。
x
));
viewMat
。
SetRow
(
1
,
new
Vector4
(
0
,
1
,
0
,
c2m
。
y
));
viewMat
。
SetRow
(
2
,
new
Vector4
(
0
,
0
,
1
,
c2m
。
z
));
viewMat
。
SetRow
(
3
,
new
Vector4
(
0
,
0
,
0
,
1
));
//因為觀察空間的Z軸是反的
viewMat
。
SetRow
(
2
,
new
Vector4
(
0
,
0
,
1
,
-
c2m
。
z
));
Vector4
viewPos
=
viewMat
*
worldPoint
;
Debug
。
Log
(
“viewPosition: ”
+
viewPos
);
return
viewPos
;
}
View Space - Projection Space
經過了W2V的變換後,再接下里一步是V2P的變換。投影(Projection)變換有正交投影和透視投影的區別,它們的投影矩陣是有區別的,這裡以透視投影為例:
//檢視空間到投影空間
Vector4
V2P
(){
float
fov
=
Camera
。
main
。
fieldOfView
*
Mathf
。
Deg2Rad
;
float
aspect
=
Camera
。
main
。
aspect
;
float
far
=
Camera
。
main
。
farClipPlane
;
float
near
=
Camera
。
main
。
nearClipPlane
;
float
cot
=
1
/
Mathf
。
Tan
(
fov
/
2
);
cot
=
Mathf
。
Abs
(
cot
);
float
factor1
=
cot
/
aspect
;
float
factor2
=
-((
far
+
near
)
/
(
far
-
near
));
float
factor3
=
-((
2
*
far
*
near
)
/
(
far
-
near
));
Matrix4x4
clipMat
=
new
Matrix4x4
();
clipMat
。
SetRow
(
0
,
new
Vector4
(
factor1
,
0
,
0
,
0
));
clipMat
。
SetRow
(
1
,
new
Vector4
(
0
,
cot
,
0
,
0
));
clipMat
。
SetRow
(
2
,
new
Vector4
(
0
,
0
,
factor2
,
factor3
));
clipMat
。
SetRow
(
3
,
new
Vector4
(
0
,
0
,
-
1
,
0
));
Vector4
clipPos
=
clipMat
*
viewPoint
;
Debug
。
Log
(
“ClipPosition: ”
+
clipPos
);
return
clipPos
;
}
Projection Space - NDC
NDC是一個歸一化的空間,座標空間範圍為[-1, 1]。從Projection空間到NDC空間的做法就是做了一個齊次除法!
//clip到NDC
Vector4
P2NDC
(){
Vector4
ndcPos
=
clipPoint
/
clipPoint
。
w
;
Debug
。
Log
(
“NDCPosition: ”
+
ndcPos
);
return
ndcPos
;
}
NDC - Texture Space
這個過程是將[-1, 1]對映到[0, 1]之間。
//從NDC到Texture Space
Vector4
NDC2Texture
(){
Vector4
texturePos
=
(
ndcPoint
+
Vector4
。
one
)
/
2
;
Debug
。
Log
(
“TexturePosition: ”
+
texturePos
);
return
texturePoint
;
}
Texture Space - Screen Space
這個過程就是得到頂點最終在螢幕上的座標,其實就是利用Texture Space的座標乘上螢幕的寬高。
//從Texture Space到Screen
Vector4
Texture2Screen
(){
Vector4
screenPos
=
new
Vector4
(
texturePoint
。
x
*
Screen
。
width
,
texturePoint
。
y
*
Screen
。
height
,
0
,
0
);
Debug
。
Log
(
“ScreenPosition: ”
+
screenPos
);
return
screenPoint
;
}
上面的過程中,真正涉及到矩陣的就是三個階段:
Object Space - World Space
World Space - View Space
View Space - Projection Space
其他的空間變換都是一些對映關係。
因為我所說的非常簡短,沒有涉及到任何關於座標空間的意義和內容。但是這個過程可以證明一件事情,回到最初的問題:
Camera。WorldToScreenPoint
Camera。WorldToViewportPoint
這兩個API最終得到的值對應的是什麼階段,事實證明:
Camera。WorldToViewportPoint — Texture Space
Camera。WorldToScreenPoint — Screen Space
我只能說,如果不寫這一個過程的話,但從API的字面上,是真的會誤導人的!!