Unity 渲染教程(一):矩阵

转载:http://gad.qq.com/program/translateview/7181958

 创建立方体网格。
· 支持缩放、位移和旋转。

· 使用变换矩阵。

· 创建简单的相机投影。

这是关于渲染基础的系列教程的第一部分。它涵盖了变换矩阵。 首先,从程序化网格开始,让我们先遍历下“网格基础”系列。然后你会知道网格是如何工作的。这个系列将探讨这些网格如何最终转换成显示器上的像素。

image
 

对空间中的点进行操作。

可视化空间

你现在已经知道了网格到底是什么,以及它们如何在场景中进行定位。但是这个在场景中的定位是如何工作的?着色器如何知道在哪里绘制这些网格?当然,我们可以依靠Unity的变换组件和着色器来处理这一切,但是如果你想获得对网格的完全控制,理解在引擎中实际发生的事情是至关重要的。要完全理解这个过程,最好是通过创建自己的实现来加深理解。

对网格的移动、旋转和缩放是通过操纵网格顶点的位置来完成的。这是一个空间上的变换,所以要看到它是行动的话,我们必须使整个空间可见。我们可以通过创建点的三维网格来做到这一点。点可以是任何预制件。

usingUnityEngine;

publicclassTransformationGrid : MonoBehaviour {

publicTransform prefab;

publicintgridResolution = 10;

Transform[] grid;

voidAwake () {

    grid = newTransform[gridResolution * gridResolution * gridResolution];

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

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

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

                grid[i] = CreateGridPoint(x, y, z);

            }

        }

    }

}

}

为什么不使用粒子来可视化这些点?

创建点是一个实例化一个预制件、确定它的坐标,并给它一个独特的颜色。

Transform CreateGridPoint (intx, inty, intz) {

Transform point = Instantiate<transform>(prefab);

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

point.GetComponent<meshrenderer>().material.color = newColor(

    (float)x / gridResolution,

    (float)y / gridResolution,

    (float)z / gridResolution

);

returnpoint;

}</meshrenderer></transform>

我们网格之中最明显的一个形状是立方体,所以让我们从这里开始。我们将它的中心定在原点上,所以变换 - 特别是旋转和缩放 变换- 是相对于网格立方体的中点进行操作的。

Vector3 GetCoordinates (intx, inty, intz) {

returnnewVector3(

    x - (gridResolution - 1) * 0.5f,

    y - (gridResolution - 1) * 0.5f,

    z - (gridResolution - 1) * 0.5f

);

}

我将使用默认的立方体来作为预制件,将其缩放到一半的大小,这样这些立方体之间才会有空间。

image
 

need-to-insert-img

need-to-insert-img

need-to-insert-img

小的立方体预制件

创建一个网格对象,添加我们的组件,并挂接预制件。当我们进入播放模式的时时候,网格立方体将会出现在场景中,并以我们对象空间的本地原点为中心。

image
 
image
 

这是来自Transformation grid

变换

理想情况下,我们应该能够对网格应用任意数量的变换。并且我们可以想象出许多类型的转换,但是我们最好把变换限制为位移、旋转和缩放三种类型。

如果我们为每个变换类型创建一个组件类型,那么我们就可以按照我们想要的任何顺序和数量将它们添加到网格对象中去。虽然每个变换的细节不同,但是他们都需要一个方法来将自己应用到空间中的点。

让我们为所有的变换类型创建一个基本组件,以便它们可以继承这个基础组件。这将是一个抽象类,这意味着它不能直接被使用,因为直接使用抽象类将是无意义的。我们会给它一个抽象的Apply方法,这个方法将被具体的变换组件用来做他们自己具体的工作。

usingUnityEngine;

publicabstractclassTransformation : MonoBehaviour {

publicabstractVector3 Apply (Vector3 point);

}

