通過 Unity 實踐渲染的基礎知識 #1 利用矩陣來進行空間變換

2021-12-25 泰鬥社區

本文是陳鍵梁取得 Jasper Flick 官方授權的翻譯整理的譯文,通過Unity實踐來為大家講解一些有關渲染的基礎知識。

文章內容十分豐富有趣,讀者在閱讀過程中,可以通過Unity創建自己的的可視化空間,並自行編寫所有的變換,以此來學習這些基本知識。

相信你已經知道 Meshes 是什麼,也了解如何將它們放置到場景中,但是,究竟位置上的變換是如何實際執行的呢?Shader 又是怎麼知道它該從哪個畫面的哪個位置開始繪製的呢?

當然,我們可以依賴於 Unity 自帶的 Transform 組件及 Shader 去處理這些事情,但是,如果你想要對物體的變換擁有絕對的控制權,那麼理解它們背後的實現原理就是十分重要的!為了完全理解全部過程,我們最好來創造一次自己的實現方式。

位移,旋轉及縮放 Mesh 通過控制頂點的位置來實現,我們稱其為空間變換。我們最好將整個空間可視化,以了解我們究竟在做些什麼。我們可以創造一個由一群點構成的3D網格,這些構成網格的點可以是任一的 Prefab 對象。

using UnityEngine;

public class TransformationGrid : MonoBehaviour {

    publicTransform prefab;

    publicint gridResolution = 10;

    Transform[] grid;

    void Awake () {

        grid = newTransform[gridResolution * gridResolution * gridResolution];

        for (int i = 0, z = 0; z < gridResolution; z++) {

            for (int y = 0; y < gridResolution; y++) {

                for (int x = 0; x < gridResolution; x++, i++) {

                    grid = CreateGridPoint(x, y, z);

                }

            }

        }

    }

}

我們通過實例化 Prefab 及定義其坐標位置來創建點,並給予它直觀的顏色。

Transform CreateGridPoint (int x, int y, int z) {

    Transform point = Instantiate(prefab);

    point.localPosition = GetCoordinates(x, y, z);

    point.GetComponent().material.color = new Color(

        (float)x / gridResolution,

        (float)y / gridResolution,

        (float)z / gridResolution

    );

    return point;

}

接下來我們就來創建立方體,它絕對是最顯眼直觀的網格形狀!我們將它居中於原點,這樣一來所有的變換,尤其是旋轉和縮放就會圍繞著立方體網格的中心點了!

Vector3 GetCoordinates (int x, int y, int z) {

    return new Vector3(

        x - (gridResolution - 1) * 0.5f,

        y - (gridResolution - 1) * 0.5f,

        z - (gridResolution - 1) * 0.5f

    );

}

我使用 Unity 自帶的預設 Cube 作為 Prefab,並將它縮放一半,好讓點與點之間能留點空隙。

創建一個物體,並取名為 Transformation Grid,加上我們寫好的腳本,並拖上 Prefab。這樣當我們進入 Play Mode,由立方體組成的網格則會出現,並且居中於我們物體的相對原點。

理想情況下,我們可以對我們的網格對象進行任意數量的變換,而且我們仍能想到更多的變換類型,但這一次我們先僅僅製作位移,旋轉和縮放吧。

如果將所有的變換統一製作成一種組件類型,我們就可以根據理想的順序將需要的變換類型掛在到網格對象上。有時候這些變換會有些許細節上的不同,所以他們也需要一個將它們的信息應用到空間的點上的方法。

我們先來創建所有變換方式可以繼承的初始組件吧。這會是個虛擬的類,即它無法直接使用,並且不具備任何意義。給它一個虛方法 Apply,這樣其他實質上的變換組件將會使用該方法來各別執行它們的任務。

using UnityEngine;

public abstract class Transformation : MonoBehaviour {

    public abstract Vector3 Apply (Vector3 point);

}

當我們將這些組件拖拽到我們的網格對象時,我們就需要找回他們並以某種方法來將他們應用到我們網格上的點。

我們將會使用 List 來存儲這些組件以便於之後的引用。

using UnityEngine;

using System.Collections.Generic;

 

public class TransformationGrid : MonoBehaviour {

 

    …

    List transformations;

 

    void Awake () {

        …

        transformations = new List();

    }

}

接下來我們可以加入 Update 方法來找回我們所有的變換組件,並且通過循環整個立方體網格來變換我們所有的點。

