跳过正文
  1. Posts/

GAMES101作业1 含提高

·4219 字·9 分钟·
Xenolies
作者
Xenolies
Keep On Keeping On

题目一
#

get_model_matrix(float rotation_angle): 逐个元素地构建模型变换矩 阵并返回该矩阵。在此函数中,你只需要实现三维中绕 z 轴旋转的变换矩阵, 而不用处理平移与缩放。

根据这个公式编写代码

$$ \text{绕z轴旋转: } \mathbf{R}_z(\alpha) = \begin{pmatrix} \cos\alpha & -\sin\alpha & 0 & 0 \\ \sin\alpha & \cos\alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} $$

由于给的值是角度,而numext 接受的值是弧度所以需要角度转为弧度

$$ \text{弧度} = \text{角度} \times \frac{\pi}{180} $$

然后使用三维仿射变换的旋转公式编写绕z轴旋转的代码

$$ \text{绕z轴旋转: } \mathbf{R}_z(\alpha) = \begin{pmatrix} \cos\alpha & -\sin\alpha & 0 & 0 \\ \sin\alpha & \cos\alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} $$
  float rad = rotation_angle * (MY_PI / 180);
  Eigen::Matrix4f rotation;
  // 旋转矩阵中numext::sin的三角函数接收的是弧度需要将角度转为弧度:弧度=角度*(PI/180)
  rotation << numext::cos(rad), -numext::sin(rad), 0, 0, // 第一行
      numext::sin(rad), numext::cos(rad), 0, 0,          // 第二行
      0, 0, 1, 0,                                        //第三行
      0, 0, 0, 1;

函数完整代码

//模型矩阵 参数是一个旋转的角度
Eigen::Matrix4f get_model_matrix(float rotation_angle) {
  Eigen::Matrix4f model = Eigen::Matrix4f::Identity();

  // TODO: 实现此函数
  // 创建绕 Z 轴旋转三角形的模型矩阵。
  // 然后返回该矩阵。

  float rad = rotation_angle * (MY_PI / 180);

  Eigen::Matrix4f rotation;
  // 旋转矩阵中numext::sin的三角函数接收的是弧度需要将角度转为弧度:弧度=角度*(PI/180)

  rotation << numext::cos(rad), -numext::sin(rad), 0, 0, // 第一行
      numext::sin(rad), numext::cos(rad), 0, 0,          // 第二行
      0, 0, 1, 0,                                        //第三行
      0, 0, 0, 1;

  model = rotation * model;

  return model;

}

题目二
#

get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar): 使用给定的参数逐个元素地构建透视投影矩阵并返回 该矩阵。 使用透视投影矩阵的公式

$$ M_{\text{persp}} = M_{\text{ortho}} \cdot M_{\text{persp} \to \text{ortho}} $$

可以看到透视投影矩阵相当于 正交矩阵 与 变换矩阵相乘,需要先写出正交矩阵

$$ M_{\text{ortho}} = \underbrace{ \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} }_{\text{缩放矩阵 } S} \cdot \underbrace{ \begin{bmatrix} 1 & 0 & 0 & -\frac{r+l}{2} \\ 0 & 1 & 0 & -\frac{t+b}{2} \\ 0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1 \end{bmatrix} }_{\text{平移矩阵 } T} $$

当然也可以使用合并后的形式

$$ M_{\text{ortho}} = \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{2}{n-f} & -\frac{n+f}{n-f} \\ 0 & 0 & 0 & 1 \end{bmatrix} $$

可以看到公式中需要左右边界l , r,上下边界 t , b,远近裁剪面 n , f 的值 由于相机是从-z方向看过去的(n>f),所以n需要取负号,所以需要 n > f ,观察main函数发现传入的值是正数,需要取负号.

n = -zNear; f = -zFar;

此时缺少 l , r , t , b,观察到该函数中有传入视角 eye_fov 和 ,根据如下公式可以求出 半高t

$$t = |n| \cdot \tan\left(\frac{\text{fovY}}{2}\right)$$

接下来求 l , r,已知宽高比 aspect_ratio 和 半高 t 可以由如下公式得到半宽 r :

$$ r = t \cdot \text{aspect} $$

