games101——作业1

2023-11-06


作业要求

在接下来的三次作业中,我们将要求你去模拟一个基于 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 键

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

games101——作业1 的相关文章

随机推荐

  • [思维模式-7]:《如何系统思考》-3- 认识篇 - 什么是系统?系统的特征?

    目录 第1章 什么是系统 1 1 万事万物都是一个有机的系统 1 2 系统的科学定义 1 3 系统的构成 1 4 系统的分类 第2章 动态复杂系统的八大特征 2 1 目的性 2 8 边界 2 3 结构影响行为 2 4 总体大于部分之和 2
  • 数据大屏适配方案 (scale)

    目录 适配方案分析 vw vh 什么是vw和vh vw和百分比的区别是什么 vw怎么使用 实现思路 案例 scale方案 一 scale 方法 1 scaleX x 2 scaleY y 3 scale x y 案例 大屏之关键 前期的自适
  • TemplateSyntaxError at /statistics/ ‘staticfiles‘ is not a registered tag li

    报错django template exceptions TemplateSyntaxError staticfiles is not a registered tag library Mustbe one of admin list ad
  • app基本控件

    一个完整的APP包括四大类 各种 栏 内容视图 控制元素 临时视图 各种 栏 状态栏 导航栏 标签栏 工具栏 搜索栏 范围栏 内容视图 列表视图 卡片视图 集合视图 图片视图 文本视图 控制元素 用于控制产品行为或显示的信息 临时视图 警告
  • IDEA设置启动选择项目

    IDEA设置启动选择项目 IDEA2019 3 5启动后自行选择项目 而不是进入上一次关闭的项目 通过下面的设置修改 将Reopen last project on startup勾选去除
  • 从零开始学Qt(四)信号与槽

    信号与槽 书不记 熟读可记 义不精 细思可精 1 信号 槽是啥 古有 烽火狼烟 传递消息 敌人来犯的消息迅速传达开来 是多么的聪慧啊 烟就是信号 下一个燃火台看到烟后就点燃燃料 这个就槽 换种方法说就是 一个按钮被点击了 会触发一个点击的信
  • 校园网连路由器

    1 校园网 并记住账号及其密码 2 一台电脑 Windows 系统 3 一台路由器 4 两条网线 准备完成 下面开始正式干活 第一步 电脑插上网线 路由器通电插上网线 在你的电脑登陆你的校园网 第二步 打开你的电脑搜索CMD 并以管理员权限
  • QString和QByteArray的区别

    QString和QByteArray的区别 本质 格式转换 QString字符串格式化打印 长度 本质 QString是对QByteArray的再次封装 QString可以通过char 来构造 也可以通过QByteArray来构造 QByt
  • CentripetalNet: Pursuing High-quality Keypoint Pairs for Object Detection解读

    摘要 基于关键点的检测器得到了不错的性能 但关键点匹配错误是普遍性存在的 严重的影响了其检测器的性能 作者通过centripetal shif将相同的类别的实例分开 CentripetalNet是预测角点的位置和向心偏移 通过向向心偏移来对
  • Web API: URL.createObjectURL()实践

    1 问题 URL createObjectURL的介绍如下 The URL createObjectURL static method creates a DOMString containing a URL representing th
  • 图形学数学基础之1D采样分布计算方法Inverse Method

    作者 i dovelemon 日期 2017 09 04 来源 CSDN 主题 Rendering Equation Probability Density Function Cumulative Density Function 引言 前
  • Android开发中Javassist的妙用

    Javassist Java字节码以二进制的形式存储在 class文件中 每一个class文件包含一个Java类或接口 Javassist框架就是一个用来处理Java字节码的类库 它可以在一个已经编译好的类中添加新的方法 或者修改已有的方法
  • pytorch 下载

    pytorch 下载 使用anconda 直接下载pytorch的朋友应该是知道pytorch是个啥的 所以直接上教程 anconda下载 anconda是一个用于下载和管理python依赖包和环境的一个工具 下载详情可以去看这篇文章 an
  • VMware虚拟机nat模式连不上网

    我的虚拟机总是各种连不上网 每次都要折腾一番 现在我把虚拟机连不上网的原因总体排查一下 按照流程一步步来 基本上可以解决大部分人的问题 首先 在VMware的编辑 gt 虚拟网络编辑器重新建立 网络 之前的要删掉 新建的同样选择 就可以 如
  • 线性回归总结

    向量相似理论 线性回归 比如预测房价中学区属性0 4 居住体验0 2 通勤距离0 2 商业环境0 2等因素 在同一价格区间 只有样本特征与上述属性分布一致时 各方面都加权均衡 才能取得高分 任一单一属性过高 必然导致其他属性降低 通常意义上
  • Flutter音频播放之just_audio

    just audio的使用 just audio 它是一个用于播放音频的 Flutter 插件 安装和导入 just audio 要使用 just audio 库 需要将其添加到项目的 pubspec yaml 文件中 dependenci
  • Update your application to remove the dependency cycle between beans

    Spring 高版本循环依赖问题 问题描述 提示 Spring boot 应用启动报错 Relying upon circular references is discouraged and they are prohibited by d
  • 三大战略引擎加速转动,微盟驶入智慧商业服务深水区

    2023年3月30日 微盟披露了2022年财报 经调整总收入18 39亿元 经调整毛利11 2亿元 在业务层面 订阅解决方案业务表现亮眼 其中智慧零售板块营收5 13亿元 同比内生增长45 5 拉动每用户平均收益同比增长12 3 达1296
  • (四) 区块链数据结构 – 脚本

    脚本是交易数据中的核心部分 可用于锁定输出和解锁输入 当向某人支付比特币时 我们要为交易输入设置解锁脚本 向别人证明我们有全力使用该输入 同时我们还需要对交易输出添加锁定脚本 确保只有接收者能解锁该输出 脚本 比特币系统专门设计了一套脚本语
  • games101——作业1

    文章目录 作业要求 代码框架 已有代码解读 作业部分代码 进阶部分代码 编译 结果 作业要求 在接下来的三次作业中 我们将要求你去模拟一个基于 CPU 的光栅化渲染器的简化版本 这次作业简要来说就是补全两个函数的内容 一个是 get mod