一旦我们将这样的组件添加到我们的网格对象中以后,我们将不得不检索它们,所以我们可以将它们应用到所有的网格点。我们将使用通用列表来存储对这些组件的引用。

usingUnityEngine;

usingSystem.Collections.Generic;

publicclassTransformationGrid : MonoBehaviour {

…

List<transformation> transformations;

voidAwake () {

    …

    transformations = newList<transformation>();

}

}</transformation></transformation>

现在我们可以添加一个Update方法,它会提取变换信息,然后循环遍历整个网格,对所有的点进行变换。

voidUpdate () {

GetComponents<transformation>(transformations);

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

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

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

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

        }

    }

}

}</transformation>

为什么要在每次更新的时候获取组件?

为什么要使用列表而不是数组?

对每个点的变换是通过获得原始坐标,然后依次应用每个变换来完成的。我们不能依赖于各点的实际位置,因为那些已经被变换进行了更改,我们不希望在每一帧中队这些变换的效果进行累积。

Vector3 TransformPoint (intx, inty, intz) {

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

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

    coordinates = transformations[i].Apply(coordinates);

}

returncoordinates;

}

位移

我们的第一个具体组件将用于位移操作,这似乎是最简单的变换。所以我们要创建一个扩展了Transformation的新组件,并使用一个位置来作为局部偏移量。

usingUnityEngine;

publicclassPositionTransformation : Transformation {

publicVector3 position;

}

在这一点上,编译器将非常正确地指出我们没有提供一个具体的Apply函数,所以让我们这样来提供一个具体的Apply函数。这只是将所需位置添加到原点的小问题。

publicoverrideVector3 Apply (Vector3 point) {

returnpoint + position;

}

现在,你可以向我们的网格对象添加位移组件。位移组件将允许我们移动点,而不移动实际的网格对象。我们所有的变换发生在我们对象的局部空间之中。

image
 

对这个点进行位移变换

放缩

接下来是缩放变换。它几乎与位移变换相同,除了比例分量被进行乘法操作而不是被添加到原始点以外。

usingUnityEngine;

publicclassScaleTransformation : Transformation {

publicVector3 scale;

publicoverrideVector3 Apply (Vector3 point) {

    point.x *= scale.x;

    point.y *= scale.y;

    point.z *= scale.z;

    returnpoint;

}

}

将这个组件也添加到我们的网格对象之中。现在我们可以对网格进行缩放了。请注意,我们只是调整网格点的位置,因此缩放不会改变其可视化的大小。

image
 

对放缩进行调整

同时尝试位移和缩放变换。你会发现,放缩的系数也会影响网格点的位置。这是因为我们首先重新定位空间,然后才对它进行缩放。Unity的位移组件以相反的方式来做,而这是更有用的。我们也应该这样做,这可以通过重新排序组件来完成。重新排序组件可以通过每个组件右上角齿轮图标下的弹出菜单进行移动。

image
 

改变变换的发生顺序

旋转

第三种变换类型是旋转变换。它比前两个稍微困难点。它从一个新的组件开始,只是返回的点不变。

usingUnityEngine;

publicclassRotationTransformation : Transformation {

publicVector3 rotation;

publicoverrideVector3 Apply (Vector3 point) {

    returnpoint;

}

}

那么旋转变换时如何工作的?让我们进行限制一下,这里的旋转变换只能绕着单个轴进行旋转,比如说Z轴。 围绕这个轴旋转一个点就像旋转一个轮子。由于Unity使用左手坐标系统,当沿着正Z方向观察的时候,正旋转将使得轮子按照逆时针进行旋转

image
 

围绕Z轴的二维旋转

点的坐标进行旋转的时候会发生什么?最容易考虑的情况是位于半径为一个标准单位的圆上的点,也就是单位圆。最直接的点就是那些对应于X轴和Y轴的点。 如果我们将这些点旋转90度的步长,那么我们总是以将点的坐标旋转到0、1或-1的坐标而结束。

image
 