由于经过视图变换,摄像机在原点,b=-t, l= -r 至此全部需要的数值都得到了,此时代入透视投影矩阵的公式.

  // 已知条件: 视角 eye_fov 宽高比 aspect_ratio ,z近点 zNear z远点 zFar
  // 思路 因为垂直方向半高 t = |z| * tan(fov/2) 所以可以得到半高t ,
  // 已知宽高比aspect_ratio和半高 t 可以得到水平方向半宽r = t * aspect_ratio

  Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
  // 垂直方向半高 t
  // numext 接收的是弧度值,需要将角度(eye_fov)转为弧度
  // 把角度转成弧度
  // 前面求出来t和r ,将其映射到一个[-1,1]3的坐标系中,可得出:
  // 左面 l = -r , 右面 r = r
  // 顶面 t = t , 地面 b = -t
  // 近平面 n = zNear ,远平面 f = zFar
  // 由于相机是从-z方向看过去的(n>f),所以n需要取负号
  // 可以写出正交投影矩阵 ortho

  float l, b, n, f;
  n = -zNear;
  f = -zFar;
  float rad = eye_fov * (MY_PI / 180.0);
  float t = abs(n) * numext::tan(rad / 2);
  // 水平方向半宽 r
  float r = t * aspect_ratio;
  l = -r;
  b = -t;


  // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation
  Eigen::Matrix4f scale;         // 旋转矩阵 scale
  scale << 2 / (r - l), 0, 0, 0, // 1
      0, 2 / (t - b), 0, 0,      // 2
      0, 0, 2 / (n - f), 0,      // 3
      0, 0, 0, 1;

  Eigen::Matrix4f translation;
  translation << 1, 0, 0, -(l + r) / 2, // 1
      0, 1, 0, -(t + b) / 2,            // 2
      0, 0, 1, -(n + f) / 2,            // 3
      0, 0, 0, 1;

  // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation
  Eigen::Matrix4f ortho = scale * translation;
  
  //投影变换矩阵 persp_to_ortho
  Eigen::Matrix4f persp_to_ortho;
  persp_to_ortho << n, 0, 0, 0, // 1
      0, n, 0, 0,               // 2
      0, 0, n + f, -(n * f),    // 3
      0, 0, 1, 0;

  projection = ortho * persp_to_ortho;

函数完整代码

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,

                                      float zNear, float zFar) {

  // 学生将实现此函数
  // 已知条件: 视角 eye_fov 宽高比 aspect_ratio ,z近点 zNear z远点 zFar
  // 思路 因为垂直方向半高 t = |z| * tan(fov/2) 所以可以得到半高t ,
  // 已知宽高比aspect_ratio和半高 t 可以得到水平方向半宽r = t * aspect_ratio

  Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
  // 垂直方向半高 t
  // numext 接收的是弧度值,需要将角度(eye_fov)转为弧度
  // 把角度转成弧度
  // 前面求出来t和r ,将其映射到一个[-1,1]3的坐标系中,可得出:
  // 左面 l = -r , 右面 r = r
  // 顶面 t = t , 地面 b = -t
  // 近平面 n = zNear ,远平面 f = zFar
  // 由于相机是从-z方向看过去的(n>f),所以n需要取负号
  // 可以写出正交投影矩阵 ortho

  float l, b, n, f;
  n = -zNear;
  f = -zFar;
  float rad = eye_fov * (MY_PI / 180.0);
  float t = abs(n) * numext::tan(rad / 2);
  // 水平方向半宽 r
  float r = t * aspect_ratio;
  l = -r;
  b = -t;

  

  // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation
  Eigen::Matrix4f scale;         // 旋转矩阵 scale
  scale << 2 / (r - l), 0, 0, 0, // 1
      0, 2 / (t - b), 0, 0,      // 2
      0, 0, 2 / (n - f), 0,      // 3
      0, 0, 0, 1;

  Eigen::Matrix4f translation;
  translation << 1, 0, 0, -(l + r) / 2, // 1
      0, 1, 0, -(t + b) / 2,            // 2
      0, 0, 1, -(n + f) / 2,            // 3
      0, 0, 0, 1;

  // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation
  Eigen::Matrix4f ortho = scale * translation;
  //投影变换矩阵 persp_to_ortho
  Eigen::Matrix4f persp_to_ortho;
  persp_to_ortho << n, 0, 0, 0, // 1
      0, n, 0, 0,               // 2
      0, 0, n + f, -(n * f),    // 3
      0, 0, 1, 0;

  projection = ortho * persp_to_ortho;
  // TODO: 实现此函数
  // 根据给定参数创建投影矩阵。
  // 然后返回该矩阵。

  return projection;

}

