引入
您好,我来介绍一下我的朋友z缓冲区,一个黑色的伙计。 他将帮助我们避免上一课中隐藏面移除的视觉效果。
顺便说一句,我想提一下,我在课程中大量使用的这个模型是由Vidar Rapp创建的。 他授予了我使用许可,以便我可以教授关于渲染的基础知识。虽然我对它进行了破坏,但我保证我会把眼睛还给那个人。
好吧,回到主题,理论上我们可以绘制所有三角形而不丢弃任何一个。 如果我们正确地从后到前开始绘制三角形,前面将擦除后面。,这被称为画家算法。 不幸的是,它伴随很高的计算成本:对于每一次摄像机的运动,我们需要对所有场景进行重新排序。 而且还有动态场景,复杂程度就更不必说了......这甚至还不是主要问题。 主要问题是我们并不总是能够确定正确的顺序。
我们尝试渲染一个简单的场景
想象一下由三个三角形组成的简单场景:摄像机从上到下看,我们将彩色三角形投影到白色屏幕上:
渲染结果应该如下所示:
蓝色小平面 - 它是红色背后还是前方呢?可见, 画家的算法在这里不起作用。 可以将蓝色小平面分成两个(一个在红色小平面前面,一个在后面)。 然后红色前面的那个被分成两个 - 一个在绿色三角形前面,一个在后面......我想你可能意识到了问题:在有数百万个三角形的场景中,计算起来真的代价很高。 可以使用BSP树来解决它。 顺便说一下,这个数据结构对于移动相机来说是不变的,但它确实非常混乱。 生命太短暂,不能让它变得凌乱。
简化问题:丢弃一个维度。Y缓冲区。
让我们暂时丢弃一个维度并沿着黄色平面切割上面的场景:
我的意思是,现在我们的场景由三个线段组成(黄色平面和每个三角形的交线),最终渲染具有正常宽度但是1个像素高度:
与往常一样,提交代码在此。 我们的场景是二维的,所以使用我们在第一课中编写的line()函数就可以轻松绘制它。
{ // just dumping the 2d scene (yay we have enough dimensions!)
TGAImage scene(width, height, TGAImage::RGB);
// scene "2d mesh"
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);
// screen line
line(Vec2i(10, 10), Vec2i(790, 10), scene, white);
scene.flip_vertically(); // i want to have the origin at the left bottom corner of the image
scene.write_tga_file("scene.tga");
}
如果我们从侧面看,我们2D场景的是这样的:
让我们渲染一下。 回想一下,渲染的是1像素高度。 在我的源代码中,我创建了16像素高的图像,以便于在高分辨率屏幕上阅读。 rasterize()函数仅在图像的第一行中写入。
TGAImage render(width, 16, TGAImage::RGB);
int ybuffer[width];
for (int i=0; i<width; i++) {
ybuffer[i] = std::numeric_limits<int>::min();
}
rasterize(Vec2i(20, 34), Vec2i(744, 400), render, red, ybuffer);
rasterize(Vec2i(120, 434), Vec2i(444, 400), render, green, ybuffer);
rasterize(Vec2i(330, 463), Vec2i(594, 200), render, blue, ybuffer);
所以,我声明了一个数组ybuffer,尺寸为(width,1)。 该数组初始化为负无穷大。 然后我调用rasterize()函数,并将数组和图像render作为参数。这个函数如下:
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;
if (ybuffer[x]<y) {
ybuffer[x] = y;
image.set(x, 0, color);
}
}
}
其实非常简单。我遍历了从p0.x到p1.x之间的所有x坐标,并计算了线段对应的y坐标。然后我检查了ybuffer数组中序号为x的值。如果当前y值比ybuffer中的值更接近相机,那么我将绘制当前的像素,并更新对应ybuffer的值。
让我们一步步来看。在第一个红色线段调用rasterlize()之后,我们的内存是这样的。
屏幕:
ybuffer:
在这里,品红色表示无穷小,那些堤防表示我们无法触及的屏幕。其他的表示为灰色:灰度值比较低的堤防更接近相机,灰度值更高的地方离相机远。
然后我们绘制绿色线段。
屏幕:
ybuffer:
最后绘制蓝色线段。
屏幕:
ybuffer:
恭喜,我们在一维屏幕上绘制了二维场景。让我们再次欣赏我们的渲染结果:
回到三维
对于在二维屏幕上绘制三维场景,我们需要一个二维的Z缓冲区z-buffer。
int *zbuffer = new int[width*height];
我将二维缓冲区打包成一维的,转换方式也是极简单的:
int idx = x + y*width;
z缓冲序号到x、y坐标的转换如下:
int x = idx % width;
int y = idx / width;
然后在代码中我简单地遍历所有三角形,并使用当前三角形和对z缓冲区的引用作为参数调用光栅化器函数。唯一的困难是如何计算我们想要绘制的像素的z值。 让我们回想一下我们如何计算之前y缓冲区示例中的y值:
int y = p0.y*(1.-t) + p1.y*t;
变量t的本质是什么? 其实,(1-t,t)是点(x,y)相对于区段p0的重心坐标,p1:(x,y)= p0 *(1-t)+ p1 * t。 所以我们的想法是,采用三角栅格化的重心坐标版本,对于我们想要绘制的每个像素,只需将其重心坐标乘以我们栅格化的三角形顶点的z值:
triangle(screen_coords, float *zbuffer, image, TGAColor(intensity*255, intensity*255, intensity*255, 255));
[...]
void triangle(Vec3f *pts, float *zbuffer, 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 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;
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);
}
}
}
}
我们对上一课的源代码进行了少量更改,以丢弃隐藏的部分。 这是渲染,效果是多么显著啊!
源代码在这里。
我们只是插入了Z值,还有什么可以做的吗?
答案是添加纹理! 这将是我们的家庭作业。
在.obj文件中,我们有以“vt u v”开头的行,它们给出了一组纹理坐标。 “f x / x / x x / x / x x / x / x”中间(斜线之间)的数字是该三角形的该顶点的纹理坐标。 将其插入三角形内部,乘以纹理图像的宽度 - 高度,您就能获得需要在渲染中使用的颜色。
漫反射纹理可以在这里获得。
这是我期望你渲染出来的效果:
感谢原作者Dmitry V. Sokolov的授权,原文链接:https://github.com/ssloy/tinyrenderer/wiki/Lesson-3:-Hidden-faces-removal-(z-buffer)