GAMES101-作业1详解

仅仅作业需补充的几个函数比较简单,不过本文更侧重完整解析整个代码项目,所以并非只分析需要填充的函数,即使非代码填充部分也会提及。


直接从main函数说起:

float angle = 0;
bool command_line = false;
//Eigen::Vector3f axis(1,1,1);
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;
}

以上一段定义一些变量用于给定角度和画线,条件分支作为命令行运行的参数分支,若运行的命令行参数大于3,则将command_line变量设为true,之后会用这个变量的值作为分支条件来确定程序是动态运行还是将旋转某角度的图像存储到本地。


之后定义了几个变量,分别是r, eyepos,pos,ind,定义了一个确定宽高参数的屏幕,人眼(相机)的坐标位置,三角形三个定点的坐标,和这三个点的索引值。通过loadposition与load_indices将坐标点和索引值赋给光栅化器(rasterizer),其定义在rasterizer.hpp中的rasterizer类中。


之后main函数中根据命令行参数的数目(即command_line)变量的值来决定分支,而两个分支的主体部分并没有什么不同,区别仅仅在于是否保存图片到文件和用键盘进行实时旋转控制上。(其中键盘参数获得通过waitkey调用,返回值为a,左旋10度,返回值为d右旋10度,返回值为27(即键盘对应esc键)程序退出循环直接结束)


而两个分支相同的主体部分为下面这一段代码:

r.clear(rst::Buffers::Color | rst::Buffers::Depth);

// r.set_model(get_rotation(axis, angle));
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);

首先用r.clear()清除整个显示屏幕,其函数如下:

void rst::rasterizer::clear(rst::Buffers buff)
{
    if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
    {
        std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
    }
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
    }
}

可以看到方法是将framebuf(即屏幕的像素数组,本项目中定义为700x700),用vector3f{0,0,0}来填充,该RGB三通道信息对应为纯黑色。depth_buf(深度数组,代表每个点的深度信息,将在光栅化和渲染部分用到,本次实验可忽略)用infinity()(无穷大)来填充,这个函数即将全屏幕所有像素点都变为黑色,可以视作屏幕清空或者初始化。


接下来的3行

// r.set_model(get_rotation(axis, angle));
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));

分别将main.cpp中用三个函数返回的m,v,p三个矩阵加载到光栅化器(rasterizer)中的model,view,projection这三个矩阵中去。这三个变换矩阵也就是前文理论中提到的模型变换矩阵,视图变换矩阵,投影变换矩阵,这也是本次作业要求要填充的几个矩阵内容。


首先变换先要进行模型变换,本次要求只实现绕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.
    // Then return it.
    Eigen::Matrix4f translate;
    translate << cos(rotation_angle/180.0*acos(-1)), -sin(rotation_angle/180.0*acos(-1)), 0.0, 0.0, sin(rotation_angle/180.0*acos(-1)),
        cos(rotation_angle/180.0*acos(-1)), 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0;

    model = translate * model;
    return model;
}



之后是视图变换,本项目中已经填好,不过值得一提的是,本项目中设定相机的初始方向已经是正确的方向,所以并不需要进行旋转摆正相机,只需将相机平移到原点即可(代码是给定的就不放了)。



然后是投影矩阵,首先要搞清给定的四个参数含义,eyefov代表垂直的可视角度,aspect_ratio是宽高比,zNear和zFar分别为近远平面的z轴坐标。前文提到,透视变换即就是先透视投影变换,再做正交投影变换。所以可以先写出透视投影矩阵(其中参数只有n,f),然后通过计算出b,t,l,r的值,写出正交投影的矩阵,二者相乘可得到结果:具体代码如下:

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.
    // Then return it.
    Eigen::Matrix4f m;
    m << zNear, 0, 0, 0,
        0, zNear, 0, 0,
        0, 0, zNear + zFar, -zNear * zFar,
        0, 0, 1, 0;
    
    float halve = eye_fov/2*MY_PI/180;
    float top = tan(halve) * zNear;
    float bottom = -top;
    float right = top * aspect_ratio;
    float left = -right;
    Eigen::Matrix4f n, p;
    n << 2/(right - left), 0, 0, 0,
        0, 2/(top - bottom), 0, 0,
        0, 0, 2/(zNear - zFar), 0,
        0, 0, 0, 1;

    p << 1, 0, 0, -(right + left)/2,
        0, 1, 0, -(top + bottom)/2,
        0, 0, 1, -(zFar + zNear)/2,
        0, 0, 0, 1;

    projection = n * p * m;

    return projection;
}



