从渲染管线学 GLES2.0(七)----光栅化(包含透视校正)

1、光栅化

光栅化是把图元画在屏幕上的一个过程,也就是把图元的顶点数据转换为片段的过程。该过程主要包含两部分工作,第一部分决定窗口内的哪些栅格区域被图元占有,第二部分计算出来每一个顶点的属性值。光栅化都是在视窗变换之后进行的。

2、点的光栅化

当要绘制的图元类型为点时,在点大小为 1 的情况下,一个点就是一个像素,但是当顶点大小不是 1 的时候,那么一个点就包含一块 n*n 大小的正方形区域。

假设一个顶点的坐标为 (200, 200),并且 point_size 为 10,那么这个点应该在屏幕上怎么绘制呢?

这个时候我们以给定的顶点为中心点,分别向上下左右四个方向进行增长,向下面和左面增长 (point_size - 1) / 2 个单元格,因为当前点算在下面和左面里面,上面和右面增长 point_size/2 个单元格,就比如下面这样

 在之前着色器一文中,我们有提到片段着色器有一个内置的输入变量 gl_PointCoord,只有当图元类型为点时才有意义,它表示的是一个点精灵坐标,取值范围是 [0.0, 1.0],左上角为 (0, 0),右下角为 (1, 1)

接下来我们就通过代码简单实现点的光栅化,因为点的属性值都是一样的,并不会涉及到对点属性的插值

公共结构体

struct Attribute {
    float attri[4];
};

struct Point {
    int x;
    int y;
    float z;
    float w;
    float point_coord[2];
    int attribute_size;
    Attribute attributes[8];
};

假设第一个属性值就是点的颜色 ,我们可以将光栅化结果输出到文件中

void DoRasterPoint(const Point& point, int point_size) {
    std::ofstream fout("point_output.txt", std::ios::binary | std::ios::out);
    Point top_left = point;
    top_left.x = top_left.x - (point_size - 1) / 2;
    top_left.y = top_left.y + point_size / 2;
    top_left.point_coord[0] = 0;
    top_left.point_coord[1] = 0;
    for (int y = 0; y < point_size; ++y) {
        float point_coord_y = point_size == 1 ? 0 : y * 1.0 / (point_size - 1);
        for (int x = 0; x < point_size; ++x) {
            float point_coord_x = point_size == 1 ? 0 : x * 1.0 / (point_size - 1);
            Point result = top_left;
            result.y = result.y - y;
            result.x = result.x + x;
            result.point_coord[0] = point_coord_x;
            result.point_coord[1] = point_coord_y;
            std::cout << result.x << " " << result.y << " " << result.point_coord[0] << " "
                      << result.point_coord[1] << std::endl;
            fout << result.x << " " << result.y << " " << (int)(result.attributes[0].attri[0] * 255) << " "
                 << (int)(result.attributes[0].attri[1] * 255) << " " 
                 << (int)(result.attributes[0].attri[2] * 255) << " "
                 << (int)(result.attributes[0].attri[3] * 255) << std::endl;
        }
    }
    fout.close();
}

3、线的光栅化

正常情况下,我们常见的线段,可以分为以下几种

水平线段

竖直线段

水平增长大于竖直增长长度(正负斜率),这里称为沿 x 轴增长

竖直增长大于水平增长长度(正负斜率),这里称为沿 y 轴增长

当然线段也是有宽度的,我们可以通过 glLineWidth 来设置我们的线宽,不同斜率方向的线段加上宽度表现如下

一般情况下,当 x 轴增长大于 y 轴增长的情况下,线宽是竖直增长的,当 y 轴增长大于 x 轴增长的情况下,线宽是水平增长的

在进行线段光栅化时,我们根据不同方向增长来进行光栅化

首先,判断增长方向

void DoRasterLine(std::vector<Point>& points, int linewidth) {
    int diff_x = fabs(points[1].x - points[0].x);
    int diff_y = fabs(points[1].y - points[0].y);
    bool incre_x = true;
    if (diff_y > diff_x) {
        incre_x = false;
    }
}

在数学中,我们一般使用 y = kx + b 来表示直线方程,根据两个点坐标,我们可以计算出来 k 和 b,然后可以遍历线段上的每一个点,得到每一个点的坐标

void DoRasterLine(std::vector<Point>& points, int linewidth) {
    // ......
    float line_k = 0;
    float line_b = 0;
    if ((points[1].x != points[0].x)) {
        line_k = (points[1].y - points[0].y) / (points[1].x - points[0].x);
        line_b = points[0].y - line_k * points[0].x;
    }
}

接下来我们可以按照不同的增长方向查找所有在线上面的点进行光栅化插值

沿 x 轴增长时,我们遍历两点之间的所有 x 坐标,然后计算 k 和 b 计算出来 y 坐标,然后再通过插值计算出来属性值,代码如下

if (points[0].x > points[1].x) {
    std::swap(points[0], points[1]);
}
for (int i = 0; i < diff_x; ++i) {
    int x = points[0].x + i;
    int y = line_k * x + line_b;
    float weight = i * 1.0 / (diff_x - 1);
    Point result = Lerp(points, weight);
    result.x = x;
    result.y = y;
    for (int j = 0; j < linewidth; ++j) {
        std::cout << result.x << " " << result.y << std::endl;
        result.y = result.y + 1;
    }
}

沿 x 轴增长时,我们遍历两点之间的所有 y 坐标,然后计算 k 和 b 计算出来 x 坐标