分别对(1,0)和(0,1)进行90度旋转和180度旋转

在第一步之后,点(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/ 2,1,√½,0,-√½,-1,-√½中去。如果我们继续减小步长的话,我们将得到一个正弦波。

image
 

正弦波和余弦波

在我们的例子中,正弦波在(1,0)的地方开始匹配y坐标。 而在这个点上余弦波开始匹配x坐标。 这意味着我们可以将(1,0)重新定义为(cosz,sinz)(cosz,sinz)。同样,我们可以用(-sinz,cosz)( - sinz,cosz)来替换坐标(0,1)。

因此,我们从计算围绕Z轴的正弦和余弦的期望旋转开始。我们提供角度是按照度数单位,但是正弦波和余弦波是用弧度这个单位进行工作,所以我们必须进行单位的转换。

publicoverrideVector3 Apply (Vector3 point) {

    floatradZ = rotation.z * Mathf.Deg2Rad;

    floatsinZ = Mathf.Sin(radZ);

    floatcosZ = Mathf.Cos(radZ);

    returnpoint;

}

什么是弧度?

很好,我们找到了一种旋转(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)。

1

2

3

4

5

returnnewVector3(

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

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

        point.z

    );

将旋转组件添加到网格中去,并使其出现在变换的中间位置。这意味着我们首先对网格进行缩放,然后对网格进行旋转,最后在对网格进行位移,这正是Unity的变换组件所做的事情。当然,我们现在只支持绕Z轴的旋转。我们将在后面讨论其他两个轴的旋转。

image
 

在 transformations.unitypackage中的三种变换类型

完整的旋转

现在我们只能围绕Z轴进行旋转。为了提供与Unity的变换组件相同的旋转支持,我们必须能够围绕X和Y轴对网格点旋转。虽然围绕这些轴单独的旋转类似于围绕Z轴的旋转,但是当同时围绕多个轴旋转网格点的时候这个过程会变得更复杂。为了解决这个问题,我们可以使用更好的方法来记录我们的旋转,用一种数学的方式。

矩阵

从现在起,我们将垂直地而不是水平地写入点的坐标。相比较之前的表示方法,我们将使用image这种写法。 同样地,image将被分成两行,变成image,这样可以更容易的进行读取。image请注意,x和y的因子最终排列在垂直列中。这就好像我们把一些东西乘以。 这里建议使用二维乘法。事实上,我们执行的乘法是image。 这是一个矩阵乘法。2乘2矩阵的第一列表示的是X轴,并且2乘2矩阵的第二列表示的是Y轴。imageimage使用二维矩阵定义x轴和y轴

一般来说,当两个矩阵做乘法的时候,是对第一个矩阵中逐行处理,对第二个矩阵中逐列处理。结果矩阵中的每个项是第一个矩阵的一行的项与第二个矩阵一列的对应项相乘的总和。这意味着第一个矩阵的行的长度必须与第二矩阵的列的长度相匹配。

image
 

用两个22矩阵相乘*所得的结果矩阵的第一行包含第1行×第1列,第1行×第2列,等等。第二行包含第2行×第1列,第2行×第2列,等等。 因此,所得的结果矩阵具有与第一个矩阵相同的行数,以及与第二个矩阵相同的列数。

三维旋转矩阵

到目前为止,我们有一个2乘2的矩阵,我们可以用来围绕Z轴旋转对应的二维点。但我们实际上使用的是三维点。所以我们试图用乘法,但是这样会由于矩阵的行和列的长度不匹配而无效。因此,我们必须通过包含第三个维度来将我们的旋转矩阵增加到3乘3的规模。如果我们只是用零填充会发生什么?

image  
image
 

在得到的结果之中,X和Y分量是对的,但是Z分量总是为零。这是不正确的。为了保持Z不变,我们必须在我们的旋转矩阵的右下角插入1。 这样做是有道理的,因为第三列代表的是Z轴,也就是imageimage

如果我们同时对所有三维矩阵都使用这个数据,我们将最终得到一个矩阵,沿着对角线的值都是1,而在矩阵的其他地方都是0。这是已知的单位矩阵,因为它不会改变任何与它相乘的矩阵。它就像一个过滤器,让一切物体通过而不发生任何改变。

image 

围绕X轴和Y轴旋转矩阵我们使用与绕Z轴旋转相同的方法进行推理,我们可以得到一个用于围绕Y轴进行旋转的矩阵。首先,X轴开始是,经过90度逆时针的旋转之后变为image。这意味着对X轴的旋转可以表示为image。Z轴在它后面滞后90°,所以应该是image。而Y轴保持不变,这完成了旋转矩阵。imageimage第三个旋转矩阵保持对X值的恒定并以类似的方式调整Y值和Z值。image

 统一的旋转矩阵

我们的三个旋转矩阵每个都是围绕着单个轴进行旋转的。要重现Unity的旋转变换,我们必须首先绕Z轴进行旋转,然后绕Y轴进行旋转,最后绕X轴进行旋转。我们可以先将绕Z轴进行的旋转应用到我们的网格点,然后将绕Y轴进行的旋转应用到结果,再将绕X轴进行的旋转应用于该结果。

但是我们也可以将我们的三个旋转矩阵彼此相乘。这将产生一个新的旋转矩阵,它将立即应用所有三个旋转。让我们首先执行Y×Z。

所得结果矩阵的第一个项是。整个矩阵的计算需要大量的乘法,但是很多部分最终为0,可以丢弃。

image
 
image
 

现在让我们执行X × (Y × Z)来得到我们的最终矩阵。

image
 

乘法的顺序是重要的吗?

现在我们已经有了这个矩阵,我们可以看到旋转结果中的X、Y和Z轴是如何构造的。

publicoverrideVector3 Apply (Vector3 point) {

floatradX = rotation.x * Mathf.Deg2Rad;

floatradY = rotation.y * Mathf.Deg2Rad;

floatradZ = rotation.z * Mathf.Deg2Rad;

floatsinX = Mathf.Sin(radX);

floatcosX = Mathf.Cos(radX);

floatsinY = Mathf.Sin(radY);

floatcosY = Mathf.Cos(radY);

floatsinZ = Mathf.Sin(radZ);

floatcosZ = Mathf.Cos(radZ);

Vector3 xAxis = newVector3(

    cosY * cosZ,

    cosX * sinZ + sinX * sinY * cosZ,

    sinX * sinZ - cosX * sinY * cosZ

);

Vector3 yAxis = newVector3(

    -cosY * sinZ,

    cosX * cosZ - sinX * sinY * sinZ,

    sinX * cosZ + cosX * sinY * sinZ

);

Vector3 zAxis = newVector3(

    sinY,

    -sinX * cosY,

    cosX * cosY

);

returnxAxis * point.x + yAxis * point.y + zAxis * point.z;

}

这是对三个坐标轴进行旋转。

用矩阵来执行变换

如果我们可以将三个旋转组合成一个矩阵,那我们还可以将缩放、旋转和位移合并成一个矩阵吗?如果我们可以用矩阵乘法来表示缩放和位移这些行为,那么答案毫无疑问是肯定的。

缩放矩阵可以直接构造出来。我们采用单位矩阵并缩放其组件。

image
 

但是我们如何支持位移操作呢?这不是对三个坐标轴操作的重定义,它只是一个偏移。所以我们不能用我们现在的3×3矩阵来表示它。我们需要一个额外的列来包含偏移量。

image
 

然而,这么做其实是无效的,因为我们的矩阵的行的长度已经变为4。所以我们需要添加一个第四个组件到我们的点的矩阵表示之中。因为这个分量乘以偏移量以后,它的值应该是1。我们想要保留1,所以它可以用于进一步的矩阵乘法。 这就导致4×4矩阵和四维的网格点。

image
 

所以我们必须使用4×4的变换矩阵。 这意味着缩放和旋转矩阵会得到一个额外的行和列,在底部右侧是1,在其他地方是0。所有的网格点都会得到第四个坐标,这个值总是1。

齐次坐标

我们可以让第四个坐标变得有任何意义吗?它能代表什么作用吗?我们知道我们给它的值是1,以便这个值能重新对网格点进行定位。如果这个值为0的话,则将忽略掉偏移量,但是仍将发生缩放和旋转。

有种东西可以进行缩放和旋转,但是不会发生移动。这不是一个点,这是一个向量。也是一个方向。

所以

代表着一个点,而

image
 

表示的是一个向量。这种表示方法是有用的,因为它意味着我们可以使用相同的矩阵来变换位置、法线和切线。

image
 

那么当第四个坐标的值不是0或者1的时候会发生什么?好吧,这种情况根本就不应该出现。或者实际上,它应该没有什么区别。我们现在正在使用齐次坐标。齐次坐标背后的想法是,空间中的每个点可以由无限量的坐标集表示。最直接的形式是使用1这个值作为第四个坐标的值。所有其他的替代方法可以通过将整个集合乘以任意数来找到。

image
 

所以为了得到欧几里德点 – 也就是实际的三维玩个点 - 你将每个坐标除以第四个坐标,然后丢弃第四个坐标。

image
 

当然,当第四坐标的值为0时这不会起作用。这样的点(也就是第四坐标的值为0的店)被定义为无限远。这就是为什么他们会被作为方向来使用。

使用矩阵

我们可以使用Unity的Matrix4x4结构来执行矩阵的乘法。从现在起,我们将使用它来执行变换,而不再使用当前的方法。

向Transformation 中添加抽象只读属性以便检索转换矩阵。

1publicabstractMatrix4x4 Matrix { get; }

这个类的Apply方法不再需要是抽象的。它的职责只是抓住矩阵和执行乘法。

publicVector3 Apply (Vector3 point) {

returnMatrix.MultiplyPoint(point);

}

请注意Matrix4x4。MultiplyPoint具有一个三维矢量参数。它假设缺失的第四个坐标的值是1。它还负责从齐次坐标到欧几里得坐标的转换。如果你想乘以一个方向而不是一个点,你可以使用Matrix4x4.MultiplyVector。

具体的转换类现在必须将自己的Apply方法更改为Matrixproperties。

首先是PositionTransformation。Matrix4x4.GetRow方法提供了一种方便的填充矩阵的方法。

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{

    floatradX = rotation.x * Mathf.Deg2Rad;

    floatradY = rotation.y * Mathf.Deg2Rad;

    floatradZ = rotation.z * Mathf.Deg2Rad;

    floatsinX = Mathf.Sin(radX);

    floatcosX = Mathf.Cos(radX);

    floatsinY = Mathf.Sin(radY);

    floatcosY = Mathf.Cos(radY);

    floatsinZ = Mathf.Sin(radZ);

    floatcosZ = 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中去。

1Matrix4x4 transformation;

我们将在游戏每次更新的时候更新这个转换矩阵。这涉及到提取第一个矩阵的信息,然后将其乘以所有的其他矩阵。这样才能确保它们按正确的顺序相乘。

voidUpdate () {

UpdateTransformation();

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

    …

}

}

voidUpdateTransformation () {

GetComponents<transformation>(transformations);

if(transformations.Count > 0) {

    transformation = transformations[0].Matrix;

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

        transformation = transformations[i].Matrix * transformation;

    }

}

}</transformation>

