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片元缓冲操作:根据深度信息,确定遮挡和可视情况

接下来开始进行渲染管线的具体分析;

1.main函数

int main(int argc, const char** argv)
{
    //用来记录组成三维图形的所有的小三角形
    //TriangletList是一个包含所有要进行操作的三角形(三个顶点)的数组;
    std::vector<Triangle*> TriangleList;

    float angle = 140.0;
    bool command_line = false;

    std::string filename = "output.png";
    objl::Loader Loader;//objl::Loader是一个obj文件格式的加载器,此处赋予变量Loader为加载objl图片格式;
    std::string obj_path = "E:/Games101作业/GAMES101/GAMES101_作业/作业3/Assignment3/Code/models/spot";//记录传入的文件路径,传入到变量obj_path中;
    //此处要添加上对应obj格式文件的路径;
    // Load .obj File,加载模型;
    //loadout返回一个结果,如果找不到模型则返回false,否则为true;
    bool loadout = Loader.LoadFile("E:/Games101作业/GAMES101/GAMES101_作业/作业3/Assignment3/Code/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类的指针去接受新开辟的构造函数(此处无参构造,是初始化顶点)地址,记录三角形信息
            for(int j=0;j<3;j++)//对三个顶点进行循环,分别对顶点的顶点信息(包括顶点坐标,顶点法线,顶点对应的纹理坐标等顶点信息)进行赋值
            {
                //在新的初始化Triangle中分别添加三角形顶点位置信息,三角形顶点法线信息,以及三角形顶点对应的纹理坐标;
                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)用法;
            TriangleList.push_back(t);
        }
    }
    //初始化光栅化的对象,这里定义了屏幕的宽高;
    rst::rasterizer r(700, 700);
    //记录纹理到光栅化对象上;
    auto texture_path = "hmap.jpg";//此处为纹理的路径;
    r.set_texture(Texture(obj_path + texture_path));

    //记录片元处理的方式,这里默认为phong的处理方式
    //类模板function<>包装函数对象:返回值类型是Vector3f,传入参数类型是fragment_shader_payload
    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;//将着色方案active_shader改为texture_shader;
            texture_path = "spot_texture.png";//将纹理贴图的路径改为"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;//此处用normal着色模型;
        }
        else if (argc == 3 && std::string(argv[2]) == "phong")
        {
            std::cout << "Rasterizing using the phong shader\n";
            active_shader = phong_fragment_shader;//采用phone着色模型;
        }
        else if (argc == 3 && std::string(argv[2]) == "bump")
        {
            std::cout << "Rasterizing using the bump shader\n";
            active_shader = bump_fragment_shader;//采用bump着色模型;
        }
        else if (argc == 3 && std::string(argv[2]) == "displacement")
        {
            std::cout << "Rasterizing using the bump shader\n";
            active_shader = displacement_fragment_shader;//采用displacement着色模型;
        }
    }
    //设置人眼的位置;
    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));
        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 != 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.1main.cpp功能:

1.1.1实现了将顶点以三角形的方式每三个记录在一起,下图是Triangle类的一些属性,可以发现这一个类里装了三个顶点的顶点坐标、颜色、对应纹理坐标、法线。

class Triangle{

public:
    Vector4f 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= nullptr;
    Triangle();

    Eigen::Vector4f a() const { return v[0]; }
    Eigen::Vector4f b() const { return v[1]; }
    Eigen::Vector4f c() const { return v[2]; }

