这是用户在 2025-6-9 10:55 为 https://catlikecoding.com/unity/tutorials/rendering/part-1/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Catlike Coding
published 2016-02-26

Rendering 1

Matrices

  • Create a cube grid.
  • Support scaling, positioning, and rotating.
  • Work with transformation matrices.
  • Create simple camera projections.

This is the first part of a tutorial series about the basics of rendering. It covers transformation matrices. First, go through the Mesh Basics series, which starts with Procedural Grid. Then you will know how meshes work. This series will explore how those meshes actually end up as pixels on a display.
这是关于渲染基础知识的系列教程的第一部分。它涵盖了变换矩阵。首先,请学习从“程序化网格”开始的“网格基础知识”系列教程。通过该系列,您将了解网格的工作原理。本系列将探讨这些网格最终如何在显示器上呈现为像素。

This tutorial was made with Unity 5.3.1.
本教程使用 Unity 5.3.1 制作。

Manipulating points in space.
在空间中操纵点。

Visualizing Space  可视化空间

You already know what meshes are and how they can be positioned in a scene. But how does this positioning actually work? How does a shader know where to draw? Of course we can just rely on Unity's transform component and shaders to take care of it all, but understanding what's actually going on is crucial if you want to gain total control. To understand this process fully, it's best if we create our own implementation.
你已经知道什么是网格以及如何在场景中定位它们了。但是,这种定位究竟是如何实现的呢?着色器又是如何知道在哪里进行绘制的呢?当然,我们可以完全依赖 Unity 的变换组件和着色器来处理这一切,但如果你想完全掌控,理解实际发生的情况至关重要。为了充分理解这个过程,最好我们自己实现一个。

Moving, rotating, and scaling a mesh is done by manipulating the positions of its vertices. This is a transformation of space, so to see it in action we have to make space visible. We can do this by creating a 3D grid of points. The points could be any prefab.
移动、旋转和缩放网格是通过操纵其顶点的位置来实现的。这是一种空间变换,因此要看到其效果,我们必须使空间可见。我们可以通过创建一个点的 3D 网格来实现这一点。这些点可以是任何预制件。

using UnityEngine;

public class TransformationGrid : MonoBehaviour {

	public Transform prefab;

	public int gridResolution = 10;

	Transform[] grid;

	void Awake () {
		grid = new Transform[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);
				}
			}
		}
	}
}

Creating a point is a matter of instantiating the prefab, determining its coordinates, and giving it a distinct color.
创建点就是实例化预制件,确定其坐标,并赋予其独特的颜色。

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

The most obvious shape of our grid is a cube, so let's go with that. We center it at the origin, so transformations – specifically rotation and scaling – are relative to the midpoint of the grid cube.
我们的网格最明显的形状是立方体,所以我们就用它。我们将它置于原点,因此变换——特别是旋转和缩放——都将相对于网格立方体的中点。

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

I'll use a default cube as a prefab, scaled to half size so there's room between them.
我将使用一个默认立方体作为预制件,将其缩放至一半大小,以便它们之间有空间。

Small cube prefab.  小立方体预制件。

Create a grid object, add our component, and hook up the prefab. When entering play mode, the grid cube will appear, centered on our object's local origin.
创建一个网格对象,添加我们的组件,并连接预制件。进入播放模式时,网格立方体将出现在我们对象的局部原点居中位置。

inspector scene
Transformation grid.  变换网格。
unitypackage

Transformations  变换

Ideally, we should be able to apply an arbitrary amount of transformations to our grid. And there are many types of transformations that we could dream up, but let's restrict ourselves to positioning, rotating, and scaling.
理想情况下,我们应该能够对网格应用任意数量的变换。我们可以构想出许多类型的变换,但我们这里只限定为平移、旋转和缩放。

If we created a component type for each transformation, we could add those to our grid object in any order and quantity that we want. And while the details of each transformation is different, they'll all need a method to apply themselves to a point in space.
如果我们为每种变换创建一种组件类型,我们就可以按照我们想要的任意顺序和数量将它们添加到我们的网格对象中。虽然每种变换的细节不同,但它们都需要一个方法来将自身应用于空间中的一个点。

Let's create a base component for all transformations, that they can inherit from. This will be an abstract class, which means that it cannot be used directly, as that would be pointless. Give it an abstract Apply method that will be used by the concrete transformation components to do their job.
让我们为所有变换创建一个基础组件,供它们继承。这将是一个抽象类,这意味着它不能直接使用,因为那将毫无意义。给它一个抽象的 Apply 方法,具体的变换组件将使用该方法来完成它们的工作。

using UnityEngine;

public abstract class Transformation : MonoBehaviour {

	public abstract Vector3 Apply (Vector3 point);
}

