games101 作业笔记(作业3)

这次作业算是比较重要的一次,算是整合了前面学的东西,不过代码框架依然有些问题,

rasterize_triangle 函数中插值的注释里提到顶点的 w 分量是观察空间的深度 z,这点没有任何问题,经过透视投影的变换后,w 就变成了原本的 z

v[i].w() is the vertex view space depth value z.

但是助教的常见问题中却提到 v 是 toVector4得到的,但事实在作业三框架中的 t.v 已经是 Vector4f 了(作业二是 Vector3f),而这个函数里直接强制把 w 分量赋值成了 1

v = t.toVector4()

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;
}

这样做的话,就导致后面透视校正时除以 v[i].w() 变的毫无意义了,而且把 v[i].w() = 1带入式子后化简就可以发现,Z = 1(因为 α+β+γ = 1),zp = α * z1 +β * z2 + γ * z3。很显然 zp 退化成了直接进行一次线性插值。而老师上课说过透视投影后重心坐标会变,直接用变换后的 z 来做插值是不对的,同样的后面其他各种属性的插值都出问题了,网上大部分人的代码都没考虑到这个问题,可能是因为实际上这点透视投影导致的误差较小,实际效果和透视校正后的插值没什么区别,所有都没发现这个问题。

那么如何正确的进行插值呢,知乎上有位大佬有详细推导过程,可以看这篇矫正透视投影插值及属性插值详解,我这里就直接放结果了

image-20220706163318164

image-20220706163335608

当 v[i].w() 是正确的 view space depth value z 时,作业框架提供的 Z 的计算公式就是上面那个 zp 的计算公式,而下面那个公式就是对任意属性进行插值的通用公式,其实可以发现,作业框架的 zp 就是用下面的通用公式插值出来的,至于为什么要再进行插值算一遍深度,我没找到一个特别合适的理由,这篇《GAMES101》作业框架问题详解中的解释如下

有一种解释是:如果代入正确,因为透视投影矩阵的千差万别[公式] 分量代入的很有可能是这个点在观察(摄像机)空间的 [公式] 值的某个比值 [公式],这里 [公式] 是一个常数(比如上文给出那个矩阵对应的 [公式] 就是 [公式]),所以可以认为 [公式] 代入可能不是深度值。

这段话我是没有太理解,不过其实我们用 Z 还是二次插值出来的 zp 来做深度测试,应该都不会有什么太大影响,毕竟只要前后关系保证没有问题,其他并不影响结果,不过后面其他属性的插值,我们就应该使用 Z 来代入上面的公式进行插值了,下面以颜色的插值为例,其他基本相同,就是换一下后面插值的三个属性即可

auto interpolated_color = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.color[0], t.color[1], t.color[2], 1/Z);

代码思路

这一次在三角形内的判定已经帮我们写好了不需要再写了,光栅化其他的基本和上次是一样的了,只是多了几个其他属性的插值,以及最后的像素变成了着色后的像素

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos) 
{   
    // 切记这里不能调用 toVector4
    auto v = t.v;
    // TODO: From your HW3, get the triangle rasterization code.
    int x_min = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
    int x_max = ceil(std::max(std::max(v[0].x(), v[1].x()), v[2].x()));
    int y_min = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
    int y_max = ceil(std::max(std::max(v[0].y(), v[1].y()), v[2].y()));

    for(int x=x_min; x<x_max; x++)
        for(int y=y_min; y<y_max; y++)
        {
            if(insideTriangle(x, y, t.v))
            {
                // TODO: Inside your rasterization loop:
                //    * v[i].w() is the vertex view space depth value z.
                //    * Z is interpolated view space depth for the current pixel
                //    * zp is depth between zNear and zFar, used for z-buffer
                auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
                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;

                if(zp < depth_buf[get_index(x, y)])
                {
                    depth_buf[get_index(x, y)] = zp;

                    // TODO: Interpolate the attributes:
                    auto interpolated_color = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.color[0], t.color[1], t.color[2], 1/Z);
                    auto interpolated_normal = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.normal[0], t.normal[1], t.normal[2], 1/Z);
                    auto interpolated_texcoords = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1/Z);
                    auto interpolated_shadingcoords = interpolate(alpha/v[0].w(), beta/v[1].w(), gamma/v[2].w(), view_pos[0], view_pos[1], view_pos[2], 1/Z);
          
                    fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
                    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;
                    auto pixel_color = fragment_shader(payload);
                    set_pixel(Eigen::Vector2i(x, y), pixel_color);
                }
            }
        }
 
}