提高题
#

在 main.cpp 中构造一个函数Eigen::Matrix4f get_rotation(Vector3f axis, float angle),该函数的作用是得到绕任意过原点的轴的旋转变换矩阵。 任意过原点的轴旋转可以使用之前提到的罗德里格斯旋转公式.

$$ \mathbf{R}(\mathbf{n}, \alpha) = \cos(\alpha) \mathbf{I} + (1 - \cos(\alpha)) \mathbf{n} \mathbf{n}^T + \sin(\alpha) \underbrace{ \begin{pmatrix} 0 & -n_z & n_y \\ n_z & 0 & -n_x \\ -n_y & n_x & 0 \end{pmatrix} }_{\mathbf{N}} $$
  // 使用 罗德里格旋转公式的矩阵形式 进行求解
  float rad_a = angle * (MY_PI / 180.0);

  Eigen::Vector4f n;      // 单位旋转轴向量 n,使用齐次坐标
  Eigen::Matrix4f I;      // 3x3 单位矩阵 I
  Eigen::RowVector4f n_t; // 单位轴的外积矩阵 nn_t
  Eigen::Matrix4f N;      // n 的反对称矩阵 N


  n << axis.x(), axis.y(), axis.z(), 0;
  n_t << axis.x(), axis.y(), axis.z(), 0;
  I = Eigen::Matrix4f::Identity(); // 生成单位矩阵 I
  axis.normalize();


  // 提取n的三个分量
  float nx = n.x();
  float ny = n.y();
  float nz = n.z();

  

  // 根据反矩阵公式写出 N ;
  N << 0, -nz, ny, 0, // 1
      nz, 0, -nx, 0,  // 2
      -ny, nx, 0, 0,  // 3
      0, 0, 0, 1;     //第4行:齐次坐标规范(最后一位1)

  

  // 最终的旋转矩阵 R_na
  Eigen::Matrix4f R_na = numext::cos(rad_a) * I +
                         (1 - numext::cos(rad_a)) * (n * n_t) +
                         numext::sin(rad_a) * N;

做完看别人的视频对答案发现我的三角形比例不对旋转的时候会变大,问了下ai得知: 旋转矩阵由罗德里格斯公式计算,但在代码中被错误地扩展到了4x4且定义了错误的 N 矩阵。 最后导致代码中:

$$ rotation(3,3) = \cos\alpha + 0 + \sin\alpha = \cos\alpha + \sin\alpha $$

会破坏齐次坐标的规范性。所以需要将矩阵右下角改为1

完整函数代码

//[提高项 5 分] 在 main.cpp
//中构造一个函数,该函数的作用是得到绕任意过原点的轴的旋转变换矩阵。
// Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
Eigen::Matrix4f get_rotation(Vector3f axis, float angle) {
  // 使用 罗德里格旋转公式的矩阵形式 进行求解
  float rad_a = angle * (MY_PI / 180.0);

  Eigen::Vector4f n;      // 单位旋转轴向量 n,使用齐次坐标
  Eigen::Matrix4f I;      // 3x3 单位矩阵 I
  Eigen::RowVector4f n_t; // 单位轴的外积矩阵 nn_t
  Eigen::Matrix4f N;      // n 的反对称矩阵 N


  n << axis.x(), axis.y(), axis.z(), 0;
  n_t << axis.x(), axis.y(), axis.z(), 0;
  I = Eigen::Matrix4f::Identity(); // 生成单位矩阵 I
  axis.normalize();


  // 提取n的三个分量
  float nx = n.x();
  float ny = n.y();
  float nz = n.z();


  // 根据反矩阵公式写出 N ;
  N << 0, -nz, ny, 0, // 1
      nz, 0, -nx, 0,  // 2
      -ny, nx, 0, 0,  // 3
      0, 0, 0, 1;     //第4行:齐次坐标规范(最后一位1)


  // 最终的旋转矩阵 R_na
  Eigen::Matrix4f R_na = numext::cos(rad_a) * I +
                         (1 - numext::cos(rad_a)) * (n * n_t) +
                         numext::sin(rad_a) * N;
  R_na(3, 3) = 1.0f; // 强制齐次矩阵最后一位为1(防止浮点误差)

  return R_na;
}

全部代码
#

#include "Triangle.hpp"
#include "rasterizer.hpp"
#include <cmath>
#include <eigen3/Eigen/Eigen>
#include <iostream>
#include <opencv2/opencv.hpp>

  

