计算机图形:三角形光栅化

三角形光栅化

光栅化是将几何数据经一系列变换,最终转换为像素,而在屏幕上显示的过程.

直线光栅化,在2D屏幕上,对两端点间插值,绘制一条直线(段). 常用中点算法和Bresenham算法,Bresensham算法参见Bresenham画直线算法(所有斜率).

类似地,视口变换后,需要在2D屏幕上绘制三角形\(\bm{abc}\),顶点坐标分别为\(a(x_0,y_0),b(x_1,y_1),c(x_2,y_2)\). 三角形的绘制,需要用顶点的颜色或其他属性插值,即重心坐标插值(详见计算机图形:三角形及重心空间).

假设三顶点颜色\(\bm{c_0,c_1,c_2}\),那么三角形中任一点\(p(x,y)\)重心插值坐标为\((α,β,γ)\),颜色为:

\[\bm{c}=α\bm{c_0}+β\bm{c_1}+γ\bm{c_2} \]

tips: Gouraud明暗处理中,用这种方法对光强进行插值.

绘制三角形轮廓

可用中点算法(midpoint algorithm)绘制三角形轮廓.

img

图片from 计算机是怎么画线的?中点算法与Bresenham算法

中点算法思想:以斜率>0为例,画直线要确定下一像素时,将位于右侧和右上侧的2个待选像素中心点的中点代入直线方程,根据中点位于该直线的上方 or 下方决定下个像素. 当中点位于上侧,说明直线靠近右侧像素;反之,说明直线靠近右上侧像素.

填充三角形内部

总流程:重心坐标 \(\xrightarrow{α,β,γ\in (0,1)?}\) 是否像素绘制\(\xrightarrow{顶点插值颜色}\)像素颜色

img

平面上任一点\(p(x,y)\),都能用以三角形三顶点\(\bm{a,b,c}\)为基础的重心坐标表示.

\[\bm{p}=α\bm{a}+β\bm{b}+γ\bm{c},α=\frac{A_a}{A},β=\frac{A_b}{A},γ=\frac{A_c}{A} \]

其中,\(A_a,A_b,A_c,A\)分别表示三角形\(pbc,pac,pab,abc\)面积. 由于都是三角形面积比值,可以换算成同底不同高的比值.

该公式可用于颜色等属性插值.

如何判断像素点位于三角形内部?

方法一:
当像素的重心坐标\(α,β,γ\in(0,1)\)时,认为像素中心位于三角形内,而避免顺序问题、消除孔洞.

方法二:
前面针对平面多边形的奇偶规则和非零环绕数,也是适用的(见计算机图形:输出图元). 不过,针对三角形,还有更简单的办法.

img

如上图,以逆时针顺序建立\(△abc\).

平面上一点p位于三角形内部时,则叉积\(\overrightarrow{ab}\times \overrightarrow{ap}\)垂直平面向外(+z轴方向),同样地,\(\overrightarrow{bc}\times \overrightarrow{bp}, \overrightarrow{ca}\times \overrightarrow{cp}\)也是+z轴方向.

p位于三角形外部时,\(\overrightarrow{ab}\times \overrightarrow{ap}\)为-z轴方向,\(\overrightarrow{bc}\times \overrightarrow{bp}, \overrightarrow{ca}\times \overrightarrow{cp}\)为+z轴方向.

也就是,可通过向量叉积判断p是否在三角形内部:

1)当\(\overrightarrow{ab}\times \overrightarrow{ap}, \overrightarrow{bc}\times \overrightarrow{bp}, \overrightarrow{ca}\times \overrightarrow{cp}\)符号相同时,p在三角形内部;否则,在三角形外部.

2)当符号为0时,p在对应的三角形边上.

三角形内部像素颜色插值

如何利用重心插值,填充三角形内部像素?

可遍历屏幕所有像素点,当点位于三角形内部时,通过颜色插值进行绘制. 实践中,为减少要遍历的像素点,可用三顶点构造一个边界矩形,将检索范围限制在矩形内.

img

算法如下:

// a(xa,ya),b(xb,yb),c(xc,yc) consits of a 2D triangle, including color property
xmin = floor(x[i]);
xmax = ceiling(x[i]);
ymin = floor(y[i]);
ymax = ceiling(y[i]);
for (y = ymin; y <= ymax; y++) {
    for (x = xmin ; x <= xmax; x++) {
        alpha = f_bc(x, y) / f_bc(xa, ya);
        beta  = f_ac(x, y) / f_ac(xb, yb);
        gamma = f_ab(x, y) / f_ab(xc, yc);
        if (alpha > 0 && beta > 0 && gamma > 0) {
            color = alpha * a + beta * b + gamma * c; // color of pixel(x,y)
            drawpixel(x, y, color);
        }
    }
}

