[GAMES101]计算机图形学 Assignment 作业2 插值 解析手记

Abstract

相比作业1仅进行线框的绘制,作业2要求通过光栅化技术对三角形内部的颜色进行填充,并通过z-buffer技术实现遮挡效果,实现MSAA多重采样抗锯齿当然更好。

Reference:


闫教授已为我们于main函数中将需绘制的两个三角形的顶点坐标、索引、以及顶点颜色值定义好了。

注意到rasterizer的draw函数中将rasterize_wireframe函数换成了rasterize_triangle,传入的实参依然是构造好的Triangle实例:

void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
        ...
        rasterize_triangle(t);
}

如何对三角形内部点进行光栅化呢?在闫教授的提示下,我们采用一个bounding box来将三角形框住

对于每个三角形,我们对其顶点的x与y值进行遍历,并找出x_min、x_max、y_min以及y_max。

由于顶点坐标是浮点型,为了不丢失信息,对于"min"值,bounding box的边界进行向下取整;对于"max"值,bounding box的边界进行向上取整:

void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    // x_l = x_min ; x_r = x_max ; y_b = y_min ; y_t = y_max
    int x_l = std::floor(std::min(v[0][0], std::min(v[1][0], v[2][0])));
    int x_r = std::ceil(std::max(v[0][0], std::max(v[1][0], v[2][0])));
    int y_b = std::floor(std::min(v[0][1], std::min(v[1][1], v[2][1])));
    int y_t = std::ceil(std::max(v[0][1], std::max(v[1][1], v[2][1])));
    ...

接下来用两层循环来遍历bounding box的所有点。我们需要判断当前点是否被包含在三角形内部,如果包含,则对它进行着色:

void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    ...
    for(int x = x_l ; x <= x_r ; x++)
        for(int y = y_b ; y <= y_t ; y++) {
            if(insideTriangle(x + 0.5, y + 0.5, t.v)) {
                ...
            }
        }

由于(x,y)表示当前点像素的左下角坐标,我们需要判断的是当前点的中心是否在三角形内:

如何实现insideTriangle呢:

insideTriangle
static bool insideTriangle(double x, double y, const Vector3f* _v)
{   
    Vector2f point(x, y);

    Vector2f AB = _v[1].head(2) - _v[0].head(2);
    Vector2f BC = _v[2].head(2) - _v[1].head(2);
    Vector2f CA = _v[0].head(2) - _v[2].head(2);

    Vector2f AP = point - _v[0].head(2);
    Vector2f BP = point - _v[1].head(2);
    Vector2f CP = point - _v[2].head(2);

    return    AB[0] * AP[1] - AB[1] * AP[0] > 0
           && BC[0] * BP[1] - BC[1] * BP[0] > 0
           && CA[0] * CP[1] - CA[1] * CP[0] > 0;
}

向函数传入需判断的点,以及三角形的顶点数组。我们算出三角形三条边的向量值(逆时针),并算出需判断点与三角形三顶点分别连线的向量值。

向量两两对应做叉积,只要叉积结果符号均相同,则说明判断点在三角形内部。

只要点在三角形内部,我们马上对当前点的深度进行插值计算:

if(insideTriangle(x + 0.5, y + 0.5, t.v)) {
    auto[alpha, beta, gamma] = computeBarycentric2D((float)x + 0.5f, (float)y + 0.5f, t.v);
    float w_reciprocal = 1.0f/(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的函数了。它接受一个三角形内部点的x、y值,以及该三角形的顶点坐标数组作为参数:

computeBarycentric2D
static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
    float xp = x, yp = y;
    float xa = v[0].x(), ya = v[0].y();
    float xb = v[1].x(), yb = v[1].y();
    float xc = v[2].x(), yc = v[2].y();

    float gamma = ((xb - xa) * (yp - ya) - (xp - xa) * (yb - ya)) / 
                  ((xb - xa) * (yc - ya) - (xc - xa) * (yb - ya));

    float beta = (xp - xa - gamma * (xc - xa)) / (xb - xa);

    float alpha = 1.0f - beta - gamma;

    return {alpha,beta,gamma};
}

此处的插值算法涉及到"重心坐标"的计算,你会发现我在这里放置的实现与框架的实现不一样。闫教授的代码为了效率没有太照顾可读性,我自己用解方程的方式重新推了一下:Barycentric-Coordinates

回到之前的代码:

if(insideTriangle(x + 0.5, y + 0.5, t.v)) {
    auto[alpha, beta, gamma] = computeBarycentric2D((float)x + 0.5f, (float)y + 0.5f, t.v);
    float w_reciprocal = 1.0f/(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;
}

回忆作业1解析手记对draw函数的分析过程,我们仅是对两个三角形的三个顶点进行了投影变换,之后才在这三个顶点内部进行插值着色。虽然最后的光栅化步骤仅在2D平面内进行,理论上只需三个顶点的(x,y)值便能完成插值计算,但是为了实现z-buffer,我们没有将原顶点的z值丢弃,而是将它们一齐进行了投影变换,并被用来插值三角形内部点的深度值,但这会带来一个问题。

根据闫教授的B站课程Lecture 09-17:38位置重心坐标并不具备变换一致性,也即,经过变换后的三个顶点的重心坐标和变换前的对应原顶点的重心坐标并不相同,这会导致,变换后的顶点插值出来的内部点的z深度会和变换前的顶点的深度插值结果不一致。而我们容易知道,用来进行遮挡判断的z值应该是变换前的原值,所以在光栅化这一步骤,我们不应计算变换后的插值z,而是通过当前插值点算出其变换前的z值。

原文地址:https://www.cnblogs.com/1Kasshole/p/14029833.html