原理
背景
将三维空间中一个旋转的三角形,显示在屏幕上。
假设三角形初始点位为 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)。它是将三维模型空间的点变换到二维屏幕上的关键变换矩阵链,包括以下几个步骤:
- 模型变换(Model Transformation):将物体从其本地模型空间变换到世界空间,比如平移、旋转、缩放。
- 视图变换(View Transformation):将世界空间变换到相机(观察者)空间,模拟相机位置和方向。
- 投影变换(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缓冲区)用来管理和实现隐藏面消除,确保在渲染场景时,前面(离观察者近)的像素遮挡后面(离观察者远)的像素。
- 隐藏面处理:在绘制每个像素时,比较当前像素的深度值(距离相机的远近)与已存储的深度值。
- 遮挡关系:只保留离观察者更近的像素,屏蔽被遮挡的远处的像素,从而实现正确的遮挡关系。
具体流程
- 当绘制一个像素时,计算其深度(z值)。
- 将该深度值与深度缓冲区中对应位置的值进行比较。
- 如果新像素更接近(深度值小),则更新颜色和深度缓冲区中的值。
- 如果远离(深度值大),则跳过该像素,不更新。
代码中的表现
在 clear()
函数中,深度缓冲区被初始化为无穷大(std::numeric_limits<float>::infinity()
),表示还没有进行任何像素绘制,所有像素的深度都比任何实际像素都远。
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;
}
}
我们根据代码重新理解一下上面的知识。
- 模型变换(Model Transformation):将物体从其本地模型空间变换到世界空间。可以看到
get_model_matrix(angle)
的函数只是构建了一个旋转矩阵。所以原本的三角形,即std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
就是在世界坐标系下面表示的。旋转三角形,即直接在世界坐标系下面旋转,就可以做到在屏幕上旋转 - 视图变换(View Transformation):将世界空间变换到相机(观察者)空间,模拟相机位置和方向。代码在
get_view_matrix(eye_pos)
中,也可以看到相机的位置用世界坐标表示,但是经过视图变换后坐标系将以相机为原点。 - 投影变换(Projection Transformation):将相机空间变换到裁剪空间,实现透视投影或正交投影。需要实现:
get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
输入是 eyefov表示视野角度,aspect_ratio表示xy的比例。需要转换一下才能套公式。
三个矩阵怎么计算的很多博主都写过,我这里就不写了
参考 :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);
}
}
三个顶点的位置都计算完毕,现在开始画三角形:
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。就不多写了。
计算出画在屏幕上的点的位置之后使用函数:
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;
}
这里在图像上对应的位置填上颜色。