UE4 - 基础篇:矩阵是什么?

想看为什么不问我要密码呢?想看为什么不问我要密码呢?想看为什么不问我要密码呢?想看为什么不问我要密码呢?

在哪些辅助项目中,所谓的矩阵就是视图投影矩阵(有些人喜欢叫他相口)。在UE4源码中,这个矩阵通常被命名为:

  • ULocalPlayer::ViewProjectionMatrix

  • FSceneView::ViewProjectionMatrix

这个矩阵包含了相机的变换信息,用于将世界坐标转换为屏幕坐标。一般ESP绘制方框的基本步骤是:

  1. 获取实体的世界坐标(World Position),又叫做WorldLocation。

  2. 获取玩家相机的ViewProjectionMatrix

  3. 使用矩阵乘法: 世界坐标 × ViewProjectionMatrix = 屏幕坐标

  4. 使用转换后的屏幕坐标绘制2D方框

如果您想找到这个矩阵的具体内存位置, 建议一般是:

  1. 在游戏进程中搜索类似矩阵的特征值

  2. 通过UE4的GEngine等全局对象来定位

  3. 查找引擎渲染相关的函数调用链

在虚幻引擎中有保存好了的视图投影矩阵,我们可以直接从内存中去读取,当然你也可以口算这个矩阵....

不是白痴可以跳过这节

欧拉角

我这里有个小工具可以实现四元数和欧拉角的转换,还能复刻万向死锁呢 >> 伏秋洛的小玩具

哦,你说我这个小玩具不直观?其实我还有一个https://compsci290-s2016.github.io/CoursePage/Materials/EulerAnglesViz/

Euler angles,莱昂哈德·欧拉用欧拉角来描述刚体在三维欧几里得空间的取向。对于任何参考系,一个刚体的取向,是依照顺序,从这参考系,做三个欧拉角的旋转而设定的。所以,刚体的取向可以用三个基本旋转矩阵来决定。换句话说,任何关于刚体旋转的旋转矩阵是由三个基本旋转矩阵复合而成的。

其实我也不能说什么,我数学一坨狗屎吧,这里有一个文档说的比较清晰

这里说一下Pitch/Yaw/Roll的区别

机体坐标系:固定在飞行器上的坐标系,一般沿机身方向为X轴,沿机翼方向为Y轴,垂直机身方向为Z轴,如上图所示。下面说的旋转都是指沿机体坐标系。

图片摘自:http://www.crazepony.com/wiki/pitch-yaw-roll.html

这里有一个著名的弱智知识《万向死锁》

其实就是欧拉角的定义出现了问题,无法修复,是一个软件问题,没什么好讨论的,好吧,我说一下就是:欧拉角是一个按指定顺序的变换。

详细定义视频:https://www.bilibili.com/video/BV1Nr4y1j7kn

视图矩阵

UE的视图矩阵(View Matrix)是一个4x4的矩阵,用于将物体从世界空间转换到摄像机空间(也叫观察空间)。这个东西的作用就是将世界空间中的点转换到摄像机空间,其定义了摄像机的位置(Eye)和方向(Forward, Right, Up),是渲染管线中的关键变换矩阵之一。

在ue中可以通过一些调用获取视图矩阵

// 从PlayerController获取
FMatrix ViewMatrix = PlayerController->PlayerCameraManager->GetCameraViewMatrix();

// 从Camera组件获取
FMatrix ViewMatrix = CameraComponent->GetComponentToWorld().ToMatrixNoScale().Inverse();

视图矩阵的变换顺序:

  • 先平移:将场景原点移动到摄像机位置

  • 再旋转:将场景按摄像机的方向进行旋转

  • 最终结果:物体从世界空间变换到以摄像机为原点的观察空间

// 最终屏幕上的点 = 投影矩阵 * 视图矩阵 * 世界矩阵 * 物体本地坐标
FVector4 ScreenPosition = ProjectionMatrix * ViewMatrix * WorldMatrix * LocalPosition;

这里有一些课程视频:

需要注意的是,ue中视图矩阵的格式是:

{
	M[0][0] = InX.X; M[0][1] = InX.Y;  M[0][2] = InX.Z;  M[0][3] = 0.0f;
	M[1][0] = InY.X; M[1][1] = InY.Y;  M[1][2] = InY.Z;  M[1][3] = 0.0f;
	M[2][0] = InZ.X; M[2][1] = InZ.Y;  M[2][2] = InZ.Z;  M[2][3] = 0.0f;
	M[3][0] = InW.X; M[3][1] = InW.Y;  M[3][2] = InW.Z;  M[3][3] = 1.0f;
}

口算视图投影矩阵

首先我们要获取到当前人物RotationLocation构建ViewMatrix。

哦,这个东西有个缺点,因为在UE外挂开发里面,我们一般是不知道相机的Rota和Loc的,所以说用人物的绘制,可能因为别的视野按钮的移动导致绘制偏移,这是我们不愿意看到的,但是还是和你说一下口算吧...

首先需要将欧拉角转换为方向向量(要转换是因为你可以获取到的Rotation使用的是FRotator保存的):

// pitch(X), yaw(Y), roll(Z)
FVector GetForwardVector(FRotator rotation) {
    float cp = cos(rotation.pitch * PI / 180); // c++三角函数用的是弧度,而ue里面的rota是度(degree)
    float sp = sin(rotation.pitch * PI / 180);
    float cy = cos(rotation.yaw * PI / 180);
    float sy = sin(rotation.yaw * PI / 180);
    
    return FVector(cp * cy, cp * sy, sp);
}

FVector GetRightVector(FRotator rotation) {
    float cy = cos(rotation.yaw * PI / 180);
    float sy = sin(rotation.yaw * PI / 180);
    
    return FVector(sy, -cy, 0);
}

FVector GetUpVector(FRotator rotation) {
    float cp = cos(rotation.pitch * PI / 180);
    float sp = sin(rotation.pitch * PI / 180);
    float cy = cos(rotation.yaw * PI / 180);
    float sy = sin(rotation.yaw * PI / 180);
    
    return FVector(-sp * cy, -sp * sy, cp);
}

然后我们构建视图矩阵:

FMatrix BuildViewMatrix(FVector location, FRotator rotation) {
    FVector forward = GetForwardVector(rotation);
    FVector right = GetRightVector(rotation);
    FVector up = GetUpVector(rotation);
    
    // 构建旋转矩阵
    FMatrix viewMatrix;
    viewMatrix.M[0][0] = right.x;
    viewMatrix.M[0][1] = right.y;
    viewMatrix.M[0][2] = right.z;

    viewMatrix.M[1][0] = up.x;
    viewMatrix.M[1][1] = up.y;
    viewMatrix.M[1][2] = up.z;

    viewMatrix.M[2][0] = -forward.x;
    viewMatrix.M[2][1] = -forward.y;
    viewMatrix.M[2][2] = -forward.z;
    
    // 加入平移
    viewMatrix.M[3][0] = -(location.x * right.x + location.y * right.y + location.z * right.z);
    viewMatrix.M[3][1] = -(location.x * up.x + location.y * up.y + location.z * up.z);
    viewMatrix.M[3][2] = (location.x * forward.x + location.y * forward.y + location.z * forward.z);
    viewMatrix.M[3][3] = 1.0f;
    
    return viewMatrix;
}

PS:

  • UE4中角度是以度为单位,需要转换为弧度

  • Forward向量在ViewMatrix中需要取负值

  • Translation部分也需要取负值