作业要求
在接下来的三次作业中,我们将要求你去模拟一个基于 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.hpp 和 main.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);
其构造函数如下:这里的 w
和 h
分别表示视口的宽和高,这里的 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;
}
- 透视投影变换矩阵,首先计算出
t
、r
、l
、b
,然后分别计算出 Mortho(旋转+缩放),以及 Mpersp2ortho,透视投影变换矩阵就是 MorthoMpersp2ortho,下面的n
在代码中对应zNear
,f
在代码中对应zFar
,fovY
对应eye_fov
,aspect
对应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;
}
需要注意的是,闫教授在课上推的公式,是以 zNear
,zFar
均为负数为前提的,因此在 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 键