A. Unity Shader 獲取深度紋理和法線紋理
深度紋理實際就是一張渲染紋理,只不過它裡面存儲的像素值不是顏色值,而是一個高精度的深度值。由於被存儲在一張紋理中,深度紋理里的深度值范圍是[0, l], 而且通常是非線性分布的。這些深度值來自於頂點變換後得到的歸一化的設備坐標 (Normalized Device Coordinates , 簡稱NDC) 。一個模型要想最終被繪制在屏幕上,需要把它的頂點從模型空間變換到齊次裁剪坐標系下,這是通過在頂點著色器中乘以 MVP 變換矩陣得到的 。在變換的最後一步,我們需要使用一個投影矩陣來變換頂點,當我們使用的是透視投影類型的攝像機時,這個投影矩陣就是非線性的。
下圖 顯示了Unity 中透視投影對頂點的變換過程。最左側的圖顯示了投影變換前,即觀察空間下視錐體的結構及相應的頂點位置,中間的圖顯示了應用透視裁剪矩陣後的變換結果,即頂點著色器階段輸出的頂點變換結果 ,最右側的圖則是底層硬體進行了透視除法後得到的歸一化的設備坐標(NDC)。需要注意的是,這里的投影過程是建立在Unity對坐標系的假定上的,也就是說,針對的是觀察空間為右手坐標系,使用列矩陣在矩陣右側進行相乘,且變換到NDC後 z分量范圍將在[-1, l] 之間的情況。而在類似 DirectX 這樣的圖形介面中,變換後z分量范圍將在[0, 1]之間 。如果是在其他圖形介面下,需要對一些計算參數做出相應變化。
下圖 顯示了在使用正交攝像機時投影變換的過程。同樣,變換後會得到一個范圍為 [-1, 1]的立方體。正交投影使用的變換矩陣是線性的。
在得到NDC後,深度紋理中的像素值就可以很方便地計算得到了,這些深度值就對應了NDC中頂點坐標的z分量的值。由於NDC中 z分量的范圍在[-1, I], 為了讓這些值能夠存儲在一張圖像中,需要使用下面的公式對其進行映射:
其中,d對應了深度紋理中的像素值, 對應了NDC坐標中的z分量的值。
在Unity中,深度紋理可以直接來自於真正的深度緩存,也可以是由一個單獨的 Pass 渲染而得,這取決於使用的渲染路徑和硬體。通常來講,當使用延遲渲染路徑(包括遺留的延遲渲染路徑)時,深度紋理理所當然可以訪問到,因為延遲渲染會把這些信息渲染到 G-buffer 。而當無法直接獲取深度緩存時,深度和法線紋理是通過一個單獨的Pass渲染而得 。具體實現是Unity會使用著色器替換技術選擇那些渲染類型(即 SubShader RenderType 標簽)為 Opaque 的物體,判斷它們使用的渲染隊列是否小於等於2500(內置的 Background Geometry AlphaTest 渲染隊列均在此范圍內),如果滿足條件,就把它渲染到深度和法線紋理中。因此,要想讓物體能夠出現在深度和法線紋理中,就必須在Shader中設置正確的RenderType標簽。
在 Unity中,可以選擇讓一個攝像機生成一張深度紋理或是一張深度+法線紋理。當選擇前者,即只需要 張單獨的深度紋理時, Unity 會直接獲取深度緩存或是按之前講到的著色器替換技術,選取需要的不透明物體,並使用它投射陰影時使用的 Pass (即 LightMode 被設置為ShadowCaster Pass)來得到深度紋理。如果 Shader 中不包含這樣一個 Pass, 那麼這個物體就不會出現在深度紋理中(當然,它也不能向其他物體投射陰影)。深度紋理的精度通常24 位或 16 位,這取決於使用的深度緩存的精度。如果選擇生成一張深度+法線紋理, Unity 創建一張和屏幕解析度相同、精度為 32 位(每個通道為 位)的紋理,其中觀察空間下的法線信息會被編碼進紋理的 通道,而深度信息會被編碼進 通道。法線信息的獲取在延遲渲染中是可以非常容易就得到的, Unity 只需要合並深度和法線緩存即可。而在前向渲染中,默認情況下是不會創建法線緩存的,因此 Unity 底層使用了一個單獨的 Pass 把整個場景再次渲染一遍來完成。這個 Pass 被包含在 Unity 內置的一個 Unity Shader 中,可以在內置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormaITexture.shader 文件中找到這個用於渲染深度和法線信息的 Pass。
獲取深度紋理,先設置攝像機的 depthTextureMode:
然後在 Shader 中通過聲明_CameraDepthNormalsTexture 變數來訪問它。
同理,如果需要獲取獲取深度+法線紋理,設置攝像機的 depthTextureMode為:
然後在 Shader 中通過聲明_CameraDepthN ormalsTexture 變數來訪問它。
還可以組合這些模式,讓一個攝像機同時產生一張深度和深度+法線紋理:
在 Shader 中訪問到深度紋理_CameraDepthTexture 後,我們就可以使用當前像素的紋理坐標對它進行采樣。絕大多數情況下,我們直接使用 tex2D 函數采樣即可,但在某些平台(例如 PS3 PSP2) 上,我們需要 一些特殊 處理 Unity 為我們提供了一個統一的宏SAMPLE_DEPTH_TEXTURE, 用來處理這些由於平台差異造成的問題。而我們只需要在 Shader中使用 SAMPLE_DEPTH_TEXTURE 宏對深度紋理進行采樣,例如:
其中,i.scrPos是在頂點著色器中通過調用ComputeScreenPos(o.pos)得到的屏幕坐標。上述這些宏的定義,可以在Unity 內置的HLSLSupport.cginc文件中找到。
當通過紋理采樣得到深度值後,這些深度值往往是非線性的,這種非線性來自於透視投影使用的裁剪矩陣。然而,在我們的計算過程中通常是需要線性的深度值,也就是說,我們需要把投影後的深度值變換到線性空間下,例如視角空間下的深度值,我們只需要倒推頂點變換的過程即可。下面以透視投影為例,推導如何由深度紋理中的深度信息計算得到視角空間下的深度值。
當我們使用透視投影的裁剪矩陣 對視角空間下的一個頂點進行變換後,裁剪空間下頂點的z和w分量為:
其中,Far和Near分別是遠近裁剪平面的距離。然後,我們通過齊次除法就可以得到NDC下的z分量:
而深度紋理中的深度值是通過下面的公式由NDC 計算而得的:
由上面的這些式子,可以推導出用d表示而得的 的表達式:
由於在Unity 使用的視角空間中,攝像機正向對應的z值均為負值,因此為了得到深度值的正數表示,我們需要對上面的結果取反,最後得到的結果如下:
它的取值范圍就是視錐體深度范圍,即[Near, Far]。如果我們想得到范圍在[0, l]之間的深度值,只需要把上面得到的結果除以Far即可。這樣,0就表示該點與攝像機位於同一位置,1表示該點位於視錐體的遠裁剪平面上。結果如下:
其實,Unity提供了兩個輔助函數來為我們進行上述的計算過程LinearEyeDepth 和LinearOlDepth。LinearEyeDepth 負責把深度紋理的采樣結果轉換到視角空間下的深度值,也就是我們上面得到的 。而Linear01Depth則會返回一個范圍在[0, 1]的線性深度值,也就是我們上面得到的 。這兩個函數內部使用了內置的_ZBufferParams變數來得到遠近裁剪平面的距離。
如果需要獲取深度+法線紋理,可以直接使用tex2D函數對_CameraDepthNormalsTexture 進行采樣,得到裡面存儲的深度和法線信息。Unity提供了輔助函數來為我們對這個采樣結果進行解碼,從而得到深度值和法線方向。這個函數是DecodeDepthNormal,它在UnityCG.cginc 里被定義為:
DecodeDepthNormal 的第一個參數是對深度+法線紋理的采樣結果,這個采樣結果是 Unity 深度和法線信息編碼後的結果 它的 xy 分量存儲的是視角空間下的法線信息 而深度信息被編碼進了 zw 分量。通過調用 DecodeDepthNormal 函數對采樣結果解碼後, 我們就可 得到解碼後的深度值和法線。這個深度值是范圍在[O l] 的線性深度值(這與單獨的深度紋理中存儲 深度值不同),而得到的法線則是視角空間下的法線方向。同樣也可以通過調用 DecodeFloatRG和DecodeViewNormalStereo 來解碼深度+法線紋理中的深度和法線信息。
B. 從條帶狀陰影談Unity Shadow
不知道你曾經在Unity中遇到過陰影異常沒有,比如近處馬賽克狀的陰影塊?亦或是脫離投影物體的「懸浮」陰影?還是稍遠處陰影的突然消失?多塊陰影互相疊加處顏色異常的亮度等等。我感覺最讓人無法直觀理解,也相對常見的一種陰影異常要數「條帶狀陰影」了。請允許我暫時如此稱呼它,因為這很形象。
要重現「條帶狀陰影」其實很簡單,我們甚至可以在不修改任何Unity默認設置的情況下獲得這種現象。做個實驗,新建一個Unity工程(2019),創建2個Quad對象,然後上下堆疊起來,設置頂層Quad的高度(Y軸世界坐標)為0.05,設置底層高度為0,調節一下主方向光,使之保持垂直朝向的姿勢,既Rotation: (90, 0, 0),一切OK的話大概能得到下圖所示的表現,看起來還不錯,姑且稱之為 現象<0> 。
我們保持光照和平面位置不變,接下來我們要調節2個重要參數,叫 bias 和 normal bias ,Unity在標准渲染管線模式下,將這2個參數暴露在了Light組件有關shadow的設置參數中;URP模式則可以在 Render Feature的設置欄中找到它們倆。總之找到並設置 bias = 0 , normal bias = 0 ,我們再看看有什麼變化:
簡單盤點下,在具有一定傾斜角度直接光照下,觀察兩塊相聚非常近的面片中,上方面板向下方面片的投影,我們會發現「條帶狀陰影」;在此基礎上,我們將 bias 和 normal bias 這兩個參數歸零,面片間投射的陰影恢復正常,但是面片上原本非陰影區域又出現了「條帶狀陰影」。
無論如何,在解讀之前,我們都應該明確這種現象是Unity在處理陰影效果時產生的,因為我們全程只調節了2個控制陰影顯示的參數而已。而對於Unity或者一般游戲引擎是如何渲染物體投影效果這一部分,考慮到網上相關內容的資料非常豐富,這里就不再詳細闡述了,凝練一下,可以把通用渲染邏輯整理為3個步驟,羅列如下:
步驟1略微展開一下,它涉及到將在光源位置放置攝像機,然後基於光源的視角生成深度圖,場景中光源的陰影區也就是光源位置攝影機看不到的地方。ShadowMap本質上是一張深度圖,它記錄了從該光源的位置出發,能看到的場景中,距離光源最近的表面的深度值。
上圖左側為深度圖。右側在相機位置繪制場景時,能夠看到場景中的點Va,其對應的ShadowMap中的位置為a,Va的深度不大於深度圖中a位置存儲的深度,因此Va不處於陰影
考慮到上述採用shadowmap技術渲染陰影的技術最早可以追述到1978年,人們已經對這項技術能給圖形渲染帶來的利與弊有了充分的認識。回到 現象<2> 來,在圖形學中描述這類現象自然也有其慣用的術語,叫做「Shadow Acne」或者「Self-Shadowing」。這兩個英文單詞,前者可以翻譯為「陰影粉刺」,後者則一般翻譯為「自陰影」,一個表形,一個表意,可以說描述得挺好,後續我們還會提到,如果有興趣了解更多細節,這里有一篇對各種陰影異常表現總結得很好的 技術博文 可以作為參考。
在有了一定的陰影紋理演算法基礎,以及對所使用數據結構有一定了解的前提下,我們不妨從復盤光照模型開始研究 現象<2> 的成因。
在PL上紅色斑點的右側標記了另一個黑色斑點,毫無疑問它也落在了可見區域中,而且由於光照是垂直向下的,光源到這個黑色斑點的最短距離和一旁的紅色斑點應當是一致的,數值上等於圖中紅色連線的長度,既{Depth}。
當傾斜光照,使之與物體平面(PL)成一定角度相交後,事情就變得有意思起來了。我們都知道,由於透視原理,攝像機近處的物體(PL)相較於遠處的物體(光源)會擠占更多的屏幕空間像素,而生成陰影紋理(ShadowMap)的光源空間也有類似的現象,只不過不是因為透視投影導致的(直接光照使用的是正交投影),而是由角度和解析度共同作用導致的:
現在考察上圖中黃色虛線填充的BCP三角形,在這塊區域內的物體(比如頂點P1),到光源處的最短距離必然小於圖中紅色實線表示的深度值{Depth};同樣的在黑色虛線填充的三角形PED中,所有點到光源的距離必然大於深度值{Depth}。根據光照投影演算法的定義,在BCP三角形中的點P1,轉換到光源空間後的深度值小於該處記錄的光源深度,所以P1不在陰影中,這顯然是期望的結果;然而另一側三角形PED中的P2卻在相同的演算法中被分配到了陰影中,這顯然是錯誤的結論。
通過前面的分析,我們看到,本該處於非陰影狀態的三個平面PL上的點 P1、P和P2,在一定光照角度外加一定的紋理解析度條件下,經過標准光照投影演算法處理後會得出錯誤的結論(三角形PED區域的投影結果),而且導致投影錯誤的原因也以作圖的方法進行的展示。為了能更直觀,更量化的反應問題,我們不妨對上述圖例作如下簡化():
在上圖中,黑色粗線表示場景中的平面,黃色粗線表示為光源所對應的近平面。該近平面會對應產生的ShadowMap。AB表示為這個近平面上對應ShadowMap上一個紋素的區域。假設近平面是一個規范尺寸的正方形,且邊長為FSize。對應的ShadowMap解析度被設置為SSize。那麼AB所對應的坐標尺寸通過如下計算可獲得: AB = FSize / SSize
我們希望通過某些方式,讓紋素AB對應區域在物體平面上的投影CD部分,能夠完整的處於圖中藍色虛線的左側。解決的方法也很樸素直觀,按道理只需將ShadowMap中存放的深度值稍微增大點即可,最好是大得剛剛好,恰巧確保CD段處於非陰影區域。
這是第一種修正,針對光線的方向進行偏移:
觀察線段(向量)GD,這是我們按照光線方向的最短平移距離,藍色虛線在平移後恰好位於線段CD的右側,從而確保了CD段在計算陰影深度時,其在光源空間的深度能夠小於ShadowMap采樣深度(既圖中的BD線段長度)。
求解GD:
除了沿著光線方向直接增加深度之外,還可以沿著法線方向作偏移,間接影響深度值,這就是我們接下來要研究的normal bias法線偏移。
法線偏移其實還分兩種實現方式,一種是在將物體頂點轉換到光源空間之前,先沿著法線正方向作移動,達到減小片元的深度,具體參考下圖:
另一種是在生成ShadowMap時,將物體的頂點沿著法線反方向進行偏移,從而變相得增大陰影的深度值,具體參考下圖:
先說第一種方式,它的作用機制不是直接修改陰影紋理的深度值,而是嘗試縮小物體在光源空間的深度,因此需要在片元著色器中實現。參考圖[normal 1],物體表面沿著自身法線方向移動了距離GM,獲得新的位置C'G。將原本CM段的區域整體提升到了藍色虛線的左側。我們知道C'G區域的深度取值必然小於等於F點的深度值EF,現在會被光源正常照亮了;至於點G右側的部分線段可以不用擔心,它們將會被相鄰的ShadowMap覆蓋和處理。
此處的α指的還是光線方向和法線方向形成的夾角。
對於第二種方式,理解起來會直觀一些,因為它直接作用於ShadowMap,修改(增加)紋素中的深度值。參考圖[normal 2],修正後藍色虛線向左下方偏移了距離FN, 從而確保CD段區域處於陰影深度線的左側,被正常照亮。至於偏移量MN的計算,需要分為2步:
其中α指光線方向與法線的夾角;φ指光源方向與法線的夾角。
Unity引入2個bias變數作為控制陰影深度參數的理由,我想至少有2層。其一是增加了靈活性,讓用戶可以依據場景本身的特質調整光源方向或者法線方向的深度,試想如果Unity只提供了normal bias作為參數,在某些極端情況下,過大的normal bias會導致物體投射的陰影與物體本體脫離的現象(Peter Panning)。
至於另一層理由,可以參考上圖,bias對於圖中的tanθ曲線,normal bias對於sinθ曲線,θ是光源平面與物體平面的夾角。當光線方向與物體表面越加趨向平行時,光源平面越加垂直於物體平面,θ角越接近90度(π/2),此時使用bias計算得出的修正距離也將隨著tan一起趨向無窮大(圖中斜向上藍色箭頭)。若此時引入sinθ,則可以平衡bias的影響,這是因為很樸素的道理:sin函數是有上界的(圖中藍色水平箭頭),我們可以在θ角小於45度(π/4)時優先使用bias的計算結果,而當θ角超過45度後,將計算偏移距離的權重倒向normal bias一邊。
也許有人會問,既然bias計算出的偏移會趨於無窮大,為何不只用normal bias呢?對此之前已經有了回答:我們需要避免Peter Panning現象。
Normal Bias的兩種移動方式,在實際效果上並不完全相同。採用在頂點著色器中改變ShadowMap深度值的方式,會在某些情況下導致Normal Bias失效。參考上圖,最左側為產生陰影的普通情況,由於整個單元內採用C點的深度進行比較,所以ED段會錯誤產生陰影。中間示意圖為頂點著色器增加 ShadowMap 深度的做法,在這種方法中,已知從C到D的深度差距記為d,光線和AB段的法線角度為θ,則最小的消除Shadow Acne的沿法線反方向的距離計算為:
在這種情況下,當θ趨向於90°(即光線與AD近乎平行時),最小的距離趨向於無窮大,Normal Bias就很難達到消除Acne的大小。右側示意圖為片元著色器移動片元位置的計算方法,在角度變化的情況下也不會出現最小距離趨近無窮大的情況。但在片元著色器上進行這部分計算,就比前一種方式要更多的計算量。
條帶狀的「亮暗」漸變,在Unity2019.4.13版本下,使用標注渲染管線,開啟軟陰影後截屏。
「粉刺」狀或者「銼刀」狀的暗斑,在Unity2019.4.13版本下,使用標注渲染管線,關閉軟陰影後截屏。
吐槽:曾經我也很好奇,為何自陰影的別名叫做 Shadow Acne(陰影粉刺),而不是更加常見的陰影條帶。直到我關閉了軟陰影並清空了bias修正後看到上圖。確實這才是二維格子狀紋理出問題後該有的表現,就像下圖這樣,每個獨立的紋素所對應的區域應當被分割成了正方形(正交投影),在二維平面上遇到一定角度的入射光,形成了上圖這種規則排列的「粉刺」狀暗斑。
進過了上面的分析,我們知道 現象<2> 的出現本質是由於標注陰影演算法的缺陷,導致物體自己產生的陰影紋理遮蔽了自己(部分),也就是所謂的「自陰影」。現在我們來看看 現象<1> 的成因,不妨先從2種現象在條件上的差異開始入手。
當場景中存在正常產生陰影的物體時(上圖藍色矩形表示障礙物),全局添加一個Bias,在上圖情況中解決了 Self Shadow 的問題,但是正常應該產生的陰影也被消除了。當Bias從小到大逐漸變化時,對一個正常產生陰影的物體來說,它的陰影表現為由靠近物體的一端開始,逐漸消失。這種現象有個專用術語,叫「漏光」(Light Bleeding)。
接下來,我們通過上圖復現文章開頭引入 現象<1> 的場景布局。還是傾斜入射,這次在物體平面(PL)的正下方多了一層新的平面(UD)。如果不對陰影深度進行修正(bias皆為0),那麼來自陰影紋理中像素A投射的等深線CD會與PL平面相交於點P,此時我們在PL上能看到自陰影效果,既圖中由點BCP包圍的黃色三角形以及由點PDH圍成的黑色三角形區域。當開啟bias後,等深線CD分別沿著光線方向和物體法線的反方向做了2次偏移(圖中紅色箭頭),並來帶了線段GI所在之處。當UD平面與PL平面距離足夠近時,線段GI將於UD平面相交於點P2,同時形成由點EGP2和IJP2圍成了2塊不同區域。此時我們說在平面UD上出現了「漏光」,既本來不該被照亮的區域(EP2)現在被錯誤的照亮了。
取消2個bias,有可能消除下層平面UD上投影異常效果。參考上圖線段CD所代表的陰影紋理等深平面,並沒有出現在平面UD上EJ段的右側(或者說與線段EJ沒有相交),可見這種情況下,UD上能正確顯示來自PL的投影。
還是在取消bias前提下,當我們進一步縮小2個平面之間的距離,可以再次復現異常,此時上下層平面可以近似認為是一個平面,下層平面UD會產生源自PL的自陰影。
現象<1> 成因可以說是Unity為了解決 現象<2> ,在引入bias後間接導致的。下面是我總結的一些有助於減緩 現象<1> 的辦法,和大家一起分享,當然問題的解決方案是開放的,如果大家有其他好方案,希望不要吝嗇得分享出來。
從 現象<1> 的成因分析,它只會出現在光照方向與物體表面成較小夾角時,我們可以通過合理的布置場景中會導致此類現象的物件位置,優化它們的迎光角度,從而避免出現透光現象。
如果我們對出現 現象<1> 的問題物體投影質量要求不高,完全可以取消該物體的投影選項(不參與陰影紋理的生成,但是可以被動接受陰影),然後使用結構相對簡單的投影代理來為該物體實際生成陰影。簡言之,我們解決不了問題,那就解決產生問題的物體就好。
如上圖中綠色箭頭,我們需要通過某種方法,在片原階段,將平面UD沿著其法線方向的反方向做一次小偏移,使之遠離被下壓過來的等深線段GI。這裡面有個難點,我們如何判斷一個像素需要做額外偏移?或者說我們該如何知道物體平面UD需要做額外偏移,而不是物體平面PL呢?一種最簡單的方法是把問題丟給CPU,在預處理階段對不同頂點進行標記,運行時帶入到GPU中參與計算。
C. unity shader 頂點變化之後法線怎麼辦
BumpMap一種是Emboss Bump Map(浮雕凹凸貼圖),它使用的是Height map,原理是在原始圖像的基礎上,對高度場圖像進行復制、輕微平移、差操作。
但它存在很多嚴重的局限性,它只能用於漫反射表面,對於鏡面高光來說是不可能的。
D. Unity Shader:相關數學基礎
本文同時發布在我的個人博客上: https://dragon_boy.gitee.io
有左手坐標系和右手坐標系兩種,Unity使用的是右手坐標系。
我們使用兩個或三個以上的實數來表示一個點的坐標,如 。
矢量是指n維空間中一種包含了模和方向的有向線段。矢量的表示方法和點類似,如 。
矢量通常由一個箭頭表示,由起點指向終點。矢量常被用於表示相對於某個點的偏移,只要矢量的模和方向保持不變,無論在哪裡,都是同一個矢量。
對乘法,將矢量的每個分量和標量相乘即可:
對除法,同理: ,標量需非零。
兩個矢量加減法,把對應的分量進行相加或相減即可:
幾何意義上,矢量相加即前者起點連接至後者終點,矢量相減即後者終點指向前者終點。
矢量的模即矢量的長度:
單位矢量即模為1的矢量。將某個非零矢量轉化為單位矢量的過程稱為歸一化。
矢量之間的乘法有兩種,點積和叉積。
點積公式有兩個:
1:
2( 為兩個矢量的夾角):
點積的結果在幾何意義上是獲得矢量 在 上的投影與 的模的乘積,如果二者都是單位矢量,那麼點積結果就是前者在後者上的投影。
叉積公式:
下面是另一種表示:
上述表明矢量叉積的結果的模是兩矢量構成平行四邊形的面積。
在幾何意義的上,叉積的結果是一個新的矢量,分別垂直於 和 ,且沿 、 和叉積的方向可構成一個右手坐標系。
矩陣有行列之分,以 矩陣為例:
矢量可以看作時 的列矩陣或 的行矩陣。
將矩陣的每個元素和標量相乘即可。
兩個矩陣相乘,要求前一個矩陣的列數等於後一個矩陣的行數,相乘得到的矩陣的行數是第一個矩陣的行數,列數是第二個矩陣的列數。如 的維度是 , 的維度是 ,那麼 的維度是 。
矩陣的乘法即前一個矩陣的每一行( 行)與後一個矩陣的每一列( 列)相乘(可看作行構成的矢量和列構成的矢量點積),每次相乘的結果填寫在對應的 行 列上。
注意,矩陣乘法不滿足交換律,但滿足結合律。
方陣即行列數相等的矩陣。
同時,如果除對角線的元素外全是0的方陣稱為對角矩陣。
針對上述的對角矩陣,如果對角線的元素全是0的話,那麼就稱為單位矩陣。
對於一個 的矩陣 ,它的轉置 為 的矩陣。轉置運算即將原矩陣的行列翻轉。原矩陣的 行變為 列, 列變為 行。
注意,矩陣的轉置的轉置等於原矩陣。
矩陣的串接的轉置等於反向串接各個矩陣的轉置:
只有方陣才有逆矩陣。
一個矩陣和它的逆矩陣的乘積為單位矩陣:
零矩陣沒有逆矩陣。如果一個矩陣有逆矩陣,那麼這個矩陣是可逆的,或非奇異的,相反則是不可逆的或奇異的。
如果一個矩陣的行列式為不為0,那麼該矩陣就是可逆的(具體不解釋)。
注意,逆矩陣的逆矩陣為原矩陣。
單位矩陣的逆矩陣是它本身。
轉置矩陣的逆矩陣是逆矩陣的轉置:
矩陣串接相乘後的逆矩陣等於反向串接各個矩陣的逆矩陣:
注意,如果某個矩陣或矢量進行了一個矩陣變化,那麼使用逆矩陣可以還原這個變換:
如果一個方陣和它的轉置矩陣的乘積是單位矩陣,那麼這個方陣就是正交矩陣:
將正交矩陣與逆矩陣的概念結合起來,就可以得到,如果一個矩陣正交,那麼它的轉置矩陣和逆矩陣相等:
上述式子在實際計算中非常有用,因為逆矩陣的計算量很大(涉及伴隨矩陣),所以如果能判斷某個變換矩陣是正交的話,可以很簡單地用轉置矩陣代替它的逆矩陣進行計算。
那麼如何判斷一個矩陣是否正交,我們把一個矩陣的列看作是一個矢量,稱為基矢量,那麼根據正交矩陣定義:
根據上述等式,得:
, , 都是單位矢量,且兩兩垂直。那麼滿足這樣條件的矩陣的就是正交矩陣。
在Unity中,常將矢量當做是列矩陣來使用,即放到矩陣的右邊來進行乘法。這種情況下,矩陣乘法通常是右乘,即:
變換是將一些數據通過某種方式進行轉換的過程。
一個非常常見的變換是線性變換,指的是可以保留矢量加和標量乘的變換。數學公式為:
縮放是一種線性變換,旋轉也是一種線性變換,我們可以使用一個 的矩陣就可以對一個三維向量進行線性變換。除此之外還有錯切,鏡像,正交投影。
但平移變換不是線性變換,我們不能使用一個 的矩陣對一個三維向量進行變換。
由此出現仿射變換,即合並線性變換和平移變換的變換類型。仿射變換使用一個 的矩陣來表示,這樣我們需要將矢量擴展到四維空間下,即齊次坐標空間。
對於一個三維向量,我們擴展一個維度,分量稱為 。對於坐標, 常設為1,而對於方向,常設為0。因為我們只想對位置進行平移變換,方向不行。
我們使用一個 的矩陣來表示平移、旋轉和縮放。一個基礎的變換矩陣可以分解為4個部分:
表示旋轉縮放的矩陣, 表示平移。
下面是一個平移矩陣(針對點)的例子:
如果是方向矢量的話,由於 分量為0,平移變換不會有影響:
平移變換的逆矩陣就是反向平移得到的矩陣:
平移矩陣並不是正交矩陣。
下面是一個縮放矩陣(針對點)的例子:
對方向矢量縮放:
如果 ,那麼這樣的縮放為統一縮放,否則為非統一縮放。由於非統一縮放會改變角度和比例信息,所以針對方向向量的縮放不能使用上述的方式。
縮放矩陣的逆矩陣如下:
縮放矩陣不是正交矩陣。
上面的矩陣只適合沿坐標軸方向縮放,如果要在任意方向縮放,就要使用一個復合變換,先縮放軸,再沿坐標軸縮放。
繞x軸旋轉:
繞y軸旋轉:
繞z軸旋轉:
旋轉矩陣是正交矩陣。
大多數情況下,將平移、旋轉、縮放結合起來的順序往往是:先縮放,後旋轉,再平移,也就是:
注意,針對旋轉的順序,Unity的順序是zxy,即旋轉變換矩陣是:
旋轉時的坐標系有兩種可以選擇:
在上述兩種方式中,旋轉的結果不一樣,Unity是第一種。
模型空間即模型的局部坐標系,在Unity中是左手坐標系。
世界空間被用來描述物體在場景中的位置,在Unity中,世界空間是左手坐標系。
將頂點從模型空間變換到世界空間的變換稱為模型變換(Model)。這一變換通常由 組成。
也被稱為攝像機空間。
攝像機決定了我們渲染游戲所使用的視角。在觀察空間中,攝像機位於原點,在Unity中,+x指向攝像機的右方,+y指向攝像機的上方,+z指向攝像機的後方,即攝像機指向-z軸。
將頂點從世界空間轉換到觀察空間的變換為觀察變換(View)。
為得到頂點在觀察空間中的位置,有兩種方法:一是計算觀察空間的三個坐標軸在世界空間下的表示,然後構建出從觀察空間變換到世界空間的變換矩陣,再對矩陣求逆來得到從世界空間變換到世界空間的矩陣。二是想像平移整個觀察空間,讓攝像機的原點位於世界坐標的原點,坐標軸與世界空間的坐標軸重合即可。
這里使用第二種方法,即想辦法讓攝像機變換到世界空間的原點即可(注意,由於觀察空間使用右手坐標系,因此需要對z分量取反)。
將頂點從觀察空間轉換到裁剪空間的變換矩陣稱為裁剪矩陣,也被稱為投影矩陣(Projection)。裁剪空間由視錐體決定。
視錐體是空間中的一塊區域,視錐體內的區域是攝像機可以看到的空間。視錐體由六個平面構成,這些平面稱為裁剪平面。視錐體有兩種類型,透視投影和正交投影。
在視錐體的裁剪平面中有兩塊很特殊,分別稱為近裁剪平面和遠裁剪平面,它們決定了攝像機可以看到的深度范圍。
投影矩陣有兩個目的:
我們利用上圖的參數可以計算出近裁剪平面和遠裁剪平面的高度:
裁剪平面的寬度我們可以通過橫縱比得到。假設攝像機的橫縱比為 :
根據上述信息,投影矩陣為:
使用上述投影矩陣後:
可以看到,投影矩陣本質上是對x、y、z坐標進行一些縮放,同時z分量也進行了平移,縮放的目的是為了方便裁剪。此時w分量不再是1,而是-z,通過這個w,我們可以判斷一個頂點是否在視錐體內,在的話必須滿足下面條件:
透視投影後,空間從右手坐標系變為左手坐標系。
假設橫縱比為 ,那麼:
正交投影矩陣:
使用正交投影矩陣後:
使用正交投影後,w分量仍是1。
這一步的變換,我們會得到真正的像素位置。
將頂點從裁剪空間投影到屏幕空間有兩個步驟:首先進行透視除法,用x、y、z分量除以w分量。這一步得到的坐標被稱為NDC,經過透視除法的裁剪空間變換到一個立方體內:
對於正交投影沒什麼區別。
在Unity中,屏幕空間左下角的像素坐標是(0,0)。接下來就是將NDC縮放值屏幕坐標系。
透視除法和屏幕映射的過程總結如下:
法線是一個方向矢量,垂直於表面,與其垂直的一個矢量被稱為切線。
切線由兩個頂點之間的差值計算得來,因此可以使用變換頂點的變換矩陣來變換切線。假設不考慮平移變換,變換後的切線為:
不過,如何直接這么變換的話,法線就有可能不垂直於表面了:
下面我們可以來推導一下正確的法線變換矩陣。
首先,切線與法線垂直,即 ,假設變換法線的矩陣為 ,變換後法線仍與切線垂直,那麼:
經推導得:
由於 ,如果 ,則上式成立,即 。
如果變換矩陣 是正交矩陣,那麼G = M_{A->B},當然,這限於只包含旋轉變換。如果只包含旋轉和統一縮放,那麼 。
內置變數: Built-in shader variables
內置方法: Built-in shader helper functions
E. 如何在Unity3D中計算「法向量A向任意法向量B旋轉,使得兩個向量夾角減小N度後的A向量」
其實我不太明白你究竟想要什麼……
如果只是返回zero或者B的話,我只需要判斷AB方向就可以了,何苦還要計算旋轉呢?
判斷方向很容易,可以使用
Vector3.Angle()
靠攏的話,有這樣一個函數
public static Vector3 RotateTowards(Vector3 current, Vector3 target, float maxRadiansDelta, float maxMagnitudeDelta);
第一個參數是原向量A,第二個是目標向量B,第三個是角度,第四個設置成0.0f就可以了。
(當然,A,B都是單位向量的話。)
但是這個函數看起來是這樣的意思,我並沒有實踐過。
可參考:http://docs.unity3d.com/ScriptReference/Vector3.RotateTowards.html
F. 怎樣能將unity3dcube的頂點坐標移動,讓正方形成為一個梯形
我用C#寫的腳本,JavaScript你自己改一下語法吧
首先那個Cube要有一個Collider,相當於一個碰撞檢測范圍
你創建了Cube之後點擊Add Componet,點Physics,然後點Mesh Collider創建Collider
在Collider的面板中,修改Mesh為你當前的這個Cube
有了Collider便可以在腳本中檢測滑鼠移進移出
public class MouseOverTest : MonoBehaviour {
bool isShowTip;
// Use this for initialization
void Start () {
isShowTip=false;
}
void OnMouseEnter () {
isShowTip=true;
}
void OnMouseExit () {
isShowTip=false;
}
void OnGUI () {
if (isShowTip){
GUI.Label(new Rect(Input.mousePosition.x,Screen.height-Input.mousePosition.y,100,40),"Tips!!");
}
}
}