Once we add such components to our grid object, we'll have to retrieve them somehow so we can apply them to all our grid points. We will use a generic list to store references to these components.
一旦我们将这些组件添加到我们的网格对象中,我们将不得不以某种方式检索它们,以便我们可以将它们应用于所有网格点。我们将使用一个通用列表来存储对这些组件的引用。

using UnityEngine;
using System.Collections.Generic;

public class TransformationGrid : MonoBehaviour {
	
	…
	List<Transformation> transformations;

	void Awake () {
		…
		transformations = new List<Transformation>();
	}
}

Now we can add an Update method which retrieves the transformation, then loops through the entire grid and transforms all of our points.
现在我们可以添加一个`transform`方法,该方法用于获取变换矩阵,然后遍历整个网格并变换所有点。

	void Update () {
		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);
				}
			}
		}
	}

Transforming each point is done by getting the original coordinates, and then applying each transformation. We cannot rely on the actual position of each point, because those have already been transformed and we don't want to accumulate transformations each frame.
每个点的变换是通过获取原始坐标,然后应用每个变换来完成的。我们不能依赖每个点的实际位置,因为这些点已经被变换过了,我们不希望每帧都累积变换。

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

Translation  翻译

Our first concrete component will be for translation, which seems the simplest. So create a new component which extends Transformation, with a position to be used as a local offset.
我们的第一个具体组件将用于翻译,这看起来最简单。因此,创建一个新组件,它扩展自 Transformation ,并带有一个用于局部偏移的定位。

using UnityEngine;

public class PositionTransformation : Transformation {

	public Vector3 position;

}

At this point the compiler will correctly complain that we're not providing a concrete version of Apply, so let's do that. It's simply a matter of adding the desired position to the original point.
此时,编译器会正确地报错,指出我们没有提供 Apply 的具体版本,所以我们来解决这个问题。这仅仅是将所需位置添加到原始点的问题。

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

Now you can add a position transformation component to our grid object. This allows us to move the points around, without moving the actual grid object. All our transformations take place in the local space of our object.
现在,你可以向我们的网格对象添加一个位置变换组件。这允许我们移动点,而无需移动实际的网格对象。我们所有的变换都在我们对象的局部空间中进行。

inspector
Transforming the position.
转换位置。

Scaling  缩放

Next up is the scaling transformation. It's almost the same as positioning, except that the scale components are multiplied instead of added to the original point.
接下来是缩放变换。它与位置变换几乎相同,只是缩放分量是与原始点相乘而不是相加。

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

Add this component to our grid object as well. Now we can scale the grid too. Note that we're only adjusting the positions of our grid points, so scaling won't change the size of their visualizations.
将此组件也添加到我们的网格对象中。现在我们也可以缩放网格了。请注意,我们只调整网格点的位置,因此缩放不会改变其可视化效果的大小。

inspector
Adjusting scale.  调整缩放。

Try both positioning and scaling at once. You will find that the scale also affects the position. This happens because we first reposition space, and then scale it. Unity's transform component does it the other way around, which is much more useful. We should do so as well, which can be done by reordering the components. They can be moved via the pop-up menu under the gear icon at the top right of each component.
同时尝试定位和缩放。你会发现缩放也会影响位置。这是因为我们首先重新定位空间,然后缩放它。Unity 的 Transform 组件则反其道而行之,这样做会更有用。我们也应该这样做,可以通过重新排列组件来完成。它们可以通过每个组件右上角齿轮图标下的弹出菜单进行移动。

Changing the order of transformations.
变换顺序变更

Rotation  旋转

The third transformation type is rotation. It is a bit more difficult than the previous two. Start with a new component which just returns the point unchanged.
第三种变换类型是旋转。它比前两种稍难。从一个新组件开始,该组件只返回未经修改的点。

using UnityEngine;

public class RotationTransformation : Transformation {

