tinyrenderer-三角形光栅化和背面剔除

画空心三角形

根据之前的画线算法,可以很简单画出一个空心三角形,对三角形三个顶点,按顺序分别首尾画连线就可以

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

在这里插入图片描述

填充三角形

好的三角形算法需要具备:

  1. 简单并快速
  2. 对称的,图片不依赖于传给绘图函数的顶点的顺序(顺时针画和逆时针画是一样的三角形)
  3. 如果两个三角形有共同顶点,由于光栅化舍入,他们之间应该没有孔

传统方法是扫线法:

  1. 对三角形的顶点按y排序
  2. 同时栅格化三角形左右两边
  3. 在左右边界点间画线

三个点t0,t1,t2按y排序后,以中间顶点t1做一条水平线可以将三角形分为上下两个部分,都是三角形,并且以水平横线分割。这样可以对上下两个三角形做相同的画横线填充处理
填充横线是按y轴递增,根据y轴递增值求对应左右两个边界的x值(线性求值)
注意三角形里画横线自己实现,不要调用line函数,因为横线不需要去求多余的斜率增长那些,只要x轴水平递增,y不变
按自己的思路实现了一下

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color, bool isSolid) {
    if (isSolid) {
        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);
        for (int y = t0.y; y < t1.y; y++) {
            float ratioA = (y - t0.y) / (float)(t2.y - t0.y + 1);
            float ratioB = (y - t0.y) / (float)(t1.y - t0.y + 1);
            Vec2i tA = t0 + (t2 - t0) * ratioA;
            Vec2i tB = t0 + (t1 - t0) * ratioB;
            if (tA.x < tB.x) {
                for (int x = tA.x; x <= tB.x; x++) {
                    image.set(x, tA.y, color);
                }
            }
            else {
                for (int x = tB.x; x <= tA.x; x++) {
                    image.set(x, tA.y, color);
                }
            }
        }
        for (int y = t1.y; y <= t2.y; y++) {
            float ratioA = (y - t0.y) / (float)(t2.y - t0.y + 1);
            float ratioB = (y - t1.y) / (float)(t2.y - t1.y + 1);
            Vec2i tA = t0 + (t2 - t0) * ratioA;
            Vec2i tB = t1 + (t2 - t1) * ratioB;
            if (tA.x < tB.x) {
                for (int x = tA.x; x <= tB.x; x++) {
                    image.set(x, tA.y, color);
                }
            }
            else {
                for (int x = tB.x; x <= tA.x; x++) {
                    image.set(x, tA.y, color);
                }
            }
        }
        image.set(t2.x, t2.y, color);
    }
    else {
        line(t0.x, t0.y, t1.x, t1.y, image, color);
        line(t1.x, t1.y, t2.x, t2.y, image, color);
        line(t2.x, t2.y, t0.x, t0.y, image, color);
    }
}

和官方代码对比了一下,逻辑是一样的

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    if (t0.y==t1.y && t0.y==t2.y) return; // I dont care about degenerate triangles 
    // sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!) 
    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 
        } 
    } 
}
triangle(t0[0], t0[1], t0[2], image, red, false);
triangle(t1[0], t1[1], t1[2], image, white, true);
triangle(t2[0], t2[1], t2[2], image, green, true);

在这里插入图片描述

使用Bounding Box填充三角形

找到三角形的包围盒,及对比三个顶点的minX,minY,maxX,maxY。
遍历包围盒里的每个点,是否在三角形内,确定是否填充颜色
伪代码

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

找包围盒,只需要三角形三个顶点分别对比获取最大最小的x,y就行。
判断点是否在三角形内,官方用的是重心坐标的性质判断

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

我这里直接用的是向量叉乘判断

