games101——作业2

作业2在作业1的基础上,增加了遮挡情况,且需要填充三角形



总览

在上次作业中,虽然我们在屏幕上画出一个线框三角形,但这看起来并不是
那么的有趣。所以这一次我们继续推进一步——在屏幕上画出一个实心三角形,
换言之,栅格化一个三角形。上一次作业中,在视口变化之后,我们调用了函数rasterize_wireframe(const Triangle& t)。但这一次,你需要自己填写并调用函数 rasterize_triangle(const Triangle& t)
该函数的内部工作流程如下:

  1. 创建三角形的 2 维 bounding box。
  2. 遍历此 bounding box 内的所有像素(使用其整数索引)。然后,使用像素中
    心的屏幕空间坐标来检查中心点是否在三角形内。
  3. 如果在内部,则将其位置处的插值深度值 (interpolated depth value) 与深度
    缓冲区 (depth buffer) 中的相应值进行比较。
  4. 如果当前点更靠近相机,请设置像素颜色并更新深度缓冲区 (depth buffer)。
    你需要修改的函数如下:

rasterize_triangle(): 执行三角形栅格化算法

static bool insideTriangle(): 测试点是否在三角形内。你可以修改此函数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。

因为我们只知道三角形三个顶点处的深度值,所以对于三角形内部的像素,我们需要用插值的方法得到其深度值。我们已经为你处理好了这一部分,因为有关这方面的内容尚未在课程中涉及。插值的深度值被储存在变量 z_interpolated 中。

请注意我们是如何初始化 depth buffer 和注意 z values 的符号。为了方便同学们写代码,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远。

在此次作业中,你无需处理旋转变换,只需为模型变换返回一个单位矩阵。最后,我们提供了两个 hard-coded 三角形来测试你的实现,如果程序实现正确,你将看到如下所示的输出图像:


开始编写

在你自己的计算机或虚拟机上下载并使用我们更新的框架代码。你会注意到,在 main.cpp 下的get_projection_matrix() 函数是空的。请复制粘贴你在第一次作业中的实现来填充该函数。


作业代码

get_projection_matrix 函数,与作业一一样,具体讲解可以看我作业一的文章

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
    // Students will implement this function

    Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();

    // TODO: Implement this function
    // Create the projection matrix for the given parameters.

    // Compute l, r, b, t
    float t = tan((eye_fov*MY_PI/180)/2) * fabs(zNear);
    float r = aspect_ratio * t;
    float l = -r;
    float b = -t;
    // std::cout<<t<<" "<<r<<" "<<" "<<l<<" "<<b<<std::endl;
    // Orthographic projection
    // Translate to origin
    Eigen::Matrix4f translate = Eigen::Matrix4f::Identity();
    translate(0,3) = -(r+l)/2;
    translate(1,3) = -(t+b)/2;
    translate(2,3) = -(zNear+zFar)/2;

    // Sclae to [-1,1]^3
    Eigen::Matrix4f scale = Eigen::Matrix4f::Identity();
    scale(0,0) = 2/(r-l);
    scale(1,1) = 2/(t-b);
    scale(2,2) = 2/(zNear-zFar);

    // get Orthographic projection
    Eigen::Matrix4f ortho = scale * translate;
    // std::cout<<"Orthographic:"<<std::endl<<ortho<<std::endl;

    // Perspective projection
    // get Matrix_persp2ortho
    Eigen::Matrix4f persp2ortho = Eigen::Matrix4f::Zero();
    persp2ortho(0,0) = zNear;
    persp2ortho(1,1) = zNear;
    persp2ortho(2,2) = zNear+zFar;
    persp2ortho(2,3) = -zNear*zFar;
    persp2ortho(3,2) = 1;
    // std::cout<<persp2ortho<<std::endl;
    // get Perspective projection
    projection = ortho * persp2ortho;
    // std::cout<<"Perspective:"<<std::endl<<projection<<std::endl;
    // Then return it.

    return projection;
}

main 函数部分,需要注意的是,和作业一一样需要把 zNearzFar 由正数改为负数,因为课程中的透视投影矩阵是按 zNearzFar 推导的,如果不改为负数,会发现三角形颠倒

r.set_projection(get_projection_matrix(45, 1, -0.1, -50));

下面进入 rasterizer.cpp

insideTriangle 函数判断当前像素是否在三角形内部,具体原理如下