	public Vector3 rotation;

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

So how does rotation work? Let's limit ourselves to rotation around a single axis for now, the Z axis. Rotating a point around this axis is like spinning a wheel. As Unity uses a left-handed coordinate system, a positive rotation will make the wheel go counterclockwise, when looking at it in the positive Z direction.
那么旋转是如何工作的呢?我们先将旋转限制在一个单一轴上,即 Z 轴。围绕该轴旋转一个点就像旋转一个轮子。由于 Unity 使用左手坐标系,当沿着 Z 轴正方向观察时,正向旋转会使轮子逆时针转动。

2D rotation around the Z axis.
绕 Z 轴的 2D 旋转。

What happens to a point's coordinates when it rotates? It is easiest to consider points that lie on a circle with a radius of one unit, the unit circle. And the most straightforward points correspond with the X and the Y axes. If we rotate those points by 90° steps, we always end up with coordinates that are either 0, 1, or −1.
一个点的坐标旋转时会发生什么变化?最简单的方法是考虑位于单位圆上的点,即半径为一的圆。最直接的点是与 X 轴和 Y 轴对应的点。如果我们将这些点旋转 90 度,我们总是会得到坐标为 0、1 或-1 的值。

Rotating (1,0) and (0,1) by 90 and 180 degrees.
将(1,0)和(0,1)旋转 90 度和 180 度。

The point (1,0) becomes (0,1) after the first step. The next step puts it at (−1,0). Then comes (0,−1), and finally back to (1,0).
点(1,0)在第一步后变为(0,1)。下一步它到达(-1,0)。然后是(0,-1),最后回到(1,0)。

If we start with the point (0,1) instead, we're just one step ahead compared to the previous sequence. We go from (0,1) to (−1,0) to (0,−1) to (1,0) and back.
如果我们从点(0,1)开始,我们只是比之前的序列领先一步。我们从(0,1)到(-1,0)到(0,-1)到(1,0)然后返回。

So the coordinates of our points go through the cycle 0, 1, 0, −1. They just have different starting points.
所以我们的点坐标会经历一个循环:0、1、0、-1。它们只是起始点不同。

What if we rotated in 45° increments instead? That will produce points that lie on the diagonals in the XY plane. And as the distance to the origin doesn't change, we have to end up with coordinates of the form (±√½, ±√½). This expands our cycle to 0, √½, 1, √½, 0, −√½, −1, −√½. If we keep decreasing the step size, we'll end up with a sine wave.
如果我们改为按 45°增量旋转会怎样?这将产生位于 XY 平面对角线上的点。由于到原点的距离没有变化,我们最终将得到形如 (±√½, ±√½) 的坐标。这使得我们的循环扩展为 0, √½, 1, √½, 0, −√½, −1, −√½。如果我们继续减小步长,最终会得到一个正弦波。

Sine and cosine.  正弦和余弦。

In our case, the sine wave matches the y coordinate when starting at (1,0). The cosine matches the x coordinate. This means that we can redefine (1,0) as (cosz,sinz). Likewise, we can replace (0,1) with (-sinz,cosz).
在我们的例子中,当从(1,0)开始时,正弦波与 y 坐标匹配。余弦与 x 坐标匹配。这意味着我们可以将(1,0)重新定义为 (cosz,sinz) 。同样,我们可以用 (-sinz,cosz) 替换(0,1)。

So we start by computing the sine and cosine of the desired rotation around the Z axis. We provide the angles in degrees, but the sine and cosine work with radians, so we have to convert.
我们首先计算围绕 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;
	}

It is nice that we found a way to rotate (1,0) and (0,1), but what about rotating arbitrary points? Well, these two points define the X and Y axes. We can decompose any 2D point (x,y) into xX+yY. Without any rotation, this is equal to x(1,0)+y(0,1) which indeed is just (x,y). But when rotating, we can now use x(cosZ,sinZ)+y(-sinZ,cosZ) and end up with a correctly rotated point. You can think of it like scaling a point so it falls on the unit circle, rotating, and then scaling back. Compressed into a single coordinate pair, this becomes (xcosZ-ysinZ,xsinZ+ycosZ).
我们找到了旋转 (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
		);

Add a rotation component to the grid and make it the middle transformation. This means we scale first, then rotate, and finally reposition, which is exactly what Unity's transform component does. Of course we're only supporting rotation around Z at this point. We'll deal with the other two axes later.
给网格添加一个旋转组件,并将其设为中间变换。这意味着我们先缩放,然后旋转,最后再重新定位,这正是 Unity 的变换组件所做的事情。当然,目前我们只支持绕 Z 轴旋转。我们稍后会处理其他两个轴。

inspector
All three transformations.
所有三种变换。
unitypackage

Full Rotations  完整旋转

Right now we can only rotate around the Z axis. To provide the same rotation support that Unity's transform component does, we have to enable rotation around the X and Y axes as well. While rotating around these axes in isolation is similar to rotating around Z, it gets more complicated when rotating around multiple axes at once. To tackle that, we could use a better way to write down our rotation math.
目前我们只能绕 Z 轴旋转。为了提供与 Unity 变换组件相同的旋转支持,我们还必须启用绕 X 轴和 Y 轴的旋转。虽然独立绕这些轴旋转与绕 Z 轴旋转相似,但当同时绕多个轴旋转时会变得更复杂。为了解决这个问题,我们可以使用一种更好的方法来表达我们的旋转数学。

Matrices  矩阵

From now on, we'll write the coordinates of a point vertically instead of horizontally. Instead of (x,y) we'll use [xy]. Likewise, (xcosZ-ysinZ,xsinZ+ycosZ) is split into two rows and becomes [xcosZ-ysinZxsinZ+ycosZ], which is easier to read.
从现在起,我们将点的坐标垂直书写,而非水平。不再使用 (x,y) ,而是使用 [xy] 。同样, (xcosZ-ysinZ,xsinZ+ycosZ) 被分成两行,变为 [xcosZ-ysinZxsinZ+ycosZ] ,这样更易于阅读。