    void setVertex(int ind, Vector4f 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 setNormals(const std::array<Vector3f, 3>& normals);
    void setColors(const std::array<Vector3f, 3>& colors);
    void setTexCoord(int ind,Vector2f uv ); /*set i-th vertex texture coordinate*/
    std::array<Vector4f, 3> toVector4() const;
};

1.1.2设置MVP矩阵,将三维空间的点映射到屏幕空间上来;

1.1.3 根据调用命令的不同选择不同的光栅化着色方式,再进入draw函数将三角形画到屏幕上;

2.draw函数

//draw函数传入的是包含不同三角形的一个列表,注意类型为Triangle*;
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
        //的坐标,用于后续确定光源与物体表面的作用
        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在指定的范围内应用于给定的操作,并将结果存储在指定的另一个范围内。
        //要使用std::transform函数需要包含<algorithm>头文件。

        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]
        };
        //Homogeneous division
        //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视图空间下的各个顶点的法向量方向
        //注意此时的三角形每一个顶点存放的法线值为该顶点在viewspace即观测空间中对应顶点的法线值;
        Eigen::Matrix4f inv_trans = (view * model).inverse().transpose();//inverse()是逆矩阵,transpose()是转置矩阵
        Eigen::Vector4f n[] = {
                //此处要利用to_vec4函数将三维法线值转换成四维;
                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[]里面,同时将变化后的顶点坐标位置,以及经过MV变换的顶点法线信息传入newtri中,并传入设置的三个顶点的颜色,便于后面光栅化进行插值;
        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>());
        }
        //设置三个顶点的颜色;
        //setColor函数用于设置颜色,后面三个传入的参数为RGB;
        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
        //调用rasterizer_triangle,实现光栅化
        //注意此时仍在循环中,故实际是对每个小三角形分别进行光栅化
        //此时也传入了viewspace的顶点坐标,用于判断真实的光线作用
        // Also pass view space vertice position
        rasterize_triangle(newtri, viewspace_pos);
    }
}
2.1.1draw函数的作用:

2.1.1.1首先注意看到draw函数传入的是包含不同三角形的一个列表,其类型为Triangle*,该函数的主要作用是对每一个小三角形的顶点相关信息进行处理,处理之后再进入光栅化rasterize_triangle函数中对三角形进行光栅化(注意光栅化的过程仍然是在draw中对于每个三角形进行处理的循环内部);

2.1.1.2对仅仅进行了MV操作,还未进行透视投影,也就是观测空间viewspace中的三个顶点的坐标先保存在mm中,紧接着传入到之后的viewspace_pos空间中,用于后续确定光源与物体表面的作用;

2.1.1.3在将三角形顶点坐标(在viewspace中的)保存在mm中并随之保存在viewspace中后,再对三角形顶点通过MVP变换和其次化(x,y,z均除以w),此时并未处理齐次坐标的w维,因为经过了MVP变换后,w坐标记录了原本的z值,这用于后面进行深度插值(此处的w即为顶点的深度)!

2.1.1.4对于顶点的法向量(原本存的是再viewspace中的)则齐次化后(用to_vex4()函数进行变换)再传入到数组n中,其目的是为了后来光栅化过程中物体在光线作用下需要法线及位置信息;

2.1.1.5注意代码最初令newtri == *t,即利用newtri记录小三角形三个顶点的位置、法线、纹理、颜色,但后续修改了三角形顶点的位置,将其改成了经过MVP变换和视图变换后的(利用setVertex()函数),法线则将其改成了viewspace空间的(利用setNormal()函数)。同时,viewspace的位置坐标虽然没有记录在newtri中,但是以参数的形式传递给了后续的rasterizer_triangle函数。所以传入光栅化rasterize_triangle函数的其实是屏幕三角形以及viewspace的位置坐标。

!!注意重点分析根据原本的法向量求出在MV操作之后的法向量的方法:

//这里只是用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)
        };

首先提醒,我们需要计算的是viewspace下的法线,是仅进行了MV操作的,不是最初保存在t里的法向量,也不是进行了MVP变换的法向量。

假设,最初保存在三角形中的法向量为,切线为,经过MV操作后的法向量为,设MV变换矩阵为,则变换后切线为。现在需要计算的即为。因为Eigen中,代表矩阵乘法,需要满足前一个矩阵的列数等于后一个矩阵的行数,所以有以下公式:

3.rasterize_triangle函数实现的思路:

注意操作对象是传入的一个三角形:

1.首先将存储有三角形三个顶点坐标信息的一个数组从三维坐标变换成齐次坐标,2.然后根据三角形的顶点位置找到三角形的包围盒,接着对包围盒内部的所有像素进行遍历,如果遍历到某一像素的中心点位于三角形内部,则通过重心坐标以及透视矫正进行深度插值,如果插值后的结果小于已经存在的该店的插值,则对插值数组的对应位置进行更新,3.深度插值结束之后,再通过像素点的重心坐标[alpha,beta,gamma]求出该像素点的颜色,法向量,对应纹理坐标,着色点坐标的信息(即color,normal,texcoords、shadingcoords);4.最后,将上诉信息传入着色模块,并更新着色点的坐标值,最后将着色模块传入片元着色器frame_shader中得出像素最终的颜色。

三.着色模型

    • normal着色模型

1.1.对于normal法线着色,虽然这个着色结果是各处地方颜色不同的小牛,但实际是由法线的变化形成的,并不涉及phong系统下的光照。

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值,得到不同的着色效果)。

