找回密码
 立即注册
查看: 145|回复: 0

GAMES101作业分析——作业1(MVP矩阵)

[复制链接]
发表于 2023-3-6 21:51 | 显示全部楼层 |阅读模式
此文章系列源自2020年2月闫令琪所教授的 GAMES101 现代计算机图形学入门课程。课程视频地址如下:
此系列主要包含个人在作业完成过程中遇到的各类问题、解决方案、后续思考,以及……吐槽……
<hr/>作业需求:get_model_matrix函数中,实现绕 z 轴旋转的变换矩阵

在main.cpp文件中的get_model_matrix函数中有一段“TODO”注释,在下面构建一个z轴旋转矩阵并与函数开头定义的model矩阵相乘,最后返回矩阵乘法结果。
绕z轴旋转的矩阵在原课程第4课的课件第8页:


唯一需要注意的点是,get_model_matrix入参rotation_angle是角度值,计算正弦和余弦前需要转为弧度。
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.
    Matrix4f rotationZ;
    float rad = rotation_angle / 180.0 * MY_PI;
    rotationZ << cos(rad), -sin(rad), 0, 0,
                sin(rad), cos(rad), 0, 0,
                0, 0, 1, 0,
                0, 0, 0, 1;
    model = rotationZ * model;

    return model;
}作业需求:使用给定的参数逐个元素地构建透视投影矩阵

透视投影矩阵的构建分为两大步:1. 将视锥体变换为长方体;2. 将长方体变换为中心为三维空间原点且边长为2的立方体。
第一步的变换矩阵在第四课的课件29-36页,不过最后没把推导出的矩阵完整结果写出来。这里补充一下:
\begin{pmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n+f & -nf \\ 0 & 0 & 1 & 0 \end{pmatrix}\\ 在函数get_projection_matrix的入参中,zNear和zFar分辨就是近平面和原平面在z轴方向的值,即n和f,可以直接使用:
    float n = zNear, f = zFar;
    Matrix4f perspToOrthoMatrix;
    perspToOrthoMatrix << n, 0, 0, 0,
                        0, n, 0, 0,
                        0, 0, n+f, -n*f,
                        0, 0, 1, 0;第二部的变换矩阵在第四课的课件24页,由缩放和平移两个矩阵相乘获得:


没有旋转,因为之前view部分的变换已经处理好了。
    // tan(eye_fov/2)=t/abs(n)
    float t = tan(eye_fov / 2 / 180 * MY_PI) * (-n);
    // aspect_ratio=r/t
    float r = aspect_ratio * t;
    Matrix4f orthoTransMatrix;
    Matrix4f orthoScaleMatrix;
    float l = -r, b = -t;
    orthoTransMatrix << 1, 0, 0, -(r+l)/2,
                      0, 1, 0, -(t+b)/2,
                      0, 0, 1, -(n+f)/2,
                      0, 0, 0, 1;
    orthoScaleMatrix << 2/(r-l), 0, 0, 0,
                      0, 2/(t-b), 0, 0,
                      0, 0, 2/(n-f), 0,
                     0, 0, 0, 1;

    Matrix4f orthoProjMatrix = orthoScaleMatrix * orthoTransMatrix;这里有个坑,在t的值(即top,视锥体顶部平面的y值)计算时需要求反。原因在于作业框架中将视锥近平面和远平面的值设置为正数,并且远平面的值大于近平面的值,也可以理解为将视锥体放在了视点正z轴上。而课程推导过程中,近平面和远平面在视点负z轴上,近平面的值大于远平面。如果此处计算t时不加负号,最终结果是个大头朝下的三角形(其实也没什么大问题)。
最后,简化及合并一些变量:
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.
    float n = zNear, f = zFar;
    Matrix4f perspToOrthoMatrix;
    perspToOrthoMatrix << n, 0, 0, 0,
                        0, n, 0, 0,
                        0, 0, n+f, -n*f,
                        0, 0, 1, 0;

    float t = tan(eye_fov / 2 / 180.0 * MY_PI) * (-n);
    float r = aspect_ratio * t;
    Matrix4f orthoTransMatrix;
    Matrix4f orthoScaleMatrix;

    orthoTransMatrix << 1, 0, 0, 0,
                      0, 1, 0, 0,
                      0, 0, 1, -(n+f)/2,
                      0, 0, 0, 1;
    orthoScaleMatrix << 1/r, 0, 0, 0,
                      0, 1/t, 0, 0,
                      0, 0, 2/(n-f), 0,
                      0, 0, 0, 1;
    Matrix4f orthoProjMatrix = orthoScaleMatrix * orthoTransMatrix;

    projection = orthoProjMatrix * perspToOrthoMatrix;

    return projection;
}最后绘制结果如图:



