如何绘制三角面

文章目录


前言

上节课我们已经可以绘制直线了,但是还不够,模型都是由三角面构成的,所以这次我们要绘制三角面,以及剔除我们看不到的面。

Dmitry V. Sokolov
gamma公式
Games101-光栅化(三角形的离散化)
Games101-着色(插值、高级纹理映射)
本系列主要参考Gams101的相关课程和课件以及Dmitry V. Sokolov的软渲染教程,详细教程可阅读参考链接。推荐看闫老师的课程,因为讲非常清楚,浅显易懂。

一、绘制三角面

绘制一个三角面其实比绘制一条直线还要简单很多,原理也很简单,判断一个点是否在三角形内,是就绘制,不是则跳过。

1.1 判断点是否在三角形内-叉乘法

叉乘是判断点是否在三角形内最简单的方法。 将三角形三个顶点以及我们要判定的点,互相连接成向量,然后通过比较叉乘出来的三条法向量的z值的方向性来判断是否在三角形内。若方向性都相同,那么点在三角形内,若有一条不相同,那么不在三角形内。

struct triangle
{
    triangle() {};
    triangle(Vec3f _0, Vec3f _1, Vec3f _2) : p0(_0), p1(_1), p2(_2) {};
    Vec3f vec[3];

    Vec3f& p0 = vec[0];
    Vec3f& p1 = vec[1];
    Vec3f& p2 = vec[2];
};

bool inside(const triangle& tri, const Vec3f& point)
{
    Vec3f AB = tri.p1 - tri.p0;
    Vec3f BC = tri.p2 - tri.p1;
    Vec3f CA = tri.p0 - tri.p2;

    Vec3f AP = point - tri.p0;
    Vec3f BP = point - tri.p1;
    Vec3f CP = point - tri.p2;

    float a = (AB ^ AP).z;
    float b = (BC ^ BP).z;
    float c = (CA ^ CP).z;

    if ((a < 0 && b < 0 && c < 0) || (a > 0 && b > 0 && c > 0)) return true;
    return false;

}

1.2 判断点是否在三角形内-重心法

三角形的重心可以用来插值点,UV,颜色,法向等等三角形的相关图像属性…
由于三角形重心的可以用来插值这个特性,所以我建议利用重心来判断点是否在三角形中,同时也可以利用重心插值出其他我们要的属性。

三角形重心公式:
V = α V A + β V B + γ V C V = \alpha V_A + \beta V_B + \gamma V_C V=αVA​+βVB​+γVC​
然后同时闫老师告诉我们可以利用面积之比可以计算三个变量的值。
如何绘制三角面
那么就可以写出对应的代码了。

