通过 Unity 实践渲染的基础知识 #1 利用矩阵来进行空间变换

作者:陈键梁
2016-11-14
22 43 6

引言

本文是陈键梁取得 Jasper Flick 官方授权的翻译整理的译文,通过Unity实践来为大家讲解一些有关渲染的基础知识。

文章内容十分丰富有趣,读者在阅读过程中,可以通过Unity创建自己的的可视化空间,并自行编写所有的变换,以此来学习这些基本知识。

开始阅读本文之前。,建议先阅读同样来自Jasper Flick的Procedural Grid的教程并了解Mesh的原理。

点击这里查看原文。

1.可视化空间

相信你已经知道 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[i] = 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,并将它缩放一半,好让点与点之间能留点空隙。

1

创建一个物体,并取名为 Transformation Grid,加上我们写好的脚本,并拖上 Prefab。这样当我们进入 Play Mode,由立方体组成的网格则会出现,并且居中于我们物体的相对原点。

2

2.变换

理想情况下,我们可以对我们的网格对象进行任意数量的变换,而且我们仍能想到更多的变换类型,但这一次我们先仅仅制作位移,旋转和缩放吧。

如果将所有的变换统一制作成一种组件类型,我们就可以根据理想的顺序将需要的变换类型挂在到网格对象上。有时候这些变换会有些许细节上的不同,所以他们也需要一个将它们的信息应用到空间的点上的方法。

我们先来创建所有变换方式可以继承的初始组件吧。这会是个虚拟的类,即它无法直接使用,并且不具备任何意义。给它一个虚方法 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[i].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[i].Apply(coordinates);
        }
    return coordinates;
}
2.1 位移

我们第一个真正意义上的变换组件是最容易实现的位移。我们来创建一个新的继承于 Transformation 的组件,它也有个 position 的变量来作为相对位置的偏移量。

using UnityEngine;

public class PositionTransformation : Transformation {

    public Vector3 position;

}

此时,会正常地出现编译器报错,这是因为我们并没有为它提供实实在在的 Apply 方法,那我们就来写一个吧!其实只需简单地添加原始点的所需的新位置就好了。

public override Vector3 Apply (Vector3 point) {
    return point + position;
}

现在你可以对网格对象添加位置变换了。这可以让我们在不改变实际网格物体的位置下移动它所有的点,并且所有的变换都基于物体的相对位置。

3

4

位移
2.2 缩放

接下来我们要做的是缩放变换。和位移的原理几乎是一模一样的,唯一不同的是位移是将原点与数值相加,而缩放则是将原点与数值相乘。

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;
    }
}

当我们将这个组件挂载到我们的网格对象之后,它就能够随意缩放啦。值得注意的是我只是仅仅改变了所有点的位置,所以点的体积在画面表现上是不会有任何改变的。

5

6

调整缩放

我们来尝试对物体同时间进行位移与缩放,你会发现缩放同时影响了位移的距离,这是因为当我们是先对空间进行了位移后,接下来才缩放它。Unity的Transform组件使用了其他的方式来实现它们,并且比这更为有效。而我们如果要解决这样的问题,只要改变组件的顺序就可以啦。我们可以点击组件右上方的齿轮按钮,并通过弹出的窗口来移动他们。

7

2.3 旋转

第三种变换模式是旋转,旋转的实现相比前两者则显得更为困难。一开始我们先创建一个新组件并返回尚未改变的点。

using UnityEngine;

public class Rotation Transformation : Transformation {

    public Vector3 rotation;

    public override Vector3 Apply (Vector3 point) {
        return point;
    }
}

那旋转是怎么实现的呢?我们先来限制一下旋转的轴向,让其仅绕 Z 轴旋转。让点像个旋转的轮子一般绕着这个轴旋转。由于 Unity 采用左手坐标系,所以当我们从正的Z轴观察时,正值的旋转是会让这轮子往逆时钟旋转的。

8

围绕Z轴2D旋转

那么,当点的坐标开始旋转时,会发生什么呢?最简单的思考方式就是让这些点放在圆心位于圆点,半径为1的圆与坐标轴的交点上。那么当我们将这些点旋转90度的时候,我们始终会得到同样的坐标值,即是0, 1,或者-1。

