两个三角形拼成矩形,沿对角线出现一条黑缝,未必是顶点没有对齐。如果两边都拒绝恰好落在共享边上的样本,就会漏画;如果两边都接受,则会重复覆盖。几何边界相同,还需要一致的边界归属规则。

本篇在32×32图像中放置一个24×24矩形,用两个三角形组成它。程序不仅输出图像,还逐样本累计归属次数,分别复现重复、漏算和正确覆盖。相机与深度暂不参与,输入已经是屏幕空间位置。

三条有向边描述一个三角形

沿用图像左上原点、X向右、Y向下。顶点a、b确定一条有向边,定义二维边函数:

E(a,b,p)=(bxax)(pyay)(byay)(pxax).E(a,b,p)=(b_x-a_x)(p_y-a_y)-(b_y-a_y)(p_x-a_x).

它是两个二维向量的叉积标量。p落在直线上时为零,落在两侧时符号相反,绝对值等于对应平行四边形面积。交换a、b会反号,因此公式与顶点绕序必须同时固定。

对于三个顶点 v0,v1,v2v_0,v_1,v_2,令 A=E(v0,v1,v2)A=E(v_0,v_1,v_2)。若A>0,内部样本对三条有向边的E都非负;若A<0,符号整体反转。本系列屏幕Y向下,所以A>0的顶点在屏幕上看是顺时针,不能照搬Y向上图中的视觉绕序名称。

当前实现用A的符号统一覆盖判定,不改变顶点数组次序。这样反向输入仍得到相同覆盖,重心权重也仍对应原来的三个顶点。是否剔除背面是后续单独的策略,不在这里用“面积为负就跳过”偷偷实现。

检查的是像素中心

像素列i、行j对应连续样本位置 (i+0.5,j+0.5)(i+0.5,j+0.5)。本次矩形四角为(4,4)、(28,4)、(28,28)、(4,28),两个三角形沿(4,4)到(28,28)连接。

矩形应覆盖i与j都在4到27的样本,共 24×24=57624\times24=576 个。24个中心恰好落在对角线上,例如(4.5,4.5)、(5.5,5.5)。这组场景故意让边界穿过采样点,避免使用一个永远碰不到边界的测试来证明边界规则正确。

边函数只回答位置属于哪个半平面。即使一个三角形与像素方格部分相交,只要中心在外,本篇单样本规则仍不覆盖它。面积覆盖与多重采样留到第08篇;这里没有把一个中心测试称为精确像素面积积分。

为什么全含边与全排边都会失败

若三条边都采用 E0E\ge0,两个三角形会同时接受共享边上的24个中心。若改成 E>0E>0,它们又同时拒绝这24个中心。正确修复不是给每个E加一个模糊的浮点容差,而是规定一条共享边只归一侧所有。

本系列CPU实验选择top-left规则,参考Direct3D的边界定义。先按A的符号把边方向统一到内部为正的一侧,记有向差为dx、dy;屏幕Y向下时,接纳边界的条件为:

1
2
top_left = dy < 0 || (dy == 0 && dx > 0);
inside = e > 0 || (e == 0 && top_left);

非水平的左边对应dy<0,水平上边对应dy=0且dx>0。共享边在两个相邻三角形中的方向相反,因此在非退化且端点一致的条件下,恰有一侧接纳它。水平边也需要明确规则,不能只写一个dy<0

这个规则不负责修复不相接的几何。如果两个三角形分别计算出的共享端点本来就不同,覆盖规则不会自动填平间隙。它也不是本系列对所有WebGPU实现边界行为的保证;当前WebGPU规范对精确落边样本另有未定义边界,GPU实验必须按具体接口核验。

用固定子像素格得到可重复的零判断

浮点计算中,“应该共线”的三个点不一定得到精确的E=0。当前CPU光栅器先将输入屏幕坐标量化到1/256像素格:乘256后四舍五入,保存为64位整数。像素中心恰好对应整数坐标 256i+128256i+128

之后边函数只做整数乘加,共享端点使用相同量化结果,E=0的判断就不再依赖浮点容差。它仍是一个带量化的光栅器,不是无限精度几何:小于一个子像素格的细节可能合并,极小三角形可能量化后退化。

接口拒绝非有限坐标和绝对值超过32768的坐标。量化坐标的绝对值至多 2232^{23},差值至多 2242^{24},两个乘积之差的幅度有保守上界 2492^{49},落在有符号64位范围内。范围检查是避免整数溢出的具体条件,不能只因用了int64_t就假定任何输入都安全。

本次顶点都是整数或半整数,量化不改变它们。后续投影产生任意浮点屏幕位置时,插值也会使用同一量化几何;如果覆盖与属性插值采用两套位置,必须单独分析差异。

同一份边函数还能给出重心坐标