现在,网格不再调用Apply方法,而是执行矩阵乘法本身。

Vector3 TransformPoint (intx, inty, intz) {

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

returntransformation.MultiplyPoint(coordinates);

}

这种新方法更加的有效,因为我们过去为每个点分别创建变换矩阵,并单独应用它们。现在我们只创建一个统一的变换矩阵一次,并为每个点重用这个统一的变换矩阵。Unity使用相同的技巧将每个对象的层次结构减少为单个变换矩阵。

在我们的例子中,我们可以使这个变换矩阵更加的高效。我们让所有变换矩阵具有相同的底行,

。知道这一点,我们可以忘记具体是哪一行,跳过对0的计算和最后的转换除法。 Matrix4x4.MultiplyPoint4x3方法正是这样做的。但是,我们不会使用该方法,因为有有用的转换会改变底行的值。

image
 

投影矩阵

到目前为止,我们已经将点从三维空间中的一个位置转换到三维空间中的另一个位置。但是这些点最终如何绘制在二维显示器上?这需要从三维空间到二维空间的转换。我们可以创建一个转换矩阵!

为相机的投影进行新的具体变换。让我们从单位矩阵开始。

usingUnityEngine;

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

        returnmatrix;

    }

}

}

将其添加为最终转换。

image
 

相机的投影出现在最后。