voidUpdate () {

    GetComponents(transformations);

    for (int i = 0, z = 0; z < gridResolution; z++) {

        for (int y = 0; y < gridResolution; y++) {

            for (int x = 0; x < gridResolution; x++, i++) {

                grid.localPosition = TransformPoint(x, y, z);

            }

        }

    }

}

變換點是通過獲取它原始坐標,再將各種變換方式應用到該點上。我們不能依賴這些點的真實位置,因為它們都已經變換過了,而且我們也不需要將積累每一幀的所經歷的變換內容。

Vector3 TransformPoint (int x, int y, int z) {

    Vector3 coordinates = GetCoordinates(x, y, z);

        for (int i = 0; i < transformations.Count; i++) {

            coordinates = transformations.Apply(coordinates);

        }

    return coordinates;

}

我們第一個真正意義上的變換組件是最容易實現的位移。我們來創建一個新的繼承於 Transformation 的組件,它也有個 position 的變量來作為相對位置的偏移量。

using UnityEngine;

public class PositionTransformation : Transformation {

    public Vector3 position;

}

此時,會正常地出現編譯器報錯,這是因為我們並沒有為它提供實實在在的 Apply 方法,那我們就來寫一個吧!其實只需簡單地添加原始點的所需的新位置就好了。

public override Vector3 Apply (Vector3 point) {

    return point + position;

}

現在你可以對網格對象添加位置變換了。這可以讓我們在不改變實際網格物體的位置下移動它所有的點,並且所有的變換都基於物體的相對位置。

位移

接下來我們要做的是縮放變換。和位移的原理幾乎是一模一樣的,唯一不同的是位移是將原點與數值相加,而縮放則是將原點與數值相乘。

using UnityEngine;

public class ScaleTransformation : Transformation {

    public Vector3 scale;

    public override Vector3 Apply (Vector3 point) {

        point.x *= scale.x;

        point.y *= scale.y;

        point.z *= scale.z;

        return point;

    }

}

當我們將這個組件掛載到我們的網格對象之後,它就能夠隨意縮放啦。值得注意的是我只是僅僅改變了所有點的位置,所以點的體積在畫面表現上是不會有任何改變的。

調整縮放

我們來嘗試對物體同時間進行位移與縮放,你會發現縮放同時影響了位移的距離,這是因為當我們是先對空間進行了位移後,接下來才縮放它。

Unity的Transform組件使用了其他的方式來實現它們,並且比這更為有效。而我們如果要解決這樣的問題,只要改變組件的順序就可以啦。我們可以點擊組件右上方的齒輪按鈕,並通過彈出的窗口來移動他們。

第三種變換模式是旋轉,旋轉的實現相比前兩者則顯得更為困難。一開始我們先創建一個新組件並返回尚未改變的點。

using UnityEngine;

public class Rotation Transformation : Transformation {

    public Vector3 rotation;

    public override Vector3 Apply (Vector3 point) {

        return point;

    }

}

那旋轉是怎麼實現的呢?我們先來限制一下旋轉的軸向,讓其僅繞 Z 軸旋轉。讓點像個旋轉的輪子一般繞著這個軸旋轉。由於 Unity 採用左手坐標系,所以當我們從正的Z軸觀察時,正值的旋轉是會讓這輪子往逆時鐘旋轉的。

圍繞Z軸2D旋轉

那麼,當點的坐標開始旋轉時,會發生什麼呢?最簡單的思考方式就是讓這些點放在圓心位於圓點,半徑為1的圓與坐標軸的交點上。那麼當我們將這些點旋轉90度的時候,我們始終會得到同樣的坐標值,即是0, 1,或者-1。

當旋轉90度及180度的時候,則坐標為(1,0)和(0,1)

在開始的第一步,點的坐標會從(1,0)變成(0,1),接下來則會是(-1,0),然後是(0,-1),最後又回到(1,0)。

相反的,如果我們是從(0,1)開始,那麼我們會比上一個順序快一步,我們會從(0,1)到(-1,0)到(0,-1)到(1,0)然後又回到原位。

所以我們的點的坐標是個0,1,0,-1的循環,他們僅僅是開始的點不同而已。

那如果我們只是旋轉45度呢?

那樣在XY平面上對角線的產生新的點,並且該點到原點的距離保持不變,我們必須將這些坐標規範於(±√½, ±√½)之內,這回將我們的循環展開為0,,√½,,1, √½, 0, −√½, −1, −√½,如果我們繼續降低步長,最終會得到一個正弦波。

正弦和餘弦