Notice that the x and y factors end up arranged in vertical columns. It's as if we multiplied something with [xy]. That would suggest a 2D multiplication. Indeed, the multiplication we performed is [cosZ-sinZsinZcosZ][xy]. This is a matrix multiplication. The first column of the 2 by 2 matrix represents the X axis and its second column represents the Y axis.
请注意,x 和 y 因子最终垂直排列。就好像我们用 [xy] 乘了某个东西。这表明是二维乘法。实际上,我们执行的乘法是 [cosZ-sinZsinZcosZ][xy] 。这是一种矩阵乘法。2x2 矩阵的第一列代表 X 轴,第二列代表 Y 轴。

Defining the X and Y axes with a 2D matrix.
使用 2D 矩阵定义 X 轴和 Y 轴。

In general, when multiplying two matrices, you go row by row in the first matrix and column by column in the second matrix. Each term in the result matrix is the sum of the terms of a row multiplied with the corresponding terms of a column. This means that the rows of the first matrix and columns of the second matrix must have the same amount of elements.
一般来说,在两个矩阵相乘时,你会沿着第一个矩阵的行,并沿着第二个矩阵的列进行操作。结果矩阵中的每个项都是一行中的项与一列中对应项相乘之和。这意味着第一个矩阵的行和第二个矩阵的列必须具有相同数量的元素。

Multiplying two 2 by 2 matrices.
两个 2 乘 2 矩阵相乘。

The first row of the resulting matrix contains row 1 × column 1, row 1 × column 2, and so on. The second row contains row 2 × column 1, row 2 × column 2, and so on. Thus, it has the same amount of rows as the first matrix, and the same amount of columns as the second matrix.
所得矩阵的第一行包含“第 1 行 × 第 1 列”、“第 1 行 × 第 2 列”等。第二行包含“第 2 行 × 第 1 列”、“第 2 行 × 第 2 列”等。因此,它与第一个矩阵具有相同的行数,与第二个矩阵具有相同的列数。

3D Rotation Matrices  三维旋转矩阵

So far, we have a 2 by 2 matrix that we can use to rotate a 2D point around the Z axis. But we're actually using 3D points. So we're attempting the multiplication [cosZ-sinZsinZcosZ][xyz], which in invalid because the row and column length of the matrices don't match. So we have to increase our rotation matrix to 3 by 3, by including the third dimension. What happens if we just pad it with zeros?
到目前为止,我们有一个 2x2 的矩阵,可以用来围绕 Z 轴旋转一个二维点。但我们实际上使用的是三维点。所以我们尝试进行 [cosZ-sinZsinZcosZ][xyz] 乘法,这是无效的,因为矩阵的行和列长度不匹配。所以我们必须将旋转矩阵增加到 3x3,通过包含第三维。如果我们只是用零填充它会发生什么?

[cosZ-sinZ0sinZcosZ0000][xyz]=[xcosZ-ysinZ+0zxsinZ+ycosZ+0z0x+0y+0z]=[xcosZ-ysinZxsinZ+ycosZ0]

The X and Y components of the result are good, but the Z component always becomes zero. That is incorrect. In order to keep Z unchanged, we have to insert a 1 at the bottom right of our rotation matrix. This makes sense, because the third column represents the Z axis, which is [001].
结果的 X 和 Y 分量是正确的,但 Z 分量总是变为零。这是不正确的。为了保持 Z 不变,我们必须在旋转矩阵的右下角插入一个 1。这是有道理的,因为第三列代表 Z 轴,即 [001]

[cosZ-sinZ0sinZcosZ0001][xyz]=[xcosZ-ysinZxsinZ+ycosZz]

If we used this trick for all three dimension at once, we'd end up with a matrix with 1s along its diagonal and 0s everywhere else. This is known as an identity matrix, as it doesn't change whatever it is multiplied with. It's like a filter that lets everything through unchanged.
如果我们将这个技巧同时用于所有三个维度,最终会得到一个对角线上全是 1,其他位置全是 0 的矩阵。这被称为单位矩阵,因为它与任何矩阵相乘都不会改变该矩阵。它就像一个能让所有东西都原样通过的滤镜。

[100010001][xyz]=[xyz]

Rotation Matrices for X and Y
X 轴和 Y 轴的旋转矩阵

Using the same reasoning we applied to find a way to rotate around Z, we can come up with a matrix for rotating around Y. First, the X axis starts as [100] and becomes [00-1] after a 90° counterclockwise rotation. This means that the rotated X axis can be represented with [cosY0-sinY]. The Z axis lags 90° behind it, so that's [sinY0cosY]. The Y axis remains unchanged, which completes the rotation matrix.
使用我们为绕 Z 轴旋转而找到的相同推理,我们可以推导出一个绕 Y 轴旋转的矩阵。首先,X 轴最初为 [100] ,逆时针旋转 90°后变为 [00-1] 。这意味着旋转后的 X 轴可以用 [cosY0-sinY] 表示。Z 轴比它滞后 90°,所以是 [sinY0cosY] 。Y 轴保持不变,这便完成了旋转矩阵。