至此三个变换矩阵全部构造完成,接着就是在main函数中调用r.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);
    }
}

首先buf,ind两个量分别将之前加载到光栅化器中的三角形坐标和索引取出。f1和f2两个量是为了之后拉伸z轴从(-1,1)的标准立方体到(0,100)(虽然我也不知道为啥这里要拉伸z轴,可能是opencv绘图的标准接口?)总之将mvp变换矩阵分别乘以三角形三个顶点的坐标,最终将三角形转化到[-1,1]^3的标准立方体中。再通过视口变换,将x方向从[-1,1]变为[0,width],y方向从[-1,1]变到[0,height],z方向由[-1,1]变到[0,100]左右。(做完视口变换,这时三角形的三个点坐标就和最终要显示在屏幕上的像素对应起来了)。

然后通过setColor函数给三个点设置颜色(其实吧这个实验里应该无所谓,毕竟一个像素点的颜色设了最终也看不出来),最后调用rasterize_wireframe(t)函数,该函数定义如下:

void rst::rasterizer::rasterize_wireframe(const Triangle& t)
{
    draw_line(t.c(), t.a());
    draw_line(t.c(), t.b());
    draw_line(t.b(), t.a());
}

我们可以看到这个函数就是画三角形的三条边,调用了三次给出始终点画出线段的drawline函数,而draw_line函数很长,代码就不放了,老实说这函数看懂确实费了一番功夫,尤其是中间有两行关于dx1,dy1的增量语句刚开始的时候读得很蒙,后来完整画图和代数推了一下才知道什么意思。再后来才知道这个原理其实就是直线光栅化的Bresenham算法。如果有兴趣可以直接搜原理看,在这里结合代码解释起来有点累,就鸽了23333~


总之通过drawline函数定义了直线颜色(所以想改颜色可以直接改这个函数定义的linecolor变量),并通过直线光栅化算法将直线转化为像素矩阵信息传入frame_buf中,再将此作为参数传入构造出image对象,最后通过opencv库中的imshow函数将信息完整绘制出来,这就是本实验完整流程解析。


哦对,作业中的附加题是实现绕任意轴旋转,这里其实就是之前提到过的Rodrigues坐标变换,具体理论前面已经说过,get_rotation()函数实现如下:

Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
{

    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
    Eigen::Matrix3f temp = Eigen::Matrix3f::Identity();
    float ag = angle/180*MY_PI;
    Eigen::Matrix3f tr;
    Eigen::Matrix3f mul;
    mul << 0, -axis[2], axis[1],
        axis[2], 0, -axis[0],
        -axis[1], axis[0], 0;
    tr = cos(ag)*temp + (1-cos(ag))*axis*axis.adjoint() + mul*sin(ag);
    model << tr(0,0), tr(0,1), tr(0,2), 0,
            tr(1,0), tr(1,1), tr(1,2), 0,
            tr(2,0), tr(2,1), tr(2,2), 0,
            0, 0, 0, 1;
    return model;
}

注意实现get_rotation()函数后需将main函数中的r.set_model(get_model_matrix(angle))替换成r.set_model(get_rotation(axis, angle)),才能正确实现三角形绕给定轴旋转后的投影。


总之实验一最核心的点还是构造几个变换矩阵(也是作业要求),不得已涉及很多光栅化的知识,均会在后面内容中提到,写了半天快累死,over~

编辑于 2021-07-14 18:30