Rasterizer.hpp中对光栅化器的基本功能做了定义,但其中有些部分的逻辑比较复杂,此处做最核心的分析。

两个枚举类

对于Buffers类,它实际上是为了clear函数服务的,用于指定clear哪个类型的缓冲数据。

enum class Buffers
{
    Color = 1,
    Depth = 2
};

然后来看下面两个操作符重载

inline Buffers operator|(Buffers a, Buffers b)
{
    return Buffers((int)a | (int)b);
}

inline Buffers operator&(Buffers a, Buffers b)
{
    return Buffers((int)a & (int)b);
}

首先是按位或|的重载:它的作用是为了让clear函数能够同时清除两种缓存:

clear(Buffers::Color | Buffers::Depth);

然后是按位与&的重载:它的作用是为了在其他函数内部实现中便于判断某个缓冲中包含哪些类型的缓冲。

比如,clear的组合清除实现可以这么写:

void rasterizer::clear(Buffers buff)

{
    if ((buff & Buffers::Color) == Buffers::Color)
    {
        // 清除颜色缓冲区
    }

    if ((buff & Buffers::Depth) == Buffers::Depth)

    {
        // 清除深度缓冲区
    }
}

position与indices

后来发现,接下来的内容仅在作业2中有效,后面的作业用了更简洁直观的实现方式

首先来看pos_idind_id的定义:

struct pos_buf_id
{
    int pos_id = 0;
};

struct ind_buf_id
{
    int ind_id = 0;
};

很简单,这里用一个struct包装一下是为了防止混用(防呆接口hhh)

这里重点看下面两个缓存结构及对应的缓存函数:

std::map<int, std::vector<Eigen::Vector3f>> pos_buf;
std::map<int, std::vector<Eigen::Vector3i>> ind_buf;

rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{
    auto id = get_next_id();
    pos_buf.emplace(id, positions);

    return {id};
}

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

    return {id};
}

ind_id实际上是顶点在一定范围内的唯一索引标识,而pos_id就是这个"范围"的唯一索引标识。比如pos_id=1可以从pos_buf中取出一个数组,这个数组的作用类似"命名空间"。随后再通过ind_id从这个"命名空间"中取出顶点。

下面是一个使用示例:

// 顶点组 A(id = 1)
auto pos_id_1 = load_positions({
    {1,1,1}, {0,0,0}, {1,0,0}
});

// 顶点组 B(id = 2)
auto pos_id_2 = load_positions({
    {0,0,1}, {0,1,0}, {1,1,0}
});

// 索引组 A(id = 1)——适配 pos_id_1
auto ind_id_1 = load_indices({
    {0,1,2}
});

// 索引组 B(id = 2)——适配 pos_id_2
auto ind_id_2 = load_indices({
    {0,2,1}
});

draw(pos_id_2, ind_id_2, Primitive::Triangle);

load_positionsload_indices将数据缓存到缓冲区中,并返回各自对应的id。这里的对应关系需要自己显示维护。比如draw函数中指明了pos_id=pos_id_2,那么在draw函数中所有的ind_id最终索引都将对应顶点组B中的顶点

此处注意,ind_buf中的value是Vector3i,意味着每个ind_id都会对应一系列的三元组。实际上此处每个ind_id都对应了一组三角形。下面给出draw的大致逻辑:

auto& positions = pos_buf[pos_id.pos_id];     // 取出这组顶点
auto& indices = ind_buf[ind_id.ind_id];       // 取出这组索引

for (auto& triangle : indices) {
    Vector3f v0 = positions[triangle[0]];     // index 0
    Vector3f v1 = positions[triangle[1]];     // index 1
    Vector3f v2 = positions[triangle[2]];     // index 2

    // 构造 Triangle 对象并 rasterize
}

frame_buf与depth_buf

首先来看这两个缓存的定义:

std::vector<Eigen::Vector3f> frame_buf;
std::vector<float> depth_buf;

它们的作用是存储二维平面上各个像素的zbuffer与rgb颜色信息,但既然是每个像素一个缓存数据,为什么是一维数组?

它的实际操作方式是这样的,先通过get_index(x,y)将二维坐标转换为一维索引

int rst::rasterizer::get_index(int x, int y)
{
    return (height - 1 - y) * width + x;
}

然后通过这个索引去一维缓存中存取数据

//获取坐标(x0,y0)的缓存数据:
int index = get_index(x0,y0);

Eigen::Vector3f f_buf0 = frame_buf[index];
float d_buf0 = depth_buf[index];

为什么要这么麻烦套一层?

为了提高灵活性、支持动态分辨率等,我们大部分数据结构都最好使用容器。此处如果做成二维数组,形式就是vector<vector<Eigen::Vector3f>> ,每一行之间都是不同的vector,内存不连续,性能弱于一维数组

因此上述这种映射方法已经是图形学中的标准做法了