在我們的案例裡,正弦波與Y坐標在以(1,0)為起點的時候相匹配,而餘弦則與X坐標相匹配。這意味著我們可以重新定義(1,0)為(cosz,sinz)。同樣的,我們可以將(0, 1)替換為(−sinz,cosz)。

接下來我們開始計算正弦和餘弦繞著Z軸旋轉所需的值。我們雖然提供的是角度,但實際上正弦及餘弦只能作用於弧度,所以我們需要轉化它。

public override Vector3 Apply (Vector3 point) {

    float radZ = rotation.z * Mathf.Deg2Rad;

    float sinZ = Mathf.Sin(radZ);

    float cosZ = Mathf.Cos(radZ);

 

    return point;

}

真開心,我們終於找到辦法旋轉點(1,0)和點(0,1)了!那麼,下一步,我們要如何才能旋轉任意點呢?我們注意到:這兩個點恰好也定義了 X 軸與 Y 軸,我們可以將任意 2D 點(x,y)分解成 xX + yY。

在沒有任何旋轉的時候,它相等於 x(1,0)+y(0,1) 也即是僅僅的(x,y)。但是,當它開始旋轉的時候,我們就可以使用x(cosZ,sinZ)+y(-sinZ,cosZ)並且最終得到一個正確旋轉的點。

你可以設想成一個點調到了一個單位圓上,旋轉,然後再縮放回來。我們將其壓縮成(xcosZ - ysinZ, xsinZ + ycosZ )單一的坐標對。

return new Vector3(

    point.x * cosZ - point.y * sinZ,

    point.x * sinZ + point.y * cosZ,

    point.z

);

我們將這旋轉組件添加到網格上並讓它成為在變換的順序中擺在中間的位置,即是說我們先縮放,旋轉,最後才進行移位,這也即是Unity's Transform的做法。當然我們現在只是僅僅的支持繞著Z軸旋轉,我們稍後才處理其他兩個軸。

正在進行三種變換模式

現在我們只能繞Z軸旋轉。為了能提供與Unity Transform組件相同功能的旋轉支持,我們必須讓繞X軸及Y軸旋轉成為可能。

如果只是單獨的繞著某個軸旋轉它就會與僅繞Z軸旋轉極為相似,一旦繞著多個軸旋轉則會變得更為複雜。為了解決這一點,我們可以用更為直觀的方式來記下我們的旋轉數學。

從現在開始,我們將點的坐標寫成垂直的形式以取代原本的水平形式。我們將使用:

來取代:

同樣地,我們將

分兩列寫成:

這樣,閱讀起來就更加直觀了。

注意,現在,X和Y已經寫成垂直的列表示的形式,這是因為,我們需要使用

與其他因子相乘,這意味著我們需要計算2維矩陣乘法。實際上,我們前面已經計算過的:

就是一個矩陣乘法。在這2x2矩陣裡,第一列表示的是X軸,而第二列表示的則是Y軸。

2D矩陣定義的x軸/y軸

一般來說,兩個矩陣的相乘是以第一矩陣的逐行(橫向)與第二矩陣的逐列(豎向)相乘。最後所獲得的矩陣中每個項是行的項與列的對應項相乘的總和,即是說第一矩陣的行長度必須與第二矩陣的列長度相等。

2個2x2矩陣相乘

所得矩陣的第一行將會包含行1×列1,行1×列2,等等。 第二行則是包含行2×列1,行2×列2,等等。 因此,它的行長度會與第一矩陣相同,而列長度與第二矩陣相同。

目前我們已經有個能夠讓2D點圍繞著Z軸旋轉的2x2矩陣了呢,但是我們實際上的變換還是需要使用3D點的。所以我們試圖計算乘法:

但是,該乘法因矩陣的行和列長度不匹配而無法成立,因此我們需要將我們的旋轉矩陣擴充到3x3才行。 如果我們只是用零填充會發生什麼?

所得的X值和Y值十分正確,但是Z值總是為零,這顯然是錯誤的。 為了保持Z值不變,我們必須在我們的旋轉矩陣的右下角插入1。 這應該很好理解,畢竟第三行代表著Z軸呀。也即是:

如果我們同時間對所有的三個維度使用這技巧,我們將最終得到一個由沿著對角線的1且其他值為0的矩陣,也即是眾所皆知的單位矩陣,因為它即使與任何矩陣相乘都不會改變它的值。它就像一個過濾器,讓一切事物通過它並保持不變。

我們繼續沿用繞Z軸旋轉的思路,就可以推斷出繞著Y軸旋轉的矩陣。