//这里要传入片元着色模块,payload为变量;
Eigen::Vector3f normal_fragment_shader(const fragment_shader_payload& payload)
{
    //此操作先对片元的法线进行归一化(由于法线此时都存在于[-1,1]空间内,要将法线变换到[0,1]内);
    //可以先分别加1变换到[0,2]空间内,再进行除2操作,即可得到在[0,1]内;
    Eigen::Vector3f return_color = (payload.normal.head<3>().normalized() + Eigen::Vector3f(1.0f, 1.0f, 1.0f)) / 2.f;
    //result用来承载最后的颜色值(分别乘255即可得到颜色值);
    //(可以自己修改着色x,y,z方向的RGB值,得到不同的着色效果)。
    Eigen::Vector3f result;
    result << return_color.x() * 255, return_color.y() * 255, return_color.z() * 255;
    return result;
}

结果如下:

    • Phone着色模型

(1.phone着色模型的漫反射系数是根直接用前面求出的片元的颜色。2.phone着色模型的具体公式如下,计算出环境光,漫反射以及高光的值后相加,再*255来获得每一个片元最终的颜色值)

//Phone着色模型,要传入的数据为着色模块payload.Phone着色模型对每一个片元进行着色,开销大,但效果接近真实,
//在对于每一个点的着色上,该点的法向量由三角形顶点法线经过插值形成;
Eigen::Vector3f phong_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);//高光强度系数;
    //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_point即为光线作用的点位置;
    Eigen::Vector3f point = payload.view_pos;
    //此处为片元的法线;
    Eigen::Vector3f normal = payload.normal;
    //(上述信息都在光栅化过程中传入到了着色模块中);
    Eigen::Vector3f result_color = {0, 0, 0};
    //result_color为不同光照的叠加,这里先进行初始化;
    //这里对所有光进行遍历,求出每一束光对物体的作用;
    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.
        // 要注意向量要化为单位向量
        //眼睛观察方向;
        auto v= eye_pos - point;
        //光的方向;
        auto l = light.position - point;
        //衰减因子r;
        auto r = l.dot(l);
        //半程向量h;
        auto h = (v + l).normalized();//对半程向量进行归一化;
        //环境光ambient
        //注意下面的计算和结果都是以三维向量的形式进行表示,因此用cwiseProduct;
        auto ambient = ka.cwiseProduct(amb_light_intensity);//由于都是用三维向量表示(结果也是),所以用cwiseProduct,这里是x,y,z分别相乘得到新的x,y,z
        //漫反射;
        auto diffuse = kd.cwiseProduct(light.intensity / r) * std::max(0.0f, normal.normalized().dot(l));
        //高光;
        auto specular = ks.cwiseProduct(light.intensity / r) * std::pow(std::max(0.0f, normal.normalized().dot(h)), p);
        //最后会将各种光进行相加
        result_color = result_color+(ambient + diffuse + specular);
    }
    //对所有光线进行遍历,会不断叠加效果,并输出最后的着色;
    return result_color * 255.f;
}

结果如下:

如果光照强度系数变大就会变成下面这样:

3.texture着色模型

先放代码:

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<light> 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;//如果有多束光,会不断叠加效果,输出最后的着色
    }
    //最后每个片元的result_color含有贴图的颜色,以及phone模型的各种光颜色;
    return result_color * 255.f;
}

(1.注意phone着色模型和texture着色模型的一个差别:在phone模型中漫反射的系数是根据重心坐标插值计算出来的颜色作为系数,而在texture着色模型中,漫反射的系数是由传入的片元模块在u,v坐标里对应的纹理的颜色;2.在对片元进行贴图操作之后,同样加上了phone光照模型,具体代码和上面的phone模型代码类似。)

结果如下:

加强了些高光,附上一直更好看的奶牛:

4.bump着色模型