if (points[0].y > points[1].y) {
    std::swap(points[0], points[1]);
}
for (int i = 0; i < diff_y; ++i) {
    int y = points[0].y + i;
    int x = points[0].x;
    if (points[1].x != points[0].x) {
        x = (y - line_b) * 1.0 / line_k;
    }
    float weight = i * 1.0 / (diff_y - 1);
    Point result = Lerp(points, weight);
    result.x = x;
    result.y = y;
    for (int j = 0; j < linewidth; ++j) {
        std::cout << result.x << " " << result.y << std::endl;
        result.x = result.x + 1;
    }
}

全部代码如下

Point Lerp(const std::vector<Point>& points, float weight) {
    Point result;
    result.x = points[0].x * (1 - weight) + points[0].x * weight;
    result.y = points[0].y * (1 - weight) + points[0].y * weight;
    result.z = points[0].z * (1 - weight) + points[0].z * weight;
    result.w = points[0].w * (1 - weight) + points[0].w * weight;
    result.attribute_size = points[0].attribute_size;
    for (int i = 0; i < points[0].attribute_size; ++i) {
        result.attributes[i].attri[0] = points[0].attributes[i].attri[0] * (1 - weight) + 
                                        points[1].attributes[i].attri[0] * weight;
        result.attributes[i].attri[1] = points[0].attributes[i].attri[1] * (1 - weight) + 
                                        points[1].attributes[i].attri[1] * weight;
        result.attributes[i].attri[2] = points[0].attributes[i].attri[2] * (1 - weight) + 
                                        points[1].attributes[i].attri[2] * weight;
        result.attributes[i].attri[3] = points[0].attributes[i].attri[3] * (1 - weight) + 
                                        points[1].attributes[i].attri[3] * weight;
    }
    return result;
}

void DoRasterLine(std::vector<Point>& points, int linewidth) {
    std::ofstream fout("line_output.txt", std::ios::binary | std::ios::out);
    int diff_x = fabs(points[1].x - points[0].x);
    int diff_y = fabs(points[1].y - points[0].y);
    bool incre_x = true;
    if (diff_y > diff_x) {
        incre_x = false;
    }
    float line_k = 0; 
    float line_b = 0;
    if (points[1].x != points[0].x) {
        line_k = (points[1].y - points[0].y) * 1.0 / (points[1].x - points[0].x);
        line_b = points[0].y - line_k * points[0].x;
    }
    if (incre_x) {
        if (points[0].x > points[1].x) {
            std::swap(points[0], points[1]);
        }
        for (int i = 0; i < diff_x; ++i) {
            int x = points[0].x + i;
            int y = line_k * x + line_b;
            float weight = i * 1.0 / (diff_x - 1);
            Point result = Lerp(points, weight);
            result.x = x;
            result.y = y;
            for (int j = 0; j < linewidth; ++j) {
                fout << result.x << " " << result.y << " " << (int)(result.attributes[0].attri[0] * 255) << " "
                    << (int)(result.attributes[0].attri[1] * 255) << " "
                    << (int)(result.attributes[0].attri[2] * 255) << " "
                    << (int)(result.attributes[0].attri[3] * 255) << std::endl;
                result.y = result.y + 1;
            }
        }
    }
    else {
        if (points[0].y > points[1].y) {
            std::swap(points[0], points[1]);
        }
        for (int i = 0; i < diff_y; ++i) {
            int y = points[0].y + i;
            int x = points[0].x;
            if (points[1].x != points[0].x) {
                x = (y - line_b) * 1.0 / line_k;
            }
            float weight = i * 1.0 / (diff_y - 1);
            Point result = Lerp(points, weight);
            result.x = x;
            result.y = y;
            for (int j = 0; j < linewidth; ++j) {
                fout << result.x << " " << result.y << " " << (int)(result.attributes[0].attri[0] * 255) << " "
                    << (int)(result.attributes[0].attri[1] * 255) << " "
                    << (int)(result.attributes[0].attri[2] * 255) << " "
                    << (int)(result.attributes[0].attri[3] * 255) << std::endl;
                result.x = result.x + 1;
            }
        }
    }
    fout.close();
}

4、三角形的光栅化

相比于点和线的光栅化来讲,三角形的光栅化就会显得比较复杂了,因为点、线可以很好的计算出图元包含的点,但是三角形的判断就没有那么简单了。对于三角形的光栅化来说,我们也可以分为两步,第一步计算出三角形内部的点,第二部计算出内部点的属性值,这里在计算属性值的时候我们使用重心插值。

4.1 判断一个点是否在三角形内部

 如图所示,为了判断一个点是否在三角形(这里三角形为逆时针)内部,首先先将三角形按照如图所示的方向连接起来,会有 3 个向量 AB,BC,CA,然后再以每个顶点为基准,获得向量 AP,BP,CP,如果 P 在三角形内部,那么由叉乘的性质

\overline{AB}\cdot\overline{AP}=\|\overline{AB}\|\cdot\|\overline{AP}\|\cdot\sin\theta

如果 P 在三角形内部,会有 AB * AP >= 0,BC * BP >= 0,CA * CP >= 0,=0,刚好是在某一条边上面,反之 P 就在三角形的外部,如果三角形为顺时针,那么判断条件则相反。

4.2 三角形的 Bounding Box(边界盒)

我们可以通过三角形的三个顶点,计算出来一个三角形的边界盒