bool insideTriangle(Vec2i* pts, int x, int y) {
    Vec2f a = Vec2f(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
    Vec2f b = Vec2f(pts[2].x - pts[1].x, pts[2].y - pts[1].y);
    Vec2f c = Vec2f(pts[0].x - pts[2].x, pts[0].y - pts[2].y);
    Vec2f pa = Vec2f(x - pts[0].x, y - pts[0].y);
    Vec2f pb = Vec2f(x - pts[1].x, y - pts[1].y);
    Vec2f pc = Vec2f(x - pts[2].x, y - pts[2].y);
    bool flag = pa.x * a.y - pa.y * a.x < 0;
    if (flag != pb.x * b.y - pb.y * b.x < 0) return false;
    if (flag != pc.x * c.y - pc.y * c.x < 0) return false;
    return true;
}
void triangle(Vec2i* pts, TGAImage& image, TGAColor color) {
    int minX = image.get_width() - 1;
    int minY = image.get_height() - 1;
    int maxX = 0;
    int maxY = 0;
    for (int i = 0; i < 3; i++) {
        minX = std::min(minX, pts[i].x);
        maxX = std::max(maxX, pts[i].x);
        minY = std::min(minY, pts[i].y);
        maxY = std::max(maxY, pts[i].y);
    }
    minX = std::max(0, minX);
    maxX = std::min(maxX, image.get_width() - 1);
    minY = std::max(0, minY);
    maxY = std::min(maxY, image.get_height() - 1);
    for (int x = minX; x <= maxX; x++) {
        for (int y = minY; y <= maxY; y++) {
            if (insideTriangle(pts, x, y)) {
                image.set(x, y, color);
            }
        }
    }
}

得到的效果是一样的
在这里插入图片描述

平面阴影渲染

用随机颜色填充模型的三角形
在这里插入图片描述
在这里插入图片描述
用黑白色做为亮度来代替随机色。
定义一束平行光源,水平朝-z的方向
根据叉乘求出模型每个三角形的法线向量。根据光源方向和法线的夹角求出光照强度
根据点乘定义,光源向量点乘三角形法线向量,越接近0,说明光源越和法线垂直,则越和三角形平面平行,三角形越暗。反之越接近于1,越亮。
(类似布林冯漫反射方程原理,只是 k d k_d kd系数为白色,忽略了光强随距离的减弱)
根据原理实现了一下代码

Model* model = new Model("obj/african_head.obj");
    float maxNum = model->getMaxNum();
    Vec3f lightDir(0, 0, -1);
    for (int i = 0; i < model->nfaces(); i++) {
        std::vector<int> face = model->face(i);
        Vec2i vp[3];
        Vec3f vf[3];
        for (int j = 0; j < 3; j++) {
            Vec3f v0 = model->vert(face[j]);
            vp[j] = Vec2i((v0.x / maxNum + 1.) * 800 / 2., (v0.y / maxNum + 1.) * 800 / 2.);
            vf[j] = v0;
        }
        //triangle(vp, image, TGAColor(rand() % 255, rand() % 255, rand() % 255, 255));
        Vec3f n = (vf[1] - vf[0]) ^ (vf[2] - vf[0]);
        n.normalize();
        float I =  n * lightDir;
        if (I < 0) {
        triangle(vp, image, TGAColor(-I * 255, -I * 255, -I * 255, 255));
    }

注意我这里和官方代码不同的是 I < 0 I < 0 I<0, 说明 > 0 >0 >0 时三角形背向光源方向,直接剔除
并且颜色为 − I × 255 -I{\times}255 I×255
因为我求的三角形是按逆时针排序才法线朝外

Vec3f n = (vf[1] - vf[0]) ^ (vf[2] - vf[0]);

官方是

Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]);

正好相反

效果如下
在这里插入图片描述
发现三角形边缘有黑边显示,怀疑是之前的判断点是否在三角形内,只有 < 0 <0 <0,没有 ≤ 0 {\leq}0 0,所以在三角形边上的点被忽略了,改下叉乘结果判断

bool flag = pa.x * a.y - pa.y * a.x <= 0;
    if (flag != pb.x * b.y - pb.y * b.x <= 0) return false;
    if (flag != pc.x * c.y - pc.y * c.x <= 0) return false;

最后正常了
在这里插入图片描述
其他模型效果
在这里插入图片描述
项目跟随练习代码地址

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值