//凹凸贴图(法线贴图);
//同样传入的是着色模块,对象是一个片元(存储像素信息的模块);
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;
    //kh*kn是影响系数(是常数,上面已经定义了值),表示纹理法线对真实物体的影响程度,和课上的c1c2是同一个东西

    // 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();
    Eigen::Vector3f n = normal;
    Eigen::Vector3f t = { x * y / sqrt(x * x + z * z),sqrt(x * x + z * z),z * y / sqrt(x * x + z * z) };//t是切线空间x方向的表示;
    Eigen::Vector3f b = n.cross(t);
    Eigen::Matrix3f TBN = Eigen::Matrix3f::Identity();//初始化一个三维矩阵;
    //TBN矩阵装的是三组切线空间的向量;
    TBN << t.x(), t.y(), t.z(),
        b.x(), b.y(), b.z(),
        n.x(), n.y(), n.z();
    //注意:所求得的在这个局部空间中的法向量通过TBN矩阵变换就可以得到原本世界空间所对应的法向量;
    float u = payload.tex_coords.x();//此处为对应点在贴图上的u坐标;
    float v = payload.tex_coords.y();//此处为对应点在贴图上的v坐标;
    float w = payload.texture->width;//纹理的宽度;
    float h = payload.texture->height;//纹理的高度;
    //[注意payload.texture->getColor函数的返回值是一个三维向量Vector3f,下面的操作中要记得求模norm()];
    //纹理图上的颜色值本身就是存储的矢量信息,是用矢量来保存的RGB值,h(u+1,v)-h(u,v)其实就是u坐标相邻两个点颜色信息求模的差
    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+1.0f/w而不是u+1.0f,下面会细说,先做一个拆分;
   
    /*一步步的拆分:
      1.kh*kn就不解释了,上面有说
      2.payload是所写函数输入的一个struct,这个struct包含了texture等信息,这个结构是在shader.hpp中定义的
      3.payload.texture ———— 取payload这个struct里的texture
        texture ———— 是Texture.hpp中定义的一个class,里面包含了纹理的宽高(width/height)和getColor()函数等信息
        payload.texture->getColor() ———— 访问到定义的这个getColor()函数
      4.为什么要用“u+1.0f/w”而不是直接“u+1”?我们仔细看Texture.hpp对getColor()的一段定义:
        ...
        auto u_img = u * width;
        auto v_img = (1 - v) * height;
        auto color = image_data.at<cv::Vec3b>(v_img, u_img);
        return Eigen::Vector3f(color[0], color[1], color[2]);
        ...
        这里的u v值都×了纹理对应的宽高,变换过来的话移动一个单位应该是“u*width+1”因此在我们的函数里1个单位对应的应该是1/width,1/h同理
      5.getColor().norm() ———— .norm()是Eigen库里定义的一个求范数的函数,就是求所有元素²的和再开方。
        向量的范数则表示的是原有集合的大小,范数的本质是距离,存在的意义是为了实现比较。
        这部分为什么要给个norm,我的理解是:getColor返回的是一个储存颜色值的向量:(color[0],color[1],color[2])对应的是RGB值
        dU和dV都是一个float值,并不是Vector,想要实现h()表示的实数高度值,就要用到norm.()将向量映射成实数(个人理解,不确定对不对)
      6.还需要注意,这里的dU和dV对应的是老师课上给的dp/du和dp/dp/dv
    */
    Eigen::Vector3f ln = { -dU, -dV, 1.0f };//在切线空间下经过凹凸变换的向量表达形式;
    normal = TBN * ln;//将normal修改成为从切线空间变换回到世界空间的法向量值;
    Eigen::Vector3f result_color = {0, 0, 0};
    result_color = normal.normalized();//进行归一化,转换到[0,1]内;
    
    return result_color * 255.f;//再乘上255即为对应的颜色值;
}

4.1bump着色模型的分析

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());

这两行的一些分析:

4.3.1注意颜色是一个三维向量,(拿u方向进行说明)第一行是表示在u方向上改变一个单位长度,计算两个点保存的颜色向量的模的差值再乘以很小的系数(表示很小的变化量),用此来描述u方向上的单位颜色变化,而后面求出来的ln就是新法线的方向,(法线的方向,其实对应着RGB三个数值,一定意义上就是颜色的矢量信息,其实在重心坐标插值就可以看出来),所以法线扰动的实现其实靠的是不同位置颜色的变化,因为颜色就是一个可用来计算的矢量。

4.3.2我们仔细看Texture.hpp对getColor()的一段定义:

 Eigen::Vector3f getColor(float u, float v)
    {
        // 坐标限定
        if (u < 0) u = 0;
        if (u > 1) u = 1;
        if (v < 0) v = 0;
        if (v > 1) v = 1;
        auto u_img = u * width;
        auto v_img = (1 - v) * height;
        auto color = image_data.at<cv::Vec3b>(v_img, u_img);
        return Eigen::Vector3f(color[0], color[1], color[2]);
    }

