这是个人第一次的图形学相关知识的博客分享,主要内容是Games101作业3的渲染管线梳理和分析、作业要求任务的实现和一些相关知识的补充,若有不足,还请指正。
在这次编程任务中,我们会进一步模拟现代图形技术。我们在代码中添加了ObjectLoader(用于加载三维模型),VertexShader与FragmentShader,并且支持了纹理映射。而在本次实验中,你需要完成的任务是:
1.修改函数rasterize_triangle(constTriangle&t)inrasterizer.cpp:在此处实现与作业2类似的插值算法,实现法向量、颜色、纹理颜色的插值。
2.修改函数get_projection_matrix()inmain.cpp:将你自己在之前的实验中实现的投影矩阵填到此处,此时你可以运行./Rasterizeroutput.pngnormal来观察法向量实现结果。
3.修改函数phong_fragment_shader()inmain.cpp:实现Blinn-Phong模型计算FragmentColor.
4.修改函数texture_fragment_shader()inmain.cpp:在实现Blinn-Phong的基础上,将纹理颜色视为公式中的kd,实现TextureShadingFragmentShader.
5.修改函数bump_fragment_shader()inmain.cpp:在实现Blinn-Phong的基础上,仔细阅读该函数中的注释,实现Bumpmapping.
6.修改函数displacement_fragment_shader()inmain.cpp:在实现Bumpmapping的基础上,实现displacementmapping.。
在Shading中,闫老师讲了图形管线的一般流程,概括起来分别是:
1.Vertex Processing 点操作 :将三维空间上的点投影到二维
2.Triangle Processing 三角形操作 :将三个点连成一个三角形/线,构造出一个平面/线,使得原三维空间物体变成由若干三角形组成的物体
3.Raserization 光栅化:进行离散化的采样,用像素表示采样结果
4.Fragment Processing 片元处理 :进行着色处理
5.Framebuffer Operations片元缓冲操作:根据深度信息,确定遮挡和可视情况
接下来我就按渲染管线的顺序,进行此次渲染管线的分析。
代码如下:
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 = "C:/Users/86189/Desktop/c++/Games_assignment/Games101_assignment/Games101_assignment3/Games101_assignment3/models/spot/";
//此处加上自己文件的储存地址
// Load .obj File,加载模型
bool loadout = Loader.LoadFile("C:/Users/86189/Desktop/c++/Games_assignment/Games101_assignment/Games101_assignment3/Games101_assignment3/models/spot/spot_triangulated_good.obj");
for (auto mesh : Loader.LoadedMeshes)
{
//将三维图形中的每一个面(即每一个三角形)的三个点记录下来
for (int i = 0; i+3 <= mesh.Vertices.size()-1; i += 3)
{
Triangle* t = new Triangle();//每次遍历一个面的三个点,便用一个Triangle类的指针去接受新开辟的构造函数(此处无参构造,是初始化顶点)地址,记录三角形信息
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.push_back(t);//将每一个小三角形放进TriangleList
}
}
//初始化光栅化的对象,这里定义了屏幕的长度和宽度
rst::rasterizer r(700, 700);
//记录纹理到对象
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;//类模板function<>包装函数对象:返回值类型是Vector3f,传入参数类型是fragment_shader_payload
//根据传入的参数的不同,改变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);//设置顶点着色方式,将vertex_shader对应数据传入rasterizer类的成员变量
r.set_fragment_shader(active_shader);//设置片元着色方式,将active_shader对应数据传入rasterizer类的成员变量
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));//前面有定义float angle = 140.0,只是为了将每一个片元的点都旋转一个角度,即使模型旋转一个角度,角度可自定义,也可以修改旋转的函数get_model_matrix()
r.set_view(get_view_matrix(eye_pos));//这里相当于人眼在屏幕外10个单位,屏幕上显示正常的带点范围,而get_view_matrix()将人眼往z轴的负方向(即屏幕里)前进10个单位,将点也往屏幕内缩进10个单位
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 != 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.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(10);
if (key == 'a')
{
angle -= 0.1;
}
else if (key == 'd')
{
angle += 0.1;
}
}
return 0;
}
1.1.1将顶点以三角形的方式每三个记录起来,下图是Triangle类的一些属性,注意留意各个变量的名称和存储的内容,可以发现这一个类里装了三个顶点的顶点坐标、颜色、对应纹理坐标、法线。
1.1.2设置光栅化器的MVP模型(用于将三维的点变换到屏幕空间)
1.1.3根据调用命令的不同设置光栅化器的着色方式,之后便进入了rasterizer的draw函数。
代码如下:
void rst::rasterizer::draw(std::vector<Triangle *> &TriangleList) {
//f1是zFar和zNear之间距离的一半
float f1 = (50 - 0.1) / 2.0;
//f2是zFar和zNear的中心点z坐标
float f2 = (50 + 0.1) / 2.0;
Eigen::Matrix4f mvp = projection * view * model;//定义MVP变换矩阵,projection、view、model是rasterizer类的成员,主函数已经将MVP三个矩阵的值传进去rasterizer类成员了
//遍历每一个储存好信息的小三角形
//这里开始即是对每个小三角形进行MVP、深度插值、着色等操作
for (const auto& t:TriangleList)
{
Triangle newtri = *t;
//这里只是用mm记录了每一个三角形的三个顶点经过model、view变换后的位置(没有进行透视投影变换),此时的三个顶点的位置是作为视图空间viewspace中每一个三角形的三个顶点坐标!!
//这里是作为后续确定光源与物体表面位置的用处
std::array<Eigen::Vector4f, 3> mm {
(view * model * t->v[0]),
(view * model * t->v[1]),
(view * model * t->v[2])
};
std::array<Eigen::Vector3f, 3> viewspace_pos;
//记录viewspace空间下当前三角形的三个点的坐标
std::transform(mm.begin(), mm.end(), viewspace_pos.begin(), [](auto& v) {
return v.template head<3>();
});
//更新v[],变为经过MVP变换,投影到屏幕的的点坐标
//记得像前面一样先记录下经过MV变换的三角形的坐标
Eigen::Vector4f v[] = {
mvp * t->v[0],
mvp * t->v[1],
mvp * t->v[2]
};
//x,y,z同时处于w,得到齐次坐标!!(因为在屏幕上操作都是对齐次坐标进行操作的),但这里没有对w进行处理,还保存着此处的深度,用于光栅化里面的深度插值
//Homogeneous division
for (auto& vec : v) {
vec.x()/=vec.w();
vec.y()/=vec.w();
vec.z()/=vec.w();
}
//这里只是用n[]来表示只经过MV变换,即在viewspace视图空间下的各个顶点的法向量方向
Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();//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)
};
//Viewport transformation
//这里是对屏幕三角形顶点进行x、y、z的变换(拉伸等),调整展现出来的图像大小等,以及同步将所有屏幕三角形位置都移到偏中心的位置,整体屏幕投影就会移到偏中心的位置
for (auto & vert : v)
{
vert.x() = 0.5*width*(vert.x()+1.0);
vert.y() = 0.5*height*(vert.y()+1.0);//x和y两个方向要进行相同形式的变换,才可以保证三角形显示出来的形状不会被压缩等
vert.z() = vert.z() * f1 + f2;//深度变换因为是平行投影,图像大小不会变化
//f1是zFar和zNear之间距离的一半
//float f1 = (50 - 0.1) / 2.0;
//f2是zFar和zNear的中心点z坐标
//float f2 = (50 + 0.1) / 2.0;
}
//将经过MVP变换、齐次坐标变换、视图变换的屏幕三角形顶点坐标记录在v[]里面
for (int i = 0; i < 3; ++i)
{
//screen space coordinates
newtri.setVertex(i, v[i]);
}
//记录法线信息
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);
// Also pass view space vertice position
//接下来调用 rasterize_triangle对每一个小三角形进行光栅化,使在屏幕位置的小三角形能够真正显示出来
//这里也是传入了viewspace_pos,即小三角形未经过透视变换的三个顶点的世界空间坐标,用于后面计算光照
rasterize_triangle(newtri, viewspace_pos);
}
}
主要是计算了每个小三角形的相关顶点信息,之后进行光栅化处理。具体计算内容:
2.1.1.对只进行MV操作、在viewspace空间中的顶点坐标,先保存在mm,后传给了viewspace_pos,并传递到rasterizer_tirangle函数中,该坐标的作用在于后续计算光线作用
2.1.2.经过MVP变换的顶点坐标,保存在v,后进行了齐次坐标化,(此时并未处理齐次坐标的w维,因为经过了MVP变换后,w坐标记录了原本的z值,这用于后面进行深度插值)
2.1.3.对于在viewspace下的顶点的法向量,保存在n,目的是在判断光线作用的时候,要知道没有变形的三维物体的形状、位置信息(此时只是进行了模型的MV变换,还没有进行透视投影),该法向量n和上方说的viewspace_pos皆是如此。
2.1.4注意代码最初令newtri == *t,即利用newtri记录小三角形三个顶点的位置、法线、纹理、颜色,但后续修改了三角形顶点的位置,将其改成了经过MVP变换和视图变换后的(利用setVertex()函数),法线则将其改成了viewspace空间的(利用setNormal()函数)。同时,viewspace的位置坐标虽然没有记录在newtri中,但是以参数的形式传递给了后续的rasterizer_triangle函数。所以传入光栅化rasterize_triangle函数的其实是屏幕三角形以及viewspace的位置坐标。
下面详细说一下viewspace下的顶点的法向量的计算过程,即下面这段代码的推导:
//这里只是用n[]来表示只经过MV变换,即在viewspace视图空间下的各个顶点的法向量方向
Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();//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)
};
代码如下:
//Screen space rasterization
//!这里传进去的参数有两个!第一个参数是屏幕空间下的三角形;第二个参数是原模型每个三角形通过视图变换后得到的三角形顶点,是视图坐标(viewspace)下的坐标
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
//构建bounding box
// 这里跟作业2里的深度插值差不多,v.w()就是该顶点深度值,用Z和zp代替w_reciprocal和z_interpolated
auto v = t.toVector4();//将三角形t中的顶点数组v变成四维的向量
//这里是求包围盒,求出能包围屏幕三角形的最小长方形四个顶点的数值
int min_x = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
int min_y = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
int max_x = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
int max_y = std::max(std::max(v[0].y(), v[1].y()), v[2].y());
//遍历包围盒中的像素
for (int x = min_x; x <= max_x; x++) {
for (int y = min_y; y <= max_y; y++) {
//判断像素中心点是否在三角形内
if (insideTriangle(x + 0.5, y + 0.5, t.v))//传入的是像素中心点的坐标以及三角形三个顶点的坐标数组
{
auto [alpha, beta, gamma] = computeBarycentric2D(x + 0.5, y + 0.5, t.v);//为了获得该点的z值,得到(x,y,z),
//用auto [alpha, beta, gamma]来接受每个像素中心点的重心坐标
// 此时t.v中存储的是此时屏幕空间三角形的顶点坐标
//深度插值是插进去新的深度,原来其实已经有对应像素的深度值了
//进行深度插值之前,要对重心坐标进行透视矫正,即是在将二维像素中心点对应的重心坐标转化成三维的重心坐标
//v[n].w()就是该屏幕三角形顶点对应的viewspace对应的顶点深度值!!已经是有定义了的
//修正得到viewspace(camera space)中的情况
float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
zp *= Z;//求出像素中心在屏幕三角形上的点对应的viewspace的三角形上的点的深度
auto cur_index = get_index(x, y);//获取像素所在的数组位置
//遍历每一个小三角形,都会比较一下当前像素中心在屏幕三角形上的点对应的viewspace的三角形上的点的深度和当前最小的深度值
if (zp < depth_buf[cur_index] && cur_index < width * height)// cur_index < width * height) 是越界判断!!要加上!!
// depth_buf[cur_index]是深度数组,存储着像素中心对应的世界坐标的当前深度
//此时是判断要插入的深度值是否比现已存在的深度要小,小的话,深度就更新为刚插进去的新的深度
{
depth_buf[cur_index] = zp;
// TODO: Interpolate the attributes:
// auto interpolated_color 颜色
// auto interpolated_normal 法向量
// auto interpolated_texcoords 纹理坐标
// auto interpolated_shadingcoords camera space的像素位置,为了求r和向量l
//这里的w取值都是1,没有做透视矫正,原本alpha、 beta、 gamma应该先矫正再带进去,但这里差距不大,就直接用alpha、 beta、 gamma了
auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1);//按权重算出当前在三角形内的像素的中心点颜色值,color用于漫反射光照
auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1);//按权重算出当前在三角形内的像素的中心点法向量值,normal用于确定色彩(注意:此时法线是viewspace空间的)
auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1);//按权重算出当前在三角形内的像素的中心点纹理坐标值,texcoords用于确定纹理坐标
auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1);//按权重算出当前像素着色点坐标值,shadingcoords用于确定viewspace位置,用于光照
// Use: fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
// Use: payload.view_pos = interpolated_shadingcoords;
// Use: Instead of passing the triangle's color directly to the frame buffer, pass the color to the shaders first to get the final color;
// Use: auto pixel_color = fragment_shader(payload);
//此时已经将viewspace空间下的小三角形各个属性插值好了,之后就进行选择性的着色了
//将参数传入片元着色模块,用来得到不同的着色方案
fragment_shader_payload payload(interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;//更新着色模块中的着色点坐标,这个着色点坐标就是小三角形片上近似于像素中心对应的viewspace的三角形点(没有进行透视矫正,所以是近似)
auto pixel_color = fragment_shader(payload);//将着色模块传入shader计算出最后的像素颜色
//作业2中的set_pixel输入的是vector3f点坐标,这次作业的是vector2i坐标,因此直接输入x,y坐标即可
Vector2i vertex;
vertex << x, y;
set_pixel(vertex, pixel_color);//进行像素的着色(将着色结果通过像素展示出来)
}
}
}
}
}
(操作对象是传进来的屏幕三角形)
3.3.1.首先将储存三角形顶点的坐标数组v[]从三维坐标变为齐次坐标(四维坐标形式),然后构建包围盒子,这里是一个长方形(能包围传入的屏幕三角形(实际传入的只是顶点)的最小长方形),减少遍历的像素个数;
3.1.2.进行包围盒子的遍历,先判断当前像素的中心是否在屏幕空间三角形内,如果结果为true,则通过重心坐标(求出alpha, beta, gamma三个系数)和透视矫正(求出像素中心对应的屏幕空间三角形位置,并对其对应的世界坐标空间的坐标)进行深度插值;
深度插值的时候,要判断要当前插入的深度值是否比现已存在的深度要小,小的话,深度就更新为刚插进去的新的深度;
ps:深度插值的方法(包括透视矫正) : https://zhuanlan.zhihu.com/p/380589244utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1573365895249731584&utm_source=qq
3.1.3.深度插值结束之后,通过alpha, beta, gamma代表的权重分别计算出当前像素的color、normal、texcoords、shadingcoords的值,实现颜色、法向量、纹理坐标、着色点坐标的插值,
再将这些值传入着色模块,同时更新着色模块的着色点坐标值,之后将整个着色模块进行颜色计算并进行最终的像素着色。
3.2.1首先是插值的作用,因为光栅化只是为了将经过MVP变换、视图变换到比较恰当屏幕位置后的模型的二维图像展示出来(个人理解是此时的二维图像在屏幕的位置,但没有光栅化我们看不到它),在展示二维图像的过程中,我们只能将同一z轴下深度最小的模型位置信息显示出来,深度插值是通过重心坐标和已知的viewspace空间下的三角形三个顶点深度来操作,算出深度值,并和深度数组比较,最后更新出最新的深度数组。深度值可以用来判断遮挡的情况。
3.2.2通过插值得到三角形的各个属性,并用其初始化fragment_shader_payload结构的对象payload,fragment_shader_payload为Shader中定义的数据结构类型,payload为对象名,此对象用来记录当前像素的view_pos,颜色、法向量、纹理图中对应的坐标等。
而最后用来获取颜色的fragment_shader函数是先前在主函数通过“函数赋值”得到的着色函数,根据命令不同fragment_shader意味着不同的着色方式(即主函数中的normal、phong、texture、bump等)。
3.3.3在做笔记的时候发现,其实w是默认为1.0的,并不是表示真正的深度值。
3.3.4也可以采用下面修改的MSAA抗锯齿代码(但个人效果感觉不是很明显,可能要再加采样点):
void rst::rasterizer::rasterize_triangle_MSAA(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{
auto v = t.toVector4();
//INT_MAX是最大整数,INT_MIN是最小整数
// TODO : Find out the bounding box of current triangle.//第一步要找到包含三角形的最小矩形,使遍历的像素少一点
int min_x, max_x, min_y, max_y;//定义bounding box的四个角的坐标
float alpha, beta, gamma;//为下面做深度插值定义float变量
// 接下来要找到bounding box的边界坐标
min_x = min(t.v[0].x(), min(t.v[1].x(), t.v[2].x()));
max_x = max(t.v[0].x(), max(t.v[1].x(), t.v[2].x()));
min_y = min(t.v[0].y(), min(t.v[1].y(), t.v[2].y()));
max_y = max(t.v[0].y(), max(t.v[1].y(), t.v[2].y()));
// iterate through the pixel and find if the current pixel is inside the triangle
//逐个遍历像素。判断每个像素中心是否在三角形内,这里使用了MSAA采样
std::vector<Eigen::Vector2f> super_sample_step//定义的超采样数组,使用了类模板来定义
{
{0.125,0.125},{0.125, 0.375},{0.125, 0.625},{0.125,0.875},
{0.375,0.125},{0.375, 0.375},{0.375, 0.625},{0.375,0.875},
{0.625,0.125},{0.625, 0.375},{0.625, 0.625},{0.625,0.875},
{0.875,0.125},{0.875, 0.375},{0.875, 0.625},{0.875,0.875},
};
for (int x = min_x; x < max_x; x++)
{
for (int y = min_y; y < max_y; y++)
{
int judge = 0;//判断是否通过了深度测试,只要一个采样点通过就进行着色
//判断像素中心点是否在三角形内,如果是,就尝试对该像素进行深度测试,如果通过再进行着色,并同时将深度传入缓存
float z_interpolated;
Vector3f pixel_color;
//开始进行采样点遍历
for (int count_i = 0; count_i < 16; count_i++)
{
if (insideTriangle(x + super_sample_step[count_i][0], y + super_sample_step[count_i][1], t.v))//insideTriangle()函数传入的是float原因就是这里找中心点是浮点类型的
{
if (super_depth_buf[get_index_super_frame(x * 4 + count_i % 4, y * 4 + count_i / 4)] > z_interpolated)//深度缓存和着色步骤
{//如果此时的超采样深度缓存中的深度比该点的深度要大,就将缓存数值变成当前的最小值
judge = 1;
//这里根据count_i的不同,选择四个采样点
float z_interploated_super_sample;
super_depth_buf[get_index_super_frame(x * 4 + count_i % 4, y * 4 + count_i / 4)] = interpolate_depth(x + super_sample_step[count_i][0], y + super_sample_step[count_i][1], t, z_interploated_super_sample);//深度存入缓存
//此时将采样点传入超采样深度数组
super_frame_buf[get_index_super_frame(x * 4 + count_i % 4, y * 4 + count_i / 4)] = interpolate_depth_color(x + super_sample_step[count_i][0], y + super_sample_step[count_i][1], t);//颜色存入缓存
//此时将采样点传入超采样颜色数组
}
}
}//一个像素所有采样点判断完就进行着色
if (insideTriangle(x + 0.5, y + 0.5, t.v))//传入的是像素中心点的坐标以及三角形三个顶点的坐标数组
{
auto [alpha, beta, gamma] = computeBarycentric2D(x + 0.5, y + 0.5, t.v);//为了获得该点的z值,得到(x,y,z),
//用auto [alpha, beta, gamma]来接受每个像素中心点的重心坐标
// 此时t.v中存储的是此时屏幕空间三角形的顶点坐标
//深度插值是插进去新的深度,原来其实已经有对应像素的深度值了
//进行深度插值之前,要对重心坐标进行透视矫正,即是在将二维像素中心点对应的重心坐标转化成三维的重心坐标
//v[n].w()就是该屏幕三角形顶点对应的viewspace对应的顶点深度值!!已经是有定义了的
//修正得到viewspace(camera space)中的情况
float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
zp *= Z;//求出像素中心在屏幕三角形上的点对应的viewspace的三角形上的点的深度
auto cur_index = get_index(x, y);//获取像素所在的数组位置
//遍历每一个小三角形,都会比较一下当前像素中心在屏幕三角形上的点对应的viewspace的三角形上的点的深度和当前最小的深度值
if (zp < depth_buf[cur_index] && cur_index < width * height)// cur_index < width * height) 是越界判断!!要加上!!
// depth_buf[cur_index]是深度数组,存储着像素中心对应的世界坐标的当前深度
//此时是判断要插入的深度值是否比现已存在的深度要小,小的话,深度就更新为刚插进去的新的深度
{
depth_buf[cur_index] = zp;
// TODO: Interpolate the attributes:
// auto interpolated_color 颜色
// auto interpolated_normal 法向量
// auto interpolated_texcoords 纹理坐标
// auto interpolated_shadingcoords camera space的像素位置,为了求r和向量l
//这里的w取值都是1,没有做透视矫正,原本alpha、 beta、 gamma应该先矫正再带进去,但这里差距不大,就直接用alpha、 beta、 gamma了
auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1);//按权重算出当前在三角形内的像素的中心点颜色值,color用于漫反射光照
auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1);//按权重算出当前在三角形内的像素的中心点法向量值,normal用于确定色彩(注意:此时法线是viewspace空间的)
auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1);//按权重算出当前在三角形内的像素的中心点纹理坐标值,texcoords用于确定纹理坐标
auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1);//按权重算出当前像素着色点坐标值,shadingcoords用于确定viewspace位置,用于光照
// Use: fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
// Use: payload.view_pos = interpolated_shadingcoords;
// Use: Instead of passing the triangle's color directly to the frame buffer, pass the color to the shaders first to get the final color;
// Use: auto pixel_color = fragment_shader(payload);
//此时已经将viewspace空间下的小三角形各个属性插值好了,之后就进行选择性的着色了
//将参数传入片元着色模块,用来得到不同的着色方案
fragment_shader_payload payload(interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
payload.view_pos = interpolated_shadingcoords;//更新着色模块中的着色点坐标,这个着色点坐标就是小三角形片上近似于像素中心对应的viewspace的三角形点(没有进行透视矫正,所以是近似)
pixel_color = fragment_shader(payload);//将着色模块传入shader计算出最后的像素颜色
}
}
if (judge > 0)//只要有一个采样点在三角形内就进行着色
{
//接下来进行着色处理
Vector3f point((float)x, (float)y, z_interpolated);//此时的点坐标
Vector3f pixel_color_average1 = (super_frame_buf[get_index_super_frame(x * 4, y * 4)] + super_frame_buf[get_index_super_frame(x * 4, y * 4 + 1)] + super_frame_buf[get_index_super_frame(x * 4, y * 4 + 2)] + super_frame_buf[get_index_super_frame(x * 4 + 1, y * 4 + 3)]);
Vector3f pixel_color_average2 = (super_frame_buf[get_index_super_frame(x * 4 + 1, y * 4)] + super_frame_buf[get_index_super_frame(x * 4 + 1, y * 4 + 1)] + super_frame_buf[get_index_super_frame(x * 4 + 1, y * 4 + 2)] + super_frame_buf[get_index_super_frame(x * 4 + 1, y * 4 + 3)]);
Vector3f pixel_color_average3 = (super_frame_buf[get_index_super_frame(x * 4 + 2, y * 4)] + super_frame_buf[get_index_super_frame(x * 4 + 2, y * 4 + 1)] + super_frame_buf[get_index_super_frame(x * 4 + 2, y * 4 + 2)] + super_frame_buf[get_index_super_frame(x * 4 + 2, y * 4 + 3)]);
Vector3f pixel_color_average4 = (super_frame_buf[get_index_super_frame(x * 4 + 3, y * 4)] + super_frame_buf[get_index_super_frame(x * 4 + 3, y * 4 + 1)] + super_frame_buf[get_index_super_frame(x * 4 + 3, y * 4 + 2)] + super_frame_buf[get_index_super_frame(x * 4 + 3, y * 4 + 3)]);
Vector3f pixel_color_average = (pixel_color_average1 + pixel_color_average2 + pixel_color_average3 + pixel_color_average4) / 16;
//着色,将一个像素四个采样点的颜色数据相加后除以4就得到最平均的颜色,使过渡就比较自然
Vector2i vertex;
vertex << x, y;
set_pixel(vertex,pixel_color_average.cwiseProduct(pixel_color));//进行像素的着色(将着色结果通过像素展示出来)
}
}
}
}
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});
std::fill(super_frame_buf.begin(), super_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());
std::fill(super_depth_buf.begin(), super_depth_buf.end(), Eigen::Vector3f{ 0,0,0 });
}
}
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
frame_buf.resize(w * h);
depth_buf.resize(w * h);
super_frame_buf.resize(16 * w * h);
super_depth_buf.resize(16 * w * h);
texture = std::nullopt;
}
int rst::rasterizer::get_index_super_frame(int x, int y)
{
return (4 * height - 1 - y) * width * 4 + x;
}
首先安利一篇关于表面着色的文章:https://mbd.baidu.com/ug_share/mbox/4a83aa9e65/shareproduct=smartapp&tk=5585206cf583e1b909bd15a319cdc871&share_url=https%3A%2F%2Fwjrsbu.smartapps.cn%2Fzhihu%2Farticle%3Fid%3D267059125%26isShared%3D1%26_swebfr%3D1%26_swebFromHost%3Dbaiduboxapp&domain=mbd.baidu.com
代码如下:
Eigen::Vector3f normal_fragment_shader(const fragment_shader_payload& payload)//传入片元着色模块
{
Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() + Eigen::Vector3f(1.0f, 1.0f, 1.0f)) / 2.f;
Eigen::Vector3f result;
result << return_color.x() * 255, return_color.y() * 255, return_color.z() * 255;
return result;
}
1.1.1对于normal法线着色,虽然这个着色结果是各处地方颜色不同的小牛,但实际是由法线的变化形成的,并不涉及phong系统下的光照。
1.1.2首先取出当前待着色像素点的法向量的x,y,z坐标并归一化,故此时x,y,z都在[-1,1]之间,加上(1.0f, 1.0f, 1.0f)后,变为[0,2],再除以2,即得[0,1],再分别乘以255即可得到各个颜色值了。(可以自己修改着色x,y,z方向的RGB值,得到不同的着色效果)。
代码如下:
//Phone Shading(冯氏着色) 以片元为单位进行着色,对每个点计算一次光照,点的法向量是通过顶点法向量插值得到的。
//冯氏着色最接近现实,可以在减少三角面数的情况下达到相同的效果(插值后法向量会光滑变化),当然,性能开销也非常大。
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);//ka:环境光强强度系数
Eigen::Vector3f kd = payload.color;//kd:漫反射系数
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);//ks:镜面反射高光系数
//light为数据类型,记录了灯光位置和光照强度
//struct light
//{
//Eigen::Vector3f position;
//Eigen::Vector3f intensity;
//};
auto l1 = light{ {20, 20, 20}, {500, 500, 500} };
auto l2 = light{ {-20, 20, 0}, {500, 500, 500} };
std::vector<light> lights = { l1, l2 };
//环境光强度
Eigen::Vector3f amb_light_intensity{ 10, 10, 10 };
//人眼位置
Eigen::Vector3f eye_pos{ 0, 0, 10 };
float p = 150;
Eigen::Vector3f color = payload.color;
//这里就凸显view_pos的作用,光线作用的点
Eigen::Vector3f point = payload.view_pos;
//此时片元的法线
Eigen::Vector3f normal = payload.normal;
//result_color是不同光线累加作用的结果,这里先初始化为{0,0,0}
Eigen::Vector3f result_color = { 0, 0, 0 };
//这里遍历了vector lights,要计算每一束光对物体的作用
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
//光照的方向
Eigen::Vector3f light_direct = light.position - point;//光照方向是从物体着色点指向灯光
//视线方向
Eigen::Vector3f view_direct = eye_pos - point;//视线方向是从物体指向人眼
//着色点和灯光的距离r
float r = sqrt(light_direct.dot(light_direct));
//环境光照ambient
Eigen::Vector3f la = ka.cwiseProduct(amb_light_intensity);//下面一样,因为都是用三维向量表示(结果也是),所以用cwiseProduct,这里是x,y,z分别相乘得到新的x,y,z
//漫反射diffuse
Eigen::Vector3f ld = kd.cwiseProduct(light.intensity / pow(r, 2)) ;
ld *= std::max(0.0f, normal.normalized().dot(light_direct.normalized()));
//镜面反射高光specular
Eigen::Vector3f ls = ks.cwiseProduct(light.intensity / pow(r, 2));
//求半程向量
Eigen::Vector3f h = (light_direct.normalized() + view_direct.normalized()).normalized();//此时半程向量已经是单位向量
float max_p =pow(max(0.0f, normal.normalized().dot(h)),p);//p系数前面已经给了,可以自行修改p来观察效果
ls *= max_p;
//最后将环境光照结果、漫反射结果、高光结果加在一起
result_color += la + ld + ls;//如果有多束光,会不断叠加效果,输出最后的着色
}
return result_color * 255.f;
}
2.1.1这里漫反射系数直接是用之前插值的颜色值,其实可用光谱来准确描述某一点的颜色值,但开销太大,就简化为RGB值。
ps:相关模型可以看之前提到的表面着色的文章,也可以看这个笔记:https://zhuanlan.zhihu.com/p/144331612
2.1.2相关的phone模型,即为下图公式,具体推导可参照前面提及的文章
2.1.3通过调节光照强度参数可以实现不同效果,但最好不要调的太大,不然结果会是下面第二个图所示的效果。
代码如下:
Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f return_color = { 0, 0, 0 };//初始化返回颜色
if (payload.texture)
{
// TODO: Get the texture value at the texture coordinates of the current fragment
return_color = payload.texture->getColor(payload.tex_coords.x(),payload.tex_coords.y());//获取纹理的颜色,getColor函数返回的是三维向量的颜色值
}//payload.tex_coords.x()是着色片元中纹理坐标的u轴值,payload.tex_coords.x()是着色片元中纹理坐标的v轴值;
Eigen::Vector3f texture_color;//定义一个纹理颜色对象
texture_color << return_color.x(), return_color.y(), return_color.z();//初始化
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);//ka:环境光强强度系数
Eigen::Vector3f kd = texture_color/255.f;//kd:漫反射系数为纹理颜色归一化的RGB值,下面都是对单位向量的颜色RGB进行处理,在结尾的时候会乘上255.f进行颜色修正
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);//ks:镜面反射高光系数
//light为数据类型,记录了灯光位置和光照强度
//struct light
//{
//Eigen::Vector3f position;
//Eigen::Vector3f intensity;
//};
auto l1 = light{ {20, 20, 20}, {500, 500, 500} };
auto l2 = light{ {-20, 20, 0}, {500, 500, 500} };
std::vector<light> lights = { l1, l2 };
//环境光强度
Eigen::Vector3f amb_light_intensity{ 10, 10, 10 };
//人眼位置
Eigen::Vector3f eye_pos{ 0, 0, 10 };
float p = 150;
Eigen::Vector3f color = payload.color;
//这里就凸显view_pos的作用,光线作用的点
Eigen::Vector3f point = payload.view_pos;
//此时片元的法线
Eigen::Vector3f normal = payload.normal;
//result_color是不同光线累加作用的结果,这里先初始化为{0,0,0}
Eigen::Vector3f result_color = { 0, 0, 0 };
//这里遍历了vector lights,要计算每一束光对物体的作用
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
//光照的方向
Eigen::Vector3f light_direct = light.position - point;//光照方向是从物体着色点指向灯光
//视线方向
Eigen::Vector3f view_direct = eye_pos - point;//视线方向是从物体指向人眼
//着色点和灯光的距离r
float r = sqrt(light_direct.dot(light_direct));
//环境光照ambient
Eigen::Vector3f la = ka.cwiseProduct(amb_light_intensity);//下面一样,因为都是用三维向量表示(结果也是),所以用cwiseProduct,这里是x,y,z分别相乘得到新的x,y,z
//漫反射diffuse
Eigen::Vector3f ld = kd.cwiseProduct(light.intensity / pow(r, 2));
ld *= std::max(0.0f, normal.normalized().dot(light_direct.normalized()));
//镜面反射高光specular
Eigen::Vector3f ls = ks.cwiseProduct(light.intensity / pow(r, 2));
//求半程向量
Eigen::Vector3f h = (light_direct.normalized() + view_direct.normalized()).normalized();//此时半程向量已经是单位向量
float max_p = pow(max(0.0f, normal.normalized().dot(h)), p);//p系数前面已经给了,可以自行修改p来观察效果
ls *= max_p;
//最后将环境光照结果、漫反射结果、高光结果加在一起
result_color += la + ld + ls;//如果有多束光,会不断叠加效果,输出最后的着色
}
return result_color * 255.f;
}
3.1.1注意之前phone模型的漫反射反射的颜色向量是插值的颜色,但这里用了贴图之后反射的是贴图对应贴图坐标的颜色。
3.1.2贴图使用后,也是用了phone模型进行光照的着色。
3.1.3结果会显示出如下的小牛,也可以换不同的贴图,尝试一下不同的效果。
代码如下:
//关于凹凸贴图、切线空间相关知识可以参考冯乐乐大佬的Unity shader入门精要
Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{ {20, 20, 20}, {500, 500, 500} };
auto l2 = light{ {-20, 20, 0}, {500, 500, 500} };
std::vector<light> lights = { l1, l2 };
Eigen::Vector3f amb_light_intensity{ 10, 10, 10 };
Eigen::Vector3f eye_pos{ 0, 0, 10 };
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;
// TODO: Implement bump mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Normal n = normalize(TBN * ln)
float x = normal.x();
float y = normal.y();
float z = normal.z();//x,y,z代表的是世界空间坐标下片元的法向量n的x、y、z方向
Eigen::Matrix3f t { x * y / sqrt(x * x + z * z),sqrt(x * x + z * z),z * y / sqrt(x * x + z * z) };//t是切线空间的x方向在世界空间下的表示
Eigen::Matrix3f b = normal.cross(t);//b是切线空间的y方向在世界坐标下的表示,可由法线(法线normal在切线空间下的方向是z轴的方向)和t的叉乘得出来,注意叉乘时两变量的先后顺序,由右手螺旋判断
//normal是在世界坐标下切线空间z坐标的表示
Eigen::Matrix3f TBN = Eigen::Matrix3f::Identity();
//TBN是切线空间变换到世界空间的矩阵,注意空间定义的变换方向
TBN << t.x(), b.x(), normal.x(),
t.y(), b.y(), normal.y(),
t.z(), b.z(), normal.z();
float u = payload.tex_coords.x();//这个变量就是对应贴图的uv中的u方向的值
float v = payload.tex_coords.y();
float w = payload.texture->width;//纹理的宽度
float h = payload.texture->height;//纹理的长度
//纹理图上的颜色值本身就是存储的矢量信息,是用矢量来保存的RGB值
float dU = kh * kn * (payload.texture->getColor(u + 1.0f/w, v).norm() - payload.texture->getColor(u, v).norm());//1.0f/w这里表示的是u方向的颜色变化的一个单位量
float dV = kh * kn * (payload.texture->getColor(u, v + 1.0f/h).norm() - payload.texture->getColor(u, v).norm());//1.0f/h这里表示的是v方向的颜色变化的一个单位量
Eigen::Vector3f ln{ -dU, -dV, 1.0f };//ln为扰动之后的法线,此时法线是切线空间中的表示!!
normal = TBN * ln;//此时法线已经变换成[0,1]分量范围了
Eigen::Vector3f result_color = normal.normalized();//归一化
return result_color * 255.f;
}
4.1关于凹凸贴图、切线空间相关知识可以参考冯乐乐大佬的Unity shader入门精要;
4.2凹凸贴图的部分计算
4.3对于代码中
float dU = kh * kn * (payload.texture->getColor(u + 1.0f/w, v).norm() - payload.texture->getColor(u,v).norm());
float dV = kh * kn * (payload.texture->getColor(u, v + 1.0f/h).norm() - payload.texture->getColor(u, v).norm());
这两行的一些分析:
注意颜色是一个三维向量,(拿u方向进行说明)第一行是表示在u方向上改变一个单位长度,计算两个点保存的颜色向量的模的差值再乘以很小的系数(表示很小的变化量),用此来描述u方向上的单位颜色变化,而后面求出来的ln就是新法线的方向,(法线的方向,其实对应着RGB三个数值,一定意义上就是颜色的矢量信息,其实在重心坐标插值就可以看出来),所以法线扰动的实现其实靠的是不同位置颜色的变化,因为颜色就是一个可用来计算的矢量。
4.4对于新法线ln,其实已经是某一点切线空间的一个扰动后的法线。
代码如下:
Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
Eigen::Vector3f kd = payload.color;
Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{ {20, 20, 20}, {500, 500, 500} };
auto l2 = light{ {-20, 20, 0}, {500, 500, 500} };
std::vector<light> lights = { l1, l2 };
Eigen::Vector3f amb_light_intensity{ 10, 10, 10 };
Eigen::Vector3f eye_pos{ 0, 0, 10 };
float p = 150;
Eigen::Vector3f color = payload.color;
Eigen::Vector3f point = payload.view_pos;
Eigen::Vector3f normal = payload.normal;
float kh = 0.2, kn = 0.1;
// TODO: Implement displacement mapping here
// Let n = normal = (x, y, z)
// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
// Vector b = n cross product t
// Matrix TBN = [t b n]
float x = normal.x();
float y = normal.y();
float z = normal.z();
Eigen::Vector3f t{ x * y / std::sqrt(x * x + z * z), std::sqrt(x * x + z * z), z * y / std::sqrt(x * x + z * z) };
Eigen::Vector3f b = normal.cross(t);
Eigen::Matrix3f TBN;
TBN << t.x(), b.x(), normal.x(),
t.y(), b.y(), normal.y(),
t.z(), b.z(), normal.z();
// dU = kh * kn * (h(u+1/w,v)-h(u,v))
// dV = kh * kn * (h(u,v+1/h)-h(u,v))
// Vector ln = (-dU, -dV, 1)
// Position p = p + kn * n * h(u,v)
// Normal n = normalize(TBN * ln)
float u = payload.tex_coords.x();
float v = payload.tex_coords.y();
float w = payload.texture->width;
float h = payload.texture->height;
float dU = kh * kn * (payload.texture->getColor(u + 1.0f / w, v).norm() - payload.texture->getColor(u, v).norm());
float dV = kh * kn * (payload.texture->getColor(u, v + 1.0f / h).norm() - payload.texture->getColor(u, v).norm());
Eigen::Vector3f ln{ -dU,-dV,1.0f };
point += (kn * normal * payload.texture->getColor(u, v).norm());//直接改变了顶点坐标的高度
normal = TBN * ln;
normal = normal.normalized();
Eigen::Vector3f result_color = { 0, 0, 0 };
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
Eigen::Vector3f light_direct = light.position - point;
Eigen::Vector3f view_direct = eye_pos - point;
float r = light_direct.dot(light_direct);
// ambient
Eigen::Vector3f La = ka.cwiseProduct(amb_light_intensity);
// diffuse
Eigen::Vector3f Ld = kd.cwiseProduct(light.intensity / r);
Ld *= std::max(0.0f, normal.dot(light_direct.normalized()));
// specular
Eigen::Vector3f h = (light_direct.normalized() + view_direct.normalized()).normalized();
Eigen::Vector3f Ls = ks.cwiseProduct(light.intensity / r);
Ls *= std::pow(std::max(0.0f, normal.dot(h)), p);
result_color += (La + Ld + Ls);
}
return result_color * 255.f;
}
代码如下:
//双线性插值
Eigen::Vector3f getColorBilinear(float u, float v)//首先是传入纹理贴图的u、v坐标,这个坐标不是纹理映射后的,就是最原始的texture上的单位坐标
{
if (u < 0) u = 0.0f;
if (u > 1) u = 0.999f;
if (v < 0) u = 0.0f;
if (v > 1) u = 0.999f;
//先定义定位的纹理位置和周围的三个纹理块的位置(一共四个纹理位置)
//下面进行四个周围方块的坐标定义
//左下角的纹理映射块坐标
int w1 = u * width; int h1 = v * height;//这里的w1和h1是纹理单位坐标映射到屏幕坐标后的单位,如果要进行纹理颜色坐标对应颜色提取的话,还要/width(/height)
//这里要转成int类型,是为了找出双线性插值的点(float类型-int类型)
//右下角的纹理映射块坐标
float w2 = w1 + 1; float h2 = h1;
//左上角的纹理映射块坐标
float w3 = w1; float h3 = h1 + 1;
//右上角的纹理映射块坐标
float w4 = w1 + 1; float h4 = h1 + 1;
float s = u * width - w1;
float t = v * height - h1;
//下面进行颜色的双线性插值
Eigen::Vector3f color1, color2, color3, color4, color_interpolation_1_1, color_interpolation_1_2,color_interpolation_2, color;//定义颜色变量
color1 = getColor((float)w1 /(float) width, (float)h1 / (float)height);//获取左下角纹理块的颜色
color2 = getColor(w2 / width, h2 / height);//获取右下角纹理块的颜色
color3 = getColor(w3 / width, h3 / height);//获取左下角纹理块的颜色
color4 = getColor(w4 / width, h4 / height);//获取左下角纹理块的颜色
//先进行横着的两次插值
//获取第一次线性插值的颜色(下面两块纹理块)
color_interpolation_1_1 = lerp(color1,color2,s);
//获取第一次线性插值的颜色(上面两块纹理块)
color_interpolation_1_2 = lerp(color3, color4,s);
//获取第二次线性插值的颜色(竖直的一次线性插值)
color_interpolation_2 = lerp(color_interpolation_1_1, color_interpolation_1_2,t);
return Eigen::Vector3f(color_interpolation_2[0],color_interpolation_2[1],color_interpolation_2[2] );
}
};
以下用压缩的100*100的纹理贴图进行试验:
双线性插值主要是用于纹理贴图对应颜色的选取,一般来说,纹理映射的时候会有多个纹理坐标取同一个纹理单位的颜色,会使得颜色过渡不够平滑,双线性插值使纹理坐标可以提取到精确点的颜色,使颜色过渡更好。
一般来说,使用的定位系数是通过传入的float值减去int值得到0
双线性插值前:
双线性插值后:
很明显,双线性插值之后,颜色过渡要平滑很多了。