static bool insideTriangle(int x, int y, const Vector3f* _v)
{   
     // TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
     float x_mid = x + 0.5;
     float y_mid = y + 0.5;
     Eigen::Vector2f CA(_v[0].x()-_v[2].x(), _v[0].y()-_v[2].y());
     Eigen::Vector2f AP(x_mid -_v[0].x(), y_mid -_v[0].y());
     Eigen::Vector2f AB(_v[1].x()-_v[0].x(), _v[1].y()-_v[0].y());
     Eigen::Vector2f BP(x_mid-_v[1].x(), y_mid-_v[1].y());
     Eigen::Vector2f BC(_v[2].x()-_v[1].x(), _v[2].y()-_v[1].y());
     Eigen::Vector2f CP(x_mid-_v[2].x(), y_mid-_v[2].y());
    
     // cross product have equal sign
     if ((CA.x()*AP.y()-CA.y()*AP.x()>=0) == (AB.x()*BP.y()-AB.y()*BP.x()>=0) && (AB.x()*BP.y()-AB.y()*BP.x()>=0) == (BC.x()*CP.y()-BC.y()*CP.x()>=0)) {
         return true;    
     } else {
         return false;
     } 

}

rasterize_triangle 找出三角形的 bounding box,然后遍历 bounding box 里的每一个像素,判断其是否在三角形内,如果在三角形内部,再判断其深度是否能更新 Z_Buffer,如果能更新,就把该点涂上该三角形的颜色。需要注意,这里需要注意,要把 depth_buf 初始为0,然后深度越大离相机越近(具体原因不太懂,有时间深入研究一下)。

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    
    // TODO : Find out the bounding box of current triangle.
    int boundingbox_x_left = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
    int boundingbox_x_right = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
    int boundingbox_y_left = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
    int boundingbox_y_right = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));

    // iterate through the pixel and find if the current pixel is inside the triangle
    for (auto x=boundingbox_x_left; x<=boundingbox_x_right; ++x)
        for (auto y=boundingbox_y_left; y<=boundingbox_y_right; ++y) {
            std::cout<<x<<" "<<y<<std::endl;
            if (insideTriangle(x, y, t.v)) {
                // If so, use the following code to get the interpolated z value.
                auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
                float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                // std::cout<<"v[0].z(): "<<v[0].z()<<std::endl;
                z_interpolated *= w_reciprocal;
                // std::cout<<"z: "<<z_interpolated<<std::endl;
                // z_interpolated = std::fabs(z_interpolated);
                // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
                if (z_interpolated > depth_buf[get_index(x,y)]) {
                    depth_buf[get_index(x,y)] = z_interpolated;
                    set_pixel(Eigen::Vector3f(x, y, z_interpolated), t.getColor());
                }
            }
        }
}

这里有用到重心坐标插值获得采样点深度,这个部分的代码如下:

auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;

computeBarycentric2D 函数就是获得该像素点对应的重心坐标 ( α , β , γ ) (\alpha,\beta,\gamma) (α,β,γ),其具体代码如下

static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
    float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
    float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
    float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
    return {c1,c2,c3};
}

其具体计算就是下面公式推导出来的

之后就是 α , β , γ \alpha,\beta,\gamma α,β,γ 分别乘上对应深度在求和,就是该点的插值深度。

至于为什么这里是 z 值越大越靠前,是因为其在 draw 中,有一处变换了 z 值,这里 f1(50 - 0.1)/2.0f2(50 + 0.1)/2.0,而 z 因为透视投影变换,已经变为 [-1,1],因为 z 之前是负的,投影变换之后还是负数,而越小,其经过下面变换之后,变为正数就越大。所以越近,其变换后 z 值越大。

vert.z() = vert.z() * f1 + f2;

其结果如下:


可以看到放大后,边缘有锯齿,这就是出现走样问题


进阶代码

使用 2x2 MSAA 缓解走样问题,即对每个像素进行 2x2 超采样,看一个像素的四个部分,有几个部分在三角形内,就把该点像素颜色乘上对应占比。(其实就是进行一次模糊操作)
因为存在遮挡问题,如果不处理,会出现下方异常黑线的问题
所以对于每个超像素,也要考虑其深度信息,rasterizer.hpp添加以下变量

std::vector<Eigen::Vector3f> msaa_frame_buf;
std::vector<float> msaa_depth_buf;

其大小为之前 buffer 的四倍。