这里的u v值都×了纹理对应的宽高,变换过来的话移动一个单位应该是“u*width+1”因此在我们的函数里1个单位对应的应该是1/width,1/h同理。

.norm()

.norm()是Eigen库里定义的一个求范数的函数,就是求所有元素²的和再开方。向量的范数则表示的是原有集合的大小,范数的本质是距离,存在的意义是为了实现比较。

至于这部分为什么要给个norm,我的理解是:getColor返回的是一个储存颜色值的向量(color[0],color[1],color[2]),对应的是RGB值,dU和dV都是一个float值,并不是Vector3f,想要实现h()表示的实数高度值,就要用到norm.()将向量映射成实数.

4.4对于新法线ln,其实已经是某一点切线空间的一个扰动后的法线。

结果如下:

    • displacement着色模型

先放代码:

//displacement与bump的区别在于displacement会使得几何实际的形态发生改变;
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();//此处为对应点在纹理贴图上的u坐标;
    float v = payload.tex_coords.y();//此处为对应点在纹理贴图的v坐标;
    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 l = light.position - point;
        Eigen::Vector3f v = eye_pos - point;
        Eigen::Vector3f h = (v + l).normalized();
        //衰减因子;
        float r = l.dot(l);
        //环境光;
        Eigen::Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
        Eigen::Vector3f diffuse = kd.cwiseProduct(light.intensity / r) * std::max(0.0f, n.dot(l));
        Eigen::Vector3f specular = ks.cwiseProduct(light.intensity / r) * std::pow(std::max(0.0f, n.dot(h)), p);
        result_color += ambient + diffuse + specular;
    }
    return result_color * 255.f;
}

结果如下:

对于TBN矩阵的了解可以参考以下文章:

计算机图形学八:纹理映射的应用(法线贴图,凹凸贴图与阴影贴图等相关应用的原理详解) - 知乎 (zhihu.com)

https://zhuanlan.zhihu.com/p/144357517

四.双线性插值(作业3提高部分)

先放代码:

Eigen::Vector3f getColorBilinear(float u, float v)//首先是传入纹理贴图的u、v坐标,这个坐标不是纹理映射后的,就是最原始的texture上的单位坐标
    {
        //首先要限定纹理的边界;
        if (u < 0)u = 0;
        if (u > 1)u = 1;
        if (v < 0)v = 0;
        if (v > 1)v = 1;
        //先确定四个点的坐标以及其对应纹理位置关系;
        //左下角;
        int w1 = u * width; int h1 = v * height;//这里的w1和h1是纹理单位坐标映射到屏幕坐标后的单位,如果要进行纹理颜色坐标对应颜色提取的话,还要/width(/height)
                                                              //这里要转成int类型,是为了找出双线性插值的点(float类型-int类型)
        //右下角;
        float w2 = u * width + 1; float h2 = h1;
        //左上角;
        float w3 = u * width; float h3 = h1 + 1;
        //右上角
        float w4 = u * width + 1; float h4 = h3;

        float s = u * width - w1;
        float t = v * height - h1;
        //先提取四个坐标所对应的颜色;
        Eigen::Vector3f color1 = getColor((float)w1 / (float)width, (float)h1 / (float)height);
        Eigen::Vector3f color2 = getColor(w2 / width, h2 / height);
        Eigen::Vector3f color3 = getColor(w3 / width, h3 / height);
        Eigen::Vector3f color4 = getColor(w4 / width, h4 / height);
        //对四个点的坐标颜色进行双线性插值,确定所求点的对应颜色;
        Eigen::Vector3f tem_color1 = lerp(color1, color2, s);
        Eigen::Vector3f tem_color2 = lerp(color3, color4, s);
        Eigen::Vector3f tem_color3 = lerp(tem_color1, tem_color2, t);
        return Eigen::Vector3f(tem_color3[0], tem_color3[1], tem_color3[2]);
    }
};

原理如下:

双线性插值主要用于解决纹理过小的问题。在纹理映射的时候,会有多个纹理坐标取同一个纹理单位的颜色,使得颜色过渡不够平滑(颜色过渡过快,比如从蓝色直接变到黄色而没有中间过渡),双线性插值可以使得纹理坐标可以提取到精确点的颜色,这样映射到屏幕之后的颜色过渡就会比较明显,颜色的过渡会更好.

猜你喜欢

转载自blog.csdn.net/2201_75303014/article/details/128729515
今日推荐