9

当旋转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, −√½,如果我们继续降低步长,最终会得到一个正弦波。

10

正弦和余弦

在我们的案例里,正弦波与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轴旋转,我们稍后才处理其他两个轴。

11

12

正在进行三种变换模式

3.完全旋转

现在我们只能绕Z轴旋转。为了能提供与Unity Transform组件相同功能的旋转支持,我们必须让绕X轴及Y轴旋转成为可能。如果只是单独的绕着某个轴旋转它就会与仅绕Z轴旋转极为相似,一旦绕着多个轴旋转则会变得更为复杂。为了解决这一点,我们可以用更为直观的方式来记下我们的旋转数学。

3.1 矩阵

从现在开始,我们将点的坐标写成垂直的形式以取代原本的水平形式。我们将使用:

13

来取代:

14

同样地,我们将

15

分两列写成:

16

这样,阅读起来就更加直观了。

注意,现在,X和Y已经写成垂直的列表示的形式,这是因为,我们需要使用

17

与其他因子相乘,这意味着我们需要计算2维矩阵乘法。实际上,我们前面已经计算过的:

18

就是一个矩阵乘法。在这2x2矩阵里,第一列表示的是X轴,而第二列表示的则是Y轴。

19

2D矩阵定义的x轴/y轴

一般来说,两个矩阵的相乘是以第一矩阵的逐行(横向)与第二矩阵的逐列(竖向)相乘。最后所获得的矩阵中每个项是行的项与列的对应项相乘的总和,即是说第一矩阵的行长度必须与第二矩阵的列长度相等。

20

2个2x2矩阵相乘

所得矩阵的第一行将会包含行1×列1,行1×列2,等等。 第二行则是包含行2×列1,行2×列2,等等。 因此,它的行长度会与第一矩阵相同,而列长度与第二矩阵相同。

3.2 3D旋转矩阵

目前我们已经有个能够让2D点围绕着Z轴旋转的2x2矩阵了呢,但是我们实际上的变换还是需要使用3D点的。所以我们试图计算乘法:

21

但是,该乘法因矩阵的行和列长度不匹配而无法成立,因此我们需要将我们的旋转矩阵扩充到3x3才行。 如果我们只是用零填充会发生什么?

22

所得的X值和Y值十分正确,但是Z值总是为零,这显然是错误的。 为了保持Z值不变,我们必须在我们的旋转矩阵的右下角插入1。 这应该很好理解,毕竟第三行代表着Z轴呀。也即是:

23

24

如果我们同时间对所有的三个维度使用这技巧,我们将最终得到一个由沿着对角线的1且其他值为0的矩阵,也即是众所皆知的单位矩阵,因为它即使与任何矩阵相乘都不会改变它的值。它就像一个过滤器,让一切事物通过它并保持不变。

25

3.3 针对X和Y的旋转矩阵

我们继续沿用绕Z轴旋转的思路,就可以推断出绕着Y轴旋转的矩阵。

首先,X轴开始的矩阵为:

26

逆时针旋转90度后会变成:

27

也即是说,X轴的旋转矩阵可以表示为:

28

Z轴与X轴相差了-90度,所以表示为:

29

而Y轴的旋转矩阵保持不变,最终可得完整的旋转矩阵:

30

第三个旋转矩阵则将X轴设为常量,并以刚才的方法推算出Y轴与Z轴的旋转公式。

31

3.4 旋转矩阵合而为一

目前我们每单个旋转矩阵仅仅是绕着一个轴旋转。为了重现与Unity一样的旋转变换,我们得遵循一定的顺序。首先绕着Z轴旋转,接下来绕Y轴,最后才绕X轴旋转。我们首先将Z旋转应用到我们的点上,然后再将Y旋转应用到刚才的结果上,最后才将X的旋转应用到最终的结果上,这样就可以达到我们理想中的旋转变换了。

我们也可以将旋转矩阵彼此相乘,结果将会诞生出能够同时应用3种旋转方式的新矩阵。我们首先计算 Y x Z。

我们在所得的矩阵中第一条目的计算为:cosXcosZ - 0sinZ - 0sinY = cosYcosZ。

