[Unity 学习] - 进阶篇 - 基础渲染系列(一)图形学的基石——矩阵
本文并非原创,只是本人的学习记录,原文是由放牛的星星老师翻译Catlike系列教程
链接: https://zhuanlan.zhihu.com/p/137786467
文章目录
1 创建一个立方体构建的Grid网格(空间可视化)
这里创建一个Grid网格,主要是为了深入理解Unity的gameobject从本地坐标到世界坐标时,是如何改变自己的缩放,旋转,位移。
代码创建一个10 * 10 * 10的物体
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TransformationGrid : MonoBehaviour
{
public Transform prefab;
public int gridResolution = 10;
private Transform[] grid;
private 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);
}
}
}
}
// 创建每个Grid位置和颜色
private 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;
}
private 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
);
}
}
2 创建Transformation
一个物体的变换主要有三种:定位,缩放,旋转
我们应该为Transform组件创建一个可以继承的基类,他是一个抽象类,不能直接使用,有一个抽象的方法Apply。
using UnityEngine;
public abstract class Transformation : MonoBehaviour
{
public abstract Vector3 Apply(Vector3 point);
}
这些组件对物体的操作,需要我们用某种方式进行检索,可以使用List存储这些组件的引用
private List<Transformation> _transformations;
private void Awake()
{
...
// 将组件添加到网络对象后,用list来存储这些组件的引用
_transformations = new List<Transformation>();
}
使用Updata获取组件,然后进行修改坐标,这样做的好处是为了在播放模式中使用Transform组件,并立即看到结果
private 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);
}
}
}
}
通过原始坐标和各个变换找到每个点变换的的位置,但是我们不能通过每个点的实际位置进行变换,因为从本地坐标到世界坐标已经做完了一次变换
2.1 偏移转换
最简单是偏移转换,所以我们创建的第一个扩张Transformation是PositionTransformation
using UnityEngine;
public class PositionTransformation : Transformation
{
public Vector3 position;
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;
}
}
2.3 旋转变换
第三种变换类型是旋转。这是三个变换中最难的一部分,我们一点一点来。
首先我们创建一个新的组件。
using UnityEngine;
public class RotationTransformation : Transformation
{
public Vector3 rotation;
public override Vector3 Apply (Vector3 point)
{
return point;
}
}
接下来需要理解一些数学上的知识
当我们面向Z轴负半轴以逆时针为正方向时; 设x轴为(1,0)时,旋转90°,180°,270°结果是(0,1)(-1,0)(0,-1)
同理,设y轴为(0,1)时,旋转90°,180°,270°结果是(-1,0)(0,-1)(1,0)
同理可得,旋转45°,135°,225°,315°可以得出是正弦和y坐标匹配,余弦和x坐标匹配。所以将x轴定义为(cosz, sinz),y轴定义为(-sinz, cosz).(这里翻译得有些不好懂,其实我们旋转的是相对于物体每个点的整个坐标系,原文中直接使用(1,0)和(0,1)并不是表示两个点,而是两个点所对应的轴,也不知道我这样理解有没有问题)
先计算x轴和y轴的旋转角度
public override Vector3 Apply(Vector3 point)
{
float radZ = rotation.z * Mathf.Deg2Rad;
// 这里需要注意一下,rotation是度数,也就是旋转多少度
// Mathf.Sin 和 Math.Cos是用的是弧度,所以需要进行转换
// Mathf.Deg2Rad: 角度值转换为弧度值
// Mathf.Rad2Deg: 弧度值转化为角度值
float sinZ = Mathf.Sin(radZ);
float cosZ = Mathf.Cos(radZ);
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/089f91eee10f43c285948957ec7fe9e0.png
举个例子:这里我们有一个坐标系,上面有一个点(3,2),它对应的x坐标为3 * (1,0) + 2 * (0,1)
当旋转时,就变成了3*(cosZ, sinZ) + 2 * (-sinZ,cosZ) = (3cosZ - 2sinZ, 3sinZ + 2cosZ)
所以对于当前的任意点(x,y)在旋转后都是(xcosZ - ysinZ, xsinZ + ycosZ)
return new Vector3(
point.x * cosZ - point.y * sinz,
point.x * sinZ + point.y * cosZ,
point.z
);
3 完全体的旋转
现在我们只能绕Z轴旋转,当我们需要更多旋转的时候,会变得更加复杂,当然我们也可以写三个函数分别控制旋转,但是那样每个旋转都会影响到其他角度的旋转,所以我们需要将多个轴的旋转组合为一个旋转。
3.1 矩阵
矩阵乘法是大学基础数学,行 * 列,公共的位置,就是结果的位置。
三点性质:1,结果具有第一矩阵的行数和第二矩阵的列数,2,第一矩阵的列数也需要等于第二矩阵的行数
3,a矩阵 * b矩阵 ~= b矩阵 * a矩阵
3.2 3D旋转矩阵
我们将旋转矩阵增加到3 * 3,第三维空间用0表示
Z分量始终为0 ,这是不合理的,所以我们在表示Z轴的第三列插入1
结果变为:
这是合理的,我们可以直接用这里矩阵去乘以每个点,得到旋转后的点位。
旋转Y轴可以用表示
旋转X轴可以用表示
3.4 统一旋转矩阵
现在我们有了三个旋转矩阵,我们有两种方式让旋转举证作用于我们点,
第一种方式:将Z旋转应用于结果,Y旋转应用于结果,X旋转应用于结果
第二种方式:将三个矩阵相乘,产生一个新的旋转矩阵
结果就是
这里需要注意的是 X * (Y * Z) = (X * Y ) * Z 但是 X * Y * Z ≠ Z * Y * X
Unity的实际轮换顺序为ZXY,而且面对Z轴负向的时候,顺时针为正。不过这里具体是如何其实并不重要。我们只需要知道Unity是如何做到物体旋转的就可以了,无非就是调整的数据参数是不同的,这个不重要。
public override Vector3 Apply(Vector3 point)
{
float radX = rotation.x * Mathf.Deg2Rad;
float radY = rotation.y * Mathf.Deg2Rad;
float radZ = rotation.z * Mathf.Deg2Rad;
// 这里需要注意一下,rotation是度数,也就是旋转多少度
// Mathf.Sin 和 Math.Cos是用的是弧度,所以需要进行转换
// Mathf.Deg2Rad: 角度值转换为弧度值
// Mathf.Rad2Deg: 弧度值转化为角度值
// Debug.Log(Mathf.Sin(90.0f * 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;
}
4 矩阵转换
既然我们能够将三个旋转方向组合到一个矩阵,那我们是不是可以将缩放,旋转,定位也组合在一起,这样可以极大的减少数据的计算。(当我们对某个物体进行操作后,保存的是一个数据而不是三个数据,在有大量物体时,这里很好的一个方案)。
缩放矩阵容易构建:
那么定位矩阵如何定义偏移变量呢?
我们需要得到这样的一组数据:
这时候就需要我们不能动每行的数据,因为会对xyz进行乘积,最好的方式就用 (4* 3 )(14)矩阵相乘
但我们通常不会这样做,我们会将最后一位设为w,这种方式会在结果中w抹除掉,并有利于后续的计算,最好的方式是将43矩阵改为4*4矩阵,将结果中的w保留下来。
4.1齐次坐标
第四个坐标是什么呢?当我们当我们赋值1时:他可以改变点的坐标,当赋值0的时候,偏移量会被忽略。
所以当w=1时,表示点,当w=0时,表示向量。
PS : 我们可以将点变成向量吗?可以,当最后一行是(0,0,0,0)时,会将点变成向量。
有什么意义呢? 在计算两个点直接的距离,或者是将3D物体投射到2D空间的时候我们需要用到
当我们计算完一个齐次坐标后,需要将这齐次坐标转化为3D坐标,所以需要将权重W删除
4.2 使用矩阵
将一个抽象的只读属性添加到Transformation用于检索转换矩阵
public abstract Matrix4x4 Matrix { get; }
在子类中将Apply方法更改为Matrix属性
PositionTransformation
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中加入一个4x4的矩阵
private Matrix4x4 transformation; // 将Transform矩阵字段添加到TransformationGrid
将三个变换矩阵相乘组合起来
void Update()
{
UpdateTransformation();
...
}
// 更新Transformation,保证每帧更新
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;
}
}
}
为什么要在update中函数,因为当我改变参参数的时候,我希望可以直接在游戏中看到改变
现在网格中需要调用自己的矩阵乘法
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 transformation.MultiplyPoint(coordinates);
//按这个矩阵transformation变换位置, 传入的是一个点的坐标Vector3
}
5 投影矩阵
之前我们说过,齐次坐标可以将3D物体转换到2D空间
首先从单位矩阵开始
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;
}
}
}
放弃Z轴,就是正交投影
matrix.SetRow(2, new Vector4(0f, 0f, 0f, 0f));
5.2 透视摄像机
使用焦距为1,可产生90°的视野。
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));
配置焦距
public class CameraTransformation : Transformation
{
public float focalLength = 0;
public override Matrix4x4 Matrix
{
get
{
Matrix4x4 matrix = new Matrix4x4();
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));
return matrix;
}
}
}
这章我们理解了整个立方体网格如何转换,而且我们知道了矩阵是使用方式,重点是矩阵,矩阵,矩阵
矩阵在后续的shader中很用。