04 深度缓冲

从二维开始

假设有这样一张图片,横轴是X轴,纵轴是Y轴

04 深度缓冲

想象一下,假设我们现在是从正面在看这张图,那么如果我们从顶部来看,俯视的看,会是什么样的情况?可能很抽象,尽可能的想象一下,应该是如此,我们看到的应该是图中我画出来的那一条线段。

04 深度缓冲

为什么最开始是一段绿色而不是红色?很明显,因为我们是俯视来看的,那么上面的绿色肯定会遮住下面的红色,后面也同理,是什么决定了我们能看到的颜色?是高度,或者说Y的值,Y更大的颜色会遮挡住Y更小的颜色,这就是深度缓冲。

那么对于这么一张俯视的图来说,我们需要多少个深度缓冲呢?Width x Height吗?其实完全不用,最终我们看到的只是一条线段,我们只需要记录这条线段上,每一个点的深度就好,也就是需要Wdith个深度缓冲。

写段代码可能理解起来更加容易

首先我们画出这四条线段,这利用我们之前创建的函数,应该是非常容易的

const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
const TGAColor green = TGAColor(0, 255, 0, 255);
const TGAColor blue = TGAColor(0, 0, 255, 255);
const int width = 800;
const int height = 800;

int main()
{
	TGAImage scene(width, height, TGAImage::RGB);

	// 红黄蓝三条线段,代表我们正面看到的情况
	line(Vec2i(20, 34), Vec2i(744, 400), scene, red);
	line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
	line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);

	// 底部的一根线段,代表我们从顶部俯视看到的情况
	line(Vec2i(10, 10), Vec2i(790, 10), scene, white);

	scene.flip_vertically(); 
	scene.write_tga_file("scene.tga");

	return 0;
}

应该会得到这样一个结果

04 深度缓冲

此时底部的线段是全白的,当然这是因为我们还没开始写我们的缓冲区算法。。

// 注意,这个算法还是会画出红蓝绿三条线段的,你会发现和直线算法非常的像,只是多了一个y轴深度缓冲区
void rasterize(Vec2i p0, Vec2i p1, TGAImage& image, TGAColor color, int ybuffer[])
{
	if (p0.x > p1.x)
	{
		std::swap(p0, p1);
	}
	for (int x = p0.x; x <= p1.x; x++)
	{
		float t = (x - p0.x) / (float)(p1.x - p0.x);
		int y = p0.y * (1. - t) + p1.y * t + .5;
		image.set(x, y, color);
        // 到底为止,其实就是在画直线,还是利用x算出y,然后给对应坐标的像素赋颜色
        // =====================
        
        // =====================
        // x代表着当前这个像素的横坐标,利用他,我们可以获取到深度缓冲数组中,对应的像素的深度,拿出来和当前的y作比较
        // 如果当前的y更大,那么说明当前像素在上面,应该要遮住下面的像素,那么就给白色线段赋值
		if (ybuffer[x] < y)
		{
			ybuffer[x] = y;
			image.set(x, 10, color);
		}
	}
}

代码其实简单,但是初看可能没法抽象出来,多看,多画图,原理真的很简单

结果如下,非常完美

04 深度缓冲

线性插值

其实一维缓冲能够明白的话,那二维是完全没有任何理解障碍的,对我来说唯一的区别就是,缓冲数组必须从一维变成二维,但其实这也是无所谓的,用一维数组一样可以写

当然这里还有一个问题,对于一个三角形而言,我们是知道他3个顶点的XYZ的,毕竟这在建模时就确定了,但是!但是一个三角形是会覆盖很多像素的,我们如何确定每一个像素的深度呢?

插值...还是插值...

04 深度缓冲

如图所示,假如我们知道A的横坐标是X1,B的横坐标是X2,t代表着AC/AB,也就是AC线段在AB线段中的占比

那么我们应该如何描述C的横坐标呢?

其实很简单,而且我们上面也一直是这么做的...

04 深度缓冲

这就是线性插值,t相当于一个权重,很明显的能发现,如果t是1,那么完全就是B点,即Xc=X2,反过来说,如果t是0,那么完全就是A点,而当C在AB中间时,具体的值就由两边的权重决定,从直觉上来说,这个公式是很容易理解的...

重心插值

但是,在这里,我们并不是要对两个点进行差值,而是要对三角形内的某一点进行差值,说的更具体一点,我们是要利用三角形三个顶点的Z值,插值出三角形内部某一点的Z值

对于线段的插值很好理解,而想要插值三角形,一般就会利用重心公式,说的更简单点,那就是面积法

04 深度缓冲

假设三角形的总面积是A

那么A1的占比为A1/A,A2的占比为A2/A,A3的占比为A3/A

如何插值呢?其实这里的面积占比就等于上面的权重,权重都知道了,那么插值也就很简单的

04 深度缓冲

好了,现在问题就变成了,怎么求三角形的面积?利用向量的叉积...

04 深度缓冲

