games101——作业1


作业要求

在接下来的三次作业中,我们将要求你去模拟一个基于 CPU 的光栅化渲染器的简化版本。

这次作业简要来说就是补全两个函数的内容,一个是 get_model_matrix 完成模型变换——这里要求是传入角度 rotation_angle 完成绕 z 轴旋转 rotation_angle 的旋转矩阵,一个是 get_projection_matrix 完成透视投影矩阵。

当你在上述函数中正确地构建了模型与投影矩阵,光栅化器会创建一个窗口显示出线框三角形。由于光栅化器是逐帧渲染与绘制的,所以你可以使用 A 和 D 键去将该三角形绕 z 轴旋转 (此处有一项提高作业,将三角形绕任意过原点的轴旋转)。当你按下 Esc 键时,窗口会关闭且程序终止。

另外,你也可以从命令行中运行该程序。你可以使用以下命令来运行和传递旋转角给程序,在这样的运行方式下,是不会生成任何的窗口,输出的结果图像会被存储在给定的文件中 (若未指定文件名,则默认存储在 output.png 中)。图像的存储位置在可执行文件旁,所以如果你的可执行文件是在 build 文件夹中,那么图像也会在该文件夹内。命令行的使用命令如下:

./Rasterizer //循环运行程序,创建一个窗口显示,且你可以
			 //使用A键和D键旋转三角形
./Rasterizer -r 20 //运行程序并将三角形旋转20度,然后将
				   //结果存在output.png中
./Rasterizer -r 20 image.png //运行程序并将三角形旋转20度
							 //然后将结果存在image.png中

代码框架

在本次作业中,因为你并不需要去使用三角形类,所以你需要理解与修改的文件为 rasterizer.hppmain.cpp。其中 rasterizer.hpp 文件作用是生成渲染器界面与绘制。
光栅化器类在该程序系统中起着重要的作用,其成员变量与函数如下。
成员变量:

  • Matrix4f model, view, projection:三个变换矩阵
  • vector<Vector3f> frame_buf:帧缓冲对象,用于存储需要在屏幕上绘制的颜色数据

成员函数:

  • set_model(const Eigen::Matrix4f& m):将内部的模型矩阵作为参数传递给光栅化器
  • set_view(const Eigen::Matrix4f& v):将视图变换矩阵设为内部视图矩阵
  • set_projection(const Eigen::Matrix4f& p):将内部的投影矩阵设为给定矩阵 p,并传递给光栅化器
  • set_pixel(Vector2f point, Vector3f color):将屏幕像素点 (x, y) 设为 (r, g, b) 的颜色,并写入相应的帧缓冲区位置。

main.cpp 中,我们模拟了图形管线。我们首先定义了光栅化器类的实例,然后设置了其必要的变量。然后我们得到一个带有三个顶点的硬编码三角形 (请不要修改它)。在主函数上,我们定义了三个分别计算模型、视图和投影矩阵的函数,每一个函数都会返回相应的矩阵。接着,这三个函数的返回值会被 set_model(),set_view()set_projection() 三个函数传入光栅化器中。最后,光栅化器在屏幕上显示出变换的结果。

在用模型、视图、投影矩阵对给定几何体进行变换后,我们得到三个顶点的正则化空间坐标 (canonical space coordinate)。正则化空间坐标是由三个取值范围在 [-1,1] 之间的 x, y, z 坐标构成。我们下一步需要做的就是视口变换,将坐标映射到我们的屏幕中 (window_width * window_height),这些在光栅化器中都已完成,所以不需要担心。但是,你需要去理解这步操作是如何运作的,这一点十分重要。


已有代码解读

下面对代码主要部分进行解读

main 函数入手

float angle = 0; //绕Z轴逆时针旋转角度
bool command_line = false; //判断是否有-r参数,true有-r参数
std::string filename = "output.png"; //默认输出文件名

判断参数个数,参数个数大于4说明有问题

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

其构造函数如下:这里的 wh 分别表示视口的宽和高,这里的 frame_buf 表示屏幕的像素数组,depth_buf 代表每个点的深度信息的数组(光栅化和渲染部分用到,本次实验可忽略)

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
    frame_buf.resize(w * h);
    depth_buf.resize(w * h);
}
Eigen::Vector3f eye_pos = {0, 0, 5}; // 表示相机的位置

std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}}; // 三角形三点的坐标