整个矩阵虽然充斥着大量的乘法,但是许多部分所得的最终值都会是0,这些结果都可以忽略不计。

32

现在我们通过计算X × (Y × Z)来获得我们最终的矩阵。

33

现在我们可以通过所获得的矩阵来观察X,Y和Z轴的旋转是如何构造形成的。

34

围绕3个轴的旋转

4.矩阵变换

既然我们可以将三种旋转结合为一个矩阵,那我们是否也能将缩放、旋转和位移也结合为一个矩阵呢?如果我们能将缩放及位移同样以矩阵乘法来表示,那我们自然能办得到。

我们只要取单位矩阵并直接缩放其值,就可以直接生成新的缩放矩阵。

35

那我们又要如何支持重新定位呢?这并不是重新定义三个轴向,它只是一种偏移量,所以我不能以现有的3x3矩阵来表示它。我们需要额外增加新的行来包含偏移量。

36

但是现在的问题是我们的矩阵列长度变成了4,使得接下来的计算将无法顺利进行。所以我们需要添加新的列到我们的点上,并且该列与对应的偏移量相乘后的值必须为1,而且我们需要好好保存该值,这样我们可以将其应用在往后的矩阵乘法中。这致使我们进入到了4x4矩阵及4D点的领域。

37

所以接下来我们必须使用4x4矩阵作为我们的变换矩阵了。即是说我们的缩放及旋转矩阵也需要添加新的行和列,它将由一群0及右下角的值为1组成。现在我们所有的点将拥有第4个坐标,并且其值将永远保持为1。

4.1 齐次坐标

试问我们是否能明白第四坐标的存在意义是什么吗?它是否表示着任何有用的东西?我们知道当我们赋予它的值为1的时候,表示我们可以重新定位这些点;如果其值为0,那我们将忽视它的偏移量,但是缩放和旋转还是会照常运作。

仅仅能缩放及旋转,但是不会移动的东西绝不会是点。那是向量,是方向。

所以点可以表示成:

38

而向量则表示为:

39

太棒了,因为这样一来,我们就能使用同一个矩阵来同时变换位置,法线及切线了。

那如果第四坐标的值是0和1以外的值,会发生什么呢?好吧,这不该发生,又或者说它即使发生了也不会有任何实质上的区别。我们现在是在和其次坐标打交道。其概念是指空间中的每个点都可以表示成无限量的坐标集合。最直接的表现形式就是使用1作为第四坐标,而其他的替代形式可以在坐标集合与任一数值的乘法中找到。

40

那我们将每个坐标除以即将被抛弃的第四坐标后可以得到欧几里得点,也即是实质上的3D点。

41

当然在第四坐标为0的时候,这方法根本不管用。这样点的位置会被放置到无限远,这就是为何它们会被称为方向。

4.2 矩阵的使用

我们可以使用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;
    }
}
4.3 矩阵的组合

我们现在将所有的变换矩阵组合为一个新矩阵。在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[i].Matrix * transformation;
        }
    }
}

现在开始TransformationGrid将不再调用Apply方法,取而代之的是它会直接执行自身的矩阵乘法。

Vector3 TransformPoint (int x, int y, int z) {
    Vector3 coordinates = GetCoordinates(x, y, z);
    returntransformation.MultiplyPoint(coordinates);
}

现在我们的新方法显得更有效率,因为我们之前的方式是为每个点创建个别的变换矩阵,并单独的应用它们,而现在我们创建的是统一的变换矩阵并且在每个点上重复使用它。Unity也使用相同的技巧来创建单一的变换矩阵,从中减少了每个对象的层次结构。

我们可以在我们的例子中继续提高它们的执行效率。所有的变换矩阵都具有相同的底列:

42

发现这一点后,我们可以直接忽略该列,跳过其所有0的计算和最后的转化除法。 Matrix4x4.MultiplyPoint4x3 方法也确实是这么做的。 但是, 依然有一些很有用的变换会改变底列的值。

5. 投影矩阵

目前,我们已经实现将点从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;
        }
    }
}

把它挂载到对象上,并把它的变换顺序安排到最后一个。

43

摄像机投影是最后才执行
5.1 正交摄影机