首先,X軸開始的矩陣為:

逆時針旋轉90度後會變成:

也即是說,X軸的旋轉矩陣可以表示為:

Z軸與X軸相差了-90度,所以表示為:

而Y軸的旋轉矩陣保持不變,最終可得完整的旋轉矩陣:

第三個旋轉矩陣則將X軸設為常量,並以剛才的方法推算出Y軸與Z軸的旋轉公式。

目前我們每單個旋轉矩陣僅僅是繞著一個軸旋轉。為了重現與Unity一樣的旋轉變換,我們得遵循一定的順序。首先繞著Z軸旋轉,接下來繞Y軸,最後才繞X軸旋轉。

我們首先將Z旋轉應用到我們的點上,然後再將Y旋轉應用到剛才的結果上,最後才將X的旋轉應用到最終的結果上,這樣就可以達到我們理想中的旋轉變換了。

我們也可以將旋轉矩陣彼此相乘,結果將會誕生出能夠同時應用3種旋轉方式的新矩陣。我們首先計算 Y x Z。

我們在所得的矩陣中第一條目的計算為:cosXcosZ - 0sinZ - 0sinY = cosYcosZ。

整個矩陣雖然充斥著大量的乘法,但是許多部分所得的最終值都會是0,這些結果都可以忽略不計。

現在我們通過計算X × (Y × Z)來獲得我們最終的矩陣。

現在我們可以通過所獲得的矩陣來觀察X,Y和Z軸的旋轉是如何構造形成的。

圍繞3個軸的旋轉

既然我們可以將三種旋轉結合為一個矩陣,那我們是否也能將縮放、旋轉和位移也結合為一個矩陣呢?如果我們能將縮放及位移同樣以矩陣乘法來表示,那我們自然能辦得到。

我們只要取單位矩陣並直接縮放其值,就可以直接生成新的縮放矩陣。

那我們又要如何支持重新定位呢?這並不是重新定義三個軸向,它只是一種偏移量,所以我不能以現有的3x3矩陣來表示它。我們需要額外增加新的行來包含偏移量。

但是現在的問題是我們的矩陣列長度變成了4,使得接下來的計算將無法順利進行。

所以我們需要添加新的列到我們的點上,並且該列與對應的偏移量相乘後的值必須為1,而且我們需要好好保存該值,這樣我們可以將其應用在往後的矩陣乘法中。這致使我們進入到了4x4矩陣及4D點的領域。

所以接下來我們必須使用4x4矩陣作為我們的變換矩陣了。即是說我們的縮放及旋轉矩陣也需要添加新的行和列,它將由一群0及右下角的值為1組成。現在我們所有的點將擁有第4個坐標,並且其值將永遠保持為1。

試問我們是否能明白第四坐標的存在意義是什麼嗎?它是否表示著任何有用的東西?我們知道當我們賦予它的值為1的時候,表示我們可以重新定位這些點;如果其值為0,那我們將忽視它的偏移量,但是縮放和旋轉還是會照常運作。

僅僅能縮放及旋轉,但是不會移動的東西絕不會是點。那是向量,是方向。

所以點可以表示成:

而向量則表示為:

太棒了,因為這樣一來,我們就能使用同一個矩陣來同時變換位置,法線及切線了。

那如果第四坐標的值是0和1以外的值,會發生什麼呢?好吧,這不該發生,又或者說它即使發生了也不會有任何實質上的區別。

我們現在是在和其次坐標打交道。其概念是指空間中的每個點都可以表示成無限量的坐標集合。最直接的表現形式就是使用1作為第四坐標,而其他的替代形式可以在坐標集合與任一數值的乘法中找到。

那我們將每個坐標除以即將被拋棄的第四坐標後可以得到歐幾裡得點,也即是實質上的3D點。

當然在第四坐標為0的時候,這方法根本不管用。這樣點的位置會被放置到無限遠,這就是為何它們會被稱為方向。

我們可以使用Unity的 Matrix4x4 結構體來執行矩陣的乘法並用它來取代我們現有的變換方法。

在 Transformation 中添加一個只讀的虛屬性來找回執行變換的矩陣。

public abstract Matrix4x4 Matrix { get; }

它的 Apply 方法已經不再需要使用虛方法了。在往後的乘法中它會僅僅通過獲取矩陣來執行變換。

public Vector3 Apply (Vector3 point) {

    return Matrix.MultiplyPoint(point);

}

