填充三角形与深度缓冲
目标
使用重心坐标填充三角形、插值顶点属性、用深度缓冲区判断前后关系,并剔除背面。完成后,你将亲手掌握一套光栅化器的核心。
为什么重要
屏幕是像素网格,而三角形边缘并不与网格对齐,因此需要规则判断哪些像素属于三角形。重心坐标同时解决包含判断与属性插值:三个系数都不小于 0 时位于内部,再用同一组系数混合颜色、法线、纹理坐标和深度即可。
深度缓冲区更进一步。只要为每个像素记录目前最小的深度,就能不受绘制顺序影响地得到相同图像。无需对三角形排序,互相穿插的物体也能自然处理。这正是硬件选择它而非画家算法的原因。
步骤
- 在
/root/raster中放置工具箱。 - 在
/root/raster/bary.py中实现重心坐标。 - 在
/root/raster/out/tri.png中填充三角形。 - 在
/root/raster/out/tri_rgb.png中插值顶点颜色。 - 使用深度缓冲区将两个三角形绘制到
/root/raster/out/zbuffer.png。 - 仅交换深度,生成
/root/raster/out/zswap.png。 - 读取
/root/raster/tris.txt,剔除背面并生成/root/raster/out/cull.png和/root/raster/out/07-cull.txt。
参考
- 运行
nohup python3 -m http.server 8080 -d /root/raster/out &后,在网页预览中打开http://localhost:8080/查看图像。并排比较 zbuffer.png 与 zswap.png 是本实验的关键场景。 - 像素中心为 (x+0.5, y+0.5)。若以整数坐标判断,边界会偏移一行。
- 常见错误 1:进行了深度比较,却没有更新深度缓冲区。这样后绘制的内容总会获胜。
- 常见错误 2:不计算边界框而遍历整个屏幕。结果相同,但在 Python 中会慢数倍。
放置绘图工具箱
按示例原样保存 /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|back 到 t6= 共九行。
带符号面积计算如下。
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() 切分即可。
在封闭物体中,背面本来就会被正面遮挡,因此仅用该符号就能在光栅化前丢弃一半三角形。