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<Transform>(prefab); point.localPosition = GetCoordinates(x, y, z); point.GetComponent<MeshRenderer>().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,由立方体组成的网格则会出现,并且居中于我们物体的相对原点。
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<Transformation> transformations; void Awake () { … transformations = new List<Transformation>(); } }
接下来我们可以加入Update方法来找回我们所有的变换组件,并且通过循环整个立方体网格来变换我们所有的点。
voidUpdate () { GetComponents<Transformation>(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; }
现在你可以对网格对象加上位置的变换了。这可以让我们在不改变实际网格物体的位置下移动它所有的点,并且所有的变换是基于物体的相对位置之上的。
位移
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; } }
当我们将这个组件挂载到我们的网格对象之后,它就能够随意缩放啦。值得注意的是我只是仅仅改变了所有点的位置,所以点的体积在画面表现上是不会有任何改变的。
调整缩放
我们来尝试对物体同时间进行位移与缩放,你会发现缩放同时影响了位移的距离,这是因为当我们是先对空间进行了位移后,接下来才缩放它。Unity的Transform组件使用了其他的方式来实现它们,并且比这更为有效。而我们如果要解决这样的问题,只要改变组件的顺序就可以啦。我们可以点击组件右上方的齿轮按钮,并通过弹出的窗口来移动他们。
2.3 旋转
第三种变换模式是旋转,旋转的实现相比前两者则显得更为困难。一开始我们先创建一个新组件并返回尚未改变的点。
using UnityEngine; public class Rotation Transformation : Transformation { public Vector3 rotation; public override Vector3 Apply (Vector3 point) { return point; } }
那么旋转是怎么实现的?我们先来限制我们旋转的轴向吧——仅仅绕着Z轴旋转。让点像个旋转的轮子一般绕着这个轴旋转。由于Unity是左手坐标系,所以当我们从正的Z轴观察时,正值的旋转是会让这轮子往逆时钟旋转的。
2D围绕着Z轴旋转
那么当点的坐标开始旋转的时候,会发生什么?最简单的思考方式就是让这些点位于一个以半径为一为单位的圆,并且直接对应其X轴与Y轴。那么当我们将这些点旋转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轴旋转,我们稍后才处理其他两个轴。
正在进行三种变换模式
3. 完全旋转
现在我们只能绕Z轴旋转。为了能提供与Unity Transform组件相同功能的旋转支持,我们必须让绕X轴及Y轴旋转成为可能。如果只是单独的绕着某个轴旋转它就会与仅绕Z轴旋转极为相似,一旦绕着多个轴旋转则会变得更为复杂。为了解决这一点,我们可以用更为直观的方式来记下我们的旋转数学。
3.1 矩阵
从现在开始,我们将点的坐标写成垂直的形式以取代原本的水平形式。我们将使用取代。
同样的,我们把分成两列好让它阅读起来更为直观。
注意,现在X和Y元素已经被编排成了垂直的列表示,这是因为我们需要使用与其他的东西相乘,这意味着它会形成两个维度的乘法。事实上,我们前面已经执行过的就是一个矩阵乘法。在这2x2矩阵里,第一列表示的是X轴,而第二列表示的则是Y轴。
以2D的矩阵来定义x轴与y轴
一般来说,两个矩阵的相乘是以第一矩阵的逐行(横向)与第二矩阵的逐列(竖向)相乘。最后所获得的矩阵中每个项是行的项与列的对应项相乘的总和,即是说第一矩阵的行长度必须与第二矩阵的列长度相等。
2个2x2矩阵的相乘
所得矩阵的第一行将会包含行1×列1,行1×列2,等等。 第二行则是包含行2×列1,行2×列2,等等。 因此,它的行长度会与第一矩阵相同,而列长度与第二矩阵相同。
3.2 3D旋转矩阵
目前我们已经有个能够让2D点围绕着Z轴旋转的2x2矩阵了呢,但是我们实际上的变换还是需要使用3D点的。所以我们试图计算乘法,但是该乘法因矩阵的行和列长度不匹配而无法成立,因此我们需要将我们的旋转矩阵扩充到3x3才行。 如果我们只是用零填充会发生什么?
所得的X值和Y值十分正确,但是Z值总是为零,这显然是错误的。 为了保持Z值不变,我们必须在我们的旋转矩阵的右下角插入1。 这应该很好理解,毕竟第三行代表着Z轴呀。也即是。
如果我们同时间对所有的三个维度使用这技巧,我们将最终得到一个由沿着对角线的1且其他值为0的矩阵,也即是众所皆知的单位矩阵,因为它即使与任何矩阵相乘都不会改变它的值。它就像一个过滤器,让一切事物通过它并保持不变。
3.3 针对X和Y的旋转矩阵
当我们将绕Z轴旋转的推理方法继续应用起来,我们就可以得出一个绕着Y轴旋转的矩阵。First, the X axis starts as and becomes after a 90° counterclockwise rotation.
首先,X轴开始的矩阵为
,在逆时钟旋转90度会形成矩阵,即是说X轴的旋转矩阵可以用来表示。Z轴与X轴相差了-90度,所以是以来表示,而Y轴的旋转矩阵保持不变,最终可得完整的旋转矩阵The third rotation matrix keeps X constant and adjust Y and Z in a similar way. 而第三个旋转矩阵则将X轴设为常量,并以刚才的方法推算出Y轴与Z轴的旋转公式。
3.4 旋转矩阵合而为一
目前我们每单个旋转矩阵仅仅是绕着一个轴旋转。为了重现与Unity一样的旋转变换,我们得遵循一定的顺序。首先绕着Z轴旋转,接下来绕Y轴,最后才绕X轴旋转。我们首先将Z旋转应用到我们的点上,然后再将Y旋转应用到刚才的结果上,最后才将X的旋转应用到最终的结果上,这样就可以达到我们理想中的旋转变换了。
我们也可以将旋转矩阵彼此相乘,结果将会诞生出能够同时应用3种旋转方式的新矩阵。我们首先计算 Y x Z.
我们在所得的矩阵中第一条目的计算为cosXcosZ - 0sinZ - 0sinY = cosYcosZ。整个矩阵虽然充斥着大量的乘法,但是许多部分所得的最终值都会是0,这些结果都可以忽略不计。
现在我们通过计算X × (Y × Z)来获得我们最终的矩阵。
现在我们可以通过所获得的矩阵来观察X,Y和Z轴的旋转是如何构造形成的。
围绕3个轴的旋转
4 矩阵变换
既然我们可以将三种旋转结合为一个矩阵,那我们是否也能将缩放、旋转和位移也结合为一个矩阵呢?如果我们能将缩放及位移同样以矩阵乘法来表示,那我们自然能办得到。
我们只要取单位矩阵并直接缩放其值,就可以直接生成新的缩放矩阵。
那我们又要如何支持重新定位呢?这并不是重新定义三个轴向,它只是一种偏移量,所以我不能以现有的3x3矩阵来表示它。我们需要额外增加新的行来包含偏移量。
但是现在的问题是我们的矩阵列长度变成了4,使得接下来的计算将无法顺利进行。所以我们需要添加新的列到我们的点上,并且该列与对应的偏移量相乘后的值必须为1,而且我们需要好好保存该值,这样我们可以将其应用在往后的矩阵乘法中。这致使我们进入到了4x4矩阵及4D点的领域。
所以接下来我们必须使用4x4矩阵作为我们的变换矩阵了。即是说我们的缩放及旋转矩阵也需要添加新的行和列,它将由一群0及右下角的值为1组成。现在我们所有的点将拥有第4个坐标,并且其值将永远保持为1.
4.1 其次坐标
试问我们是否能明白第四坐标的存在意义是什么吗?它是否表示着任何有用的东西?我们知道当我们赋予它的值为1的时候,表示我们可以重新定位这些点;如果其值为0,那我们将忽视它的偏移量,但是缩放和旋转还是会照常运作。
仅仅能缩放及旋转,但是不会移动的东西绝不会是点。那是向量,是方向。
所以表示的是点,而则表示为向量。这很棒,因为这样的话我们就能使用同一个矩阵来同时变换位置,法线及切线了。
那如果第四坐标的值是0和1以外的值会发生什么呢?好吧,这不该发生,又或者说它即使发生了也不会有任何实质上的区别。我们现在是在和其次坐标打交道。其概念是指空间中的每个点都可以表示成无限量的坐标集合。最直接的表现形式就是使用1作为第四坐标,而其他的替代形式可以在坐标集合与任一数值的乘法中找到。
那我们将每个坐标除以即将被抛弃的第四坐标后可以得到欧几里得点,也即是实质上的3D点。
当然在第四坐标为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方法就能填充矩阵,使用起来简单方便。
public override Matrix4x4 Matrix { get { Matrix4x4 matrix = new Matrix4x4(); matrix.SetRow(0, new Vector4(1f, 0f, 0f, position.x)); matrix.SetRow(1, new Vector4(0f, 1f, 0f, position.y)); matrix.SetRow(2, new Vector4(0f, 0f, 1f, position.z)); matrix.SetRow(3, new Vector4(0f, 0f, 0f, 1f)); return matrix; } }
接下来是ScaleTransformation。
public override Matrix4x4 Matrix { get { Matrix4x4 matrix = new Matrix4x4(); matrix.SetRow(0, new Vector4(scale.x, 0f, 0f, 0f)); matrix.SetRow(1, new Vector4(0f, scale.y, 0f, 0f)); matrix.SetRow(2, new Vector4(0f, 0f, scale.z, 0f)); matrix.SetRow(3, new Vector4(0f, 0f, 0f, 1f)); return matrix; } }
至于RotationTransformation
, 将矩阵逐行的设置显得更为方便,而且它也与既有的代码匹配。
public override Matrix4x4 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 = new Matrix4x4(); matrix.SetColumn(0, new Vector4( cosY * cosZ, cosX * sinZ + sinX * sinY * cosZ, sinX * sinZ - cosX * sinY * cosZ, 0f )); matrix.SetColumn(1, new Vector4( -cosY * sinZ, cosX * cosZ - sinX * sinY * sinZ, sinX * cosZ + cosX * sinY * sinZ, 0f )); matrix.SetColumn(2, new Vector4( sinY, -sinX * cosY, cosX * cosY, 0f )); matrix.SetColumn(3, new Vector4(0f, 0f, 0f, 1f)); return matrix; } }
4.3 矩阵的组合
我们现在将所有的变换矩阵组合为一个新矩阵。在TransformationGrid中添加一个矩阵变量transformation
Matrix4x4 transformation;
我们会在每次Update中更新transformation的矩阵变量。它的具体行为包括获取第一个矩阵,然后将它与其他的矩阵相乘。必须确认它们是按照正确的顺序相乘的。
void Update () { UpdateTransformation(); for (int i = 0, z = 0; z < gridResolution; z++) { … } } void UpdateTransformation () { GetComponents<Transformation>(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); return transformation.MultiplyPoint(coordinates); }
现在我们的新方法显得更有效率,因为我们之前的方式是为每个点创建个别的变换矩阵,并单独的应用它们,而现在我们创建的是统一的变换矩阵并且在每个点上重复使用它。Unity也使用相同的技巧来创建单一的变换矩阵,从中减少了每个对象的层次结构。
我们可以在我们的例子中继续提高它们的执行效率。所有的变换矩阵都具有相同的底列,。在发现这一点后,我们可以直接忽略该列,跳过其所有0的计算和最后的转化除法。 Matrix4x4.MultiplyPoint4x3
方法也确实是这么做的。 但是, 有些好用的变换还是会改变底列的值呢。
5. 投影矩阵
目前,我们已经实现将点从3D空间里的位置变换到另一个位置上,那我们该如何在2D的屏幕上最终绘制出那些点呢?这需要实现从3D空间到2D空间的变换,我们可以为此创建一个新的变换矩阵!
我们从单位矩阵开始来实现摄像机投影的变换吧!
using UnityEngine; public class CameraTransformation : Transformation { public override Matrix4x4 Matrix { get { Matrix4x4 matrix = new Matrix4x4(); matrix.SetRow(0, new Vector4(1f, 0f, 0f, 0f)); matrix.SetRow(1, new Vector4(0f, 1f, 0f, 0f)); matrix.SetRow(2, new Vector4(0f, 0f, 1f, 0f)); matrix.SetRow(3, new Vector4(0f, 0f, 0f, 1f)); return matrix; } } }
把它挂载到对象上,并把它的变换顺序安排到最后一个。
摄像机投影是最后才执行
5.1 正交摄影机
从3D到2D的变换最直接的方式是简单地舍弃一个维度,这会将3D空间折叠成一个平面。这平面就好像是用来渲染场景的画布一般。我们就直接抛弃Z的维度看看结果会如何。
我们的网格确实变成2D了。你仍然可以执行所有的变换如缩放、旋转、包括重新定位,但是最终的结果会输出到XY的平面上。这就是正交摄影机投影的初步实现。
现在我们的原始摄像机位置坐落在原点并望向正Z的方向,试问我们能否移动并旋转它?答案是肯定的,事实上我们已经这么做了。移动摄影机与将整个世界往反方向移动在视觉表现上是一样的,当然旋转与缩放的结果也是。尽管有些尴尬,但是我们可以用现有变换方式来移动摄像机。Unity则使用了逆矩阵来达到同样的效果。
5.2 透视摄像机
尽管正交摄影机的效果很赞,但是它并不能呈现我们现实中所看到的世界,我们仍然需要一个透视摄像机。透视的概念既是物体距离我们越远,它看上去就会越小。我们可以通过点与摄像机的距离来缩放它们以重现我们想要的效果。
我们直接让所有的值除以Z的坐标。那我们是否可以使用矩阵乘法来实现它?必须的,我们通过改变单位矩阵的底列,使之成为。改变之后第四坐标会相等于原始的Z坐标。将其次坐标变换为欧几里得坐标并且执行所需的除法。
matrix.SetRow(0, new Vector4(1f, 0f, 0f, 0f)); matrix.SetRow(1, new Vector4(0f, 1f, 0f, 0f)); matrix.SetRow(2, new Vector4(0f, 0f, 0f, 0f)); matrix.SetRow(3, new Vector4(0f, 0f, 1f, 0f));
与正交摄影机最大的差异是那些点不会直接下移到投影平面。相反的,它们会往摄像机的方向—— 原点移动,一直移动它们触碰到平面为止。当然这仅仅适用于位于摄像机前方的点。那些位移摄像机后方的点将会投影错误。由于我们不能舍弃这些点,所以我们得确保所有的一切通过重新定位的方式呈现到摄像机前方。如果你并没缩放或旋转你的网格,那距离5就足够呈现完整的网格了,否则你可能需要更远的距离。
原点与投影平面之间距离也会影响投影的效果。它就像是摄像机的焦距,当你把它放的越大,那你的视野范围就越小。现在我们使用的焦距为1,它将会提供我们角度为90的视野范围。我们也可以将它的值设为可随意配置。
public float focalLength = 1f;
焦距
当焦距越大意味着我们正在放大画面,它将有效的增加我们最终点的大小,所以我们可以通过这种方式来实现它。而由于我们已经将Z维度折叠起来了,我们就不用去缩放它了。
matrix.SetRow(0, new Vector4(focalLength, 0f, 0f, 0f)); matrix.SetRow(1, new Vector4(0f, focalLength, 0f, 0f)); matrix.SetRow(2, new Vector4(0f, 0f, 0f, 0f)); matrix.SetRow(3, new Vector4(0f, 0f, 1f, 0f));
调整焦距中
我们现在实现十分简单的透视摄像机了。如果我们想要完完全全的仿造Unity的摄像机投影,我们还是需要与近距离平面和远距离平面打交道。那需要将网格对象投影到立方体而非平面,所以深度的信息必须保留,并且我们还需要考虑到视图的宽高比。此外,Unity的摄影是往负Z方向看的,那即是说我们还需要取消部分数值。你可以将这些信息包含到投影矩阵里,不过接下来就是你的作业了,自己去实现吧!
我们这么做的目的是什么?平常鲜少会自行去构建矩阵,即使有也绝不会是投影矩阵。关键是你现在明白投影矩阵究竟是怎么回事了。矩阵并不可怕,它们只不过是将点及向量从某个空间变换到另一个空间。现在你对矩阵也有了进一步的了解了,这很棒,因为当我们在开始写自己的Shader的时候,我们还是会再次接触矩阵的。
原文地址:http://catlikecoding.com/unity/tutorials/rendering/part-1/
所以说,代码是C#咯...
支持发表技术性文章,很棒~
@nemo:unity支持C+\C#\BOO 三种语言~
棒棒棒