std::vector<Eigen::Vector3i> ind{{0, 1, 2}}; // 坐标索引

下面把三角形的坐标及索引载入光栅化器 r,并获取其对应id

auto pos_id = r.load_positions(pos);
auto ind_id = r.load_indices(ind);
int key = 0; //输入的按键对应ASCII码
int frame_count = 0; //一共生成帧的数量

如果是有 -r 参数的,就保存图像到文件中

if (command_line) {
		// 把 frame_buf 与 depth_buf 初始化 
        r.clear(rst::Buffers::Color | rst::Buffers::Depth);
		
		// 载入模型变换矩阵、视图变换矩阵、投影变换矩阵进光栅器类r中
        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_32FC3 32表示一个像素点占32位 F表示浮点型 C3表示RGB彩色图像(三通道)
        cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
        // 8表示一个像素占8位,U表示无符号整型,convertTo就是改变图像的数据类型,且可以选择尺度缩放,这里是1代表不缩放
        image.convertTo(image, CV_8UC3, 1.0f);
	
		//保存图像到文件中
        cv::imwrite(filename, image);

        return 0;
    }

下面主要看 rst::rasterizer::draw 做些什么

void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type)
{
    if (type != rst::Primitive::Triangle) //只定义了三角形,输入其他类型抛异常
    {
        throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!");
    }
    auto& buf = pos_buf[pos_buffer.pos_id]; // 三角形三点坐标
    auto& ind = ind_buf[ind_buffer.ind_id]; // 对应索引

    float f1 = (100 - 0.1) / 2.0;
    float f2 = (100 + 0.1) / 2.0;

    Eigen::Matrix4f mvp = projection * view * model;
    for (auto& i : ind)
    {
        Triangle t;
		// 转为齐次坐标
        Eigen::Vector4f v[] = {
                mvp * to_vec4(buf[i[0]], 1.0f),
                mvp * to_vec4(buf[i[1]], 1.0f),
                mvp * to_vec4(buf[i[2]], 1.0f)
        };
		
        for (auto& vec : v) {
            vec /= vec.w();
        }

        for (auto & vert : v) // 说明在下方
        {
            vert.x() = 0.5*width*(vert.x()+1.0);
            vert.y() = 0.5*height*(vert.y()+1.0);
            vert.z() = vert.z() * f1 + f2;
        }

        for (int i = 0; i < 3; ++i)
        {
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
        }
			
		// 设置三个点的颜色
        t.setColor(0, 255.0,  0.0,  0.0);
        t.setColor(1, 0.0  ,255.0,  0.0);
        t.setColor(2, 0.0  ,  0.0,255.0);
		
		// 连线
        rasterize_wireframe(t);
    }
}

这里需要把经过变换后的平面进行移到

对应上面代码的这个部分

for (auto & vert : v) // 说明在下方
{
     vert.x() = 0.5*width*(vert.x()+1.0);
     vert.y() = 0.5*height*(vert.y()+1.0);
     vert.z() = vert.z() * f1 + f2;
}

这里连线使用 Bresenham算法,具体可以看这篇文章

下面给点上色的函数 set_pixel,这里可以看到坐标 y 被改为了 height - y,这是因为在opencv中,其坐标系是和正常坐标系不同的(看的是行列,按列存储),为了保持形状不变,要对 y 进行修改:

void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
    //old index: auto ind = point.y() + point.x() * width;
    if (point.x() < 0 || point.x() >= width ||
        point.y() < 0 || point.y() >= height) return;
    auto ind = (height-point.y())*width + point.x();
    frame_buf[ind] = color;
}

-r 参数的处理与有 -r 参数的处理差不多,就是要保证时时生成新的一帧,按 A 键逆时针旋转10度,按 D 键顺时针旋转10度,按 ESC 退出窗口

    while (key != 27) {
        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::imshow("image", image);
        key = cv::waitKey(10); // 表示等10ms关闭窗口,且可以获取所按键的ASCII码

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

        if (key == 'a') {
            angle += 10;
        }
        else if (key == 'd') {
            angle -= 10;
        }
    }

作业部分代码