[cosY0sinY010-sinY0cosY]

The third rotation matrix keeps X constant and adjust Y and Z in a similar way.
第三个旋转矩阵保持 X 轴不变,并以类似的方式调整 Y 轴和 Z 轴。

[1000cosX-sinX0sinXcosX]

Unified Rotation Matrix  统一旋转矩阵

Our three rotation matrices each rotate around a single axis. To combine them, we have to apply one after the other. Let's rotate around Z first, then around Y, and finally around X. We can do this by first applying the Z rotation to our point, then applying the Y rotation to the result, and then applying the X rotation to that result.
我们的三个旋转矩阵都只绕单一轴旋转。为了组合它们,我们必须依次应用。让我们先绕 Z 轴旋转,然后绕 Y 轴,最后绕 X 轴。我们可以通过先将 Z 轴旋转应用于点,然后将 Y 轴旋转应用于结果,最后将 X 轴旋转应用于该结果来做到这一点。

But we can also multiply our rotation matrices with each other. That will produce a new rotation matrix, which would apply all three rotations at once. Let's fist perform Y × Z.
但我们也可以将旋转矩阵相互相乘。这将产生一个新的旋转矩阵,一次性应用所有三次旋转。让我们先执行 Y × Z。

The first entry of the result matrix is cosYcosZ-0sinZ-0sinY=cosYcosZ. The entire matrix requires a lot of multiplications, but many parts end up as 0s and can be discarded.
结果矩阵的第一个条目是 cosYcosZ-0sinZ-0sinY=cosYcosZ 。整个矩阵需要大量的乘法运算,但许多部分最终为 0,可以舍弃。

[cosYcosZ-cosYsinZsinYsinZcosZ0-sinYcosZsinYsinZcosY]

Now perform X × (Y × Z) to get our final matrix.
现在执行 X × (Y × Z) 以获得我们的最终矩阵。

[cosYcosZ-cosYsinZsinYcosXsinZ+sinXsinYcosZcosXcosZ-sinXsinYsinZ-sinXcosYsinXsinZ-cosXsinYcosZsinXcosZ+cosXsinYsinZcosXcosY]

Now that we have this matrix, we can see how the X, Y, and Z axes of the rotation result can be constructed.
现在我们有了这个矩阵,我们就可以看到旋转结果的 X、Y 和 Z 轴是如何构建的。

	public override Vector3 Apply (Vector3 point) {
		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);

		Vector3 xAxis = new Vector3(
			cosY * cosZ,
			cosX * sinZ + sinX * sinY * cosZ,
			sinX * sinZ - cosX * sinY * cosZ
		);
		Vector3 yAxis = new Vector3(
			-cosY * sinZ,
			cosX * cosZ - sinX * sinY * sinZ,
			sinX * cosZ + cosX * sinY * sinZ
		);
		Vector3 zAxis = new Vector3(
			sinY,
			-sinX * cosY,
			cosX * cosY
		);

		return xAxis * point.x + yAxis * point.y + zAxis * point.z;
	}
Rotating around three axes.
绕三个轴旋转。
unitypackage

Matrix Transformations  矩阵变换

If we can combine three rotations into a single matrix, could we also combine scaling, rotating, and repositioning into one matrix? If we can represent scaling and repositioning as matrix multiplications, then the answer is yes.
如果我们可以将三次旋转组合成一个矩阵,那么我们是否也可以将缩放、旋转和重新定位组合成一个矩阵呢?如果我们可以将缩放和重新定位表示为矩阵乘法,那么答案是肯定的。

A scaling matrix is straightforward to construct. Take the identity matrix and scale its components.
缩放矩阵的构建非常简单。取单位矩阵并缩放其分量即可。

[200030004][xyz]=[2x3y4z]

But how could we support repositioning? This is not a redefinition of the three axes, it is an offset. So we cannot represent it with the 3 by 3 matrix that we have right now. We need an additional column to contain the offset.
但我们如何支持重新定位呢?这并非对三个轴的重新定义,而是一个偏移量。因此,我们无法用我们现有的 3x3 矩阵来表示它。我们需要额外一列来包含偏移量。

[100201030014][xyz]=[x+2y+3z+4]

However, this is invalid because our matrix's row length has become 4. So we need to add a fourth component to our point. As this component gets multiplied with the offset, it should be 1. And we want to preserve that 1, so it can be used in further matrix multiplications. This leads to a 4 by 4 matrix and a 4D point.
然而,这是无效的,因为我们的矩阵的行长度已变为 4。因此我们需要给我们的点添加第四个分量。由于该分量会与偏移量相乘,所以它应该为 1。我们希望保留这个 1,以便它可以在后续的矩阵乘法中使用。这就引出了一个 4 乘 4 的矩阵和一个 4D 点。