值得注意的是 Matrix4x4.MultiplyPoint 中只有一個3D向量的參數,不使用4D向量是因為它的第4維度的值已被假設為1.它也同時負責將其次坐標變換回歐幾裡得坐標的任務。如果你想要使用方向而不是點來相乘,你可以使用 Matrix4x4.MultiplyVector。

目前已經具體實現的變換類需要將Apply方法更改成用矩陣屬性來實現。

首先是PositionTransformation。Matrix4x4.SetRow方法就能填充矩陣,使用起來簡單方便。

publicoverrideMatrix4x4 Matrix {

    get {

        Matrix4x4 matrix = newMatrix4x4();

        matrix.SetRow(0, newVector4(1f, 0f, 0f, position.x));

        matrix.SetRow(1, newVector4(0f, 1f, 0f, position.y));

        matrix.SetRow(2, newVector4(0f, 0f, 1f, position.z));

        matrix.SetRow(3, newVector4(0f, 0f, 0f, 1f));

        returnmatrix;

    }

}

接下來是ScaleTransformation。

 

publicoverrideMatrix4x4 Matrix {

    get {

        Matrix4x4 matrix = newMatrix4x4();

        matrix.SetRow(0, newVector4(scale.x, 0f, 0f, 0f));

        matrix.SetRow(1, newVector4(0f, scale.y, 0f, 0f));

        matrix.SetRow(2, newVector4(0f, 0f, scale.z, 0f));

        matrix.SetRow(3, newVector4(0f, 0f, 0f, 1f));

        returnmatrix;

    }

}

至於 RotationTransformation, 將矩陣逐行的設置顯得更為方便,而且它也與既有的代碼匹配。

publicoverrideMatrix4x4 Matrix {

    get {

        float radX = rotation.x * Mathf.Deg2Rad;

        float radY = rotation.y * Mathf.Deg2Rad;

        float radZ = rotation.z * Mathf.Deg2Rad;

        float sinX = Mathf.Sin(radX);

        float cosX = Mathf.Cos(radX);

        float sinY = Mathf.Sin(radY);

        float cosY = Mathf.Cos(radY);

        float sinZ = Mathf.Sin(radZ);

        float cosZ = Mathf.Cos(radZ);

 

        Matrix4x4 matrix = newMatrix4x4();

        matrix.SetColumn(0, newVector4(

            cosY * cosZ,

            cosX * sinZ + sinX * sinY * cosZ,

            sinX * sinZ - cosX * sinY * cosZ,

            0f

        ));

        matrix.SetColumn(1, newVector4(

            -cosY * sinZ,

            cosX * cosZ - sinX * sinY * sinZ,

            sinX * cosZ + cosX * sinY * sinZ,

            0f

        ));

        matrix.SetColumn(2, newVector4(

            sinY,

            -sinX * cosY,

            cosX * cosY,

            0f

        ));

        matrix.SetColumn(3, newVector4(0f, 0f, 0f, 1f));

        returnmatrix;

    }

}

我們現在將所有的變換矩陣組合為一個新矩陣。在TransformationGrid中添加一個矩陣變量transformation:

Matrix4x4 transformation;   

我們會在每次Update中更新transformation的矩陣變量。它的具體行為包括獲取第一個矩陣,然後將它與其他的矩陣相乘。必須確認它們是按照正確的順序相乘的。

voidUpdate () {

    UpdateTransformation();

    for (int i = 0, z = 0; z < gridResolution; z++) {  

         …

    }

}

 

void UpdateTransformation () {

    GetComponents(transformations);

    if (transformations.Count > 0) {

        transformation = transformations[0].Matrix;

        for (int i = 1; i < transformations.Count; i++) {

            transformation = transformations.Matrix * transformation;

        }

    }

}

現在開始TransformationGrid將不再調用Apply方法,取而代之的是它會直接執行自身的矩陣乘法。

Vector3 TransformPoint (int x, int y, int z) {

    Vector3 coordinates = GetCoordinates(x, y, z);

    returntransformation.MultiplyPoint(coordinates);

}

現在我們的新方法顯得更有效率,因為我們之前的方式是為每個點創建個別的變換矩陣,並單獨的應用它們,而現在我們創建的是統一的變換矩陣並且在每個點上重複使用它。

Unity也使用相同的技巧來創建單一的變換矩陣,從中減少了每個對象的層次結構。我們可以在我們的例子中繼續提高它們的執行效率。所有的變換矩陣都具有相同的底列:

