目录
1.3.1 重心坐标 computeBarycentric2D
2.3.2 libpng warning:iCCP:known incorrect sRGB profile
0.作业介绍:
如上图所示为本次作业需要完成的任务,在介绍我对于本次作业的心得体会之前,我想先主观的感叹一句,我真的觉得这次作业很难。虽然上课时理论的学习是可以接受并理解的,但实操问题真的很多,我感觉我本次的作业基本上是“临摹”出来的。理论与实践脱轨的一个关键原因,我认为是有很多变量、对象没有弄清,没有理解它们对应的物理意义,所以在编写代码有些无从下手,不知道怎么把公式“变现”,所以我决定很有必要先介绍一下整个代码的实现流程,其中部分流程其实是在前两个作业已经出现的,但因为当时代码相对好写,所以没有彻底弄清楚,哎~
1.0.0 管线分析:
在Shading中,闫老师讲了图形管线的一系列操作[1],概括下来分别是[2]:
- Vertex Processing 点操作 :将三维空间上的点投影到二维
- Triangle Processing 三角形操作 :将三个点连成一个三角形/线,构造出一个平面/线,使得原三维空间物体变成由若干三角形组成的物体
- Raserization 光栅化:进行离散化的采样,用像素表示采样结果
- Fragment Processing 片元处理 :着色
- Framebuffer Operations片元缓冲操作:根据深度信息,确定遮挡和可视情况

下面我就按照管线的顺序,理一下整个流程,记录在注释里。
1.1.0 main函数
int main(int argc, const char** argv)
{
//记录组成三维图形的所有小三角形
std::vector<Triangle*> TriangleList;
float angle = 140.0;
bool command_line = false;
std::string filename = "output.png";
objl::Loader Loader;
std::string obj_path = "./models/spot/";
// Load .obj File,加载模型
bool loadout = Loader.LoadFile("./models/spot/spot_triangulated_good.obj");
for(auto mesh:Loader.LoadedMeshes)
{
//对于图形中的每个面(即一个小三角形)的三个点记录在一起
for(int i=0;i<mesh.Vertices.size();i+=3)
{
Triangle* t = new Triangle();
//用Triangle类,记录一个小三角形的三个顶点的信息:
//setVertex顶点位置,setNormal顶点的法线,setTexCoord顶点对应的纹理
for(int j=0;j<3;j++)
{
t->setVertex(j,Vector4f(mesh.Vertices[i+j].Position.X,mesh.Vertices[i+j].Position.Y,mesh.Vertices[i+j].Position.Z,1.0));
t->setNormal(j,Vector3f(mesh.Vertices[i+j].Normal.X,mesh.Vertices[i+j].Normal.Y,mesh.Vertices[i+j].Normal.Z));
t->setTexCoord(j,Vector2f(mesh.Vertices[i+j].TextureCoordinate.X, mesh.Vertices[i+j].TextureCoordinate.Y));
}
//每三个顶点构成的一个小三角形放入TriangleList
TriangleList.push_back(t);
}
}
//初始化光栅化对象,定义屏幕长宽
rst::rasterizer r(700, 700);
//记录纹理到对象,注意rasterizer.hpp类有属性 optional<Texture> texture
auto texture_path = "hmap.jpg";
r.set_texture(Texture(obj_path + texture_path));
//记录片元处理方式,类似于“赋值函数”,现默认方式是phong
std::function<Eigen::Vector3f(fragment_shader_payload)> active_shader = phong_fragment_shader;
//处理传入的参数,注意在此处根据调用方式不同,修改了rasterizer对象的片元处理方式!
if (argc >= 2)
{
command_line = true;
filename = std::string(argv[1]);
if (argc == 3 && std::string(argv[2]) == "texture")
{
std::cout << "Rasterizing using the texture shader\n";
active_shader = texture_fragment_shader;
texture_path = "spot_texture.png";
r.set_texture(Texture(obj_path + texture_path));
}
else if (argc == 3 && std::string(argv[2]) == "normal")
{
std::cout << "Rasterizing using the normal shader\n";
active_shader = normal_fragment_shader;
}
else if (argc == 3 && std::string(argv[2]) == "phong")
{
std::cout << "Rasterizing using the phong shader\n";
active_shader = phong_fragment_shader;
}
else if (argc == 3 && std::string(argv[2]) == "bump")
{
std::cout << "Rasterizing using the bump shader\n";
active_shader = bump_fragment_shader;
}
else if (argc == 3 && std::string(argv[2]) == "displacement")
{
std::cout << "Rasterizing using the bump shader\n";
active_shader = displacement_fragment_shader;
}
}
//人眼所在位置
Eigen::Vector3f eye_pos = {0,0,10};
//设置顶点着色方式,获取顶点位置
r.set_vertex_shader(vertex_shader);
//设置片元着色方式,根据调用方式不同已赋值到active_shader
r.set_fragment_shader(active_shader);
int key = 0;//修改这个值,可以选择输出图片或动态旋转渲染的模型
int frame_count = 0;
if (command_line)
{
//清空两个缓冲区,在最后处理遮挡显示情况时发挥作用
r.clear(rst::Buffers::Color | rst::Buffers::Depth);
//分别设置MVP矩阵,用于对点操作,实现将三维的点映射到平面
r.set_model(get_model_matrix(angle));
r.set_view(get_view_matrix(eye_pos));
r.set_projection(get_projection_matrix(45.0, 1, 0.1, 50));
//光栅化、片元处理
r.draw(TriangleList);
cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
image.convertTo(image, CV_8UC3, 1.0f);
cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
cv::imwrite(filename, image);
return 0;
}
while(key != 'q')
{
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.0, 1, 0.1, 50));
//r.draw(pos_id, ind_id, col_id, rst::Primitive::Triangle);
r.draw(TriangleList);
cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
image.convertTo(image, CV_8UC3, 1.0f);
cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
cv::imshow("image", image);
cv::imwrite(filename, image);
key = cv::waitKey(0);
if (key == 'a' )
{
angle -= 0.1;
}
else if (key == 'd')
{
angle += 0.1;
}
}
return 0;
}
简而言之,主函数实现了:
①将顶点以三角形的方式每三个记录在一起,下图是Triangle类的一些属性,注意留意各个变量的名称和存储的内容,可以发现这一个类里装了三个顶点的顶点坐标、颜色、对应纹理坐标、法线。