主要就是三个变换矩阵,分别是模型变换矩阵、视图变换矩阵和透视投影矩阵。

  • 模型变换矩阵 get_model_matrix,因为是绕 Z 轴旋转,其变换矩阵如下:

    那么代码实现也很简单,注意角度要转成弧度制传进三角函数
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();

    // TODO: Implement this function
    // Create the model matrix for rotating the triangle around the Z axis.
    float rotation_angle_radian = rotation_angle*MY_PI/180;
    model(0,0) = cos(rotation_angle_radian); 
    model(0,1) = -sin(rotation_angle_radian);
    model(1,0) = sin(rotation_angle_radian);
    model(1,1) = cos(rotation_angle_radian);
    // std::cout<<"Rotate Model: "<<std::endl<<model<<std::endl;
    // Then return it.

    return model;
}
  • 视图变换,这里默认视角已经不需要旋转了,所以只需要平移到原点即可,即 Tview
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;
}
  • 透视投影变换矩阵,首先计算出 trlb,然后分别计算出 Mortho(旋转+缩放),以及 Mpersp2ortho,透视投影变换矩阵就是 MorthoMpersp2ortho,下面的 n 在代码中对应 zNearf 在代码中对应 zFarfovY 对应 eye_fovaspect 对应 asoect_ratio




Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar)
{
    // Students will implement this function

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

    // TODO: Implement this function
    // Create the projection matrix for the given parameters.

    // Compute l, r, b, t
    float t = tan((eye_fov*MY_PI/180)/2) * fabs(zNear);
    float r = aspect_ratio * t;
    float l = -r;
    float b = -t;

    // Orthographic projection
    // Translate to origin
    Eigen::Matrix4f translate = Eigen::Matrix4f::Identity();
    translate(0,3) = -(r+l)/2;
    translate(1,3) = -(t+b)/2;
    translate(2,3) = -(zNear+zFar)/2;

    // Sclae to [-1,1]^3
    Eigen::Matrix4f scale = Eigen::Matrix4f::Identity();
    scale(0,0) = 2/(r-l);
    scale(1,1) = 2/(t-b);
    scale(2,2) = 2/(zNear-zFar);

    // get Orthographic projection
    Eigen::Matrix4f ortho = scale * translate;
    // std::cout<<"Orthographic:"<<std::endl<<ortho<<std::endl;

    // Perspective projection
    // get Matrix_persp2ortho
    Eigen::Matrix4f persp2ortho = Eigen::Matrix4f::Zero();
    persp2ortho(0,0) = zNear;
    persp2ortho(1,1) = zNear;
    persp2ortho(2,2) = zNear+zFar;
    persp2ortho(2,3) = -zNear*zFar;
    persp2ortho(3,2) = 1;

    // get Perspective projection
    projection = ortho * persp2ortho;
    // std::cout<<"Perspective:"<<std::endl<<projection<<std::endl;
    // Then return it.

    return projection;
}

需要注意的是,闫教授在课上推的公式,是以 zNearzFar 均为负数为前提的,因此在 main 数中要加上负号,否则三角形会上下颠倒。


进阶部分代码

绕任意轴旋转,使用罗德里得旋转公式,其对应矩阵如下,具体可以看这篇文章

Eigen::Matrix4f get_model_matrix_rotateanyaxis(Eigen::Vector3f axis, float rotation_angle)
{
    float rotation_angle_radian = rotation_angle*MY_PI/180; 
    Eigen::Matrix3f I3f = Eigen::Matrix3f::Identity();
    Eigen::Matrix3f Maxis_product; // axis product matrix
    Maxis_product << 0, -axis(2) , axis(1), axis(2), 0, -axis(0), -axis(1), axis(0), 0;
    Eigen::Matrix3f model3 = I3f + (1-cos(rotation_angle_radian))*(Maxis_product*Maxis_product) + sin(rotation_angle_radian)*Maxis_product;

    Eigen::Matrix4f model4 = Eigen::Matrix4f::Identity();
    model4.block(0,0,3,3) << model3;
    // std::cout<<model4<<std::endl;
    return model4;
}

然后在 main 函数中,需要改为 r.set_model(get_model_matrix_rotateanyaxis(Vector3f(0,0,1), angle));


编译

mkdir build // 创 建build文 件 夹 以 保 留 的 工 程 文 件。
cd build // 进 入build文 件 夹。
cmake .. // 通 过 提 供CMakeLists.txt文 件 的 路 径
		// 作 为 参 数 来 运 行CMake。
make −j4 // 通 过make编 译 代 码, −j4 表 示 通 过
		// 4个 内 核 进 行 并 行 化 编 译。
./Rasterizer // 运 行 代 码。

结果


按一下 A 键

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值