發現這一點後,我們可以直接忽略該列,跳過其所有0的計算和最後的轉化除法。 Matrix4x4.MultiplyPoint4x3 方法也確實是這麼做的。 但是, 依然有一些很有用的變換會改變底列的值。

目前,我們已經實現將點從3D空間裡的位置變換到另一個位置上,那我們該如何在2D的屏幕上最終繪製出那些點呢?這需要實現從3D空間到2D空間的變換,我們可以為此創建一個新的變換矩陣!

我們從單位矩陣開始來實現攝像機投影的變換吧!

using UnityEngine;

 

publicclassCameraTransformation : Transformation {

 

    publicoverrideMatrix4x4 Matrix {

        get {

            Matrix4x4 matrix = newMatrix4x4();

            matrix.SetRow(0, newVector4(1f, 0f, 0f, 0f));

            matrix.SetRow(1, newVector4(0f, 1f, 0f, 0f));

            matrix.SetRow(2, newVector4(0f, 0f, 1f, 0f));

            matrix.SetRow(3, newVector4(0f, 0f, 0f, 1f));

            return matrix;

        }

    }

}

把它掛載到對象上,並把它的變換順序安排到最後一個。

攝像機投影是最後才執行

從3D到2D的變換最直接的方式是簡單地捨棄一個維度,這會將3D空間摺疊成一個平面。這平面就好像是用來渲染場景的畫布一般。我們就直接拋棄Z的維度看看結果會如何。

我們的網格確實變成2D了。你仍然可以執行所有的變換如縮放、旋轉、包括重新定位,但是最終的結果會輸出到XY的平面上。這就是正交攝影機投影的初步實現。

現在我們的原始攝像機位置坐落在原點並望向正Z的方向,試問我們能否移動並旋轉它?答案是肯定的,事實上我們已經這麼做了。

移動攝影機與將整個世界往反方向移動在視覺表現上是一樣的,當然旋轉與縮放的結果也是。儘管有些尷尬,但是我們可以用現有變換方式來移動攝像機。Unity則使用了逆矩陣來達到同樣的效果。

儘管正交攝影機的效果很贊,但是它並不能呈現我們現實中所看到的世界,我們仍然需要一個透視攝像機。透視的概念既是物體距離我們越遠,它看上去就會越小。

我們可以通過點與攝像機的距離來縮放它們以重現我們想要的效果。

我們直接讓所有的值除以Z的坐標。那我們是否可以使用矩陣乘法來實現它?必須的,我們通過改變單位矩陣的底列,使之成為:

改變之後第四坐標會相等於原始的Z坐標。將其次坐標變換為歐幾裡得坐標並且執行所需的除法。

matrix.SetRow(0, newVector4(1f, 0f, 0f, 0f));

matrix.SetRow(1, newVector4(0f, 1f, 0f, 0f));

matrix.SetRow(2, newVector4(0f, 0f, 0f, 0f));

matrix.SetRow(3, newVector4(0f, 0f, 1f, 0f));

與正交攝影機最大的差異是那些點不會直接下移到投影平面。相反的,它們會往攝像機的方向—— 原點移動,一直移動它們觸碰到平面為止。當然這僅僅適用於位於攝像機前方的點。

那些位移攝像機後方的點將會投影錯誤。由於我們不能捨棄這些點,所以我們得確保所有的一切通過重新定位的方式呈現到攝像機前方。如果你並沒縮放或旋轉你的網格,那距離5就足夠呈現完整的網格了,否則你可能需要更遠的距離。

原點與投影平面之間距離也會影響投影的效果。它就像是攝像機的焦距,當你把它放的越大,那你的視野範圍就越小。現在我們使用的焦距為1,它將會提供我們角度為90的視野範圍。我們也可以將它的值設為可隨意配置。

public float focalLength = 1f;

焦距

當焦距越大意味著我們正在放大畫面,它將有效的增加我們最終點的大小,所以我們可以通過這種方式來實現它。而由於我們已經將Z維度摺疊起來了,我們就不用去縮放它了。

matrix.SetRow(0, newVector4(focalLength, 0f, 0f, 0f));

matrix.SetRow(1, newVector4(0f, focalLength, 0f, 0f));

matrix.SetRow(2, newVector4(0f, 0f, 0f, 0f));

matrix.SetRow(3, newVector4(0f, 0f, 1f, 0f));

調整焦距中

我們現在實現十分簡單的透視攝像機了。如果我們想要完完全全的仿造Unity的攝像機投影,我們還是需要與近距離平面和遠距離平面打交道。那需要將網格對象投影到立方體而非平面,所以深度的信息必須保留,並且我們還需要考慮到視圖的寬高比。

