最近在学习闫老师的计算机图形学入门,现对第一次作业的代码框架进行解读,仅代表个人观点。
官方提供的代码框架如下图,两个头文件rasterizer.hpp和Triangle.hpp三个cpp文件main.cpp、rasterizer.cpp和Triangle.cpp
//
// Created by LEI XU on 4/11/19.
//
#ifndef RASTERIZER_TRIANGLE_H
#define RASTERIZER_TRIANGLE_H
#include
using namespace Eigen;
class Triangle
{
public:
Vector3f v[3]; /*the original coordinates of the triangle, v0, v1, v2 in
counter clockwise order*/
/*Per vertex values*/
Vector3f color[3]; // color at each vertex;
Vector2f tex_coords[3]; // texture u,v
Vector3f normal[3]; // normal vector for each vertex
// Texture *tex;
Triangle();
Eigen::Vector3f a() const { return v[0]; }
Eigen::Vector3f b() const { return v[1]; }
Eigen::Vector3f c() const { return v[2]; }
void setVertex(int ind, Vector3f ver); /*set i-th vertex coordinates */
void setNormal(int ind, Vector3f n); /*set i-th vertex normal vector*/
void setColor(int ind, float r, float g, float b); /*set i-th vertex color*/
void setTexCoord(int ind, float s,
float t); /*set i-th vertex texture coordinate*/
std::array<Vector4f, 3> toVector4() const;
};
#endif // RASTERIZER_TRIANGLE_H
此头文件里主要用于创建Triangle类,class Triangle的作用是储存形成三角形的点的信息,包括位置和颜色,颜色在此次实验中不重要。
v[3]向量组用来储存三角形的顶点
color[3]向量组用来储存顶点的颜色信息,不重要
tex_coords[3]向量组用来储存材质坐标
normal[3]向量组用来储存每个顶点的法向量
函数a,b,c的作用是分别返回三角形的顶点向量坐标
Eigen::Vector3f a() const { return v[0]; }
Eigen::Vector3f b() const { return v[1]; }
Eigen::Vector3f c() const { return v[2]; }
函数setVertex的作用是将三角形的三个顶点坐标储存到v[3]中,注意,这里在rasterrizer.cpp中可看到用法,v[3]中保存的是经过视口变换后的坐标
void Triangle::setVertex(int ind, Eigen::Vector3f ver) { v[ind] = ver; }
函数setNormal的作用是将法线保存到normal[3]中,此次实验并未用到这个函数
void Triangle::setNormal(int ind, Vector3f n) { normal[ind] = n; }
函数setColor的作用是将画线的颜色信息,也就是RGB值保存到color[3]中
void Triangle::setColor(int ind, float r, float g, float b)
{
if ((r < 0.0) || (r > 255.) || (g < 0.0) || (g > 255.) || (b < 0.0) ||
(b > 255.)) {
throw std::runtime_error("Invalid color values");
}
color[ind] = Vector3f((float)r / 255., (float)g / 255., (float)b / 255.);
return;
}
函数setTexCoord的作用是设置画线的纹理,就是把线段类型保存到tex_coords[3]中,次实验并未用到这个函数,默认画的是直线
void Triangle::setTexCoord(int ind, float s, float t)
{
tex_coords[ind] = Vector2f(s, t);
}
函数toVector4的作用是把三维向量v[3]转换成四维的,多的那个元素用1补充
std::array<Vector4f, 3> Triangle::toVector4() const
{
std::array<Vector4f, 3> res;
std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) {
return Vector4f(vec.x(), vec.y(), vec.z(), 1.f);
});
return res;
}
此文件用于对class Triangle的初始化和函数的定义。
初始化不多说,函数的定义就是上面所说的函数作用翻译成代码,不难,也不多说。
//
// Created by LEI XU on 4/11/19.
//
#include "Triangle.hpp"
#include
#include
#include
Triangle::Triangle()
{
v[0] << 0, 0, 0;
v[1] << 0, 0, 0;
v[2] << 0, 0, 0;
color[0] << 0.0, 0.0, 0.0;
color[1] << 0.0, 0.0, 0.0;
color[2] << 0.0, 0.0, 0.0;
tex_coords[0] << 0.0, 0.0;
tex_coords[1] << 0.0, 0.0;
tex_coords[2] << 0.0, 0.0;
}
void Triangle::setVertex(int ind, Eigen::Vector3f ver) { v[ind] = ver; }
void Triangle::setNormal(int ind, Vector3f n) { normal[ind] = n; }
void Triangle::setColor(int ind, float r, float g, float b)
{
if ((r < 0.0) || (r > 255.) || (g < 0.0) || (g > 255.) || (b < 0.0) ||
(b > 255.)) {
throw std::runtime_error("Invalid color values");
}
color[ind] = Vector3f((float)r / 255., (float)g / 255., (float)b / 255.);
return;
}
void Triangle::setTexCoord(int ind, float s, float t)
{
tex_coords[ind] = Vector2f(s, t);
}
std::array<Vector4f, 3> Triangle::toVector4() const
{
std::array<Vector4f, 3> res;
std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) {
return Vector4f(vec.x(), vec.y(), vec.z(), 1.f);
});
return res;
}
此头文件用于创建rst命名空间,在这个命名空间里创建了所需的变量和类
Color的作用是
Depth的作用是
内联函数重载|运算,没啥解释的
内联函数重载&运算,同上
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);
}
class Primitive类的Line和Triangle作用是告诉后面的draw函数需要绘制的图形,目前只实现了三角形和直线
结构体pos_buf_id和ind_buf_id的作用是标记
class rasterizer的声明:
构造函数,初始化frame_buf(像素数组)和depth_buf(深度数组)为w*h大小
函数load_position的作用是储存位置信息
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};
}
函数load_indices的作用是储存索引信息
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};
}
函数set_model的作用是获取模型变换矩阵
void rst::rasterizer::set_model(const Eigen::Matrix4f& m)
{
model = m;
}
函数set_view的作用是获取视图变换矩阵
void rst::rasterizer::set_view(const Eigen::Matrix4f& v)
{
view = v;
}
函数set_projection的作用是获取透视投影变换矩阵
void rst::rasterizer::set_projection(const Eigen::Matrix4f& p)
{
projection = p;
}
函数set_pixel的作用是把每个需要绘制的点的像素信息保存到像素数组frame_buff里
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-1-point.y())*width + point.x();
frame_buf[ind] = color;
}
函数clear的作用是清除整个显示屏幕,实现方法是把两个缓存数组都置为0
void rst::rasterizer::clear(rst::Buffers buff)
{
if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
{
std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
}
if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
{
std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
}
}
函数draw的作用是把所有的点通过mvp变换和视口变换,转换成可以在我们的w*h大小的屏幕上显示的点,并通过setVertex函数保存到Triangle里的v[]中,这里颜色设置部分就不说了,不是此次实验的重点
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),//化为齐次坐标,这一步就是将三角形转换到[1,1]*3的立方体中
mvp * to_vec4(buf[i[1]], 1.0f),
mvp * to_vec4(buf[i[2]], 1.0f)
};
for (auto& vec : v) {
vec /= vec.w();//把w归一化,即把此向量除以w
}
for (auto & vert : v)
{
vert.x() = 0.5*width*(vert.x()+1.0); //视口变换,通过视口变换,将x方向从[-1,1]变为[0,width],
vert.y() = 0.5*height*(vert.y()+1.0); //y方向从[-1,1]变到[0,height],z方向由[-1,1]变到[0,100]左右。
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);
}
}
函数frame_buffer的作用是返回像素数组
std::vector<Eigen::Vector3f>& frame_buffer() { return frame_buf; }
函数draw_line的作用是通过Bresenham算法来计算出需要绘制的像素点
void rst::rasterizer::draw_line(Eigen::Vector3f begin, Eigen::Vector3f end)
{
auto x1 = begin.x();
auto y1 = begin.y();
auto x2 = end.x();
auto y2 = end.y();
Eigen::Vector3f line_color = {255, 0, 0};
int x,y,dx,dy,dx1,dy1,px,py,xe,ye,i;
dx=x2-x1;
dy=y2-y1;
dx1=fabs(dx);//fabs求双精度浮点数的绝对值
dy1=fabs(dy);
px=2*dy1-dx1;
py=2*dx1-dy1;
if(dy1<=dx1)
{
if(dx>=0)
{
x=x1;
y=y1;
xe=x2;
}
else
{
x=x2;
y=y2;
xe=x1;
}
Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);
set_pixel(point,line_color);
for(i=0;x<xe;i++)
{
x=x+1;
if(px<0)
{
px=px+2*dy1;
}
else
{
if((dx<0 && dy<0) || (dx>0 && dy>0))
{
y=y+1;
}
else
{
y=y-1;
}
px=px+2*(dy1-dx1);
}
// delay(0);
Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);
set_pixel(point,line_color);
}
}
else
{
if(dy>=0)
{
x=x1;
y=y1;
ye=y2;
}
else
{
x=x2;
y=y2;
ye=y1;
}
Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);
set_pixel(point,line_color);
for(i=0;y<ye;i++)
{
y=y+1;
if(py<=0)
{
py=py+2*dx1;
}
else
{
if((dx<0 && dy<0) || (dx>0 && dy>0))
{
x=x+1;
}
else
{
x=x-1;
}
py=py+2*(dx1-dy1);
}
// delay(0);
Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);
set_pixel(point,line_color);
}
}
}
函数raster_wireframe的作用是分别调用三次draw_line来计算三条线
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());
}
三个求变换矩阵的函数,也是此次的作业
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;
}
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
rotation_angle = rotation_angle / 180 * MY_PI;
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
Eigen::Matrix4f translate;
translate << cos(rotation_angle), -sin(rotation_angle), 0, 0, sin(rotation_angle), cos(rotation_angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1;
//模型变换,绕z轴旋转模型
model = translate * model;
// TODO: Implement this function
// Create the model matrix for rotating the triangle around the Z axis.
// Then return it.
return model;
}
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
// Students will implement this function
//eye_fov是y方向的视域角; aspect_ratio是近裁剪面的宽高比
eye_fov = eye_fov / 180 * MY_PI;
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
Eigen::Matrix4f translate;
translate << 1.0 / tan(eye_fov / 2), 0, 0, 0,0, 1.0 / tan(eye_fov / 2), 0, 0, 0, 0, (zFar + zNear) / (zNear - zFar), 2 * zFar * zNear / (zNear - zFar), 0, 0, -1, 0;
//透视投影
projection = translate * projection;
// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Then return it.
return projection;
}
Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
{
angle = angle / 180 * MY_PI;
Eigen::Matrix4f rotation = Eigen::Matrix4f::Zero();
Eigen::Matrix3f Temp ;
rotation(3, 3) = 1;
Eigen::Matrix3f I;
I << 1, 0, 0, 0, 1, 0, 0, 0, 1;
Eigen::Matrix3f K;
K << 0, -axis.z(), axis.y(), axis.z(), 0, -axis.x(), -axis.y(), axis.x(), 0;
Temp = (cos(angle) * I + (1 - cos(angle)) * axis * axis.transpose() + sin(angle) * K);
rotation.block(0, 0, 2, 2) = Temp.block(0, 0, 2, 2);
return rotation;
}
main函数部分就不多说了,就是根据输入台输入的数据来判断是显示三角形还是把图片保存到本地,两个部分的代码逻辑是相同的,都是先保存mvp的矩阵,然后调用光栅化器的主题算法部分,也就是draw函数求的所需的像素矩阵,然后调用opencv把像素矩阵绘制出来。
对于这个代码其实还有好几个地方还云里雾里的,我把我的问题贴在下面,希望有大佬能看见。
1、draw_line函数中调用set_pixel函数的作用是什么?这里我理解为设置颜色。draw_line函数通过Bresenham算法求出了像素点,它是如何存储进入frame_buf里的呢?是通过set_pixel函数吗?可是set_pixel函数里并没有存储点的操作。