如上图所示,三角形一定是在边界盒之内的,那么我们只要遍历边界盒内的每一个点,来判断这些点是不是在三角形之内就可以了,从左下角遍历一直到右上角

int VecCross(Point p1, Point p2, Point p3) {
    int cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
    return cross;
}

void DoRasterTriangle(std::vector<Point>& points) {
    int min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
    int min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
    int max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
    int max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
    int coef = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
    for (int y = min_y; y < max_y; ++y) {
        for (int x = min_x; x < max_x; ++x) {
            Point p;
            p.x = x;
            p.y = y;
            int cross_ab = VecCross(points[0], points[1], p) * coef;
            int cross_bc = VecCross(points[1], points[2], p) * coef;
            int cross_ca = VecCross(points[2], points[0], p) * coef;
            if (cross_ab >= 0 && cross_bc >= 0 && cross_ca >= 0) {
                std::cout << "inside: " << p.x << " " << p.y << std::endl;
            }
        }
    }
}

4.3 相邻点叉乘值关系

假设当前点坐标 p 为 (x, y),右边点 right 坐标为 (x+1, y),上面点坐标为 top(x, y+1)

那么三个点的叉乘值分别为

// p 点 (x, y)
int cross_ab = (points[1].x - points[0].x) * (y - points[0].y) - (x - points[0].x) * (points[1].y - points[0].y);
// right 点 (x+1, y)
int right_cross_ab = (points[1].x - points[0].x) * (y - points[0].y) - ((x+1) - points[0].x) * (points[1].y - points[0].y);
// top 点 (x, y+1)
int top_cross_ab = (points[1].x - points[0].x) * ((y+1) - points[0].y) - (x - points[0].x) * (points[1].y - points[0].y);

那么 right_cross_ab - cross_ab 就等于

int cross_ab_diff_x = right_cross_ab - cross_ab;
cross_ab_diff_x = -((x + 1) - points[0].x) * (points[1].y - points[0].y) + (x - points[0].x) * (points[1].y - points[0].y);
cross_ab_diff_x = (points[1].y - points[0].y) * (x - points[0].x - (x + 1) - points[0].x);
cross_ab_diff_x = points[1].y - points[0].y;

同样计算 top_cross_ab - cross_ab 就等于

int cross_ab_diff_y = top_cross_ab - cross_ab;
cross_ab_diff_y = points[1].x - points[0].x;

那么相邻点的叉乘值就可以通过前一个点的叉乘值加上一个插值得到,而不用每次都去计算叉乘值了。那么我们只需要计算出来 (min_x, min_y) 这个点的叉乘值,后面点的叉乘值都通过加法计算得到,那么我们可以把代码修改为

void DoRasterTriangle(std::vector<Point>& points) {
    int min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
    int min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
    int max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
    int max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
    int coef = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
    int cross[3];
    int cross_diff_x[3];
    int cross_diff_y[3];
    Point p;
    p.x = min_x;
    p.y = min_y;
    for (int i = 0; i < 3; ++i) {
        cross[i] = VecCross(points[i % 3], points[(i + 1) % 3], p);
        cross_diff_x[i] = points[(i + 1) % 3].x - points[i % 3].x;
        cross_diff_y[i] = points[(i + 1) % 3].y - points[i % 3].x;
    }
    for (int y = min_y; y < max_y; ++y) {
        int tmp_cross[3];
        for (int i = 0; i < 3; ++i) {
            tmp_cross[i] = cross[i];
        }
        for (int x = min_x; x < max_x; ++x) {
            bool inside = true;
            for (int i = 0; i < 3; ++i) {
                if (tmp_cross[i] * coef < 0) {
                    inside = false;
                    break;
                }
            }
            if (inside) {
                std::cout << "inside: " << x << " " << y << std::endl;
            }
            for (int i = 0; i < 3; ++i) {
                tmp_cross[i] += cross_diff_y[i];
            }
        }
        for (int i = 0; i < 3; ++i) {
            cross[i] += cross_diff_x[i];
        }
    }
}

4.4 分块判断

我们可以看到在边界框内,三角形内的像素和不在三角形内的像素都是一块一块的,这样我们以块的方式来遍历三角形可以减少三角形之外像素遍历的次数

如上图所示,我们按照一个块一个块的进行遍历,检测每个块的四个角是否在三角形中,那么每个块就会有以下三种情况

1. 完全在三角形内

2. 部分在三角形内

3. 部分在三角形外

对于 1 来说,块中的每个像素都在三角形内,可以直接计算属性插值进行渲染,对于 3 来说,可以直接将块丢弃,对于 2 来说,我们还需要使用刚在的逐像素的方式来判断块中的每个像素是否在三角形内。此方法对于占屏幕面积比较大的三角形优化效果明显,对于小的三角形和斜长的细三角形,可能效果就不好了。此外,如果分块大了会进一步降低小三角形的绘制效率,如果分块小了又不能有明显优化。因此分块的大小需要仔细的权衡。

这里还有一个问题就是,我们是否需要将边界盒内所有的块都访问一遍呢?肯定是不是的,我们可以看到从左往后遍历三角形的块时,刚开始的块不在三角形内部,中间的块在三角形内部,一旦从内部再到外部,那么我们就没有必要再去判断这一行后面的块了。

具体代码如下