②设置光栅化器的MVP模型(用于将三维的点变换到屏幕空间)
③根据调用命令的不同设置光栅化器的着色方式,之后便进入了rasterizer的draw函数。
1.2.0 draw函数
void rst::rasterizer::draw(std::vector<Triangle *> &TriangleList) {
//zfar和znear之间的距离的一半
float f1 = (50 - 0.1) / 2.0;
//zfar个znear的中心z坐标
float f2 = (50 + 0.1) / 2.0;
//MVP变换矩阵
Eigen::Matrix4f mvp = projection * view * model;
//对每个小三角形进行操作,注意这个for循环一直到draw函数的结尾,
//故管线从此处开始其实是对每一个小三角形进行的(着色、深度处理等)
for (const auto& t:TriangleList)
{
Triangle newtri = *t;
//这里记录了只进行了MV变换的三角形的三个顶点,在本次作业中将此作为了视图空间viewspace
//的坐标,用于后续确定光源与物体表面的作用
//详见[3]https://games-cn.org/forums/topic/zuoye3-interpolated_shadingcoords/
std::array<Eigen::Vector4f, 3> mm {
(view * model * t->v[0]),
(view * model * t->v[1]),
(view * model * t->v[2])
};
//记录viewspace的点坐标
std::array<Eigen::Vector3f, 3> viewspace_pos;
std::transform(mm.begin(), mm.end(), viewspace_pos.begin(), [](auto& v) {
return v.template head<3>();
});
//经过MVP后的点坐标,投影到屏幕
Eigen::Vector4f v[] = {
mvp * t->v[0],
mvp * t->v[1],
mvp * t->v[2]
};
//x,y,z同除w,得到齐次坐标
//Homogeneous division
for (auto& vec : v) {
vec.x()/=vec.w();
vec.y()/=vec.w();
vec.z()/=vec.w();
}
//这里是为了计算仅进行了MV操作,即在viewspace下各顶点的法向量,具体解释见下文
Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();
Eigen::Vector4f n[] = {
inv_trans * to_vec4(t->normal[0], 0.0f),
inv_trans * to_vec4(t->normal[1], 0.0f),
inv_trans * to_vec4(t->normal[2], 0.0f)
};
//视图变换,直接对X,Y,Z进行改变,而未使用变换矩阵,效果是相同的
//Viewport transformation
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;
}
//记录下经过MVP、齐次坐标变换和视图变换的顶点坐标
//完成顶点变换,变换到屏幕空间
for (int i = 0; i < 3; ++i)
{
//screen space coordinates
newtri.setVertex(i, v[i]);
}
//存储法向量n
for (int i = 0; i < 3; ++i)
{
//view space normal
newtri.setNormal(i, n[i].head<3>());
}
//设置颜色
newtri.setColor(0, 148,121.0,92.0);
newtri.setColor(1, 148,121.0,92.0);
newtri.setColor(2, 148,121.0,92.0);
//调用rasterizer_triangle,实现光栅化
//注意此时仍在循环中,故实际是对每个小三角形分别进行光栅化
//此时也传入了viewspace的顶点坐标,用于判断真实的光线作用
// Also pass view space vertice position
rasterize_triangle(newtri, viewspace_pos);
}
}
概括说,draw函数的作用在于对于每一个小三角形,分别计算了:
①仅进行MV操作、在viewspace空间中的顶点坐标,保存在mm,后传给了viewspace_pos,并传递到rasterizer_tirangle函数中,该坐标的作用在于后续计算光线作用,详见[3]:


②经过MVP变换的顶点坐标,保存在v,后进行了齐次坐标化,注意此时并未处理齐次坐标的w维,因为经过了MVP变换后,w坐标记录了原本的z值,这对于后续进行插值十分有用,等下文插值求深度时再展开说。
③viewspace下的顶点的法向量,保存在n,目的是在判断光线作用时,需要知道没有变形的三维物体的形状、位置信息,该法向量n和上方说的viewspace_pos皆是如此。
注意代码最初令newtri == *t,即利用newtri记录小三角形三个顶点的位置、法线、纹理、颜色,但后续修改了①位置,将其改成了经过MVP变换后的,②法线,将其改成了viewspace空间的。同时,viewspace的位置坐标虽然没有记录在newtri中,但是以参数的形式传递给了后续的rasterizer_t

本文详细分析了一个图形学作业,涵盖了图形管线、顶点和三角形处理、光栅化、深度插值、着色模型(normal、phong、texture、bump和displacement)。作者在实现过程中遇到的问题、解决方案以及对代码的解释贯穿全文,揭示了从理论到实践的转换过程。此外,还讨论了双线性插值在提升纹理效果中的应用。
最低0.47元/天 解锁文章
1012