rasterize_triangle 函数,依旧遍历三角形的 bounding box,但对于每个像素,要进行 2x2 超采样,并且要考虑每个超采样像素的深度,最后像素的颜色等于每个部分按四分之一相加。

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    
    // TODO : Find out the bounding box of current triangle.
    int boundingbox_x_left = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
    int boundingbox_x_right = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
    int boundingbox_y_left = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
    int boundingbox_y_right = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
    
    int n = 2;
    int m = 2;

    // iterate through the pixel and find if the current pixel is inside the triangle
    for (auto x=boundingbox_x_left; x<=boundingbox_x_right; ++x)
        for (auto y=boundingbox_y_left; y<=boundingbox_y_right; ++y) {
            // std::cout<<x<<" "<<y<<std::endl;
            // if (insideTriangle(x, y, t.v)) {
            //     // If so, use the following code to get the interpolated z value.
            //     auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
            //     float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
            //     float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
            //     // std::cout<<"v[0].z(): "<<v[0].z()<<std::endl;
            //     z_interpolated *= w_reciprocal;
            //     // std::cout<<"z: "<<z_interpolated<<std::endl;
            //     // z_interpolated = std::fabs(z_interpolated);
            //     // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
            //     if (z_interpolated < depth_buf[get_index(x,y)]) {
            //         depth_buf[get_index(x,y)] = z_interpolated;
            //         set_pixel(Eigen::Vector3f(x, y, z_interpolated), t.getColor());
            //     }
            // }
            int blockinTriangle = 0;
            auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
            float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
            float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
            // std::cout<<"v[0].z(): "<<v[0].z()<<std::endl;
            z_interpolated *= w_reciprocal;
            if ((blockinTriangle=MSAA(x, y, t.v, n, m, z_interpolated, t.getColor())) > 0) {
                // std::cout<<"blockinTriangle: "<<blockinTriangle<<std::endl;
                // std::cout<<"z: "<<z_interpolated<<std::endl;
                // z_interpolated = std::fabs(z_interpolated);
                // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
                int idx = get_index(x, y);
                set_pixel(Eigen::Vector3f(x, y, z_interpolated), (msaa_frame_buf[idx*4]+msaa_frame_buf[idx*4+1]+msaa_frame_buf[idx*4+2]+msaa_frame_buf[idx*4+3])/4.0);
            }
        }
}

MSAA 函数如下,四个超采样像素的起点分别是,(x, y),(x, y+0.5),(x+0.5, y),(x+0.5, y+0.5)。只有深度大于 msaa_depth_buf 才考虑是否在三角形内部。最后返回满足的超采样像素个数

int rst::rasterizer::MSAA(int x, int y, const Vector3f* _v, int n, int m, float z, Eigen::Vector3f color)
{
    float size_x = 1.0/n; // the size_x of every super sample pixel
    float size_y = 1.0/m;

    int blocksinTriangle = 0;
    for(int i=0; i<n; ++i) 
        for(int j=0; j<m; ++j) {
            if (z>msaa_depth_buf[get_index(x,y)*4 + i*2 + j] && insideTriangle(x+i*size_x, y+j*size_y, _v, size_x, size_y)) {
                msaa_depth_buf[get_index(x,y)*4 + i*2 +j] = z;
                msaa_frame_buf[get_index(x,y)*4 + i*2 +j] = color;
                blocksinTriangle ++;
            }
                // std::cout<<blocksinTriangle<<std::endl;
                // std::cout<<"true"<<std::endl;
            // } else {
            //     std::cout<<"false"<<std::endl;
            // }
        }
    
    return blocksinTriangle;
}

insideTriangle 函数也要更改,因为中点不是 (x+0.5, y+0.5),而是(x+0.25, y+0.25)

static bool insideTriangle(float x, float y, const Vector3f* _v, float size_x, float size_y)
{   
    // TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
    float x_mid = x + size_x/2.0;
    float y_mid = y + size_y/2.0;
    Eigen::Vector2f CA(_v[0].x()-_v[2].x(), _v[0].y()-_v[2].y());
    Eigen::Vector2f AP(x_mid-_v[0].x(), y_mid-_v[0].y());
    Eigen::Vector2f AB(_v[1].x()-_v[0].x(), _v[1].y()-_v[0].y());
    Eigen::Vector2f BP(x_mid-_v[1].x(), y_mid-_v[1].y());
    Eigen::Vector2f BC(_v[2].x()-_v[1].x(), _v[2].y()-_v[1].y());
    Eigen::Vector2f CP(x_mid-_v[2].x(), y_mid-_v[2].y());
    
    // cross product have equal sign
    if ((CA.x()*AP.y()-CA.y()*AP.x()>=0) == (AB.x()*BP.y()-AB.y()*BP.x()>=0) && (AB.x()*BP.y()-AB.y()*BP.x()>=0) == (BC.x()*CP.y()-BC.y()*CP.x()>=0)) {
        return true;    
    } else {
        return false;
    } 

}

可以看到走样问题被缓解
在这里插入图片描述

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值