void DoRasterTriangleBlock(std::vector<Point>& points) {
    std::ofstream fout("triangle_output.txt", std::ios::binary | std::ios::out);
    int min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
    int min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
    int max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
    int max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
    int coef = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
    int cross[3];
    int cross_diff_x[3];
    int cross_diff_y[3];
    Point p;
    p.x = min_x;
    p.y = min_y;
    for (int i = 0; i < 3; ++i) {
        cross[i] = VecCross(points[i % 3], points[(i + 1) % 3], p);
        cross_diff_x[i] = points[i % 3].y - points[(i + 1) % 3].y;
        cross_diff_y[i] = points[(i + 1) % 3].x - points[i % 3].x;
    }
    std::vector<int> block_size{4, 4};
    for (int y = min_y; y < max_y; y += block_size[1]) {
        // 计算 4 个点的叉乘值
        int tmp_cross[4][3];
        for (int i = 0; i < 3; ++i) {
            tmp_cross[0][i] = cross[i];
            tmp_cross[1][i] = tmp_cross[0][i] + (block_size[0] - 1) * cross_diff_x[i];
            tmp_cross[2][i] = tmp_cross[0][i] + (block_size[1] - 1) * cross_diff_y[i];
            tmp_cross[3][i] = tmp_cross[2][i] + (block_size[0] - 1) * cross_diff_x[i];
        }
        bool inside_block = false;
        for (int x = min_x; x < max_x; x += block_size[0]) {
            int inside_count = 0;
            for (int i = 0; i < 4; ++i) {
                bool inside = true;
                for (int j = 0; j < 3; ++j) {
                    if (tmp_cross[i][j] * coef < 0) {
                        inside = false;
                        break;
                    }
                }
                if (inside) {
                    inside_block = true;
                    inside_count++;
                }
            }
            if (inside_count == 4) {
                for (int i = 0; i < block_size[1]; ++i) {
                    for (int j = 0; j < block_size[0]; ++j) {
                        fout << x + j << " " << y + i << " 76 89 56 255" << std::endl;
                    }
                }
            }
            else if (inside_count > 0) {
                int block_cross[3];
                for (int i = 0; i < block_size[1]; ++i) {
                    for (int k = 0; k < 3; ++k) {
                        block_cross[k] = tmp_cross[0][k] + i * cross_diff_y[k];
                    }
                    for (int j = 0; j < block_size[0]; ++j) {
                        bool inside = true;
                        for (int k = 0; k < 3; ++k) {
                            if (block_cross[k] * coef < 0) {
                                inside = false;
                                break;
                            }
                        }
                        if (inside) {
                            fout << x + j << " " << y + i << " 76 89 56 255" << std::endl;
                        }
                        for (int k = 0; k < 3; ++k) {
                            block_cross[k] += cross_diff_x[k];
                        }
                    }
                }
            }
            else {
                if (inside_block) {
                    break;
                }
            }
            
            for (int i = 0; i < 3; ++i) {
                tmp_cross[0][i] += block_size[0] * cross_diff_x[i];
                tmp_cross[1][i] += block_size[0] * cross_diff_x[i];
                tmp_cross[2][i] += block_size[0] * cross_diff_x[i];
                tmp_cross[3][i] += block_size[0] * cross_diff_x[i];
            }
        }
        for (int i = 0; i < 3; ++i) {
            cross[i] += cross_diff_y[i] * block_size[1];
        }
    }
    fout.close();
}

4.5 Half Space Rasterization

尽管基于块的方法显著地有助于更快地遍历边界框的空白区域,但遍历的空块的数量仍然是显著的和多余的。下面的例子很好地说明了这个问题

在上图中,橙色表示遍历的空块,虽然这些空块很少,但是还是会涉及到大量不必要的判断和计算,影响我们的效率

下面介绍一种基于块的算法,其目的是尽量减少遍历空块的数量,从而减少不必要的计算。下图解释了遍历的本质。

其基本思想是,如果遍历从三角形内的一个公共点开始,并向两个方向移动,则可以有效地解决空块最小化问题。

比如在上图中我们可以把三角形分为上三角形和下三角形,上三角形我们分别从 Top starting block 开始,然后分别向左右遍历,分别找到左右块的边界 left_bound 和 right_bound,并且下一行块的开始 x 坐标为 (left_bound + right_bound) / 2;然后下一行按照之前的方式计算遍历。下三角形从 Bottom starting block 开始,也分别向左右遍历

上三角形和下三角形,我们可以以三角形中间点坐标划分,如下图所示

通过上面的方式可以大大减少空块的遍历,关于 Half Space Rasterization 具体的算法文档可以参考这个https://acta.uni-obuda.hu/Mileff_Nehez_Dudra_63.pdf

下面是代码实现

#include <iostream>
#include <vector>
#include <fstream>

struct Bound {
    int min_x;
    int min_y;
    int max_x;
    int max_y;
};

struct Point {
    int x{};
    int y{};

    Point(int x = 0, int y = 0) {
        this->x = x;
        this->y = y;
    }
};


class Raster {
public:
    Raster() = default;
    ~Raster() = default;

    int VecCross(Point p1, Point p2, Point p3) {
        int cross = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
        return cross;
    }

    int Align(int val, int align, bool down = true) {
        int result = val - (val & (align - 1));
        if (!down) {
            result = result + (val & (align - 1) ? align : 0);
        }
        return result;
    }

    bool Inside(float cross[3], int coef) {
        for (int j = 0; j < 3; ++j) {
            if (cross[j] * coef < 0) {
                return false;
            }
        }
        return true;
    }

    void OutputBlock(const Point& point) {
        for (int j = 0; j < block_size_[1]; ++j) {
            for (int i = 0; i < block_size_[0]; ++i) {
                fout_ << point.x + i << " " << point.y + j << " 96 127 89 255" << std::endl;
            }
        }
    }

