Tiny Renderer Lesson 3

Lesson 3 : Hidden faces removal (z-buffer)

时隔不知道多久,开坑Lesson 3!本节目标:实现z-buffer算法。再次,这篇文章仅仅代表我自己的理解,而不是完全对原文的翻译。

Go straight to Z-buffer

相信各位学习图形学的一定对z-buffer一点都不陌生,它本身的思想也很朴素:就是在每个像素点的位置保存当前离屏幕最近的深度值,当绘制三角形的时候,将要绘制的点的深度值和z-buffer中对应点的深度值作比较,这称为深度测试(Z-Test);如果对应点的深度值比z-buffer中的还要小(在深度值为正值的情况下),就将z-buffer中的深度值变成当前点的深度值,这称为深度写入(Z-Write),且通过深度测试;再有,如果深度值比z-buffer中的大,就不通过深度测试,不做绘制。

原文中试图先用一个2d的所谓"y-buffer",先让读者理解z-buffer的思想,再过渡到真正的z-buffer;我认为z-buffer的思想足够简单易懂,这样反而对理解造成了阻碍。如果真的不懂z-buffer,推荐GAMES101课程中的z-buffer部分,真的很好理解。因此,本文直接开始进行z-buffer的实现。

Z-buffer的实现分为下面的部分:

  • 需要一个数组作为z-buffer
  • 需要获得每一个像素点的深度值
  • 需要逐个和z-buffer比较

首先,为了实现z-buffer算法,我们需要一个数组来储存深度值:

#include <limits.h> 

//Temporary Z-buffer
    float* zbuffer = new float[width * height];
    for (int i = width * height; i--;) {
        zbuffer[i] = -std::numeric_limits<float>::max(); //初始化z-buffer,全部设为最大值
    }

创建了z-buffer数组,float类型是为了精度考虑,如果需要更大的精度可以用double,但一般不用。将整个数组初始化成float的最大值,记得加负号,因为模型给到的z值也是负的;可以考虑统一转换,这里直接拿负的深度值做z-buffer了。

还有一个问题:这是一个一维数组,怎么定位到对应的像素点上?原文作者采用了非常聪明的办法:index = P.x + P.y * width

P是当前的像素点坐标,width是屏幕的宽度。这相当于利用了乘法的本质,乘法的本质就是很多个一样的数加起来嘛。我们累加y个width,就到达了当前像素所在的那一行;再加上x,正正好就到了当前像素点的位置。非常聪明!

最后不要忘了delete掉这个数组。

下一步,我们要想办法得到当前像素点的深度值。

不知道大家是否还记得Lesson 2中,为了实现判断点在不在三角形内,使用到的重心坐标系统?这里我们再次拿出重心坐标系统,利用它来得到当前像素点的深度值!怎么得到?插值

为了适配整个渲染流程的变化,我们要对已有的函数做一些修改:

  • 为了让Vec3类可以用[]访问各个数,添加了函数:

    //t是模板类
    inline t	   operator [](const int i)     const { 
    		switch (i) {
    		case 0:
    			return x;
    			break;
    		case 1:
    			return y;
    			break;
    		case 2:
    			return z;
    			break;
    		default:
    			std::cerr << "Not supposed to be here!";
    			return 0;
    		}
    
    	}
    

    这也是原文没有做的很好的一点,它没有将geometry类的改动说出来(至少我没看到),因此我就按自己的需要添加了。随便写的switch分支,肯定有更好的写法,大佬轻喷,也没有做边界检查,各位自己遵守了~

  • 修改barycnetric函数,将输入参数改成Vec3f pts[3], Vec3f P

  • 修改triangle函数,同样将输入参数改成Vec3f pts[3],添加widthzbuffer参数(当然了)

  • 修改simpleShading函数:

    void simpleShading(Model* model, int width, int height, Vec3f light_dir, TGAImage& image, float * zbuffer) {
        for (int i = 0; i < model->nfaces(); i++) {
            std::vector<int> face = model->face(i);
            //screen_coords : 更新为Vec3f类,保存深度信息
            Vec3f screen_coords[3];
            Vec3f world_coords[3];
            for (int j = 0; j < 3; j++) {
                Vec3f v = model->vert(face[j]);
                //原封不动加入深度信息
                screen_coords[j] = Vec3f((v.x + 1.) * width / 2., (v.y + 1.) * height / 2., v.z);
                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;
            //如果Intensity < 0,说明面片处于背面(摄像机看不到的位置),直接discard(不做渲染)
            if (intensity > 0) {
                //可以看到triangle形参改变
                rst::triangle(screen_coords, zbuffer, image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255), width);
            }
        }
    }
    
    

    到这,我们终于可以介绍我们如何通过插值得出深度值。直接给出代码:

    //标准的三角形光栅化算法
        void rst::triangle(Vec3f pts[3], float * zbuffer, TGAImage& image, TGAColor color, const int &width) {
            //先求出bounding box 偷懒了直接用两个min\max嵌套
           /* for (int i = 0; i < 2; ++i) {
                if (pts[i].x > pts[i + 1].x) {
                    swap(pts[i].x, pts[i + 1].x)
                }
            }*/
            int minx = min(pts[0].x, min(pts[1].x, pts[2].x));
            int maxx = max(pts[0].x, max(pts[1].x, pts[2].x));
            int miny = min(pts[0].y, min(pts[1].y, pts[2].y));
            int maxy = max(pts[0].y, max(pts[1].y, pts[2].y));
            //两个for循环嵌套 这就是大规模并行计算的暴力解法?
            for (int i = minx; i <= maxx; ++i) {
                for (int j = miny; j <= maxy; ++j) {
                    Vec3f P(i, j, 0); //初始化为0
                    Vec3f coord = barycentric(pts, P);
                    //optimization for small black holes (REALLY DISGUSTING)
                    if (coord.x < -.01 || coord.y < -.01 || coord.z < -.01) continue;
                    //通过重心坐标插值得出深度值
                    for (int i = 0; i < 3; i++) {
                        P.z += pts[i].z * coord[i]; //这里用到了[]
                    }
                    //z-buffering
                    if (zbuffer[int(P.x + P.y * width)] < P.z) {
                        zbuffer[int(P.x + P.y * width)] = P.z;
                        image.set(P.x, P.y, color);
                    }
                }
            }
        }
    

    通过对三角形三个点的z值的线性组合,我们可以插值得出当前像素点的深度值,并且由于我们使用重心坐标插值,可以保证准确性。当我们得到了深度值,剩下的就很简单了,只需将其和zbuffer中对应点的深度值作比较,如果zbuffer < z值(也就是z的绝对值更小),就写入zbuffer,同时绘制这个点;否则,不做任何操作。

这样,我们已经完成了z-buffer算法的实现。正确的渲染结果如下:

Lj3ryd.png

对比Lesson 2的他,这哥们现在有一张性感的嘴唇和一双有神的眼睛了。

Be aware…

有一些小坑提醒一下:

  • 必须把z-buffer涉及到的相关值都统一成Vec3f,注意是浮点数,否则会因为int精度不足出现错误的面被剔除,渲染的结果将会出现大片的黑色块。比如我一开始barycentric用的还是整型,就出现了这个问题。

  • 要同时应用Back face culling(就是intensity < 0时discard)和z-buffer。否则会出现头顶冒白光的神奇现象:

    Lj8d7q.png

原文中的Texture作业,就等到下一次再弄吧,毕竟主要的技术点是Z-buffer嘛。

Lesson 3主要内容结束。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值