正交相机

从三维空间变换到二维空间的最直接的方法是简单地丢弃一个维度。 这将把三维空间折叠成一个平面。这个平面像一个画布,用于渲染场景。让我们丢置Z轴上的信息,看看会发生什么。

image
 

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, 0f, 1f));

image
 

正交投影

事实上,经过这样处理以后,我们的网格变成了二维网格。你仍然可以进行缩放、旋转和位移以及其他所有操作,但是它最后投影到XY平面上。这是一个最基本的正交相机投影。

为什么颜色变得不稳定?

我们的原始摄像机位于坐标原点,并且看向正Z的方向。我们可以移动它并进行旋转吗? 是的,事实上我们已经可以做到了。移动相机具有与在相反方向上移动世界相同的视觉效果。旋转和缩放也是如此。所以我们可以使用我们现有的变换来移动摄像机,虽然有点尴尬。Unity使用矩阵求逆来做同样的事情。

透视相机

正交相机是非常好的,但是不能显示我们所看到的世界。所以我们需要一个透视相机。由于透视的关系,更远的东西对我们来说看起来更小。我们可以通过根据距离相机的距离来缩放网格点来重现这个效果。

让我们把一切的值都用Z坐标除一下。我们可以用矩阵乘法吗?是的,通过将单位矩阵的底行改为[0,0,1,0]。这将使结果的第四个坐标的值等于原始的Z坐标的值。这样就从齐次坐标转换到欧几里得坐标,然后进行所需的除法。