    void CheckBlock(const Point& point, std::vector<float>& cross) {
        float tmp_cross[3];
        for (int j = 0; j < block_size_[1]; ++j) {
            for (int k = 0; k < 3; ++k) {
                tmp_cross[k] = cross[k] + cross_diff_y_[k] * j;
            }
            for (int i = 0; i < block_size_[0]; ++i) {
                if (Inside(tmp_cross, coef_)) {
                    fout_ << point.x + i << " " << point.y + j << " 96 127 89 255" << std::endl;
                }
                for (int k = 0; k < 3; ++k) {
                    tmp_cross[k] += cross_diff_x_[k];
                }
            }
        }
    }

    void DoRasterTriangleHalf(std::vector<Point>& points) {
        fout_.open("triangle_output.txt", std::ios::binary | std::ios::out);
        bound_.min_x = std::min(std::min(points[0].x, points[1].x), points[2].x);
        bound_.min_y = std::min(std::min(points[0].y, points[1].y), points[2].y);
        bound_.max_x = std::max(std::max(points[0].x, points[1].x), points[2].x);
        bound_.max_y = std::max(std::max(points[0].y, points[1].y), points[2].y);
        coef_ = VecCross(points[0], points[1], points[2]) > 0 ? 1 : -1;
        for (int i = 0; i < 3; ++i) {
            cross_diff_x_[i] = points[i % 3].y - points[(i + 1) % 3].y;
            cross_diff_y_[i] = points[(i + 1) % 3].x - points[i % 3].x;
        }

        int max_index, mid_index, min_index;
        max_index = points[0].y > points[1].y ? 0 : 1;
        max_index = points[max_index].y > points[2].y ? max_index : 2;
        min_index = points[0].y < points[1].y ? 0 : 1;
        min_index = points[min_index].y < points[2].y ? min_index : 2;
        mid_index = 3 - min_index - max_index;
        if (points[mid_index].y == points[min_index].y) {
            Point point;
            point.x = Align(points[max_index].x, block_size_[0]);
            point.y = Align(points[max_index].y, block_size_[1]);
            std::vector<float> cross(3);
            for (int i = 0; i < 3; ++i) {
                cross[i] = VecCross(points[i], points[(i + 1) % 3], point);
            }
            DoTopRaster(bound_, cross, point);
        }
        else if (points[mid_index].y == points[max_index].y) {
            Point point;
            point.x = Align(points[min_index].x, block_size_[0]);
            point.y = Align(points[min_index].y, block_size_[1]);
            std::vector<float> cross(3);
            for (int i = 0; i < 3; ++i) {
                cross[i] = VecCross(points[i], points[(i + 1) % 3], point);
            }
            DoBottomRaster(bound_, cross, point);
        }
        else {
            int mid_y = Align(points[mid_index].y, block_size_[1]);
            Bound bottom_bound = bound_;
            Bound top_bound = bound_;
            bottom_bound.max_y = mid_y;
            top_bound.min_y = mid_y;
            Point top_point;
            top_point.x = Align(points[max_index].x, block_size_[0]);
            top_point.y = Align(points[max_index].y, block_size_[1]);
            std::vector<float> top_cross(3);
            for (int i = 0; i < 3; ++i) {
                top_cross[i] = VecCross(points[i], points[(i + 1) % 3], top_point);
            }
            DoTopRaster(top_bound, top_cross, top_point);

            Point bottom_point;
            bottom_point.x = Align(points[min_index].x, block_size_[0]);
            bottom_point.y = Align(points[min_index].y, block_size_[1]);
            std::vector<float> bottom_cross(3);
            for (int i = 0; i < 3; ++i) {
                bottom_cross[i] = VecCross(points[i], points[(i + 1) % 3], bottom_point);
            }
            DoBottomRaster(bottom_bound, bottom_cross, bottom_point);
        }
        fout_.close();
    }

