手写渲染器 lesson 2 --- 三角形光栅化及背面剔除 tinyrenderer中文翻译+个人理解

        大家好,这是我。准确来说,这是我被渲染后的面部模型。我们也将在这篇文章实现这个渲染效果。与此同时,我们将会填充多边形,或者说是三角形。事实上,OpenGL几乎能够对所有多边形拆分为三角形,所以我们只需考虑怎么填充三角形即可。

绘制空心三角形

        上节课我们已经学习了Bresenham直线算法,那么我们可以使用它画出空心三角形。

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); 
}

// ...

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);

最终结果

三角形填充算法

line sweeping算法

直线平移扫描算法?应该是这样翻译的。

要想实现三角形的填充,我们需要先知道以下知识:

line sweeping算法的思路如图 ,分为上下两个三角形从下向上逐行填充。

直接上代码,下面会进行讲解

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    // 根据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(t1, t2); 

    int total_height = t2.y-t0.y; 
    // 绘制下半部分三角形
    for (int y=t0.y; y<=t1.y; y++) { 
        int segment_height = t1.y-t0.y+1; 
        float alpha = (float)(y-t0.y)/total_height; 
        float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = t0 + (t1-t0)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
    
    // 绘制上半部分三角形
    for (int y=t1.y; y<=t2.y; y++) { 
        int segment_height =  t2.y-t1.y+1; 
        float alpha = (float)(y-t0.y)/total_height; 
        float beta  = (float)(y-t1.y)/segment_height; // be careful with divisions by zero 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = t1 + (t2-t1)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
}

 

代码思路就是遍历y来计算出在自己走了的比例,再根据这个比例计算出当前点的坐标。(比如我从一个起点出发走了当前路段的十分之一,已知当前路段长度,当前坐标不就可以计算出来了吗。)\frac{y-t0.y}{t2.y-y0.y} = \frac{x-t0.x}{t2.x-t0.x}如同公式所说,y方向上的距离比和x方向上的距离比相同,计算出y的比例也就知道了x方向上的比例。

我不想重复写两次代码,对此进行了优化

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    if (t0.y==t1.y && t0.y==t2.y) return; // 面积为0的三角形不需要绘制
    // 对三角形三个顶点y值进行排序 t0最小,t2最大
    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(t1, t2); 

    int total_height = t2.y-t0.y; 
    for (int i=0; i<total_height; i++) { 
        // 判断当前在三角形上半部分还是下半部分
        bool second_half = i>t1.y-t0.y || t1.y==t0.y; 

        int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y; 
        float alpha = (float)i/total_height; 
        float beta  = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here 
        Vec2i A =               t0 + (t2-t0)*alpha; 
        Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
}

结果如图

虽然说这种方法不会很复杂,但是还是有点混乱。而且他是为单线程CPU编程而设计的一种老方法。

使用Bounding Box填充三角形

让我们看看下面的伪代码:

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); 
        } 
    } 
}
重心坐标

你喜欢它吗?寻找一个包围盒是很容易的,并且判断某一点是否在三角形内也不难。

好的,我们开始吧:首先我们得知道什么叫重心坐标(barycentric coordinates), 具体可以看这篇文章重心坐标

我们的主要思路就是用AB,AC线性表示AP,即可计算出u,v值,那w也可以算出。

接下来的目的就是计算出u,v 

推导后得到点P和三个顶点的关系

可以通过矩阵表示

[u, v, 1]成两个向量结果都为0,可知[u, v, 1]和这两个向量垂直,所以可以叉乘这两个向量得到一个垂直于这两个向量所在平面的向量,因为它和[u, v, 1]平行,所以这个向量为k[u, v, 1]。

#include <vector> 
#include <iostream> 
#include "geometry.h"
#include "tgaimage.h" 
 
const int width  = 200; 
const int height = 200; 
 
Vec3f barycentric(Vec2i *pts, Vec2i P) { 
    Vec3f u = Vec3f(pts[2][0]-pts[0][0], pts[1][0]-pts[0][0], pts[0][0]-P[0])^Vec3f(pts[2][1]-pts[0][1], pts[1][1]-pts[0][1], pts[0][1]-P[1]);
    /* `pts` and `P` has integer value as coordinates
       so `abs(u[2])` < 1 means `u[2]` is 0, that means
       triangle is degenerate, in this case return something with negative coordinates */
    if (std::abs(u.z)<1) return Vec3f(-1,1,1);
    return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z); 
} 
 
void triangle(Vec2i *pts, TGAImage &image, TGAColor color) { 
    Vec2i bboxmin(image.get_width()-1,  image.get_height()-1); 
    Vec2i bboxmax(0, 0); 
    Vec2i clamp(image.get_width()-1, image.get_height()-1); 
    for (int i=0; i<3; i++) { 
        bboxmin.x = std::max(0, std::min(bboxmin.x, pts[i].x));
	bboxmin.y = std::max(0, std::min(bboxmin.y, pts[i].y));

	bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, pts[i].x));
	bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, pts[i].y));
    } 
    Vec2i P; 
    for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) { 
        for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) { 
            Vec3f bc_screen  = barycentric(pts, P); 
            if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue; 
            image.set(P.x, P.y, color); 
        } 
    } 
} 
 
