[Unity 学习] - 进阶篇 - 基础渲染系列(一)图形学的基石——矩阵

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

物品节点的设置
物体的设置,其他都是默认,大小为0.5

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抹除掉,并有利于后续的计算,最好的方式是将4
3矩阵改为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中很用。

猜你喜欢

转载自blog.csdn.net/qq_43617207/article/details/129550703
今日推荐