然后记得把透视投影的代码从上几次作业复制过来,然后就可 ./Rasterizer output.png normal 跑一下看看效果,因为这次没用MSAA,所以边缘有明显锯齿,另外就是我发现牛脖子上有个小绿点格格不入,不知道是什么情况,但我看作业文档样例里的牛脸上有两个异常像素点,我这边只有脖子上一个,似乎效果还更好点,不知道有没用人知道这个异常像素产生的原因

image-20220706165612215

后面就是修改不同的着色器函数来实现不同效果的着色了

首先是 phong_fragment_shader,其实就是照着老师上课的 blion-pong 光照模型的公式来即可,只需要记得归一化方向即可

这边的 cwiseProduct 函数作用就是,让矩阵点乘,每个位置和对应位置的值点对点相乘,结果依旧是个矩阵,因为 eigen 里向量就是特殊的矩阵,所以同样适用(注意向量点乘返回的结果是数值,我们需要的是个向量,两者是不一样的)

还需要注意的是环境光应该只被计算一次,因此应该放在循环的外面


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

    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;

    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.
        auto l = (light.position - point).normalized();
        auto v = (eye_pos - point).normalized();
        auto h = (l + v).normalized();
        auto r2 = (light.position - point).dot(light.position - point);
        result_color += kd.cwiseProduct(light.intensity / r2) * std::max(0.f, normal.dot(l)) + ks.cwiseProduct(light.intensity / r2) * std::pow(std::max(0.f,normal.dot(h)), p);
    }

    result_color += ka.cwiseProduct(amb_light_intensity);
    return result_color * 255.f;
}

image-20220706170838276

可以看到那个坏点依旧存在,且在所有图中都存在,文档样例里的图也都是脸上有两个坏点

材质的着色器几乎完全一样,就是把颜色改成从材质中获取,循环里面一样的

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());
    }
    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);
    Eigen::Vector3f kd = texture_color / 255.f;
    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 = texture_color;
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    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.
        auto l = (light.position - point).normalized();
        auto v = (eye_pos - point).normalized();
        auto h = (l + v).normalized();
        auto r2 = (light.position - point).dot(light.position - point);
        result_color += kd.cwiseProduct(light.intensity / r2) * std::max(0.f, normal.dot(l)) + ks.cwiseProduct(light.intensity / r2) * std::pow(std::max(0.f,normal.dot(h)), p);
    }

    result_color += ka.cwiseProduct(amb_light_intensity);
    return result_color * 255.f;
}

image-20220706171714086

后面两个代码就相对难理解了,不过按照注释来就行,助教也说了相关的计算推导后面会讲,这里大概了解即可

bump mapping & displacement mapping 的计算的推导日后将会在光线追踪部分详细介绍,目前请按照注释实现。

注释里也就 h(u,v) 可能不清楚是什么,不过助教的常见问题里也给了

bump mapping 部分的 h(u,v)=texture_color(u,v).norm, 其中 u,v 是 tex_coords, w,h 是 texture 的宽度与高度

然后这里的 w 和 h 其实原本是 int 类型的,如果直接拿来计算的话,前面的 1 要改成 1.0,不然就是整数除法了,不过如果像我这样先赋值到一个 float 变量上的话改不改就无所谓了

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();
    auto t = Eigen::Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
    auto b = normal.cross(t);
    Eigen::Matrix3f TBN;
    TBN.col(0) = t;
    TBN.col(1) = b;
    TBN.col(2) = normal;

    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.0/w,v).norm() - payload.texture->getColor(u,v).norm());
    float dV = kh * kn * (payload.texture->getColor(u,v+1.0/h).norm() - payload.texture->getColor(u,v).norm());
    Eigen::Vector3f ln(-dU, -dV, 1);
    normal = (TBN * ln).normalized();

    Eigen::Vector3f result_color = {0, 0, 0};
    result_color = normal;

    return result_color * 255.f;
}

