【TinyRenderer】GitHub项目——迷你渲染器(2)

上一个项目GitHub项目——迷你渲染器(1)主要讲解了tiny renderer的环境搭配,以及手工实现了Bresenham画线算法,最后导入了一个obj模型,并简单分析了一下obj文件的组成和如何读取,接下来让我们继续完善这个项目。​​​​​​​

在本文中将主要讲解如何对一个三角形进行内部填充。

传统方法:扫描线算法

既然我们已经有了画线的方法,那么在屏幕上绘制一个三角形应该也不是很难。

这里对之前画线函数line的代码稍微进行了修改,将x, y坐标合并为一个Vec2i类型的变量。通过triangle函数,我们将三角形的三个顶点一次进行连线,便可以画出三个不同颜色的三角形。

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) {
    line(t0, t1, image, color);
    line(t1, t2, image, color);
    line(t2, t0, image, color);
}
int main(int argc, char** argv) {
    TGAImage image(width, height, TGAImage::RGB);

    Vec2i t0[3] = { Vec2i(10, 70),   Vec2i(50, 160),  Vec2i(70, 80) };
    Vec2i t1[3] = { Vec2i(180, 50),  Vec2i(150, 1),   Vec2i(70, 180) };
    Vec2i t2[3] = { Vec2i(180, 150), Vec2i(120, 160), Vec2i(130, 180) };

    triangle(t0[0], t0[1], t0[2], image, red);
    triangle(t1[0], t1[1], t1[2], image, white);
    triangle(t2[0], t2[1], t2[2], image, green);

    ...

    return 0;
}

 接下来开始思考,既然三角形的边框已经绘制出来了,那么怎么对三角形的内部进行填充呢?

最简单原始的方法被称为扫描线算法(还记得这好像是我本科计算机图形学的时候老师教我们的,但当时应该是只讲了原理,没有具体实现)。简单来说就是用一条水平线对一个三角形从顶而下进行扫面,在某一条扫面线中,遇到左边界便开始填充颜色,遇到右边界就停止。

第一步需要标记三角形的左右边界。对于三角形的三个顶点,可以按y轴坐标进行排序:

t0:y值最大(最上方),t2:y值最小(最下方),t1:位于t0和t2之间。t0和t2两个顶点的连线用红色表示,其余两条边用绿色表示。

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) {
    // 对于y坐标,应该满足t0.y > t1.y > t2.y
    if (t0.y < t1.y) std::swap(t0, t1);
    if (t0.y < t2.y) std::swap(t0, t2);
    if (t1.y < t2.y) std::swap(t2, t1);
    line(t0, t1, image, green);
    line(t1, t2, image, green);
    line(t2, t0, image, red);
}

这样画出的三个三角形是这个样子的:

其实这个时候已经可以看出来,对于一条扫面线,遇到红色边开始填充,直到绿色边界结束。但这个时候绿色边界实际上是由两条线段连成的,所以填充的时候我们需要分段进行,用一条过t1的水平线将三角形分成上下两个部分。

 从上半部分开始:

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) {
    // 对于y坐标,应该满足t0.y > t1.y > t2.y
    if (t0.y < t1.y) std::swap(t0, t1);
    if (t0.y < t2.y) std::swap(t0, t2);
    if (t1.y < t2.y) std::swap(t2, t1);
    int total_height = t0.y - t2.y + 1;  // 三角形最大垂直高度
    int segment_height = t0.y - t1.y + 1;  // 上半部分三角形高度
    for (int y = t0.y; y >= t1.y; y--) {
        float alpha = float(t0.y - y) / total_height;  // float强制转换必不可少!
        float beta = float(t0.y - y) / segment_height;
        Vec2i A = t0 + (t2 - t0) * alpha;
        Vec2i B = t0 + (t1 - t0) * beta;
        if (A.x > B.x) std::swap(A, B);
        for (int x = A.x; x <= B.x; x++) {
            image.set(x, y, color);
        }
        image.set(A.x, y, red);
        image.set(B.x, y, green);
    }
}

虽然这样画出来的边出现了上一篇文章的中的不连续现象,但其实没有关系,因为填充之后这种不连续现象便会消失。  

