写在前面

  • 这个部分对于Eigen库的使用要求尽量熟练不然每次都要查英文文档,比较痛苦,一种相对可行一些的方案就是利用ai辅助实现相应的函数调用。
  • 如果追求进行对Eigen API更详细的完善和修正,推荐一个中文版本的API查询文档:Eigen中文文档-CSDN。这个查询文档需要关注作者,但是考虑到文档的实用性,记得注册账号支持一下进行汉化工作的著作者。

作业内容

要求部分

本次作业的任务是填写一个旋转矩阵和一个透视投影矩阵。 给定三维下三个点 v0(2.0,0.0,−2.0),、 v1(0.0,2.0,−2.0)、 v2(−2.0,0.0,−2.0)。 你需要将这三个点的坐标变换为屏幕坐标并在屏幕上绘制出对应的线框三角形(在代码框架中,我们已经提供了 draw_triangle 函数,所以你只需要去构建变换矩阵即可)。简而言之, 我们需要进行模型、视图、投影、视口等变换来将三角形显示在屏幕上。 • get_model_matrix(float rotation_angle): 逐个元素地构建模型变换矩 阵并返回该矩阵。在此函数中,你只需要实现三维中绕z轴旋转的变换矩阵, 而不用处理平移与缩放。 • get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar): 使用给定的参数逐个元素地构建透视投影矩阵并返回 该矩阵。 • [Optional] main(): 自行补充你所需的其他操作。、

重点

  • 完成模型旋转方法的相关内容
  • 完成透视投影变换的相关内容
  • (选择性)利用罗德里格斯旋转公式实现对三维某个坐标方向的旋转方法。

实现

get_model_matrix的实现

数学原理

这个地方的目的是根据传入的角度计算形成对应的旋转矩阵,题目中要求完成围绕z轴旋转的方法,那么对于旋转角度zeita,其对应的旋转矩阵为:

Rz(θ)=[cosθsinθ00sinθcosθ0000100001]R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

GAMES101中的线性代数基础知识以及几何学基础课已经足够支持对这一公式的理解,在此不做过多赘述。

代码实现

  • 创建对应矩阵并预处理对应的角度
// 创建一个单位矩阵
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
// 将角度转换为弧度
float angle_in_radians = rotation_angle * MY_PI / 180.0f;
  • 不难发现,首先创建单位矩阵后将对应位置替换成与角度有关的函数就可构建成功对应的旋转矩阵。
 model(0, 0) = std::cos(angle_in_radians);
 model(0, 1) = -std::sin(angle_in_radians);
 model(1, 0) = std::sin(angle_in_radians);
 model(1, 1) = std::cos(angle_in_radians);

之后根据作业框架返回对应矩阵即可。

get_projection_matrix的实现

数学原理

在此处我们利用投影变换方式根据传入的相应参数进行对应的矩阵变换。其原理如图: 通过这种方式我们可以实现对于美术上的透视原理再现,符合人的认知内容。 而对于进行对应的透视投影,我们需要两个平面的若干物理量,这里也可以从函数方法传入的参数中得到解释:

[1aspect_ratiotan(eye_fov2)00001tan(eye_fov2)0000zFar+zNearzFarzNear2zFarzNearzFarzNear0010]\begin{bmatrix} \frac{1}{\text{aspect\_ratio} \cdot \tan\left(\frac{\text{eye\_fov}}{2}\right)} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan\left(\frac{\text{eye\_fov}}{2}\right)} & 0 & 0 \\ 0 & 0 & \frac{\text{zFar} + \text{zNear}}{\text{zFar} - \text{zNear}} & \frac{2 \cdot \text{zFar} \cdot \text{zNear}}{\text{zFar} - \text{zNear}} \\ 0 & 0 & -1 & 0 \end{bmatrix}

我们在这里严格的沿用作业框架中用到的对应参数进行透视投影矩阵的书写。