此外,Unity的攝影是往負Z方向看的,那即是說我們還需要取消部分數值。你可以將這些信息包含到投影矩陣裡,不過接下來就是你的作業了,自己去實現吧!

我們這麼做的目的是什麼?平常鮮少會自行去構建矩陣,即使有也絕不會是投影矩陣。

關鍵是你現在明白投影矩陣究竟是怎麼回事了。矩陣並不可怕,它們只不過是將點及向量從某個空間變換到另一個空間。現在你對矩陣也有了進一步的了解了,這很棒,因為當我們在開始寫自己的Shader的時候,我們還是會再次接觸矩陣的。

相關焦點

  • Unity中BVH骨骼動畫驅動的可視化理論與實現
    中驅動角色,而 unity 中的角色大部分是基於 Tpose 的,因而可以利用 Tpose 作為中間轉換姿態,將 BVH 中的所有動畫幀全部遷移到 unity 中。變換矩陣 T1/T2這一步在BVH和unity中都涉及到一個變換矩陣,如果內置姿態本來就是Tpose,那麼變換矩陣就是單位陣。
  • Unity3dCG語言編寫Shader之渲染管線
    二、幾何階段主要負責頂點坐標變換、光照、裁剪、投影以及屏幕映射,改階段基於GPU進行運算,在該階段的末端得到了經過變換和投影之後的頂點坐標、顏色、以及紋理坐標。簡而言之,幾何階段的主要工作就是「變換三維頂點坐標」和「光照計算」。問題隨之而來,為什麼要變換頂點坐標?
  • 程序丨Unity 渲染教程(十四):霧
    系列回顧:Unity 渲染教程(一):矩陣Unity 渲染教程(二):著色器基礎Unity 渲染教程(三):使用多張紋理貼圖Unity 渲染教程(四):第一個光源Unity 渲染教程(五):多個光源Unity 渲染教程(六):凹凸度Unity 渲染教程(七)
  • 仿射變換和透視變換
    ,但是他們的本質還是通過某個矩陣,將圖像每個像素點的坐標變換到另一個新的位置。這種通過某個矩陣將圖像進行變換的方法通常稱為線性變換,也就是說利用了向量加法和標量乘法。【注】本文的變換僅僅針對二維矩陣,非針對三維矩陣的變換。
  • 崩壞3角色渲染技術分析
    我們團隊在過去一年多主要關注卡通渲染效果,本次介紹我們對崩壞3這款遊戲角色渲染的研究和實踐。
  • 程序丨Unity渲染教程(十七):混合光照
    系列回顧:Unity 渲染教程(一):矩陣Unity 渲染教程(二):著色器基礎Unity 渲染教程(三):使用多張紋理貼圖Unity 渲染教程(四):第一個光源Unity 渲染教程(五):多個光源Unity 渲染教程(六):凹凸度Unity 渲染教程(七)
  • Unity可編程渲染管線系列(十一)後處理(全屏特效)
    通過使用著色器渲染全屏四邊形來完成此操作,該著色器根據其屏幕空間位置對紋理進行採樣。通過檢查幀調試器中的「Dynamic Draw」條目,可以看到一些提示。顏色紋理已分配給_MainTex,並且使用四個頂點和索引。
  • 線性代數/矩陣的幾何意義
    無意間發現了一個寶藏up主3Blue1Brown[1]在看視頻的時候,偶然發現一個集合:線性代數的本質。突然想起上學的時候矩陣論(線性代數)老師總說矩陣很優美,但我又一直沒get到,所以好奇想看一看。但是有沒有啥用呢?在評論裡看到矩陣是圖形學的基礎,於是搜了下,發現它比我想的有用多了!
  • 一次有趣的Elasticsearch+矩陣變換聚合實踐
    ,即日期的滑動窗口在1~31天(這個限定範圍是與業務部門多次討論得來,否則後面實現的代價會更大,原有是多個月的窗口期)04矩陣轉換嘗試過幾次不同的技術產品之後得出結論,單一的數據產品已有能力是無法滿足要求的,正可謂魚與熊掌不可兼得。所以必須改變思維,設計了一種矩陣變換的算法機制,結合Hive+ES實現,以下介紹這種技術實現方式。
  • Unity Shader學習:X-Ray
    本文使用Z-Test、Stencil-Test來實現。使用較老的Built-in管線完成開發,如前一篇所說,隨著學習的開展將逐步遷至URP管線。方案1. 僅Z-TestUnity通過渲染隊列(Render Queue)來控制渲染的順序,渲染隊列由整數索引號表示,按索引號從小到大的順序渲染。
  • Android OpenGL ES(7)---坐標系統與矩陣變換
    下面的這張圖展示了整個流程以及各個變換過程做了什麼:1.局部坐標系局部坐標系指的是圖中的LOCAL SPACE,又被稱作本地坐標系、本地空間、局部空間等。總是這就是一個小範圍的概念,描述的只是模型本身的坐標。也是模型文件中存儲的頂點坐標所在的坐標系。
  • 《Unity Shader入門精要》筆記(八)
    漸變紋理1.1 概念使用漸變紋理控制漫反射光照的結果,可以保證物體的輪廓線相比於之前傳統的漫反射光照更加明顯,使之產生插畫的風格。現在很多卡通風格的渲染也是用這種技術。1.2 實踐:使用漸變紋理控制漫反射光照1.2.1 準備工作完成如下準備工作:詳細操作詳見《<Unity Shader入門精要>筆記(四)》裡的案例常用操作說明。
  • 為什麼要進行傅立葉變換?
    每種傅立葉變換都分成實數和複數兩種方法,對於實數方法是最好理解的,但是複數方法就相對複雜許多了,需要懂得有關複數的理論知識,不過,如果理解了實數離散傅立葉變換 (real DFT),再去理解複數傅立葉就更容易了,所以我們先把複數的傅立葉放到一邊去,先來理解實數傅立葉變換,在後面我們會先講講關於複數的基本理論,然後在理解了實數傅立葉變換的基礎上再來理解複數傅立葉變換。
  • 為什麼要進行傅立葉變換?(必看)
    面對這種困難,方法是把長度有限的信號表示成長度無限的信號,可以把信號無限地從左右進行延伸,延伸的部分用零來表示,這樣,這個信號就可以被看成是非周期性離解信號,我們就可以用到離散時域傅立葉變換的方法。還有,也可以把信號用複製的方法進行延伸,這樣信號就變成了周期性離散信號,這時我們就可以用離散傅立葉變換方法進行變換。
  • 二維傅立葉變換與逆變換基於Unity的實現
    先把思路捋清楚講傅立葉變換的定義有很多, 推薦大家各種搜一下比如3B1B的傅立葉變換:https://www.bilibili.com/video/BV1pW411J7s8比如這篇文章:https://zhuanlan.zhihu.com/p/19763358
  • 基礎渲染系列(二)——著色器
    為了更好地控制渲染過程,它需要擺脫多餘的花哨的東西,那麼首先來關注一下我們的基礎方面。實際上,如第1部分「矩陣」中所述,使用了整個轉換層次結構。如果對象最終出現在相機的視圖中,則安排進行渲染。最後,GPU的任務是渲染對象的網格。具體的渲染說明由對象的材質定義。該材質引用了著色器(它是GPU程序)及其可能具有的任何設置。
  • Unity shader實例3則:輪廓渲染,溼滑的馬路,毛茸茸的大尾巴!
    unity shader實例#1 輪廓渲染-描邊1.法線外擴一般期望的描邊效果,就是在模型外面有一圈選邊,因此我們可以把模型擴大一點點,利用這個擴大的邊緣來實現描邊效果
  • Unity Shader實現《死亡擱淺》掃描效果!
    逆矩陣方式重建深度重建有幾種方式,先來看一個最簡單粗暴,但是看起來最容易理解的方法:我們得到的屏幕空間深度圖的坐標,xyz都是在(0,1)區間的,需要經過一步變換,變換到NDC空間,OpenGL風格的話就都是(-1,1)區間,所以需要首先對xy以及xy對應的深度z進行
  • Unity3d-C#入門基礎
    Unity3d-C#入門基礎Unity主要支持3種語言: C# (pronounced C-sharp), an industry-standard language similar to Java or C++; UnityScript
  • 其實矩陣並不陌生,完全可以從中學知識「遷移」
    尤其要注意的是向量空間和矩陣是新概念,解線性方程組作為矩陣的一個具體應用引入是非常合適和自然的,要適應這一新的工具。而消元法就是高斯消元法,這在初中的時候就已經有過接觸了。數學的學習是個認知結構建構的過程,而對於陌生的n維向量空間和矩陣兩個概念,對很多人來說有點陌生,甚至感覺很抽象,要把這兩個概念納入認知結構,首先考慮一下有沒有學過什麼知識是和它們相關的呢?