    void DoTopRaster(const Bound& bound, std::vector<float>& cross, const Point& point) {
        if (point.y < bound.min_y) {
            return;
        }
        float cross_bound[4][3];
        for (int i = 0; i < 3; ++i) {
            cross_bound[0][i] = cross[i];
            cross_bound[1][i] = cross_bound[0][i] + cross_diff_x_[i] * (block_size_[0] - 1);
            cross_bound[2][i] = cross_bound[0][i] + cross_diff_y_[i] * (block_size_[1] - 1);
            cross_bound[3][i] = cross_bound[2][i] + cross_diff_x_[i] * (block_size_[0] - 1);
        }
        int inside_count = 0;
        std::vector<bool> inside_flag(4, false);
        for (int i = 0; i < 4; ++i) {
            inside_flag[i] = Inside(cross_bound[i], coef_);
            if (inside_flag[i]) {
                inside_count++;
            }
        }
        if (inside_count == 4) {
            OutputBlock(point);
        }
        else if (inside_count > 0) {
            CheckBlock(point, cross);
        }

        Point point_left = point;
        Point point_right = point;
        if (inside_flag[1] || inside_flag[3]) {
            float right_bound[4][3];
            for (int i = 0; i < 12; ++i) {
                right_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
            }
            for (int x = point.x + block_size_[0]; x <= bound.max_x; x += block_size_[0]) {
                for (int i = 0; i < 3; ++i) {
                    right_bound[0][i] += block_size_[0] * cross_diff_x_[i];
                    right_bound[1][i] += block_size_[0] * cross_diff_x_[i];
                    right_bound[2][i] += block_size_[0] * cross_diff_x_[i];
                    right_bound[3][i] += block_size_[0] * cross_diff_x_[i];
                }
                inside_count = 0;
                std::vector<bool> right_flag(4, false);
                for (int i = 0; i < 4; ++i) {
                    right_flag[i] = Inside(right_bound[i], coef_);
                    if (right_flag[i]) {
                        inside_count++;
                    }
                }
                Point p(x, point.y);
                if (inside_count == 4) {
                    OutputBlock(p);
                }
                else if (inside_count > 0) {
                    std::vector<float> right_cross{right_bound[0][0], right_bound[0][1], right_bound[0][2]};
                    CheckBlock(p, right_cross);
                }
                if (!right_flag[1] && !right_flag[3]) {
                    point_right.x = x;
                    break;
                }
            }
        }

        if (inside_flag[0] || inside_flag[2]) {
            float left_bound[4][3];
            for (int i = 0; i < 12; ++i) {
                left_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
            }
            for (int x = point.x - block_size_[0]; x > (bound.min_x - block_size_[0]); x -= block_size_[0]) {
                for (int i = 0; i < 3; ++i) {
                    left_bound[0][i] -= block_size_[0] * cross_diff_x_[i];
                    left_bound[1][i] -= block_size_[0] * cross_diff_x_[i];
                    left_bound[2][i] -= block_size_[0] * cross_diff_x_[i];
                    left_bound[3][i] -= block_size_[0] * cross_diff_x_[i];
                }
                inside_count = 0;
                std::vector<bool> left_flag(4, false);
                for (int i = 0; i < 4; ++i) {
                    left_flag[i] = Inside(left_bound[i], coef_);
                    if (left_flag[i]) {
                        inside_count++;
                    }
                }
                Point p(x, point.y);
                if (inside_count == 4) {
                    OutputBlock(p);
                }
                else if (inside_count > 0) {
                    std::vector<float> right_cross{left_bound[0][0], left_bound[0][1], left_bound[0][2]};
                    CheckBlock(p, right_cross);
                }
                if (!left_flag[0] && !left_flag[2]) {
                    point_left.x = x;
                    break;
                }
            }
        }
        Point result;
        result.x = point_left.x + ((point_right.x - point_left.x) >> 1);
        result.x = Align(result.x, block_size_[0]);
        result.y = point.y - block_size_[1];
        for (int i = 0; i < 3; ++i) {
            cross[i] = cross[i] - cross_diff_y_[i] * block_size_[1] + (result.x - point.x) * cross_diff_x_[i];
        }
        DoTopRaster(bound, cross, result);
    }

    void DoBottomRaster(const Bound& bound, std::vector<float>& cross, const Point& point) {
        if (point.y > bound.max_y) {
            return;
        }
        float cross_bound[4][3];
        for (int i = 0; i < 3; ++i) {
            cross_bound[0][i] = cross[i];
            cross_bound[1][i] = cross_bound[0][i] + cross_diff_x_[i] * (block_size_[0] - 1);
            cross_bound[2][i] = cross_bound[0][i] + cross_diff_y_[i] * (block_size_[1] - 1);
            cross_bound[3][i] = cross_bound[2][i] + cross_diff_x_[i] * (block_size_[0] - 1);
        }
        int inside_count = 0;
        std::vector<bool> inside_flag(4, false);
        for (int i = 0; i < 4; ++i) {
            inside_flag[i] = Inside(cross_bound[i], coef_);
            if (inside_flag[i]) {
                inside_count++;
            }
        }
        if (inside_count == 4) {
            OutputBlock(point);
        }
        else if (inside_count > 0) {
            CheckBlock(point, cross);
        }

        Point point_left = point;
        Point point_right = point;
        if (inside_flag[1] || inside_flag[3]) {
            float right_bound[4][3];
            for (int i = 0; i < 12; ++i) {
                right_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
            }
            for (int x = point.x + block_size_[0]; x <= bound.max_x; x += block_size_[0]) {
                for (int i = 0; i < 3; ++i) {
                    right_bound[0][i] += block_size_[0] * cross_diff_x_[i];
                    right_bound[1][i] += block_size_[0] * cross_diff_x_[i];
                    right_bound[2][i] += block_size_[0] * cross_diff_x_[i];
                    right_bound[3][i] += block_size_[0] * cross_diff_x_[i];
                }
                inside_count = 0;
                std::vector<bool> right_flag(4, false);
                for (int i = 0; i < 4; ++i) {
                    right_flag[i] = Inside(right_bound[i], coef_);
                    if (right_flag[i]) {
                        inside_count++;
                    }
                }
                Point p(x, point.y);
                if (inside_count == 4) {
                    OutputBlock(p);
                }
                else if (inside_count > 0) {
                    std::vector<float> right_cross{right_bound[0][0], right_bound[0][1], right_bound[0][2]};
                    CheckBlock(p, right_cross);
                }
                if (!right_flag[1] && !right_flag[3]) {
                    point_right.x = x;
                    break;
                }
            }
        }

        if (inside_flag[0] || inside_flag[2]) {
            float left_bound[4][3];
            for (int i = 0; i < 12; ++i) {
                left_bound[i / 3][i % 3] = cross_bound[i / 3][i % 3];
            }
            for (int x = point.x - block_size_[0]; x > (bound.min_x - block_size_[0]); x -= block_size_[0]) {
                for (int i = 0; i < 3; ++i) {
                    left_bound[0][i] -= block_size_[0] * cross_diff_x_[i];
                    left_bound[1][i] -= block_size_[0] * cross_diff_x_[i];
                    left_bound[2][i] -= block_size_[0] * cross_diff_x_[i];
                    left_bound[3][i] -= block_size_[0] * cross_diff_x_[i];
                }
                inside_count = 0;
                std::vector<bool> left_flag(4, false);
                for (int i = 0; i < 4; ++i) {
                    left_flag[i] = Inside(left_bound[i], coef_);
                    if (left_flag[i]) {
                        inside_count++;
                    }
                }
                Point p(x, point.y);
                if (inside_count == 4) {
                    OutputBlock(p);
                }
                else if (inside_count > 0) {
                    std::vector<float> right_cross{left_bound[0][0], left_bound[0][1], left_bound[0][2]};
                    CheckBlock(p, right_cross);
                }
                
                if (!left_flag[0] && !left_flag[2]) {
                    point_left.x = x;
                    break;
                }
            }
        }
        Point result;
        result.x = point_left.x + ((point_right.x - point_left.x) >> 1);
        result.x = Align(result.x, block_size_[0]);
        result.y = point.y + block_size_[1];
        for (int i = 0; i < 3; ++i) {
            cross[i] = cross[i] + cross_diff_y_[i] * block_size_[1] + (result.x - point.x) * cross_diff_x_[i];
        }
        DoBottomRaster(bound, cross, result);
    }

private:
    Bound bound_{};
    int coef_{};
    int cross_diff_x_[3]{};
    int cross_diff_y_[3]{};
    std::vector<int> block_size_{4, 4};
    std::ofstream fout_{};
};