从3D到2D的变换最直接的方式是简单地舍弃一个维度,这会将3D空间折叠成一个平面。这平面就好像是用来渲染场景的画布一般。我们就直接抛弃Z的维度看看结果会如何。

44

45

我们的网格确实变成2D了。你仍然可以执行所有的变换如缩放、旋转、包括重新定位,但是最终的结果会输出到XY的平面上。这就是正交摄影机投影的初步实现。

现在我们的原始摄像机位置坐落在原点并望向正Z的方向,试问我们能否移动并旋转它?答案是肯定的,事实上我们已经这么做了。移动摄影机与将整个世界往反方向移动在视觉表现上是一样的,当然旋转与缩放的结果也是。尽管有些尴尬,但是我们可以用现有变换方式来移动摄像机。Unity则使用了逆矩阵来达到同样的效果。

5.2 透视摄像机

尽管正交摄影机的效果很赞,但是它并不能呈现我们现实中所看到的世界,我们仍然需要一个透视摄像机。透视的概念既是物体距离我们越远,它看上去就会越小。我们可以通过点与摄像机的距离来缩放它们以重现我们想要的效果。

我们直接让所有的值除以Z的坐标。那我们是否可以使用矩阵乘法来实现它?必须的,我们通过改变单位矩阵的底列,使之成为:

46

改变之后第四坐标会相等于原始的Z坐标。将其次坐标变换为欧几里得坐标并且执行所需的除法。

47

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就足够呈现完整的网格了,否则你可能需要更远的距离。

48

49

原点与投影平面之间距离也会影响投影的效果。它就像是摄像机的焦距,当你把它放的越大,那你的视野范围就越小。现在我们使用的焦距为1,它将会提供我们角度为90的视野范围。我们也可以将它的值设为可随意配置。

public float focalLength = 1f;

50

焦距

当焦距越大意味着我们正在放大画面,它将有效的增加我们最终点的大小,所以我们可以通过这种方式来实现它。而由于我们已经将Z维度折叠起来了,我们就不用去缩放它了。

51

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));	

52

调整焦距中

我们现在实现十分简单的透视摄像机了。如果我们想要完完全全的仿造Unity的摄像机投影,我们还是需要与近距离平面和远距离平面打交道。那需要将网格对象投影到立方体而非平面,所以深度的信息必须保留,并且我们还需要考虑到视图的宽高比。此外,Unity的摄影是往负Z方向看的,那即是说我们还需要取消部分数值。你可以将这些信息包含到投影矩阵里,不过接下来就是你的作业了,自己去实现吧!

我们这么做的目的是什么?平常鲜少会自行去构建矩阵,即使有也绝不会是投影矩阵。关键是你现在明白投影矩阵究竟是怎么回事了。矩阵并不可怕,它们只不过是将点及向量从某个空间变换到另一个空间。现在你对矩阵也有了进一步的了解了,这很棒,因为当我们在开始写自己的Shader的时候,我们还是会再次接触矩阵的。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此的讨论

  1. byzod 2016-11-14

    我竟然通过这篇文章理解了大学时一直没能直观理解的矩阵运算

  2. 太好了,一直希望有这样图文并茂介绍三维数学的教程。Saved to pocket.

  3. 传奇Legend 2018-02-13

    怎么有些代码中间少了空格,希望修复

  4. 传奇Legend 2018-03-01

    希望能更新后续课程

    最近由 传奇Legend 修改于:2018-03-01 16:15:18
  5. 唐振强(Ron) 2018-03-07 微信会员

    非常好的文章,感谢翻译分享。仔细看一遍发现英文原文和译文有一处不妥。
    “目前我们每单个旋转矩阵仅仅是绕着一个轴旋转。为了重现与Unity一样的旋转变换,我们得遵循一定的顺序。首先绕着Z轴旋转,接下来绕Y轴,最后才绕X轴旋转。”
    “ Let's rotate around Z first, then around Y, and finally around X. ”
    可以确定现在Unity的旋转顺序为zxy,而不是zyx。这一点从《Unity5.x Shader 入门精要》和 Unity2017实验可证。
    可能之前的老Unity版本顺序为zyx。

您需要登录或者注册后才能发表评论

登录/注册