image
 

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就足够了,否则你可能需要更多的空间。

image
 

透视投影

原点和投影平面之间的距离也会影响投影。原点和投影平面之间的距离的作用就像一个相机的焦距。原点和投影平面之间的距离越大,你的视野就越小。现在我们使用焦距为1来产生90°的视场。我们可以使下面这个数据来进行配置。

1publicfloatfocalLength = 1f;

image
 

焦距

由于更大的焦距意味着我们放大了视野,这有效地增加了我们最终表现出来的网格点的大小,所以我们可以用这种方法对它进行支持它。 当我们折叠Z轴的时候,根本不需要进行缩放。

image
 

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方向的,这需要对一些数字取负。 你可以将所有这些都纳入大炮投影矩阵中去。如果你愿意解决这个问题的话,我把它留给你来找出解决方案。

那么这一切的要点是什么呢?我们很少需要自己来构建矩阵,而且绝对不是投影矩阵。关键是你现在明白发生了什么。矩阵不是可怕的,它们只是将点和向量从一个空间变换到另一个空间。 你明白了如何去做。这很好,因为一旦我们开始编写自己的着色器代码的时候,你将再次遇到矩阵。我们将在这个系列的第二篇文章《着色器基础》里面继续这个话题。

posted @ 2019-08-19 10:37 26周岁 阅读(...) 评论(...) 编辑 收藏