int main(int argc, char** argv) { 
    TGAImage frame(200, 200, TGAImage::RGB); 
    Vec2i pts[3] = {Vec2i(10,10), Vec2i(100, 30), Vec2i(190, 160)}; 
    triangle(pts, frame, TGAColor(255, 0, 0)); 
    frame.flip_vertically(); // to place the origin in the bottom left corner of the image 
    frame.write_tga_file("framebuffer.tga");
    return 0; 
}

平面着色 Flat shading

既然知道了怎么绘制一个三角形,那不就可以绘制一个模型了吗(模型由无数个三角形构成)

for (int i=0; i<model->nfaces(); i++) { 
    // face变量存储了这个面的三个顶点序号
    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.); 
    } 
    // rgb被设置成了随机值
    triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(rand()%255, rand()%255, rand()%255, 255)); 
}

结果如图

可以看到这个模型没有立体感,缺少类似光影的效果,所以我们要加上光照强度。

// 添加光照
Vec3f light_dir(0, 0, -1);
light_dir.normalize();

......

// 计算当前面的法线
Vec3f n = (world_coords[2] - world_coords[0]) ^ (world_coords[1] - world_coords[0]);
n.normalize();

// 光照强度
float intensity = n * light_dir;

// 背面剔除
if (intensity <= 0)
    continue;

 // rgb被设置成了随机值
 triangle(screen_coords, image, TGAColor((rand() % 255) * intensity, (rand() % 255) * intensity, (rand() % 255)* intensity, 255));
背面剔除

如果光照和面的法线点乘结果为负,就意味着光照是照不到这个面的,使用这个方法也可以轻松去除一些面。能让我们快速去除一些面的方法也叫做背面剔除(Back-face culling)。

完整代码如下 

#include <vector> 
#include <iostream> 
#include "geometry.h"
#include "tgaimage.h" 
#include "model.h"

const int width = 800;
const int height = 800;


Vec3f barycentric(Vec2i* pts, Vec2i P) {
    Vec3f u = Vec3f(pts[2][0] - pts[0][0], pts[1][0] - pts[0][0], pts[0][0] - P[0]) ^ Vec3f(pts[2][1] - pts[0][1], pts[1][1] - pts[0][1], pts[0][1] - P[1]);
    /* `pts` and `P` has integer value as coordinates
       so `abs(u[2])` < 1 means `u[2]` is 0, that means
       triangle is degenerate, in this case return something with negative coordinates */
    if (std::abs(u.z) < 1) return Vec3f(-1, 1, 1);
    return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}

void triangle(Vec2i pts[], TGAImage& image, TGAColor color) {
    Vec2i bboxmin(image.get_width() - 1, image.get_height() - 1);
    Vec2i bboxmax(0, 0);
    Vec2i clamp(image.get_width() - 1, image.get_height() - 1);
    for (int i = 0; i < 3; i++) {
        bboxmin.x = std::max(0, std::min(bboxmin.x, pts[i].x));
        bboxmin.y = std::max(0, std::min(bboxmin.y, pts[i].y));

        bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, pts[i].x));
        bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, pts[i].y));
    }
    Vec2i P;
    for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++) {
        for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++) {
            Vec3f bc_screen = barycentric(pts, P);
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
            image.set(P.x, P.y, color);
        }
    }
}

Vec3f light_dir(0, 0, -1);
int main(int argc, char** argv) {
    TGAImage image(width, height, TGAImage::RGB);
    Model* model = new Model("E:\\tiny_renderer_dir\\models\\obj\\african_head.obj");

    for (int i = 0; i < model->nfaces(); i++) {
        // face变量存储了这个面的三个顶点序号
        std::vector<int> face = model->face(i);
        Vec2i screen_coords[3];
        Vec3f world_coords[3];
        for (int j = 0; j < 3; j++) {
            world_coords[j] = model->vert(face[j]);
            screen_coords[j] = Vec2i((world_coords[j].x + 1.) * width / 2., (world_coords[j].y + 1.) * height / 2.);
        }
        Vec3f n = (world_coords[2] - world_coords[0]) ^ (world_coords[1] - world_coords[0]);
        n.normalize();
        light_dir.normalize();
        float intensity = n * light_dir;
        if (intensity <= 0)
            continue;
        // rgb被设置成了随机值
        triangle(screen_coords, image, TGAColor((rand() % 255) * intensity, (rand() % 255) * intensity, (rand() % 255)* intensity, 255));
    }

    image.flip_vertically(); // to place the origin in the bottom left corner of the image 
    image.write_tga_file("framebuffer.tga");
    return 0;
}

加上光影后感觉还是有点问题,嘴巴看起来怪怪的。因为我们把模型的口腔覆盖了嘴巴的位置,下一章我将使用z-buffer来结果这个问题。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zmzzz666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值