对非退化三角形,使用有符号面积写出:

λ0=E(v1,v2,p)A,λ1=E(v2,v0,p)A,λ2=E(v0,v1,p)A.\lambda_0=\frac{E(v_1,v_2,p)}{A},\qquad \lambda_1=\frac{E(v_2,v_0,p)}{A},\qquad \lambda_2=\frac{E(v_0,v_1,p)}{A}.

它们之和为1,满足 p=λ0v0+λ1v1+λ2v2p=\lambda_0v_0+\lambda_1v_1+\lambda_2v_2。在顶点处,对应权重为1,其余为0;三角形内部权重非负,外部则可能出现负值。

取三角形(4,4)、(28,4)、(28,28),点(20,12)恰好是三个顶点的平均,故三个权重都为1/3。程序独立核对这一手算结果,后续插值可以复用同一个权重函数。

覆盖规则没有改变面积公式。某些实现会对非接纳边的边函数加一个整数偏置以简化比较,但那种偏置只能用于覆盖,不应带进重心权重。否则边界规则会污染颜色、UV等属性插值。当前代码直接比较边界条件,重心坐标始终使用原始E。

若A=0,面积比没有定义。本实现让退化三角形覆盖为空,并拒绝调用它的重心坐标函数。把分母强制改成一个很小的数不会创造出合法三角形。

三张程序结果与计数

下列图像来自实际C++程序,原始尺寸均为32×32,最近邻放大到320×320。橙色与蓝色分别标记仅属于一个三角形的样本,红色标记重复归属,黑色为无归属。两种普通颜色先按第04篇线性RGB约定编码,诊断红色与黑色使用端点码值。

全含边,红线上的样本归属两次:

全含边规则,红色对角线有24个重复样本

全排边,黑线上的样本无人接纳:

全排边规则,黑色对角线漏掉24个样本

top-left,共享边只由一个三角形接纳:

top-left规则,576个样本各归属一次
规则 至少一次覆盖的样本 漏算 重复归属
全含边 576 0 24
全排边 552 24 0
top-left 576 0 0

全含边的576不是执行覆盖的总次数:其中24个被接纳两次,总归属次数是600。如果只有最后写入颜色而不记录计数,后写三角形可能掩盖重复,画面未必出现明显红线。因此诊断图是由实际归属计数着色,而不是普通“后写覆盖前写”的结果。

检查器还将整个矩形移动半个像素,使其水平、竖直外边也穿过样本中心。它确认上边与左边包含、下边与右边排除,仍然恰好576个样本各归属一次。另将第一个三角形反向排列,逐点比较覆盖一致。

从正确性到扫描效率

当前小场景逐一检查32×32个样本,没有性能比较。边函数对p是线性的,若沿x方向移动一个坐标单位,有 E(x+1,y)=E(x,y)dyE(x+1,y)=E(x,y)-dy;沿y方向则加dx。在固定格实现中,跨一个像素相当于移动256个整数单位,增量也要乘256。

这提供了后续逐行增量扫描的依据。还可以先计算并裁到图像范围的三角形包围盒,减少无关样本检查。优化不应改变边界归属、量化或重心约定,否则同一份测试场景必须重新验证。

运行方式:

1
2
make -C examples/computer-graphics check
examples/computer-graphics/build/raster_check examples/computer-graphics/build

程序输出三张PPM,检查三组计数、手算重心、反向绕序、半像素边界与退化行为;缺少输出目录参数时返回非零。完整日志位于writing-plans/computer-graphics/evidence/05-cpu.txt。没有随机数、深度测试或GPU执行结论。

练习与自检

练习一:重复次数与覆盖数。 如果共享对角线上24个样本全部被两个三角形接纳,为什么“至少一次覆盖”的数量仍为576,而不是600?普通颜色输出能否保证看见这个错误?

答案:至少一次覆盖统计的是样本位置的集合,一个位置重复写也只算一个。600是按归属事件累加的次数。后写颜色可能覆盖前写颜色,不能保证肉眼看见重复;应保留每个样本的计数或执行明确的共享边断言。

练习二:退化与权重。 将顶点改为(4,4)、(8,8)、(12,12),为什么不能继续用三条边的非负条件和面积比?如果把所有权重都设成1/3是否合理?

答案:三点共线,A=0,半平面条件不再围成有面积的区域,面积比除零。人为设权重只能得到一个指定点,无法定义整个退化图元上的二维插值。本实现选择不覆盖,并在请求重心坐标时报告错误。

系列导航与资料

前篇:04:为什么平均颜色会发暗系列入口。下一篇:06:哪个表面可见

共享实现为examples/computer-graphics/raster.hpp,实验为raster_check.cpp。CPU规则与子像素精度均为本系列明确选择,不冒充已经测得的GPU硬件行为。