int main(void) {
    std::vector<Point> points(3);
    points[0].x = 200;
    points[0].y = 200;
    points[1].x = 600;
    points[1].y = 200;
    points[2].x = 400;
    points[2].y = 400;
    Raster raster;
    raster.DoRasterTriangleHalf(points);
    return 0;
}

4.6 重心插值

在上面我们已经找到了三角形内部所有的像素点,接下来我们就该进行插值计算每一个像素点的属性了,这里我们使用重心插值。

计算重心坐标

假设在三角形 ABC 内有一点 P,那么 P 的重心坐标 (i, j, k) 计算方式为

i = Area(ABP) / Area(ABC)
j = Area(BCP) / Area(ABC)
k = Area(CAP) / Area(ABC)

并且 i + j + k = 1;

在之前二维向量的叉乘中,我们提到叉乘可以计算平行四边形的面积,那么三角形的面积就是平行四边形面积的一半,而且在判断点是否在三角形内部时,都计算过对应叉乘的值,那么计算重心坐标的代码就可以变为下面这样

float cross[3];
    float cross_diff_x[3];
    float cross_diff_y[3];
    float barycentric_coord[3];
    float barycentric_diff_x[3];
    float barycentric_diff_y[3];
    Position p(min_x, min_y, 0, 1.0);
    for (int i = 0; i < 3; ++i) {
        cross[i] = VecCross(triangle.points[i].pos, triangle.points[(i + 1) % 3].pos, p);
        cross_diff_x[i] = triangle.points[(i + 1) % 3].pos.x - triangle.points[i].pos.x;
        cross_diff_y[i] = triangle.points[i].pos.y - triangle.points[(i + 1) % 3].pos.y;
        barycentric_coord[i] = cross[i] / area;
        barycentric_diff_x[i] = cross_diff_x[i] / area;
        barycentric_diff_y[i] = cross_diff_y[i] / area;
    }

int cross[3];
int cross_diff_x[3];
int cross_diff_y[3];
float barycentric_coord[3];
float barycentric_diff_x[3];
float barycentric_diff_y[3];
Point p;
p.x = min_x;
p.y = min_y;
int cross_abc = VecCross(points[0], points[1], points[2])
for (int i = 0; i < 3; ++i) {
    cross[i] = VecCross(points[i % 3], points[(i + 1) % 3], p);
    cross_diff_x[i] = points[i % 3].y - points[(i + 1) % 3].y;
    cross_diff_y[i] = points[(i + 1) % 3].x - points[i % 3].x;
    barycentric_coord[i] = cross[i] * 1.0 / cross_abc ;
    barycentric_diff_x[i] = cross_diff_x[i] * 1.0 / cross_abc ;
    barycentric_diff_y[i] = cross_diff_y[i] * 1.0 / cross_abc ;
}

后面相邻点的重心坐标也都可以通过差值计算得到

根据重心因子计算属性值

Point Lerp(const Point& p, const std::vector<Point>& triangle, float bary[3]) {
    Point result = p;
    result.z = bary[0] * triangle[2].z + bary[1] * triangle[0].z + bary[2] * triangle[1].z;
    for (int i = 0; i < result.attribute_size; ++i) {
        for (int j = 0; j < 4; ++j) {
            result.attributes[i].attri[j] = bary[0] * triangle[2].attributes[i].attri[j] + 
                                            bary[1] * triangle[0].attributes[i].attri[j] + 
                                            bary[2] * triangle[1].attributes[i].attri[j];
        }
    }
    return result;
}

4.7 透视校正