constexpr double MY_PI = 3.1415926;

// 相机视角矩阵

Eigen::Matrix4f get_view_matrix(Eigen::Vector3f eye_pos) {

  Eigen::Matrix4f view = Eigen::Matrix4f::Identity();
  Eigen::Matrix4f translate;
  translate << 1, 0, 0, -eye_pos[0], 0, 1, 0, -eye_pos[1], 0, 0, 1, -eye_pos[2],
      0, 0, 0, 1; // 齐次坐标

  view = translate * view;

  return view;

}

  

//模型矩阵 参数是一个旋转的角度
Eigen::Matrix4f get_model_matrix(float rotation_angle) {

  Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
  
  // TODO: 实现此函数
  // 创建绕 Z 轴旋转三角形的模型矩阵。
  // 然后返回该矩阵。

  float rad = rotation_angle * (MY_PI / 180);
  Eigen::Matrix4f rotation;

  // 旋转矩阵中numext::sin的三角函数接收的是弧度需要将角度转为弧度:弧度=角度*(PI/180)
  rotation << numext::cos(rad), -numext::sin(rad), 0, 0, // 第一行
      numext::sin(rad), numext::cos(rad), 0, 0,          // 第二行
      0, 0, 1, 0,                                        //第三行
      0, 0, 0, 1;

  model = rotation * model;

  return model;

}

  

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar) {

  // 学生将实现此函数
  // 已知条件: 视角 eye_fov 宽高比 aspect_ratio ,z近点 zNear z远点 zFar
  // 思路 因为垂直方向半高 t = |z| * tan(fov/2) 所以可以得到半高t ,
  // 已知宽高比aspect_ratio和半高 t 可以得到水平方向半宽r = t * aspect_ratio

  Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();

  // 垂直方向半高 t
  // numext 接收的是弧度值,需要将角度(eye_fov)转为弧度
  // 把角度转成弧度
  // 前面求出来t和r ,将其映射到一个[-1,1]3的坐标系中,可得出:
  // 左面 l = -r , 右面 r = r
  // 顶面 t = t , 地面 b = -t
  // 近平面 n = zNear ,远平面 f = zFar
  // 由于相机是从-z方向看过去的(n>f),所以n需要取负号
  // 可以写出正交投影矩阵 ortho

  float l, b, n, f;

  n = -zNear;
  f = -zFar;

  float rad = eye_fov * (MY_PI / 180.0);
  float t = abs(n) * numext::tan(rad / 2);

  // 水平方向半宽 r
  float r = t * aspect_ratio;

  l = -r;
  b = -t;

  

  // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation
  Eigen::Matrix4f scale;         // 旋转矩阵 scale
  scale << 2 / (r - l), 0, 0, 0, // 1
      0, 2 / (t - b), 0, 0,      // 2
      0, 0, 2 / (n - f), 0,      // 3
      0, 0, 0, 1;

  Eigen::Matrix4f translation;
  translation << 1, 0, 0, -(l + r) / 2, // 1
      0, 1, 0, -(t + b) / 2,            // 2
      0, 0, 1, -(n + f) / 2,            // 3
      0, 0, 0, 1;

  // 正交投影矩阵 ortho = 旋转矩阵 scale * 平移矩阵 translation
  Eigen::Matrix4f ortho = scale * translation;
  
  //投影变换矩阵 persp_to_ortho
  Eigen::Matrix4f persp_to_ortho;
  persp_to_ortho << n, 0, 0, 0, // 1
      0, n, 0, 0,               // 2
      0, 0, n + f, -(n * f),    // 3
      0, 0, 1, 0;

  projection = ortho * persp_to_ortho;
  
  // TODO: 实现此函数
  // 根据给定参数创建投影矩阵。
  // 然后返回该矩阵。
  return projection;

}

  