image-20220706173021787

总体效果不错,就是左前腿上方出现了疑似摩尔纹?

位移贴图着色器和凹凸贴图基本完全一样就是多了行修改 point

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]
    // 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 x = normal.x();
    float y = normal.y();
    float z = normal.z();
    auto t = Eigen::Vector3f(x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z));
    auto b = normal.cross(t);
    Eigen::Matrix3f TBN;
    TBN.col(0) = t;
    TBN.col(1) = b;
    TBN.col(2) = normal;

    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.0/w,v).norm() - payload.texture->getColor(u,v).norm());
    float dV = kh * kn * (payload.texture->getColor(u,v+1.0/h).norm() - payload.texture->getColor(u,v).norm());
    Eigen::Vector3f ln(-dU, -dV, 1);
    point += kn * normal * payload.texture->getColor(u,v).norm();
    normal = (TBN * ln).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.
        auto l = (light.position - point).normalized();
        auto v = (eye_pos - point).normalized();
        auto h = (l + v).normalized();
        auto r2 = (light.position - point).dot(light.position - point);
        result_color += kd.cwiseProduct(light.intensity / r2) * std::max(0.f, normal.dot(l)) + ks.cwiseProduct(light.intensity / r2) * std::pow(std::max(0.f,normal.dot(h)), p);

    }

    return result_color * 255.f;
}

image-20220706173449954

前几张图我和文档中的样例图片基本没区别,唯独这张图似乎暗处更暗一些,看上去更真实了一点,不清楚是透视校正的结果还是其他 bug 导致的莫名优化,但反正效果不错

提高

双线性插值参考课上的公式即可

image-20220706182830889

    Eigen::Vector3f getColorBilinear(float u, float v)
    {
        auto u_img = u * width;
        auto v_img = (1 - v) * height;
        float u_min = std::max(0.f, std::floor(u_img));
        float u_max = std::min((float)width, std::ceil(u_img));
        float v_min = std::max(0.f, std::floor(v_img));
        float v_max = std::min((float)height, std::ceil(v_img));

        auto u00 = image_data.at<cv::Vec3b>(v_min, u_min);
        auto u01 = image_data.at<cv::Vec3b>(v_max, u_min);
        auto u10 = image_data.at<cv::Vec3b>(v_min, u_max);
        auto u11 = image_data.at<cv::Vec3b>(v_max, u_max);

        auto u0 = u00 + (u_img - u_min) * (u10 - u00);
        auto u1 = u01 + (u_img - u_min) * (u11 - u01);
        auto color = u0 + (v_img - v_min) * (u1 - u0);
      
        return Eigen::Vector3f(color[0], color[1], color[2]);
    }

不过效果需要小一些的纹理图才行,不能直接改分辨率,那样只是模糊化,正确的方法应该用那张 svg 图改成小尺寸,因为 svg 图是矢量图缩放是不会丢失细节的,然后转成 png,下面是改成400*400的纹理图的对比效果

image-20220706183809786

image-20220706183822533

效果可能不太明显,上面的是双线性插值的,可以看到鼻子眼睛和蹄子的分界上杂乱的像素更少一点,但少的不多

最后是更换模型,其实没啥好说的,就是改一下路径,把main函数里所有的路径和文件名都改成对应的就行了,但给其他几个模型或多或少都有问题,直接用会有许多bug,我这边用下来是只有 cube 是最正常的

image-20220706195208149

rock 似乎是里镜头太近了还是什么其他的,一部分在视线外面,如果光栅化的时候没有考虑越界情况的话,就会报错了,如果想显示在中间的话就需要调节摄像机位置和视野角了。

image-20220706195316823

crate 的模型有问题,是四个顶点表示的,如果不修改模型就会是这个样子

image-20220706195937402

可以参考论坛里这个帖子 作业3换模型—crate出大问题,修改obj文件即可修复,修改obj后效果如下

image-20220706200036705

最后还有兔子,似乎是离摄像头太远了?非常非常小,我也没尝试修改