绘制结果

作业需求:按 A 键与 D 键时,三角形正确旋转

由于作业框中已经编写了检测按键事件的相关代码,所以在正确完成上面两个需求后,不需要添加其他代码,就已经实现了该需求。不过具体实现的代码值得分析一下(忽略了一部分非关键代码):
    float angle = 0;
    int key = 0;
    while (key != 27) {
        /*...*/
        key = cv::waitKey(10);
        if (key == 'a') {
            angle += 10;
        }
        else if (key == 'd') {
            angle -= 10;
        }
    }while循环保证每10毫秒检测一次键盘按键,如果检测到ESC键被按下就退出循环关闭绘图窗口,如果检测到A或D键被按下就修改变量angle的值。
作业需求:构造函数,得到绕任意过原点的轴的旋转变换矩阵

变换矩阵在第四课课件的第10页,闫教授还手写了个推导过程作为补充材料,可惜我没看懂……


直接按照结果构造一个矩阵即可:
Matrix4f get_rotation_matrix(Vector3f axis, float angle)
{
    Vector3f n = axis;
    float rad = angle / 180.0 * MY_PI;
    Matrix3f rMatrix;
    Matrix3f identityMatrix = Matrix3f::Identity();
    Matrix3f nMatrix;
    nMatrix << 0, -n.z(), n.y(),
                n.z(), 0, -n.x(),
                -n.y(), n.x(), 0;
    rMatrix = cos(rad) * identityMatrix + (1 - cos(rad)) * n * n.transpose() + sin(rad) * nMatrix;
    Matrix4f rotationMatrix;
    rotationMatrix << rMatrix(0, 0), rMatrix(0, 1), rMatrix(0, 2), 0,
                    rMatrix(1, 0), rMatrix(1, 1), rMatrix(1, 2), 0,
                    rMatrix(2, 0), rMatrix(2, 1), rMatrix(2, 2), 0,
                    0, 0, 0, 1;
    return rotationMatrix;
}注意原公式最终结果是个3阶方阵,与作业需求不同。需要重新组织计算结果并构造成4阶方阵。
<hr/>吐槽:填空题式设计

仔细回看了一番作业框架main.cpp中几个计算转换矩阵的函数,发现:都在开头定义一个单位矩阵,接着根据数据推导构建实际的变换矩阵,最后把开头定义的单位矩阵与变换矩阵相乘,并返回最终结果。作业的主要内容是中间的变换矩阵。比如,原本就已经完成的get_view_matrix函数:
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;
}我们知道,单位矩阵与任意矩阵(合法)相乘,都不会改变后者。所以这步计算不影响中间构建的translate矩阵。从计算效率考虑,这样做多构造了一个临时矩阵且多执行了一次矩阵乘法运算,属于性能浪费。如果从返回值安全的角度考量,其实也只需要声明translate矩阵时就进行初始化即可,Eigen的矩阵可以直接往非空矩阵里塞元素。不过这只是个人的浅薄看法,不一定对……
另外,整个作业框架的设计不便调试。前面一大堆矩阵运算只要出一点岔子,体现在最终渲染结果上的问题往往千奇百怪。加上还有Eigen库的各种语法问题,VS在文本编辑模式不报错没波浪线,一编译就跳到Eigen内部去了……个人的做法是,注释掉整个原来的main函数,并新建main函数用于测试编写的函数是否符合预期。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-2 23:24 , Processed in 0.137436 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表