//[提高项] 在 main.cpp
//中构造一个函数,该函数的作用是得到绕任意过原点的轴的旋转变换矩阵。
// Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
Eigen::Matrix4f get_rotation(Vector3f axis, float angle) {

  // 使用 罗德里格旋转公式的矩阵形式 进行求解
  float rad_a = angle * (MY_PI / 180.0);

  

  Eigen::Vector4f n;      // 单位旋转轴向量 n,使用齐次坐标
  Eigen::Matrix4f I;      // 3x3 单位矩阵 I
  Eigen::RowVector4f n_t; // 单位轴的外积矩阵 n_t
  Eigen::Matrix4f N;      // n 的反对称矩阵 N

  

  n << axis.x(), axis.y(), axis.z(), 0;
  n_t << axis.x(), axis.y(), axis.z(), 0;
  I = Eigen::Matrix4f::Identity(); // 生成单位矩阵 I
  
   axis.normalize(); // 将传入的旋转轴归一化

  

  // 提取n的三个分量
  float nx = n.x();
  float ny = n.y();
  float nz = n.z();

  

  // 根据反矩阵公式写出 N ;
  N << 0, -nz, ny, 0, // 1
      nz, 0, -nx, 0,  // 2
      -ny, nx, 0, 0,  // 3
      0, 0, 0, 1;     //第4行:齐次坐标规范(最后一位1)

  

  // 最终的旋转矩阵 R_na
  Eigen::Matrix4f R_na = numext::cos(rad_a) * I +
                         (1 - numext::cos(rad_a)) * (n * n_t) +
                         numext::sin(rad_a) * N;

  

  R_na(3, 3) = 1.0f; // 强制齐次矩阵最后一位为1(防止浮点误差)

  return R_na;

}

  

int main(int argc, const char **argv) {

  float angle = 0;
  bool command_line = false;
  float rangle = 0;

  // 1. 定义多组可选旋转轴
  std::vector<Eigen::Vector3f> axes = {
      Eigen::Vector3f(1, 0, 0), // X轴
      Eigen::Vector3f(0, 1, 0), // Y轴
      Eigen::Vector3f(0, 0, 1)  // Z轴
  };

  // 2. 初始化当前轴索引(默认用Z轴)
  int axis_index = 2;
  Vector3f axis = axes[axis_index];
  std::string filename = "output.png";

  if (argc >= 3) {
    command_line = true;
    angle = std::stof(argv[2]); // -r by default
    if (argc == 4) {
      filename = std::string(argv[3]);
    } else
      return 0;
  }

  rst::rasterizer r(700, 700);
  Eigen::Vector3f eye_pos = {0, 0, 5}; // 相机视角z轴距离 5
  std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
  std::vector<Eigen::Vector3i> ind{{0, 1, 2}};
  auto pos_id = r.load_positions(pos);
  auto ind_id = r.load_indices(ind);

  int key = 0;
  int frame_count = 0;

  if (command_line) {
    r.clear(rst::Buffers::Color | rst::Buffers::Depth);
    r.set_model(get_model_matrix(angle));
    r.set_view(get_view_matrix(eye_pos));
    r.set_projection(get_projection_matrix(45, 1, 0.1, 50));
    r.draw(pos_id, ind_id, rst::Primitive::Triangle);
    cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
    image.convertTo(image, CV_8UC3, 1.0f);
    cv::imwrite(filename, image);
    return 0;
  }

  while (key != 27) { // 27是ESC键的ASCII码,按ESC退出循环
    r.clear(rst::Buffers::Color | rst::Buffers::Depth);
    // Eigen 库中矩阵乘法是左乘,需保证变换顺序(模型→视图→投影)
    Eigen::Matrix4f model = get_rotation(axis, angle) * get_model_matrix(angle);
    r.set_model(model);
    r.set_view(get_view_matrix(eye_pos));
    r.set_projection(get_projection_matrix(45, 1, 0.1, 50));

    r.draw(pos_id, ind_id, rst::Primitive::Triangle);
    
    cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
    image.convertTo(image, CV_8UC3, 1.0f);
    cv::imshow("image", image);

    key = cv::waitKey(10);

    std::cout << "frame count: " << frame_count++ << '\n';

    if (key == 'a') {
      angle += 10;
    } else if (key == 'd') {
      angle -= 10;
    } else if (key == 'r') {
    
      // 循环切换轴索引(0→1→2→0...)
      axis_index = (axis_index + 1) % axes.size();
      
      // 更新当前旋转轴
      axis = axes[axis_index];

      // 打印提示,明确当前轴
      std::cout << "旋转轴已切换为:";
      if (axis_index == 0)
        std::cout << "X 轴 (1,0,0)";
      else if (axis_index == 1)
        std::cout << "Y 轴 (0,1,0)";
      else if (axis_index == 2)
        std::cout << "Z 轴 (0,0,1)";
      std::cout << std::endl;
    }
  }
}

生成结果: