games101 hw1

原理

背景

将三维空间中一个旋转的三角形,显示在屏幕上。

假设三角形初始点位为 v0(2.0, 0.0, −2.0), v1(0.0, 2.0, −2.0), v2(−2.0, 0.0, −2.0), 你需要将这三个点的坐标变换为屏幕坐标并在屏幕上绘制出对应的线框三角形。

mvp

AI回答:

在计算机图形领域,MVP指的是模型-视图-投影矩阵(Model-View-Projection matrix)。它是将三维模型空间的点变换到二维屏幕上的关键变换矩阵链,包括以下几个步骤:

  1. 模型变换(Model Transformation):将物体从其本地模型空间变换到世界空间,比如平移、旋转、缩放。
  2. 视图变换(View Transformation):将世界空间变换到相机(观察者)空间,模拟相机位置和方向。
  3. 投影变换(Projection Transformation):将相机空间变换到裁剪空间,实现透视投影或正交投影。

总结:

MVP矩阵 = 投影矩阵 × 视图矩阵 × 模型矩阵


如果一个运动的物体要实现"动起来",通常需要对物体应用变换(Transformation),即:

  • 平移(Translation):使物体移动到新位置
  • 旋转(Rotation):改变物体的方向
  • 缩放(Scaling):改变物体大小

这些变换可以组合在一起,形成一个变换矩阵,然后乘到模型矩阵上,实现物体在空间中的运动。

世界空间(World Space)是三维计算机图形中的一个坐标系,用来描述场景中的所有物体相对于一个统一的坐标系统。在这个空间里,场景中的每个物体都以其在整个场景中的位置、方向和大小进行定义。

简单来说:

  • 模型空间(Model Space):每个物体本地的坐标系统,描述物体的相对结构。
  • 世界空间(World Space):所有物体的模型空间经过模型变换(如平移、旋转、缩放)后,放在一个共同的坐标系中。
  • 相机空间(View Space):从相机的视角来看场景。
  • 裁剪空间/投影空间(Clip/Projection Space):经过投影变换后,用于最终渲染到屏幕。

一个运动的物体要"动起来",通常需要修改模型矩阵,是因为模型矩阵控制着物体在模型空间到世界空间的变换。

具体来说:

  • 模型矩阵包含平移、旋转和缩放等变换,定义了物体在场景中的位置、方向和大小。
  • 改变模型矩阵就相当于"移动"物体,或者调整它的朝向和比例,从而实现物体在空间中的运动。

通过不断更新模型矩阵,物体就能在场景中"动起来"。

代码架构

rasterizer

用于实现基本的线框和三角形光栅化(绘制)功能,主要用于软件渲染管线中。核心内容包括加载顶点和索引数据、设置变换矩阵、绘制线条和三角形、以及管理帧缓冲和深度缓冲。

主要结构和功能简介

1. 枚举和类型定义
  • Buffers:定义了缓冲区类型(颜色、深度),支持按位操作组合。
  • Primitive:定义了图元类型(线、三角形)。
  • pos_buf_id、ind_buf_id:封装了顶点位置缓冲和索引缓冲的ID,确保类型安全。
2. 类:rasterizer
  • 成员变量:

    • 变换矩阵(model、view、projection)
    • 缓冲区(顶点位置、索引、帧缓冲、深度缓冲)
    • 窗口宽度和高度
    • 一个自动递增的ID生成器
  • 核心方法:

    • load_positions()load_indices():加载顶点和索引数据,返回对应的ID。
    • set_model(), set_view(), set_projection():设置变换矩阵。
    • clear():清空颜色缓冲和深度缓冲。
    • draw():绘制三角形(支持线框轮廓作业通过 rasterize_wireframe() 实现)。
    • set_pixel():设置像素颜色到帧缓冲。
    • frame_buffer():返回当前帧缓冲内容。
  • 私有方法:

    • draw_line():实现了 Bresenham 线段绘制算法,用于绘制线段。
    • rasterize_wireframe():绘制三角形的边框线。
    • get_index():根据像素点坐标计算缓冲区索引。

深度缓冲区作用

深度缓冲区(Depth Buffer,也叫Z缓冲区)用来管理和实现隐藏面消除,确保在渲染场景时,前面(离观察者近)的像素遮挡后面(离观察者远)的像素。

  • 隐藏面处理:在绘制每个像素时,比较当前像素的深度值(距离相机的远近)与已存储的深度值。
  • 遮挡关系:只保留离观察者更近的像素,屏蔽被遮挡的远处的像素,从而实现正确的遮挡关系。

具体流程

  1. 当绘制一个像素时,计算其深度(z值)。
  2. 将该深度值与深度缓冲区中对应位置的值进行比较。
  3. 如果新像素更接近(深度值小),则更新颜色和深度缓冲区中的值。
  4. 如果远离(深度值大),则跳过该像素,不更新。

代码中的表现

clear() 函数中,深度缓冲区被初始化为无穷大(std::numeric_limits<float>::infinity()),表示还没有进行任何像素绘制,所有像素的深度都比任何实际像素都远。

cpp 复制代码
if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
{
    std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
}

处理流程

  • 加载顶点和索引后,设置变换矩阵。
  • 调用 draw() 方法,进行模型-视图-投影变换,投影到屏幕空间。
  • 裁剪、归一化后,调用线段绘制或三角形光栅化函数。
  • 图元边界由 draw_line()rasterize_wireframe() 实现。

注释亮点

  • 使用 Eigen 作为线性代数库。
  • 提供类型安全的缓冲区识别方式。
  • 支持基础的线段绘制(Bresenham算法)。
  • 支持深度测试,启用深度缓冲区。
  • 简单、直接、易扩展的设计架构。

基础版

注:只显示三角形,不旋转

复制代码
\\main.cpp 中只有下面的代码完成基础版
    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);

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

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

我们根据代码重新理解一下上面的知识。

  1. 模型变换(Model Transformation) :将物体从其本地模型空间变换到世界空间。可以看到get_model_matrix(angle)的函数只是构建了一个旋转矩阵。所以原本的三角形,即std::vector<Eigen::Vector3f> pos{``{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};就是在世界坐标系下面表示的。旋转三角形,即直接在世界坐标系下面旋转,就可以做到在屏幕上旋转
  2. 视图变换(View Transformation) :将世界空间变换到相机(观察者)空间,模拟相机位置和方向。代码在get_view_matrix(eye_pos)中,也可以看到相机的位置用世界坐标表示,但是经过视图变换后坐标系将以相机为原点。
  3. 投影变换(Projection Transformation) :将相机空间变换到裁剪空间,实现透视投影或正交投影。需要实现:get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar) 输入是 eyefov表示视野角度,aspect_ratio表示xy的比例。需要转换一下才能套公式。

三个矩阵怎么计算的很多博主都写过,我这里就不写了

正交基之间的过渡矩阵非常好求解

xyz 对应的正交基是(1,0,0) (0,1,0),(0,0,1) 所以矩阵A是单位矩阵E

\\hat{g} \\times \\hat{t} , \\hat{t}, \\hat{g} 是另一组正交基,矩阵 B = ( g ^ × t ^ , t ^ , g ^ ) B=(\hat{g} \times \hat{t} , \hat{t}, \hat{g}) B=(g^×t^,t^,g^)

则, A P = B ⇒ P = A − 1 B = E B = B AP=B \Rightarrow P=A^{-1}B = EB=B AP=B⇒P=A−1B=EB=B

而B是正交基组成的,它逆矩阵是它的转置

所以这个要求是给定的 t ^ , g ^ \hat{t}, \hat{g} t^,g^是单位向量,且正交,才能推出老师给的公式

参考 :https://www.cnblogs.com/fijiisland/p/13993749.html

函数void rst::rasterizer::draw(...)解析

传入的参数是之前生成的"顶点坐标缓冲id"以及"顶点索引缓冲id"。

首先三角形有三个顶点

复制代码
std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
auto pos_id = r.load_positions(pos);

把顶点使用map存起来,并返回顶点的id

复制代码
rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{
    auto id = get_next_id();
    //std::map<int, std::vector<Eigen::Vector3f>> pos_buf;
    pos_buf.emplace(id, positions);

    return {id};
}

然后这三个顶点有三个索引,同样存在map里面

复制代码
std::vector<Eigen::Vector3i> ind{{0, 1, 2}};
auto ind_id = r.load_indices(ind);

rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices)
{
    auto id = get_next_id();
    // std::map<int, std::vector<Eigen::Vector3i>> ind_buf;
    ind_buf.emplace(id, indices);
    return {id};
}

使用的时候,用id在map里取顶点与索引

复制代码
// 向draw函数传入之前生成的"顶点坐标缓冲id"以及"顶点索引缓冲id"
void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type)
{
    // 当前作业中,代码支持的图元(Primitive)类型仅为rst::Primitive::Triangle,即三角形
    if (type != rst::Primitive::Triangle)
    {
        throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!");
    }

    // 容易得出,auto推导出的类型为std::vector<Eigen::Vector3f>
    // buf取得对应的图元顶点坐标vector
    auto& buf = pos_buf[pos_buffer.pos_id];
    // ind取得对应的图元顶点索引vector
    auto& ind = ind_buf[ind_buffer.ind_id];

    // 下面会解释f1、f2的含义
    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
        Triangle t;
      
        // 构造一个元素为4行向量的数组v,向内插入mvp矩阵对顶点索引对应顶点坐标的变换点
        // 为了和mvp进行运算,将每个顶点坐标转为一个Eigen::Vector4f,并规定w值为1
        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;
        }

        // 将变换好的顶点坐标传入三角形实例t
        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);
    }
}

三个顶点的位置都计算完毕,现在开始画三角形:

cpp 复制代码
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());
}

这里使用的是Bresenham's line drawing algorithm。就不多写了。

计算出画在屏幕上的点的位置之后使用函数:

cpp 复制代码
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;
}

这里在图像上对应的位置填上颜色。

相关推荐
Yawesh_best21 小时前
告别系统壁垒!WSL+cpolar 让跨平台开发效率翻倍
运维·服务器·数据库·笔记·web安全
Ccjf酷儿1 天前
操作系统 蒋炎岩 3.硬件视角的操作系统
笔记
习习.y1 天前
python笔记梳理以及一些题目整理
开发语言·笔记·python
在逃热干面1 天前
(笔记)自定义 systemd 服务
笔记
DKPT1 天前
ZGC和G1收集器相比哪个更好?
java·jvm·笔记·学习·spring
QT 小鲜肉1 天前
【孙子兵法之上篇】001. 孙子兵法·计篇
笔记·读书·孙子兵法
星轨初途1 天前
数据结构排序算法详解(5)——非比较函数:计数排序(鸽巢原理)及排序算法复杂度和稳定性分析
c语言·开发语言·数据结构·经验分享·笔记·算法·排序算法
QT 小鲜肉1 天前
【孙子兵法之上篇】001. 孙子兵法·计篇深度解析与现代应用
笔记·读书·孙子兵法
love530love1 天前
【笔记】ComfUI RIFEInterpolation 节点缺失问题(cupy CUDA 安装)解决方案
人工智能·windows·笔记·python·插件·comfyui
愚戏师1 天前
MySQL 数据导出
数据库·笔记·mysql