公式中的参数说明
  • eye_fov

    • 类型float

    • 描述:视场角(Field of View Y),以度为单位。它决定了视角的范围,通常用于控制场景的“缩放”程度。

  • aspect_ratio

    • 类型float

    • 描述:宽高比(屏幕宽度 / 屏幕高度)。它用于保持图像的宽高比例,防止物体在投影后变形。

  • zNear

    • 类型float

    • 描述:近裁剪平面的距离。它定义了视锥体的近端边界,通常是一个大于 0 的值。

  • zFar

    • 类型float

    • 描述:远裁剪平面的距离。它定义了视锥体的远端边界,通常是一个大于 zNear 的值。

参数的作用
  • eye_fov

    • 控制视角的大小。较大的 eye_fov 会使场景看起来更“宽广”,但可能会导致物体变形。

    • 通常设置为 45 度到 90 度之间。

  • aspect_ratio

    • 用于保持图像的宽高比例。如果宽高比不正确,物体可能会被拉伸或压缩。

    • 例如,对于 16:9 的屏幕,aspect_ratio 应该是 16.0 / 9.0。

  • zNearzFar

    • 定义了视锥体的裁剪范围。

    • zNear 应该大于 0,zFar 应该大于 zNear

    • 这两个参数影响深度缓冲区的精度,较大的 zFar 和较小的 zNear 可能会导致深度精度问题。

代码实现

  • 先创建矩阵,预处理角度(此步骤一定要至始至终记住,以后也要经常使用!)
  Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
  float fovy_in_radians = eye_fov * MY_PI / 180.0f;
  • 计算所有根据参数所需要的对应值,并记录于对应的矩阵位置。
 projection(0, 0) = 1.0f / (aspect_ratio * tan_half_fovy);  // x 缩放
 projection(1, 1) = 1.0f / tan_half_fovy;                   // y 缩放
 projection(2, 2) = (zFar + zNear) / (zFar - zNear);        // z 缩放
 projection(2, 3) = (2.0f * zFar * zNear) / (zFar - zNear); // z 偏移
 projection(3, 2) = -1.0f;                                  // z 坐标变换
 projection(3, 3) = 0.0f;                                   // w 坐标变换
  • 返回对应的projection部分便可以完成对应的实现和解决。

提高题:get_rotation(Vector3f axis, float angle)方法的实现

原理

这一方法要求接口的实现人员完成对于任意过原点的直线做轴旋转给定角度zeita。这里使用了课程中给出的罗德里格斯旋转公式

vrot=vcosθ+(a×v)sinθ+a(av)(1cosθ)\mathbf{v}_{\text{rot}} = \mathbf{v} \cos \theta + (\mathbf{a} \times \mathbf{v}) \sin \theta + \mathbf{a} (\mathbf{a} \cdot \mathbf{v}) (1 - \cos \theta)

下面给出各个物理量的含义: 知道了原理之后这一切都不难理解了,我们很容易的就可以根据对应公式找到变换矩阵: 其中K:

K=[0azayaz0axayax0]\mathbf{K} = \begin{bmatrix} 0 & -a_z & a_y \\ a_z & 0 & -a_x \\ -a_y & a_x & 0 \end{bmatrix}

K²:

K2=[ay2az2axayaxazaxayax2az2ayazaxazayazax2ay2]\mathbf{K}^2 = \begin{bmatrix} -a_y^2 - a_z^2 & a_x a_y & a_x a_z \\ a_x a_y & -a_x^2 - a_z^2 & a_y a_z \\ a_x a_z & a_y a_z & -a_x^2 - a_y^2 \end{bmatrix}

将对应公式进行对应有效的整理,获得的过度矩阵就是:

[cosθ+ax2(1cosθ)axay(1cosθ)azsinθaxaz(1cosθ)+aysinθaxay(1cosθ)+azsinθcosθ+ay2(1cosθ)ayaz(1cosθ)axsinθaxaz(1cosθ)aysinθayaz(1cosθ)+axsinθcosθ+az2(1cosθ)]\begin{bmatrix} \cos \theta + a_x^2 (1 - \cos \theta) & a_x a_y (1 - \cos \theta) - a_z \sin \theta & a_x a_z (1 - \cos \theta) + a_y \sin \theta \\ a_x a_y (1 - \cos \theta) + a_z \sin \theta & \cos \theta + a_y^2 (1 - \cos \theta) & a_y a_z (1 - \cos \theta) - a_x \sin \theta \\ a_x a_z (1 - \cos \theta) - a_y \sin \theta & a_y a_z (1 - \cos \theta) + a_x \sin \theta & \cos \theta + a_z^2 (1 - \cos \theta) \end{bmatrix}