接下来继续把下半部分填充完成

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) {
    // 对于y坐标,应该满足t0.y > t1.y > t2.y
    if (t0.y < t1.y) std::swap(t0, t1);
    if (t0.y < t2.y) std::swap(t0, t2);
    if (t1.y < t2.y) std::swap(t2, t1);
    int total_height = t0.y - t2.y + 1;  // 三角形最大垂直高度
    int segment_height = t0.y - t1.y + 1;  // 上半部分三角形高度
    for (int y = t0.y; y > t1.y; y--) {
        float alpha = float(t0.y - y) / total_height;  // float强制转换必不可少!
        float beta = float(t0.y - y) / segment_height;
        Vec2i A = t0 + (t2 - t0) * alpha;
        Vec2i B = t0 + (t1 - t0) * beta;
        if (A.x > B.x) std::swap(A, B);
        for (int x = A.x; x <= B.x; x++) {
            image.set(x, y, color);
        }
    }
    for (int y = t1.y; y >= t2.y; y--) {
        float alpha = float(t0.y - y) / total_height;  // float强制转换必不可少!
        float beta = float(t1.y - y) / (total_height - segment_height + 1);
        Vec2i A = t0 + (t2 - t0) * alpha;
        Vec2i B = t1 + (t2 - t1) * beta;
        if (A.x > B.x) std::swap(A, B);
        for (int x = A.x; x <= B.x; x++) {
            image.set(x, y, color);
        }
    }
}

至此我们已经完成了三角形的填充。虽然在triangle函数中使用了两次for循环,显得代码有点冗余重复,但是我们可以很简单地进行合并简化,这里便不再赘诉了。 

填充算法改进

扫面线算法看起来不是很复杂,但实际上这是一种单线程CPU编程的老派算法,在现在这个多线程处理的时代有些过时。有没有一种方法可以多线程并行,加快对三角形内部的填充呢?

答案当然是有的,下面的一段伪代码先获取三角形的外接矩形,然后对矩形行的所以像素点逐点判断该像素是否在三角形内部,如果是,则对该像素上色。

triangle(vec2 points[3]) { 
    vec2 bbox[2] = find_bounding_box(points); 
    for (each pixel in the bounding box) { 
        if (inside(points, pixel)) { 
            put_pixel(pixel); 
        } 
    } 
}

文章接下来用了很大的一块篇幅,用重心坐标说明如何判断某个点是否在三角形内部。这部分内容在闫老师的GAMES101课程中也讲解过,结论是使用三角形的每条边向量与顶点和某点P的向量进行叉乘,如果结果均为同号,则点P在三角形内部。

对于如图三角形,将三个顶点ABC按同一个方向依次相连,形成AB、BC和CA三个向量 。

如果满足:\left\{\begin{matrix} \vec{AB}\times \vec{AP} > 0\\ \vec{BC}\times \vec{BP} > 0\\ \vec{CA}\times \vec{CP} > 0 \end{matrix}\right.       or      \left\{\begin{matrix} \vec{AB}\times \vec{AP} < 0\\ \vec{BC}\times \vec{BP} < 0\\ \vec{CA}\times \vec{CP} < 0 \end{matrix}\right.

则说明P点在三角形ABC内部。

平面着色(Flat Shading render)

现在用我们的三角形填充算法对上一篇文章中的线框模型进行渲染

for (int i=0; i<model->nfaces(); i++) { 
    std::vector<int> face = model->face(i); 
    Vec2i screen_coords[3]; 
    for (int j=0; j<3; j++) { 
        Vec3f world_coords = model->vert(face[j]); 
        screen_coords[j] = Vec2i((world_coords.x+1.)*width/2., (world_coords.y+1.)*height/2.); 
    } 
    triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(rand()%255, rand()%255, rand()%255, 255)); 
}

采用随机颜色渲染可以得到以下结果:

 考虑到现实情况,人的脸部模型不可能是这样五彩斑斓的(of course),人的视觉效果受到光线强弱影响,如果光线直射某个区域,则该区域会呈现高亮状态;同理,平行于光线的地方将不可视。

光线与平面的夹角可以用三角形的法向量和光线向量的点积表示(法线可以由两条边叉乘求得)。如果点积为负,说明光线从平面后方射来,那么我们可以快速的忽略掉这个三角形。

Vec3f light_dir(0,0,-1); // define light_dir

for (int i=0; i<model->nfaces(); i++) { 
    std::vector<int> face = model->face(i); 
    Vec2i screen_coords[3]; 
    Vec3f world_coords[3]; 
    for (int j=0; j<3; j++) { 
        Vec3f v = model->vert(face[j]); 
        screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.); 
        world_coords[j]  = v; 
    } 
    Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]); 
    n.normalize(); 
    float intensity = n*light_dir; 
    if (intensity>0) { 
        triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(intensity*255, intensity*255, intensity*255, 255)); 
    } 
}

 考虑光照强度,我们可到如下结果:

以上就是关于三角形渲染着色的内容,现在我们已经可以获得一个看起来不错的模型了,接下来的任务就是在这个模型中添加更多的细节。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值