[1002010300140001][xyz1]=[1x+0y+0z+20x+1y+0z+30x+0y+1z+40x+0y+0z+1]=[x+2y+3z+41]

So we have to use 4 by 4 transformation matrices. This means that the scale and rotation matrices get an additional row and column with 0s and a 1 at the bottom right. And all our points get a fourth coordinate, which is always 1.
所以我们必须使用 4x4 变换矩阵。这意味着缩放和旋转矩阵会额外增加一行一列,其中大部分为 0,右下角为 1。我们所有的点都会获得第四个坐标,其值始终为 1。

Homogeneous Coordinates  齐次坐标

Can we make any sense of that fourth coordinate? Does it represent anything useful? We know that we give it the value 1 to enable repositioning of points. If its value were 0, the offset would be ignored, but scaling and rotation would still happen.
我们能理解这第四个坐标的含义吗?它是否代表任何有用的东西?我们知道我们给它赋值 1 是为了能够重新定位点。如果它的值是 0,偏移量将被忽略,但缩放和旋转仍然会发生。

Something that can be scaled and rotated, but not moved. That is not a point, that is a vector. A direction.
可以缩放和旋转,但不能移动的东西。那不是一个点,那是一个向量。一个方向。

So [xyz1] represents a point, while [xyz0] represents a vector. This is useful, because it means that we can use the same matrix to transform positions, normals, and tangents.
因此, [xyz1] 代表一个点,而 [xyz0] 代表一个向量。这很有用,因为这意味着我们可以使用相同的矩阵来变换位置、法线和切线。

So what happens when the fourth coordinate gets a value other than 0 or 1? Well, it shouldn't. Or actually, it should make no difference. We are now working with homogeneous coordinates. The idea is that each point in space can be represented by an infinite amount of coordinate sets. The most straightforward form uses 1 as the fourth coordinate. All other alternatives can be found by multiplying the whole set with an arbitrary number.
那么当第四个坐标的值不是 0 或 1 时会发生什么?嗯,它不应该。或者说,它实际上应该没有区别。我们现在正在使用齐次坐标。其思想是空间中的每个点都可以由无限多个坐标集表示。最直接的形式是使用 1 作为第四个坐标。所有其他替代方案都可以通过将整个集合乘以任意数字来找到。

[xyz1]=[2x2y2z2]=[3x3y3z3]=[wxwywzw]=w[xyz1]

So to get the Euclidean point – the actual 3D point – you divide each coordinate by the fourth one, which is then discarded.
因此,为了得到欧几里得点——实际的 3D 点——你需要将每个坐标除以第四个坐标,然后舍弃第四个坐标。

[xyzw]=1w[xyzw]=[xwywzw1][xwywzw]

Of course that doesn't work when the fourth coordinate is 0. Such points are defined to be infinitely far away. That's why they behave as directions.
当然,当第四个坐标为 0 时,这种方法就不奏效了。这样的点被定义为无限远。这就是它们表现为方向的原因。

Using Matrices  使用矩阵

We can use Unity's Matrix4x4 struct to perform matrix multiplications. From now on, we'll use it to perform out transformations instead of the current approach.
我们可以使用 Unity 的 Matrix4x4 结构体来执行矩阵乘法。从现在开始,我们将用它来执行变换,而不是当前的方法。

Add an abstract readonly property to Transformation to retrieve the transformation matrix.
Transformation 添加一个抽象的只读属性以检索变换矩阵。

	public abstract Matrix4x4 Matrix { get; }

Its Apply method no longer needs to be abstract. It will just grab the matrix and perform the multiplication.
它的 Apply 方法不再需要是抽象的了。它将直接获取矩阵并执行乘法。

	public Vector3 Apply (Vector3 point) {
		return Matrix.MultiplyPoint(point);
	}

Note that Matrix4x4.MultiplyPoint has a 3D vector parameter. It assumes that the missing fourth coordinate is 1. It also takes care of the conversion back from homogeneous coordinates to Euclidean coordinates. If you want to multiply a direction instead of a point, you can use Matrix4x4.MultiplyVector.
请注意, Matrix4x4.MultiplyPoint 有一个 3D 向量参数。它假定缺失的第四个坐标是 1。它还负责将齐次坐标转换回欧几里得坐标。如果你想乘以一个方向而不是一个点,你可以使用 Matrix4x4.MultiplyVector

The concrete transformation classes now have to change their Apply methods into Matrix properties.
具体的变换类现在必须将其 Apply 方法更改为 Matrix 属性。