Vec3f barycentricCoord(const triangle& tri, const Vec2f& point)
{
    float alpha = (-(point.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (point.y - tri.p1.y) * (tri.p2.x - tri.p1.x)) / (-(tri.p0.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (tri.p0.y - tri.p1.y) * (tri.p2.x - tri.p1.x));
    float beta = (-(point.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (point.y - tri.p2.y) * (tri.p0.x - tri.p2.x)) / (-(tri.p1.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (tri.p1.y - tri.p2.y) * (tri.p0.x - tri.p2.x));
    float gama = 1 - alpha - beta;

    return Vec3f(alpha, beta, gama);
}

这里要特别注意的是,返回的三个值不能随便调换位置,它们是一一对应到公式上的,否则会出现图像bug。例如下面眼睛周围。
如何绘制三角面

2.1 包围盒

能判断点是否在三角内还不够,我们还得知道三角形的范围,不然难道要遍历所有的像素么,那这也太浪费时间了。
获取三角形的范围也很简单,只需要知道三角形的水平的最大最小值和垂直的最大最小值即可。

    int x_boundingBoxMax = (int)(std::max((std::max(tri.p0.x, tri.p1.x)), tri.p2.x));
    int x_boundingBoxMin = (int)(std::min((std::min(tri.p0.x, tri.p1.x)), tri.p2.x));
    int y_boundingBoxMax = (int)(std::max((std::max(tri.p0.y, tri.p1.y)), tri.p2.y));
    int y_boundingBoxMin = (int)(std::min((std::min(tri.p0.y, tri.p1.y)), tri.p2.y));

到这里其实是可以渲染出我们的mesh了,但是还不够好,因为我们把背面的三角形也给渲染出来了,这个是我们不需要的。我们得想办法剔除。

3.1 剔除三角形

画家算法,这个算法是从远到近,层层叠加的。听上去好像可行,但是还是存在着判断远近问题,以及无法解决三角面相交的情况的。有兴趣的直接搜索,这里不详细提了。
zbuffer,zbuffer是一种更加常用的剔除三角形的方式。通过记录要绘制点的z值来判断是否绘制该点到像素的方法。详细也直接搜索吧。

这里要解决的是如何计算除三角形顶点外的z值,例如三角形内部的点如何去计算?答案就在上面,上面我们说了,三角形的重心是可以插值出很多属性的,其中当然包括z值了

float zOrder = A.z * bc.x + B.z * bc.y + C.z * bc.z;

到此我们终于绘制出来我们的模型了!!

如何绘制三角面

二、完整代码

Vec3f barycentricCoord(const triangle& tri, const Vec2f& point)
{
    float alpha = (-(point.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (point.y - tri.p1.y) * (tri.p2.x - tri.p1.x)) / (-(tri.p0.x - tri.p1.x) * (tri.p2.y - tri.p1.y) + (tri.p0.y - tri.p1.y) * (tri.p2.x - tri.p1.x));
    float beta = (-(point.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (point.y - tri.p2.y) * (tri.p0.x - tri.p2.x)) / (-(tri.p1.x - tri.p2.x) * (tri.p0.y - tri.p2.y) + (tri.p1.y - tri.p2.y) * (tri.p0.x - tri.p2.x));
    float gama = 1 - alpha - beta;

    return Vec3f(alpha, beta, gama);
}

void drawTriangle(const triangle& tri,const TGAColor& color,TGAImage &image)
{
    int x_boundingBoxMax = (int)(std::max((std::max(tri.p0.x, tri.p1.x)), tri.p2.x));
    int x_boundingBoxMin = (int)(std::min((std::min(tri.p0.x, tri.p1.x)), tri.p2.x));
    int y_boundingBoxMax = (int)(std::max((std::max(tri.p0.y, tri.p1.y)), tri.p2.y));
    int y_boundingBoxMin = (int)(std::min((std::min(tri.p0.y, tri.p1.y)), tri.p2.y));

    for (int x = x_boundingBoxMin; x <= x_boundingBoxMax; x++)
    {
        for (int y = y_boundingBoxMin; y <= y_boundingBoxMax; y++)
        {
            Vec3f bc = barycentricCoord(tri, Vec2f(x, y));
            if (bc.x < 0 || bc.y < 0 || bc.z < 0) continue;// 跳过不在三角形内的点

            float zOrder = tri.p0.z * bc.x + tri.p1.z * bc.y + tri.p2.z * bc.z;
            int index = x + y * width;

            if (depthBuffer[index] < zOrder)
            {
                depthBuffer[index] = zOrder;

                image.set(x, y, color);
            }
        }
    }
}

Vec3f world2screen(Vec3f v) 
{
    return Vec3f((v.x + 1.f) * width / 2.f, (v.y + 1.f) * height / 2.f, v.z);
}

int main(int argc, char** argv) {
    TGAImage image(width, height, TGAImage::RGB);

    depthBuffer = new float[width * height];
    for (int i = width * height; i--; depthBuffer[i] = -std::numeric_limits<float>::max());


    if (2==argc) {
        model = new Model(argv[1]);
    } else {
        model = new Model("obj/african_head.obj");
    }

    triangle tri;
    Vec3f worldCoords[3];
    Vec3f lightDir(0.f, 0.f, -1.f);

    // draw line
    /*for (int i = 0; i < model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        for (int j = 0; j < 3; j++)
        {
            tri.vec[j] = world2screen(model->vert(face[j]));
        }
        drawLine(tri, white, image);
    }*/

    // drawTriangle
    for (int i = 0; i < model->nfaces(); i++)
    {
        std::vector<int> face = model->face(i);
        for (int j = 0; j < 3; j++)
        {
            tri.vec[j] = world2screen(model->vert(face[j]));
            worldCoords[j] = model->vert(face[j]);
        }

        Vec3f vertexNormal = ((worldCoords[2] - worldCoords[0]) ^ (worldCoords[1] - worldCoords[0])).normalize();
        float indensity = std::max(0.f,vertexNormal * lightDir);
        drawTriangle(tri, TGAColor(indensity * 255, indensity * 255, indensity * 255, 255), image);
    }

    image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
    image.write_tga_file("output.tga");
    delete model;
    delete depthBuffer;
    return 0;
}

上一篇:id


下一篇:wabapi一