\(f_{ab}、f_{bc}、f_{ac}\)分别表示直线ab、bc、ac,对应方程:

\[\begin{aligned} f_{ab}(x,y)&=(y_a-y_b)x+(x_b-x_a)y+x_ay_b-x_by_a\\ f_{bc}(x,y)&=(y_b-y_c)x+(x_c-x_b)y+x_by_c-x_cy_b\\ f_{ac}(x,y)&=(y_c-y_a)x+(x_a-x_c)y+x_cy_a-x_ay_c \end{aligned} \]

推导可参见计算机图形:三角形及重心空间

问题:为什么循环体内只测试\(α,β,γ>0\),而不是验证\(\in (0,1)\)

因为\(α+β+γ=1\),如果3者都>0,那么都位于区间\((0,1)\).

  • 增量法节省计算量

绘制直线时,考虑到相邻像素,有以下关系:

\[\begin{aligned} f(x,y)&=Ax+By+C\\ \implies f(x+1,y)&=f(x,y)+A\\ f(x,y+1)&=f(x,y)+B \end{aligned} \]

如此,只需1次加法,就能求出\(f(x,y+1),f(x+1,y)\),从而减少求\(α.β,γ\)时间.

处理公共边界

如果一个像素中心位于2个三角形的公共边,该如何处理?

有3种策略:

  • 下策:不绘制该像素,但是会在2个三角形之间形成孔洞.

  • 中策:2个三角形都绘制该像素,但如果三角形是透明的,会造成双重着色.

  • 上侧:属于哪个三角形不重要,选择明确即可.

一种简单的选择方法,是找一个屏幕外的点,比如\(p_0(-1,-1)\),通常认为屏幕外的点不在直线上.

\(f_{bc}(\bm{p_0})\cdot f_{bc}(\bm{{a_1}})<0\),表明\(\bm{p_0},\bm{a_1}\)在直线bc不同侧;

\(f_{bc}(\bm{p_0})\cdot f_{bc}(\bm{{a_2}})<0\),表明\(\bm{p_0},\bm{a_2}\)在直线bc同侧;

那么,选择\(△a_2bc\)绘制公共边bc.

img

\(α=0\)时, \(f_{bc}(x,y)=0\),代表p在bc上;
\(β=0\)时, \(f_{ac}(x,y)=0\),代表p在ac上;
\(γ=0\)时, \(f_{ab}(x,y)=0\),代表p在ab上.

算法如下:

// a(xa,ya),b(xb,yb),c(xc,yc) consits of a 2D triangle, including color property
xmin = floor(x[i]);
xmax = ceiling(x[i]);
ymin = floor(y[i]);
ymax = ceiling(y[i]);
p0 = (-1, -1);
f_alpha = f_bc(xa, ya);
f_beta  = f_ac(xb, yb);
f_gamma = f_ab(xc, yc);

for (y = ymin; y <= ymax; y++) {
    for (x = xmin ; x <= xmax; x++) {
        alpha = f_bc(x, y) / f_alpha;
        beta  = f_ac(x, y) / f_beta;
        gamma = f_ab(x, y) / f_gamma;

        if (alpha >= 0 && belta >= 0 && gamma >= 0 ) {
            if ((alpha > 0 || f_alpha * f_bc(p0.x, p0.y)) 
                && (beta > 0 || f_beta * f_ac(p0.x, p0.y))
                && (gamma > 0 || f_gamma * f_ab(p0.x, p0.y))
            ) {
                color = alpha * a + beta * b + gamma * c; // color of pixel(x,y)
                drawpixel(x, y, color);
            }
        }
    }
}

这样做的好处:只需在当前三角形,判断每个顶点是否与\(p_0\)一样,在对边的同侧,而无需知道是否有公共边.

当然,也存在缺点:\(p_0\)可能在三角形某条边所在直线,从而无法判定该边应在哪个三角形中绘制. 不过,大多数情况,\(p_0\)都能起到作用.

待解决疑问:有没有一种可能性,边bc不是公共边,因为\(\bm{a},\bm{p_0}\)不在同侧,因此不算在当前三角形内绘制,从而导致没有绘制?

异常处理

对于退化三角形,三角形退化为一条直线,如\(f_α=0\),即a到bc距离为0,即a在bc上. 此时,求α的除数为0,显然不行.

如何处理?
阻止异常数据. 在算法作除法前,先测试是否为退化三角形,如果是,就当成直线绘制;如果不是,就继续. 当然,浮点数的计算误差也应该考虑在内.

参考

[1] Marschner S , Shirley P , Ashikhmin M ,et al.Fundamentals of computer graphics, fourth edition[J].A. K. Peters, Ltd. 2015.

[2] 计算机图形学——三角形光栅化

posted @ 2024-05-16 11:11  明明1109  阅读(115)  评论(0编辑  收藏  举报