一個關於渲染管線中坐標變換的簡單實例

前言

關於渲染管線大家一定都不陌生,那麼關於渲染管線中的坐標變換大家到底是陌生還是不陌生呢?我曾經有段時間覺得: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)。

將模型丟到世界坐標中之後,對於整個模型而言,它有一個相對於世界空間的坐標值。我們這時候能說這個模型是位於世界空間的。

因此要讓模型中的頂點變換到對應的世界空間下,就是讓這些頂點的自身坐標變換到模型對應的世界坐標下。我們就可以利用模型在世界空間下的坐標值構建矩陣。

// 模型空間到世界空間ntVector4 O2W(){nttVector3 parentPoint = Parent.position;nttVector3 childPoint = Child.localPosition;nnttMatrix4x4 worldMat = new Matrix4x4 ();nttworldMat.SetRow (0, new Vector4 (1, 0, 0, parentPoint.x));nttworldMat.SetRow (1, new Vector4 (0, 1, 0, parentPoint.y));nttworldMat.SetRow (2, new Vector4 (0, 0, 1, parentPoint.z));nttworldMat.SetRow (3, new Vector4 (0, 0, 0, 1));nnttVector4 worldPos = worldMat * new Vector4 (childPoint.x, childPoint.y, childPoint.z, 1);nnttDebug.Log ("WorldPosition: " + worldPos);nttreturn worldPos;nt} n

World Space - View Space

經過了O2W的變換之後,下一步就是W2V的變換。其實就是將世界空間下的頂點變換到相機空間下。這個變換兩點比較特殊的:

1、因為相機和世界坐標原點可能不重合,我們要做的是讓他們重合。這裡的做法是移動相機的位置讓他與世界坐標重合。

2、因為在Unity中相機空間使用的是右手坐標系,那麼它的正方向是朝向屏幕外面的,所以在矩陣中要對Z值取反。

//世界空間到視圖空間(相機空間)ntVector4 W2V(){nttVector3 cameraPoint = Camera.main.transform.position;ntt// 讓相機坐標和世界坐標對齊,nttVector3 c2m = -cameraPoint;nnttMatrix4x4 viewMat = new Matrix4x4 ();nttviewMat.SetRow (0, new Vector4 (1, 0, 0, c2m.x));nttviewMat.SetRow (1, new Vector4 (0, 1, 0, c2m.y));nttviewMat.SetRow (2, new Vector4 (0, 0, 1, c2m.z));nttviewMat.SetRow (3, new Vector4 (0, 0, 0, 1));nntt//因為觀察空間的Z軸是反的nttviewMat.SetRow (2, new Vector4 (0, 0, 1, -c2m.z));nnttVector4 viewPos = viewMat * worldPoint;nnttDebug.Log ("viewPosition: " + viewPos);nttreturn viewPos;nt}n

View Space - Projection Space

經過了W2V的變換後,再接下里一步是V2P的變換。投影(Projection)變換有正交投影和透視投影的區別,它們的投影矩陣是有區別的,這裡以透視投影為例:

//視圖空間到投影空間ntVector4 V2P(){nttfloat fov = Camera.main.fieldOfView * Mathf.Deg2Rad;nttfloat aspect = Camera.main.aspect;nttfloat far = Camera.main.farClipPlane;nttfloat near = Camera.main.nearClipPlane;nnttfloat cot = 1 / Mathf.Tan (fov / 2);nttcot = Mathf.Abs (cot);nttfloat factor1 = cot / aspect;nttfloat factor2 = -((far + near) / (far - near));nttfloat factor3 = -((2 * far * near) / (far - near));nnttMatrix4x4 clipMat = new Matrix4x4 ();nttclipMat.SetRow (0, new Vector4 (factor1, 0, 0, 0));nttclipMat.SetRow (1, new Vector4 (0, cot, 0, 0));nttclipMat.SetRow (2, new Vector4 (0, 0, factor2, factor3));nttclipMat.SetRow (3, new Vector4 (0, 0, -1, 0));nnttVector4 clipPos = clipMat * viewPoint;nttDebug.Log ("ClipPosition: " + clipPos);nttreturn clipPos;nt}n

Projection Space - NDC

NDC是一個歸一化的空間,坐標空間範圍為[-1, 1]。從Projection空間到NDC空間的做法就是做了一個齊次除法!

//clip到NDCntVector4 P2NDC(){nttVector4 ndcPos = clipPoint / clipPoint.w;nnttDebug.Log ("NDCPosition: " + ndcPos);nttreturn ndcPos;nt}n

NDC - Texture Space

這個過程是將[-1, 1]映射到[0, 1]之間。

//從NDC到Texture SpacentVector4 NDC2Texture(){nttVector4 texturePos = (ndcPoint + Vector4.one) / 2;nnttDebug.Log ("TexturePosition: " + texturePos);nttreturn texturePoint;nt}n

Texture Space - Screen Space

這個過程就是得到頂點最終在屏幕上的坐標,其實就是利用Texture Space的坐標乘上屏幕的寬高。

//從Texture Space到ScreenntVector4 Texture2Screen(){nttVector4 screenPos = new Vector4 (texturePoint.x * Screen.width, texturePoint.y * Screen.height, 0, 0);nttDebug.Log ("ScreenPosition: " + screenPos);nttreturn screenPoint;nt}n


上面的過程中,真正涉及到矩陣的就是三個階段:

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的字面上,是真的會誤導人的!!

推薦閱讀:

Unity3D插件開發教程(四):獲取地址組件
從零開始學基於ARKit的Unity3d遊戲開發系列16
聊聊Unity里的嵌套Prefab
從零開始學基於ARKit的Unity3d遊戲開發系列10
大萌喵的Unity後期特效第一發---鏡頭炫光與光暈(Flare)

TAG:Unity游戏引擎 |