First up is PositionTransformation. The Matrix4x4.SetRow method offers a convenient way to fill a matrix.
首先是 PositionTransformationMatrix4x4.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;
		}
	}

Next up is ScaleTransformation.
接下来是 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;
		}
	}

For RotationTransformation, it's more convenient to set the matrix column by column, as that matches our already existing code.
对于 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;
		}
	}

Combining Matrices  组合矩阵

Let's now combine our transformation matrices into a single matrix. Add a transformation matrix field to TransformationGrid.
现在,我们将变换矩阵组合成一个单一矩阵。向 TransformationGrid 添加一个变换矩阵字段。

	Matrix4x4 transformation;

We'll update this transformation matrix each Update. This involves grabbing the first matrix, then multiplying it with all the others. Make sure that they are multiplied in the correct order.
我们将在每次更新时更新此变换矩阵。这包括获取第一个矩阵,然后将其与所有其他矩阵相乘。确保它们以正确的顺序相乘。

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

Now the grid no longer invokes Apply, but performs the matrix multiplication itself.
现在网格不再调用 Apply ,而是自行执行矩阵乘法。

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

This new approach is more efficient, because we used to create each transformation matrix separately for every point and apply them individually. Now we create a unified transformation matrix once and reuse it for every point. Unity uses the same trick to reduce every object hierarchy to a single transformation matrix.
这种新方法更高效,因为我们过去为每个点单独创建每个变换矩阵并单独应用它们。现在我们统一创建一次变换矩阵并将其重复用于每个点。Unity 也使用同样的技巧将每个对象层级简化为一个单一的变换矩阵。

In our case, we could make it even more efficient. All transformation matrices have the same bottom row, [0001]. Knowing this, we could forget about that row, skipping the computation of 0s and the conversion division at the end. The Matrix4x4.MultiplyPoint4x3 method does exactly that. However, we're not going to use that method, because there are useful transformations that do change the bottom row.
在我们的例子中,我们可以使其效率更高。所有变换矩阵都具有相同的底行,即 [0001] 。知道了这一点,我们可以忽略该行,跳过对 0 的计算以及末尾的转换除法。 Matrix4x4.MultiplyPoint4x3 方法正是这样做的。但是,我们不会使用该方法,因为存在一些会改变底行的有用变换。

unitypackage

Projection Matrices  投影矩阵

So far, we've been transforming points from one position in 3D to another position in 3D space. But how do those points end up drawn on a 2D display? This requires a transformation from 3D to 2D space. We can create a transformation matrix for that!
到目前为止,我们一直在将三维空间中的点从一个位置转换到另一个位置。但这些点最终是如何呈现在二维显示器上的呢?这需要从三维空间到二维空间的转换。我们可以为此创建一个转换矩阵!

Make a new concrete transformation for a camera projection. Start with the identity matrix.
为摄像机投影创建一个新的具体转换。从单位矩阵开始。

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

Add it as the final transformation.
将其添加为最终转换。

The camera projection comes at the end.
摄像机投影放在最后。

Orthographic Camera  正交相机

The most straightforward way to go from 3D to 2D is to simply discard one dimension. That will collapse 3D space into a flat plane. This plane acts like a canvas, used to render the scene. Let's just drop the Z dimension and see what happens.
从三维到二维最直接的方法是直接舍弃一个维度。这将使三维空间坍缩成一个平面。这个平面就像画布一样,用于渲染场景。让我们舍弃 Z 维度,看看会发生什么。

[1000010000000001]

			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, 0f, 1f));
scene projection
Orthographic Projection.
正交投影。

Indeed, our grid becomes 2D. You can still scale, rotate, and reposition everything, but it gets projected onto the XY plane afterwards. This is a rudimentary orthographic camera projection.
确实,我们的网格变成了二维的。你仍然可以缩放、旋转和重新定位所有东西,但之后它会被投影到 XY 平面上。这是一种基本的正交相机投影。

Our primitive camera sits at the origin and looks in the positive Z direction. Could we move it around and rotate it? Yes, in fact we can already do that. Moving the camera has the same visual effect as moving the world in the opposite direction. The same goes for rotation and scaling. So we can use our existing transformations to move the camera, although it is a bit awkward. Unity uses matrix inversion to do the same thing.
我们的原始相机位于原点,并沿着正 Z 方向观察。我们能移动和旋转它吗?是的,实际上我们已经可以做到这一点了。移动相机与向相反方向移动世界具有相同的视觉效果。旋转和缩放也是如此。所以我们可以使用我们现有的变换来移动相机,尽管这有点笨拙。Unity 使用矩阵求逆来实现相同的功能。

Perspective Camera  透视相机

An orthographic camera is nice, but doesn't show the world as we see it. We need a perspective camera for that. Due to perspective, things that are further away appear smaller to us. We can reproduce this effect by scaling points based on their distance from the camera.
正交摄像机虽好,但无法呈现我们所见的世界。我们需要透视摄像机来达到这一目的。由于透视效果,距离较远的物体在我们看来会显得更小。我们可以通过根据物体与摄像机的距离来缩放点来重现这种效果。