说到透视校正,就要提到我们在之前文章中说到过的透视变换和透视除法,透视变换会给我们呈现出一种近大远小的效果,为了呈现这种效果会改变w值,那么在透视除法中除 w 之后,坐标就从线性变为非线性的了,而且这里只对坐标做了处理,并没有对属性做处理,所以对属性进行透视校正,更为详细的文章可以参考图形学 - 关于透视矫正插值那些事 - 知乎

下面列出代码实现(仅供参考)

Point Lerp(const Point& p, const std::vector<Point>& points, float barycentric[3]) {
    Point result = p;
    std::vector<float> bary{barycentric[1], barycentric[2], barycentric[0]};
    for (int i = 0; i < 3; ++i) {
        bary[i] = (bary[i] * points[i].w);
        // bary[i] = (bary[i] * points[i].point_position.w) / (points[i].point_position.z);
    }
    float z = 1.0f / (bary[0] + bary[1] + bary[2]);
    for (int i = 0; i < 3; ++i) {
        bary[i] *= z;
    }
    int attri_count = p.attribute_size;
    for (int i = 0; i < attri_count; ++i) {
        for (int j = 0; j < 4; ++j) {
            result.attributes[i].attri[j] =
                bary[0] * points[0].attributes[i].attri[j] +
                bary[1] * points[1].attributes[i].attri[j] +
                bary[2] * points[2].attributes[i].attri[j];
        }
    }
    result.z = barycentric[1] * points[0].z +
               barycentric[2] * points[1].z +
               barycentric[0] * points[2].z;
    return result;
}

关于三角形的光栅化就说到这里了

5、参考文章

1. 图形学 - 关于透视矫正插值那些事 - 知乎

2. https://acta.uni-obuda.hu/Mileff_Nehez_Dudra_63.pdf

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: gles2-gears是一个基于OpenGL ES 2.0的图形渲染示例程序。该程序主要用于演示OpenGL ES 2.0中的基本图形渲染功能和性能。它由几个齿轮组成,每个齿轮由多个三角形组成,通过旋转和平移操作,可以观察到各个齿轮之间的相互作用和渲染效果。 通过使用OpenGL ES 2.0gles2-gears可以利用硬件加速图形渲染,提供高性能的图形处理能力。它支持通过着色器程序来实现各种渲染效果,如光照、阴影和纹理等。同时,该示例程序也提供了用户交互接口,用户可以通过触摸或鼠标操作来改变齿轮的旋转速度和方向,从而观察不同的视觉效果。 gles2-gears不仅是一个OpenGL ES 2.0的示例程序,也是一个性能测试工具。通过改变齿轮数量和分辨率等参数,可以测试设备对于大规模场景和高分辨率渲染的性能表现。这对于开发者来说是非常有价值的,可以帮助他们评估设备的图形处理能力,并根据测试结果进行相应的优化。 总而言之,gles2-gears是一个基于OpenGL ES 2.0的演示和性能测试程序,可以帮助开发者习和评估设备的图形处理能力。它充分利用硬件加速,通过渲染齿轮的旋转和交互操作,展示了OpenGL ES 2.0的高性能实时图形渲染能力。 ### 回答2: gles2-gears是一个基于OpenGL ES 2.0的开源项目,它展示了使用OpenGL ES 2.0绘制的齿轮模型。它是一个典型的图形示例,用于演示OpenGL ES 2.0的功能和特性。 在gles2-gears中,通过使用OpenGL ES 2.0的着色器语言GLSL,实现了光照、纹理贴图等高级渲染技术。整个场景由齿轮模型组成,通过旋转、缩放等操作,可以观察到齿轮之间的互动效果。 gles2-gears的源代码可用于OpenGL ES 2.0编程,了解图形渲染的基本原理和技术。通过阅读和理解其代码结构和逻辑,可以了解OpenGL ES 2.0的编程模式和渲染流程。 此外,gles2-gears还可以用作性能测试工具,用于测试硬件设备的图形渲染性能。通过调整渲染分辨率、齿轮数量等参数,可以评估设备的图形处理能力,并进行性能对比。 总的来说,gles2-gears是一个教育和性能测试的工具,用于展示和验证OpenGL ES 2.0的功能和性能。无论是初者还是专业开发者,都可以使用它来习和优化图形渲染技术。 ### 回答3: gles2-gears是一个使用OpenGL ES 2.0图形库编写的一个开源项目,它展示了三个互动的齿轮,用于测试和演示OpenGL ES 2.0的性能和功能。 在它的实现过程中,作者使用了OpenGL ES 2.0的着色器语言GLSL来处理图形渲染。齿轮之间的转动是通过在每个齿轮上应用旋转变换来实现的,通过修改齿轮的旋转角度和速度,可以调整和控制齿轮之间的相对运动。此外,作者还为齿轮和整个场景设计了适当的材质、光照和阴影效果,以增强视觉效果。 这个项目最初是为了展示OpenGL ES 2.0在移动设备上的性能和功能而创建的,但它也可以在其他支持OpenGL ES 2.0的平台上运行。用户可以通过触摸或鼠标交互来控制和改变齿轮的旋转和速度,从而创建不同的视觉效果和交互体验。 值得一提的是,该项目的源代码开放,并经常作为一种教工具,帮助人们习和理解OpenGL ES 2.0的基本概念和应用技巧。许多人使用和修改这个项目,以满足不同的需求和目标。 总的来说,gles2-gears是一个展示和测试OpenGL ES 2.0在三维图形渲染中的性能和功能的开源项目。它不仅仅是一个演示程序,还可以作为习和教工具来帮助人们更好地理解和应用OpenGL ES 2.0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值