LabHub
学习 学习路径 课程

3D 数学与软件光栅化器

填充三角形与深度缓冲

在 LabHub 中继续学习

目标

使用重心坐标填充三角形、插值顶点属性、用深度缓冲区判断前后关系,并剔除背面。完成后,你将亲手掌握一套光栅化器的核心。

为什么重要

屏幕是像素网格,而三角形边缘并不与网格对齐,因此需要规则判断哪些像素属于三角形。重心坐标同时解决包含判断与属性插值:三个系数都不小于 0 时位于内部,再用同一组系数混合颜色、法线、纹理坐标和深度即可。

深度缓冲区更进一步。只要为每个像素记录目前最小的深度,就能不受绘制顺序影响地得到相同图像。无需对三角形排序,互相穿插的物体也能自然处理。这正是硬件选择它而非画家算法的原因。

步骤

  1. /root/raster 中放置工具箱。
  2. /root/raster/bary.py 中实现重心坐标。
  3. /root/raster/out/tri.png 中填充三角形。
  4. /root/raster/out/tri_rgb.png 中插值顶点颜色。
  5. 使用深度缓冲区将两个三角形绘制到 /root/raster/out/zbuffer.png
  6. 仅交换深度,生成 /root/raster/out/zswap.png
  7. 读取 /root/raster/tris.txt,剔除背面并生成 /root/raster/out/cull.png/root/raster/out/07-cull.txt

参考

放置绘图工具箱

按示例原样保存 /root/raster/gfxlib.py,并用 /root/raster/check.py 绘制测试图案,生成 /root/raster/out/00-check.png。图案是在 64x64 黑色背景上,从 (0,0) 到 (63,63) 绘制白色(255,255,255) 对角线,再叠加从 (0,32) 到 (63,32) 的红色(255,0,0) 水平线。

从本实验开始不再重新实现 PNG 编码器。我们把第一个实验中手写的同类代码作为工具提供,因为这里要学习的不是文件格式。

实验 Pod 没有持久卷,上一个实验创建的文件不会保留。因此每个实验都从重新放置工具箱开始。

创建 Canvas(w, h, bg),用 line(x0, y0, x1, y1, rgb) 绘制两条线,再用 write_png(path) 保存。必须最后绘制水平线,交点 (32,32) 才会是红色。

重心坐标

/root/raster/bary.py 中实现 barycentric(a, b, c, p)。a、b、c、p 为二维点,返回满足 p = u*a + v*b + w*c 的三个实数 (u, v, w)

分母是带符号三角形面积的两倍。

d = (b1-c1)*(a0-c0) + (c0-b0)*(a1-c1)
u = ((b1-c1)*(p0-c0) + (c0-b0)*(p1-c1)) / d
v = ((c1-a1)*(p0-c0) + (a0-c0)*(p1-c1)) / d
w = 1 - u - v

用顶点验证:p 为 a 时应得 (1,0,0),为 b 时应得 (0,1,0),为 c 时应得 (0,0,1)。三个值在任何位置之和始终为 1。

三个值都不小于 0 时,点位于三角形内部。这个性质同时解决内外判断和属性插值。

填充三角形

使用 /root/raster/fill.py,在 /root/raster/out/tri.png(256x256、黑色背景)中将顶点为 (30,30)、(220,60)、(120,230) 的三角形填充为红色(255,60,60)。把像素 (x, y) 的中心视为 (x+0.5, y+0.5),当三个重心坐标都不小于 0 时填色。

不要扫描整个屏幕,只遍历边界框内部。用三个顶点 x、y 的最小值和最大值构造边界框即可。GPU 也以图块为单位完成同类工作。

把像素中心设为 (x+0.5, y+0.5) 十分重要。使用整数坐标判断会让边界偏移一行。

着色像素数应与三角形面积接近。可用鞋带公式计算面积并比较。

混合顶点颜色

使用 /root/raster/interp.py 将同一三角形绘制到 /root/raster/out/tri_rgb.png,顶点 A(30,30) 为红色(255,0,0),B(220,60) 为绿色(0,255,0),C(120,230) 为蓝色(0,0,255),像素颜色按 u*A색 + v*B색 + w*C색 混合。

重心坐标不仅用于内外判断。顶点附带的任何属性都可用同一组系数混合:颜色、纹理坐标、法线和深度都一样。

各通道分别混合,例如 r = u*255 + v*0 + w*0

在三个重心系数各约为 1/3 的位置(三角形重心),三个通道都应约为 85,显示为灰色。请进行验证。

用深度缓冲区判断前后

使用 /root/raster/zbuf.py 生成 /root/raster/out/zbuffer.png(256x256、黑色背景)。三角形 A 位于 (30,30),(200,30),(30,200),颜色为蓝色(60,60,255),深度 0.2;三角形 B 位于 (60,60),(230,60),(60,230),颜色为黄色(255,220,60),深度 0.8。为每个像素设置深度缓冲区,且较小深度获胜。

深度缓冲区是与屏幕同尺寸的浮点数组,初始填充很大的值,例如 float("inf")

填色前检查 z < zbuf[y][x];只有成立时,才同时写入颜色和深度。若不更新深度,后绘制者就会无条件获胜。

这里每个三角形深度恒定,但真实渲染器也用重心坐标插值深度。本步骤的要点是:无论绘制顺序如何,结果都相同

只交换深度

保持三角形的位置和颜色不变,仅交换深度(A 为 0.8,B 为 0.2),生成 /root/raster/out/zswap.png

代码几乎不变,只需交换两个深度值。

并排查看两张图。只有重叠区域颜色改变,其余部分完全相同。深度缓冲区正是以像素为单位决定前后关系。

还可交换三角形绘制顺序,结果应保持相同。顺序独立性正是使用 z-buffer 的真正原因。

筛除背面

读取 /root/raster/tris.txt 中的六个三角形,只把屏幕坐标中带符号面积为负数的三角形用各自颜色填充到 /root/raster/out/cull.png(256x256、黑色背景),并在 /root/raster/out/07-cull.txt 中写入 total=drawn=culled=,以及从 t1=front|backt6= 共九行。

带符号面积计算如下。

signed_area = ((Bx-Ax)*(Cy-Ay) - (Cx-Ax)*(By-Ay)) / 2

负数视为正面(front),正数视为背面(back)。由于屏幕坐标 y 向下增长,符号的解读与数学坐标惯例相反。

tris.txt 每行包含 x0 y0 x1 y1 x2 y2 r g b 九个整数,使用 split() 切分即可。

在封闭物体中,背面本来就会被正面遮挡,因此仅用该符号就能在光栅化前丢弃一半三角形。