Let's just divide everything by the Z coordinate. Can we do that with a matrix multiplication? Yes, by changing the bottom row of an identity matrix to [0,0,1,0]. That will make the fourth coordinate of the result equal to the original Z coordinate. Converting from homogeneous to Euclidean coordinates then takes care of the desired division.
我们只需将所有坐标除以 Z 坐标即可。这能通过矩阵乘法实现吗?可以。将单位矩阵的最后一行改为 [0,0,1,0] 。这样,结果的第四个坐标将等于原始的 Z 坐标。从齐次坐标转换为欧几里得坐标时,便会完成所需的除法。

[1000010000000010][xyz1]=[xy0z][xzyz0]

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

The big difference with the orthographic projection is that points aren't moved straight down to the projection plane. Instead, they are moved towards the camera's position – the origin – until they hit the plane. Of course this is only valid for points that lie in front of the camera. Points that lie behind the camera will be incorrectly projected. As we're not discarding those points, make sure everything lies in front of the camera, via repositioning. A distance of 5 would be enough when the grid is not scaled or rotated, otherwise you might need more.
正交投影的最大不同在于,点不是直接向下移动到投影平面,而是朝摄像机位置(原点)移动,直到它们碰到平面。当然,这仅适用于位于摄像机前方的点。位于摄像机后方的点将被错误投影。由于我们不丢弃这些点,请确保通过重新定位使所有物体都位于摄像机前方。当网格未缩放或旋转时,距离为 5 就足够了,否则可能需要更大距离。

scene projection
Perspective Projection.  透视投影。

The distance between the origin and the projection plane also influences the projection. It acts like the focal length of a camera. The larger you make it, the smaller your field of view will be. Right now we're using a focal length of 1, which produces a 90° field of view. We can make that configurable.
原点和投影平面之间的距离也会影响投影。它就像摄像机的焦距。焦距越大,视野就越小。目前我们使用的是焦距 1,这将产生 90° 的视野。我们可以使其可配置。

	public float focalLength = 1f;

Focal length.  焦距。

As a larger focal length means we're zooming in, this effectively increases the scale of our final points, so we can support it that way. As we're collapsing the Z dimension, that one doesn't need to be scaled.
由于焦距越大意味着我们放大的倍数越大,这会有效地增加我们最终点的比例,因此我们可以通过这种方式来支持它。由于我们正在折叠 Z 维度,所以它不需要进行缩放。

[fl0000fl0000000010][xyz1]=[xflyfl0z][xflzyflz0]

			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));
Adjusting the focal length.
调整焦距。

We now have a very simple perspective camera. If we were to fully mimic Unity's camera projection, we would also have to deal with the near and far plane. That would require projecting into a cube instead of a plane, so depth information is retained. Then there is the view aspect ratio to worry about. Also, Unity's camera looks in the negative Z direction, which requires negating some numbers. You could incorporate all that into the projection matrix. I leave it to you to figure out how to do that, if you want to.
我们现在有了一个非常简单的透视相机。如果我们要完全模仿 Unity 的相机投影,我们还需要处理近平面和远平面。这将需要投影到一个立方体而不是平面,这样才能保留深度信息。然后还需要担心视图的宽高比。另外,Unity 的相机朝向负 Z 方向,这需要对一些数字取反。你可以将所有这些都整合到投影矩阵中。如果你想这样做,我将留给你自己去探索如何实现。

So what was the point of all this? We rarely need to construct matrices ourselves, and definitely not projection matrices. The point is that you now understand what's going on. Matrices aren't scary, they just transform points and vectors from one space to another. And you understand how. That's good, because you'll encounter matrices again once we start writing our own shaders. We'll do so in part 2, Shader Fundamentals.
那么所有这一切的意义是什么呢?我们很少需要自己构造矩阵,尤其不会自己构造投影矩阵。重点在于你现在理解了正在发生什么。矩阵并不可怕,它们只是将点和向量从一个空间转换到另一个空间。并且你理解了其原理。这很好,因为当我们开始编写自己的着色器时,你将再次遇到矩阵。我们将在第二部分《着色器基础》中进行介绍。

unitypackage PDF
switch theme  切换主题
contents  内容
  1. Visualizing Space
    1. Transformations
      1. Translation
      2. Scaling
      3. Rotation
    2. Full Rotations
      1. Matrices
      2. 3D Rotation Matrices
      3. Rotation Matrices for X and Y
      4. Unified Rotation Matrix
    3. Matrix Transformations
      1. Homogeneous Coordinates
      2. Using Matrices
      3. Combining Matrices
    4. Projection Matrices
      1. Orthographic Camera
      2. Perspective Camera