实现

实现逻辑没有特别困难,这里实践一下。

  Eigen::Matrix4f rotation= Eigen::Matrix4f::Identity();
  // 将角度转换为弧度
  float angle_in_radians = angle * MY_PI / 180.0f;

  // 将轴向量归一化
  axis.normalize();

  // 提取轴向量的分量
  float u_x = axis.x();
  float u_y = axis.y();
  float u_z = axis.z();

  // 计算 sin 和 cos
  //这个地方用标准库的三角函数解决相应问题。
  float sin_theta = std::sin(angle_in_radians);
  float cos_theta = std::cos(angle_in_radians);
  float one_minus_cos_theta = 1.0f - cos_theta;
  //对对应旋转矩阵根据罗德里格斯旋转公式构建旋转矩阵
  rotation(0, 0) = cos_theta + u_x * u_x * one_minus_cos_theta;
  rotation(0, 1) = u_x * u_y * one_minus_cos_theta - u_z * sin_theta;
  rotation(0, 2) = u_x * u_z * one_minus_cos_theta + u_y * sin_theta;

  rotation(1, 0) = u_y * u_x * one_minus_cos_theta + u_z * sin_theta;
  rotation(1, 1) = cos_theta + u_y * u_y * one_minus_cos_theta;
  rotation(1, 2) = u_y * u_z * one_minus_cos_theta - u_x * sin_theta;

  rotation(2, 0) = u_z * u_x * one_minus_cos_theta - u_y * sin_theta;
  rotation(2, 1) = u_z * u_y * one_minus_cos_theta + u_x * sin_theta;
  rotation(2, 2) = cos_theta + u_z * u_z * one_minus_cos_theta;

验证效果

  • 因为这个方法的对应接口为我们自己加入到main函数中的,我们在完成提高题的时候并没有位置加入对应的方法以及无法进行相应的验证,我们在这种情况下应该如何验证我们写的自由轴函数如何进行呢?这里个人认为是闫老师给的一个trick:你需要明白这个main函数渲染并且变换这一图形的流程是怎么样的才能真正明晰其真正目的。这里给出需要做的添加以及对渲染流程中矩阵对应矩阵计算要改变的部分。
  • 从题目要求的阅读中我们不难发现,提高题目的要求本质上就是将旋转的轴做了进一步的要求。那么我们需要在main中自己创建一个旋转轴,并且将之前调用get_model_matrix的部分进行调整,改为其他的修正方式。此外,也要注意传入参数发生改变,注意加入旋转轴。
具体修改部分

这个地方需要大家到main方法里自己理解一下代码并找到对应的部分进行修改。

 //Update:设计一个转轴用于测试罗德里格斯公式是否使用成功
 //验证罗德里格斯公式是否正确的最好方法就是将旋转轴进行特殊的xyz旋转方法。
 Eigen::Vector3f axis= { 1, 0, 0 };
 
 //下面的部分位于while(key!=27)的那一部分
 //在这里用罗德里格斯旋转轴替换原有的z转轴方法:
r.set_model(get_rotation(axis,angle));
//这里保留原有的model方法
//r.set_model(get_model_matrix(angle));

其实如果理解了相应的原理这里不难理解

验证是否是按照道理?将旋转轴axis设为(0,0,1)会获得与先前完全相同的结果,可以从逻辑上验证这一结论。不妨再测试一下其他几个坐标轴作为主轴以及改变向量方向。

完成后的效果

作业基础实现

进阶作业实现( 采用旋转轴(1,1,1) )

这里使用的坐标轴由于没有考虑gui界面画幅显得不尽直观,感兴趣的朋友可以考虑学习一下渲染部分渲染一下三个坐标轴,会有更清晰而直观的感受/