• 制作自己的软渲染器(二) 顶点缓冲与插值


    在实时渲染过程中,我们最主要处理的对象就是顶点和片元。顶点是预设的,而片元的属性是我们通过顶点插值得出的。一个模型通常拥有一定规模的顶点,它们之间有一个最简单最重要的联系——哪些点是组成同一个三角形的。如果我们要进行多三角形的光栅化渲染,把这些数据组织好是很有必要的。

    顶点缓冲数组

    我最早是在 OpenGL 中了解这部分概念的。顶点的坐标我们可以装入数组中,然后规定数组的长度为 3 的倍数,并将每连续的 3 个顶点(也就是 9 个数值,每 3 个连续的数值为同一个顶点的坐标)设置为一组三角形。

    这样做比较直观,但是有一个显然的问题:模型中的顶点通常是在多个三角形中重用的。例如一个立方体,每个面一个矩形需要通过两个三角形画出来,一共需要 12 个三角形,也就是 36 个顶点,但其实一个立方体只有 8 个顶点,于是我们存储了 24 个冗余的顶点数据。如果顶点数规模更大一点,我们可能浪费非常多的空间用于存储不必要的顶点数据。

    减少数据冗余是非常有必要的。这个问题可以通过再创建一个顶点引用数组来解决。首先在顶点数组 a 中,每个顶点只存储一次。然后在顶点缓冲数组 b 中,每三个连续的整数表示某个三角形的三个顶点在 a 中的下标。例如我们现在有一个矩形,共四个顶点。在顶点数组中我们存储的是 {x0, y0, z0, x1, y1, z1, x2, y2, z2, x3, y3, z3},而在顶点缓冲数组中我们存储的是 {0, 1, 2, 3, 1, 2},表示 0,1,2 这三个顶点在同一个三角形上,而1,2,3 这三个顶点在另一个三角形上。

    image-20220920003018556

    如果我们采用这样的数组组织方式,那么存一个立方体的数据大小就是 3 × 8 + 12 = 36 3×8+12=36 3×8+12=36,而使用原始的方法存储需要存 3 × 36 = 108 3×36=108 3×36=108 个数。

    三维空间中的顶点都是三维点,因为还没有摄像机,测试时先简单地将 z z z 坐标掐掉(

    尝试渲染一个矩形。数据如下:

    double vertexBuffer[] = {
        200, 200, 0,
        200, 400, 0,
        400, 200, 0,
        400, 400, 0
    };
    
    int vertexIndex[] = {
        0, 1, 2,
        1, 2, 3
    };
    
    int tri_count = 6;
    

    主函数中,用一个 for 循环读取数据并渲染。

    for (int i = 0; i < tri_count * 3; i += 3) {
            drawTriangle(vertexBuffer[vertexIndex[i] * 3], vertexBuffer[vertexIndex[i] * 3 + 1],
                vertexBuffer[vertexIndex[i + 1] * 3], vertexBuffer[vertexIndex[i + 1] * 3 + 1],
                vertexBuffer[vertexIndex[i + 2] * 3], vertexBuffer[vertexIndex[i + 2] * 3 + 1],
                0xffffff
                );
        }
    

    结果如下:
    image-20220920004547913

    更多的顶点属性

    如果要渲染出多种效果,每个顶点只有位置数据肯定时不够的。最简单地,我们希望顶点能拥有一个颜色属性,这样一个三角形就不会是单调的颜色了,至少我们不需要在渲染程序一个一个地为每个三角形指定颜色,而是在模型中就决定好

    改动我们的数据:

    double vertexBuffer[] = {
        200, 200, 0, 0xff0000,
        200, 400, 0, 0x00ff00,
        400, 200, 0, 0x0000ff,
        400, 400, 0, 0x000000
    };
    
    int vertexIndex[] = {
        0, 1, 2,
        1, 2, 3
    };
    

    现在每个顶点有四个数据了,分别是三个轴的坐标和颜色值。

    为了方便,drawTriangle 的参数列表也应该简化为顶点下标,否则顶点属性过多时参数列表会很长。

    还有,为了更方便地指定各个属性,不妨把顶点也封装成结构体。

    struct Vertex {
    	double x, y, z;
    	int color;
    
    	kmath::vec3 operator - (const Vertex& another) {
    		return kmath::vec3(this->x - another.x, this->y - another.y, this->z - another.z);
    	}
    };
    

    于是数据变成这样了:

    Vertex vertexBuffer[] = {
        {200, 200, 0, 0xff0000 },
        {200, 400, 0, 0x00ff00 },
        {400, 200, 0, 0x0000ff },
        {400, 400, 0, 0x000000 }
    };
    

    给顶点颜色值,但顶点只是一个矢量点,光栅化还是需要考虑片元。那么如何根据各个顶点的值确定片元的颜色呢?答案是插值( interpolation)。一般会使用重心坐标插值法。

    重心坐标插值

    插值,即我们推算出的平均值。虽然这个位置没有给具体的值,但我通过其它给定的具体值,可以推算出这里的值设为多少是合理的。

    如果我们设三角形 A B C ABC ABC 中的一点 P P P 的坐标完全用其与三个顶点的关系表示,就可以构造出一种方法为每个坐标插值。我们认定 A B C ABC ABC 各点关于 P P P 分别拥有一个权值 α , β , γ \alpha,\beta,\gamma α,β,γ,使得 P = α A + β B + γ C P=\alpha A+\beta B+\gamma C P=αA+βB+γC,同时还有 α + β + γ = 1 \alpha+\beta+\gamma=1 α+β+γ=1. 于是得到一个三元一次方程组,可以解出这三个值。最后的插值就是 V P = α V A + β V B + γ V C V_P=\alpha V_A+\beta V_B+\gamma V_C VP=αVA+βVB+γVC.

    image-20220920220912093

    将这三个值解出来即可,我们认为 P P P 的重心坐标是 ( α , β , γ ) (\alpha,\beta,\gamma) (α,β,γ). 计算重心坐标的函数如下:

    kmath::vec3f barycentric(kmath::vec2f p, kmath::vec2f a, kmath::vec2f b, kmath::vec2f c) {
        kmath::vec2f v0 = b - a, v1 = c - a, v2 = p - a;
        float d00 = v0 * v0;
        float d01 = v0 * v1;
        float d11 = v1 * v1;
        float d20 = v2 * v0;
        float d21 = v2 * v1;
        float denom = d00 * d11 - d01 * d01;
        float v = (d11 * d20 - d01 * d21) / denom;
        float w = (d00 * d21 - d01 * d20) / denom;
        return kmath::vec3f(1.0f - v - w, v, w);
    }
    

    (返回的 vec3f(u, v, w) 就是 p 的重心坐标)

    现在,我的 drawTriangle 函数是这样。要注意 rgb 颜色值是存在一个 COLORREFunsigned long 的六位十六进制中的,最高两位为 b 值,中间两位为 g 值,最下两位为 r 值。例如 0xff0000 表示纯蓝色。插值时这个数不能简单地加权而是要利用位运算把 rgb 分别考虑。

    void drawTriangle(int xa, int ya, int xb, int yb, int xc, int yc, int c1, int c2, int c3) {
        kmath::vec2<int> v0(xb - xa, yb - ya);
        kmath::vec2<int> v1(xc - xa, yc - ya);
        kmath::vec2<int> v2;
        int xl = min(xa, min(xb, xc)), xr = max(xa, max(xb, xc));
        int yd = min(ya, min(yb, yc)), yu = max(ya, max(yb, yc));
        for (int i = xl; i <= xr; ++i) {
            for (int j = yd; j <= yu; ++j) {
                if (inTriangle(xa, ya, xb, yb, xc, yc, i, j)) {
                    kmath::vec3f interpolate = barycentric(kmath::vec2f(i, j), kmath::vec2f(xa, ya), kmath::vec2f(xb, yb), kmath::vec2f(xc, yc));
                    int b = interpolate.x * (c1 & 0x0000ff) + interpolate.y * (c2 & 0x0000ff) + interpolate.z * (c3 & 0x0000ff);
                    b = b & 0x0000ff;
                    int g = interpolate.x * (c1 & 0x00ff00) + interpolate.y * (c2 & 0x00ff00) + interpolate.z * (c3 & 0x00ff00);
                    g = g & 0x00ff00;
                    int r = interpolate.x * (c1 & 0xff0000) + interpolate.y * (c2 & 0xff0000) + interpolate.z * (c3 & 0xff0000);
                    r = r & 0xff0000;
                    putpixel(i, j, r | g | b);
                }
            } 
        }
    }
    

    这时,主函数中执行

    for (int i = 0; i < tri_count * 3; i += 3) {
            drawTriangle(vertexBuffer[vertexIndex[i]].x, vertexBuffer[vertexIndex[i]].y,
            vertexBuffer[vertexIndex[i + 1]].x, vertexBuffer[vertexIndex[i + 1]].y,
            vertexBuffer[vertexIndex[i + 2]].x, vertexBuffer[vertexIndex[i + 2]].y,
            vertexBuffer[vertexIndex[i]].color, vertexBuffer[vertexIndex[i + 1]].color, vertexBuffer[vertexIndex[i + 2]].color);
        }
    

    运行结果:

    image-20220920222007022

    有人知道中间一条亮线是插值出的问题嘛。。总觉得怪怪的,不过应该没问题?

    image-20220920222309658

    右下角的黑色换成白色就不会出现这样的亮线。

    可能是因为两个三角形插值的颜色不同,上半三角形要亮于下半,所以边界处出现了上面比下面亮的情况?

  • 相关阅读:
    微服务(六)——Ribbon负载均衡服务调用
    FBA-基于fixel的分析-扩散磁共振的处理
    网络工程师知识点
    最小体力消耗路径 -- dijkstra算法应用
    链表经典面试题(六)
    R语言使用马尔可夫链对营销中的渠道归因建模
    为什么推荐Kestrel作为网络开发框架
    Studio One6.5版本要不要更新?哪些人需要更新?更新了哪些内容
    HTML网上书店静态HTML网页作业作品 大学生三联书店网页设计制作成品 简单DIV CSS布局网站
    Unity实现设计模式——中介者模式
  • 原文地址:https://blog.csdn.net/qq_39561693/article/details/126963123