具体原理就不在这里解释了,总之,结论是三角形ABC的面积等于

04 深度缓冲

那么自然三角形APB和三角形APC的面积也很容易求,三角形BPC的面积只要用总面积减去两个小三角形的面积就能够轻松获得

回到三维中

首先,我们需要写一个向量叉乘的公式...

在geometry.h中添加

Vec3f cross(Vec3f v1, Vec3f v2)
{
	return Vec3f(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x);
}

这是两个三维向量的叉乘公式,但实际上我们这里需要的只是二维向量的叉乘,接着往下看~

我们改进一下我们的barycentric算法

Vec3f barycentric(Vec3f A, Vec3f B, Vec3f C, Vec3f P) 
{
    Vec3f s[2];
    for (int i=2; i--; ) 
    {
        s[i][0] = C[i]-A[i];
        s[i][1] = B[i]-A[i];
        s[i][2] = A[i]-P[i];
    }
    Vec3f u = cross(s[0], s[1]);
    if (std::abs(u[2])>1e-2) 
        return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
    return Vec3f(-1,1,1); 
}

咱们解析一下这个算法,首先经过for循环,我们可以得到一个长度为2的Vec3f数组,他里面的内容是这样的

04 深度缓冲

然后我们让S0叉乘S1,其算法我们上面也写了,结果还是一个三维向量,是

res = Vec3f(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x);

咱们先看他的Z

04 深度缓冲

我们现在只是在求三角形的面积,所以这里的ABC三个点是二维的,我们不需要他们的Z值

那么可以得到

04 深度缓冲

你会发现这就是上面叉乘结果的Z值!不过是相反的,但是向量叉积的正负只是代表了方向,对于二维三角形来说,这是没有意义的,咱们可以取绝对值,这就代表了三角形的总面积!即 Z = AC x AB

同理,咱们再看看res的X值和Y值

04 深度缓冲

说到这这个函数的作用就完全明白了,一次叉乘就计算出了我们所需要的所有面积

我们先把原来的代码修改一下

Vec3f barycentric(Vec3f A, Vec3f B, Vec3f C, Vec3f P)
{
	Vec3f s[2];
	for (int i = 2; i--; )
	{
		s[i][0] = C[i] - A[i];
		s[i][1] = B[i] - A[i];
		s[i][2] = A[i] - P[i];
	}
	Vec3f u = cross(s[0], s[1]);
	if (std::abs(u[2]) > 1e-2) 
		return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
	return Vec3f(-1, 1, 1); 
}

void triangle(Vec3f* pts, TGAImage& image, TGAColor color)
{
	Vec2f bboxmin(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
	Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
	Vec2f clamp(image.get_width() - 1, image.get_height() - 1);
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 2; j++)
		{
			bboxmin[j] = std::max(0.f, std::min(bboxmin[j], pts[i][j]));
			bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
		}
	}
	Vec3f P;
	for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++)
	{
		for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++)
		{
			Vec3f res = barycentric(pts[0], pts[1], pts[2], P);
			if (res[0] < 0 || res[1] < 0 || res[2] < 0)
				continue;
			image.set(P.x, P.y, color);
		}
	}
}

主要是把原来的Vec2i换成了Vec3f,并用上了真正的重心算法(之前那个朴素的算法当然也是没问题的...

尝试绘制一下

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

int main()
{
	model = new Model("obj/african_head.obj");

	TGAImage image(width, height, TGAImage::RGB);
	Vec3f light_dir(0, 0, -1);

	for (int i = 0; i < model->nfaces(); i++)
	{
		std::vector<int> face = model->face(i);
		Vec3f screen_coords[3];
		Vec3f world_coords[3];
		for (int j = 0; j < 3; j++)
		{
			Vec3f v = model->vert(face[j]);
			screen_coords[j] = world2screen(v);
			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, image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255));
		}
	}

	image.flip_vertically(); 
	image.write_tga_file("output.tga");
	delete model;
	return 0;
}

就不放图了,应该是没有问题的

然后我们补上深度缓冲

void triangle(Vec3f* pts, float* zbuffer, TGAImage& image, TGAColor color)
{
	//...
	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[0], pts[1], pts[2], P);
			if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
			P.z = 0;
            // 这里是差值计算Z
			for (int i = 0; i < 3; i++) P.z += pts[i][2] * bc_screen[i];
			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);
			}
		}
	}
}
int main()
{
	// ...
    
    // 定义缓冲区并初始化
	float* zbuffer = new float[width * height];
	for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());

	for (int i = 0; i < model->nfaces(); i++)
	{
		// ..
		if (intensity > 0)
		{
			triangle(screen_coords, zbuffer,image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255));
		}
	}

	image.flip_vertically(); 
	image.write_tga_file("output.tga");
	delete model;
	return 0;
}

结果如下,十分完美!

04 深度缓冲

上一篇:动手学深度学习v2-09-03-图像分类数据集


下一篇:用vue写一个简易计算器