光栅化渲染:光栅化算法实现

光栅化是将图元转换为二维图像的过程。 该图像的每个点都包含颜色和深度等信息。 因此,对图元进行光栅化由两部分组成。 第一个是确定窗口坐标中整数网格的哪些方格被图元占据。 第二个是为每个这样的方块分配颜色和深度值。 (OpenGL 规范)

NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎

在上一章中,我们学习了如何以某种方式执行光栅化算法的第一步,即将三角形从 3D 空间投影到画布上。 这个定义实际上并不完全准确,因为我们所做的是将三角形从相机空间转换到屏幕空间,正如上一章提到的,屏幕空间也是一个三维空间。 然而,屏幕空间中顶点的 x 和 y 坐标对应于画布上三角形顶点的位置,并且通过将它们从屏幕空间转换到 NDC 空间,然后最终从 NDC 空间转换到光栅空间, 我们最终得到的是光栅空间中顶点的二维坐标。 最后,我们还知道屏幕空间中顶点的 z 坐标保留相机空间中顶点的原始 z 坐标(反转以便我们处理正数而不是负数)。

图1:通过测试,如果图像中的像素与三角形重叠,我们就可以绘制出该三角形的图像。 这就是光栅化算法的原理

接下来我们需要做的是循环图像中的像素,并找出这些像素是否与“三角形的投影图像”重叠(图 1)。 在图形 API 规范中,此测试有时称为内部-外部测试(inside-outside test)或覆盖测试(coverage test)。 如果是,我们将图像中的像素设置为三角形的颜色。 这个想法很简单,但当然,我们现在需要想出一种方法来查找给定像素是否与三角形重叠。 这本质上就是我们在本章中要研究的内容。

我们将了解光栅化中通常使用的方法来解决这个问题。 它使用了一种称为边缘函数(edge function)的技术,我们现在将描述和研究该技术。 该边缘函数还将提供有关三角形投影图像内像素位置(称为重心坐标)的有价值的信息。 重心坐标在计算像素重叠的三角形表面上的点的实际深度(或 z 坐标)中起着至关重要的作用。 我们还将在本章中解释什么是重心坐标以及它们是如何计算的。

在本章结束时,你将能够生成一个非常基本的光栅化器(rasterizer)。 在下一章中,我们将研究这种非常简单的光栅化算法实现可能存在的问题。 我们将列出这些问题是什么,并研究它们通常是如何解决的。

为了优化算法,人们进行了大量的研究。 本课程的目标不是教你如何编写或开发基于光栅化算法的优化且高效的渲染器。 本课程的目标是教授渲染技术的基本原理。 不要认为我们在这些章节中介绍的技术没有被使用。 它们在某种程度上被使用,但它们在 GPU 或生产渲染器的 CPU 版本上的实现方式很可能是同一想法的高度优化版本。 真正重要的是理解其原理及其一般工作原理。 基于此,你可以自己研究用于加速算法的不同技术。 但本课程中介绍的技术是通用的,构成了任何光栅化器的基础。

请记住,绘制三角形(因为三角形是基元,我们将在本例中使用它)是一个两步问题:

  • 我们首先需要找到哪些像素与三角形重叠。
  • 然后,我们需要定义与三角形重叠的像素应设置为哪种颜色,这个过程称为着色

光栅化阶段主要涉及第一步。 我们说本质上而不是排他的原因是,在光栅化阶段,我们还将计算称为重心坐标(barycentric  coordinates)的东西,在某种程度上,它在第二步中使用。

1、边缘函数

如上所述,有几种可能的方法来查找像素是否与三角形重叠。 最好记录较旧的技术,但在本课中,将仅介绍当今普遍使用的方法。 该方法由 Juan Pineda 在 1988 年提出,论文名为“A Parallel Algorithm for Polygon Rasterization”。

图2:Pineda方法的原理是找到一个函数,这样当我们测试给定点在这条线的哪一侧时,函数在该点位于该线的左侧时返回正数,在该线的左侧时返回负数 它位于该线的右侧,当该点恰好在线上时为零。

在我们研究Pineda的技术之前,我们首先描述一下他的方法的原理。 假设三角形的边可以看作将 2D 平面(图像平面)一分为二的线(如图 2 所示)。 Pineda方法的原理是找到一个他称之为边缘函数(edge function)的函数,这样当我们测试给定的点(图2中的点P)在这条线的哪一侧时,该函数返回一个负数。 在线左侧,当该点在线右侧时为正数,当该点恰好在线上时为零。

在图 2 中,我们将此方法应用于三角形的第一条边(由顶点 v0-v1 定义。请注意,顺序很重要)。 如果我们现在对另外两条边(v1-v2 和 v2-v0)应用相同的方法,我们可以看到有一个区域(白色三角形),其中所有点都为正(图 3):

图 3:白色区域内包含的点均位于三角形所有三个边的右侧

如果我们在这个区域内取一个点,那么我们会发现这个点位于三角形所有三个边的右侧。 如果 P 是像素中心的点,我们就可以使用此方法来查找该像素是否与三角形重叠。 如果对于这一点,我们发现边缘函数对于所有三个边缘都返回正数,则该像素包含在三角形中(或者可能位于其边缘之一)。 Pinada 使用的函数也恰好是线性的,这意味着它可以增量计算,但我们稍后会回到这一点。

明白了原理之后,我们就来看看这个函数是什么。 边函数定义为(对于由顶点 V0 和 V1 定义的边):

正如论文中提到的,该函数具有一个有用的属性,即它的值与点 (x,y) 相对于点 V0 和 V1 定义的边缘的位置有关:

  • 如果 P 位于“右侧”,则 E(P) > 0
  • 如果 P 正好在线上,则 E(P) = 0
  • 如果 P 位于“左侧”,则 E(P) < 0

该函数在数学上相当于向量 (v1-v0) 和 (P-v0) 之间叉积的大小。 我们还可以将这些向量写成矩阵形式(将其呈现为矩阵除了整齐地呈现两个向量之外没有其他兴趣):

如果我们设 A=(P-V0)且B=(V1-v0),那么也可以将向量 A 和 B 写成 2x2 矩阵:

该矩阵的行列式可以计算为:

如果现在再次用向量 (P-V0) 和 (V1-V0) 替换向量 A 和 B,将得到:

正如你所看到的,它与我们上面定义的边缘函数类似。 换句话说,边缘函数可以被视为由 2D 向量 (P-v0) 和 (v1-v0) 的分量定义的 2x2 矩阵的行列式,也可以被视为向量 ( P-V0) 和 (V1-V0)。 两个向量的叉积的行列式和大小都具有相同的几何解释。 让我们解释一下。

当我们查看两个 3D 向量之间的叉积结果时,会更容易理解正在发生的情况(图 4):

图 4:向量 B(蓝色)和 A(红色)的叉积得出向量 C(绿色),该向量垂直于 A 和 B 定义的平面(假设采用右手定则约定)。 矢量 C 的大小取决于 A 和 B 之间的角度。它可以是正值,也可以是负值

在 3D 中,叉积返回与两个原始向量垂直(或正交)的另一个 3D 向量。 但正如你在图 4 中看到的,该正交向量的大小也会随着两个向量相对于彼此的方向而变化。 在图 4 中,我们假设右手坐标系。 当两个向量 A(红色)和 B(蓝色)完全指向相同方向或相反方向时,第三个向量 C(绿色)的大小为零。 矢量 A 的坐标为 (1,0,0) 并且是固定的。 当向量B的坐标为(0,0,-1)时,则绿色向量、向量C的坐标为(0,-1,0)。 如果我们要找到它的“有符号”大小,我们会发现它等于-1。 另一方面,当向量 B 的坐标为 (0,0,1) 时,C 的坐标为 (0,1,0),且其有符号量值等于 1。在一种情况下,“有符号”量值为负,并且在第二种情况下,有符号的幅度为正。

事实上,在 3D 中,向量的大小可以解释为以 A 和 B 为边的平行四边形的面积:

如图 5 所示:

图5:平行四边形的面积是向量A和B形成的矩阵的行列式的绝对值

尽管上式的符号指示了向量 A 和 B 相对于彼此的方向,但面积应始终为正。 当相对于A,B位于向量A和与A正交的向量(我们称这个向量D;注意A和D形成二维笛卡尔坐标系)定义的半平面内时,则方程的结果为正的。 当 B 在相对的半平面内时,方程的结果为负(图 6):

图6:平行四边形的面积是向量A和B构成的矩阵的行列式的绝对值。

解释该结果的另一种方式是,当角度theta是在范围[0,pi]内时为正,在范围[pi, 2*pi]内为负。请注意,当theta正好等于 0 或pi时,那么叉积或边缘函数返回 0。

如果角度theta小于pi,那么“有符号”面积就是正数。 如果角度大于pi,那么“有符号”面积就是负数。 该角度是相对于由向量 A 和 D 定义的笛卡尔坐标计算的。可以看出它们将平面分成两半。

为了确定一个点是否在三角形内,我们真正关心的是用于计算平行四边形面积的函数的符号。 然而,区域本身在光栅化算法中也起着重要作用; 它用于计算三角形中点的重心坐标,这是我们接下来将研究的技术。 3D 和 2D 中的叉积具有相同的几何解释,因此两个 2D 向量之间的叉积也返回由这两个向量定义的平行四边形的“有符号”面积。 唯一的区别是,在 3D 中,要计算平行四边形的面积,你需要使用以下方程:

而在 2D 中,该面积由叉积本身给出(如前所述,也可以解释为 2x2 矩阵的行列式):

从实际角度来看,我们现在需要做的就是测试为三角形的每条边计算的边函数的符号以及由点和边的第一个顶点定义的另一个向量(图 7):

图 7:如果边函数对于三个指示的向量对返回正数,则 P 包含在三角形中

公式如下:

如果所有三个测试均为正或等于 0,则该点位于三角形内部(或位于三角形的一条边上)。 如果任何一项测试为负,则该点位于三角形之外。 在代码中我们得到:

bool edgeFunction(const Vec2f &a, const Vec3f &b, const Vec2f &c)
{
    return ((c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x) >= 0);
}

bool inside = true;
inside &= edgeFunction(V0, V1, p);
inside &= edgeFunction(V1, V2, p);
inside &= edgeFunction(V2, V0, p);

if (inside == true) {
    // point p is inside triangles defined by vertices v0, v1, v2
    ...
}

2、边缘函数的替代方案

除了边缘函数方法之外,还有其他方法来查找像素是否与三角形重叠,但是正如本章介绍中提到的,我们在本课中不会研究它们。 但仅供参考,另一种常见技术称为扫描线光栅化(scanline rasterization)。 它基于通常用于画线的Brenseham算法。 GPU 使用边缘方法主要是因为它比扫描线方法更通用,扫描线方法也比边缘方法更难并行运行,但我们不会在本课中提供有关此主题的更多信息。

3、当心! 边缘顺序问题

图8:顺时针和逆时针环绕

我们一直在讨论但在 CG 中非常重要的事情之一是声明构成三角形的顶点的顺序。 它们是两种可能的约定,如图 8 所示:顺时针或逆时针排序或环绕。 顺序很重要,因为它本质上定义了三角形的一个重要属性,即法线的方向。 请记住,三角形的法线可以通过两个向量 A=(V2-V0) 和 B=(V1-V0) 的叉积来计算。 假设 V0={0,0,0}、V1={1,0,0} 且 V2={0,-1,0},则 (V1-V0)={1,0,0} 且 (V2 -V0)={0,-1,0}。 现在让我们计算这两个向量的叉积:

但是,如果按逆时针顺序声明顶点,则 V0={0,0,0}、V1={0,-1,0} 且 V2={1,0,0}、(V1-V0)= {0,-1,0} 且 (V2-V0)={1,0,0}。 让我们再次计算这两个向量的叉积:

图 9:顺序定义了法线的方向

图 10:顺序定义三角形内的点是正值还是负值

正如预期的那样,两条法线指向相反的方向。 由于许多不同的原因,法线的方向非常重要,但最重要的原因之一是面部剔除。 大多数光栅化器甚至光线追踪器都可能无法渲染法线背向相机的三角形。 这称为背面剔除(backface culling)。 大多数渲染 API(例如 OpenGL 或 DirectX)都提供关闭背面剔除的选项,但是,你仍然应该意识到顶点排序在渲染内容等方面发挥着重要作用。 毫不奇怪,边缘函数是其中之一。

在我们解释为什么它在我们的特定情况下很重要之前,我们先假设在选择顺序时没有特定的规则。 实际上,渲染器实现中的许多细节可能会改变法线的方向,因此不能假设通过按特定顺序声明顶点,你将得到法线将以某种方式定向的保证。 例如,可以使用 (V0-V1) 和 (V2-V1) 来代替叉积中的向量 (V1-V0) 和 (V2-V0)。 它会产生相同的法线,但会翻转。 即使你使用向量 (V1-V0) 和 (V2-V0),请记住叉积中向量的顺序会改变法线的符号:A x B = - B x A。因此法线的方向也取决于叉积中向量的顺序。 出于所有这些原因,不要试图假设以一种顺序而不是另一种顺序声明顶点会给你一个结果或另一个结果。 但重要的是,一旦你坚持了你所选择的约定。 通常,OpenGL 和 DirectX 等图形 API 期望以逆时针顺序声明三角形。 我们还将使用逆时针环绕。 现在让我们看看排序如何影响边缘函数。

为什么环绕书讯对于边缘函数很重要? 你可能已经注意到,从本章开始,在所有图中我们都是按顺时针顺序绘制三角形顶点的。 我们还将边缘函数定义为:

如果我们遵守这个约定,那么顶点 A 和 B 定义的线右侧的点将为正。 例如,V0V1、V1V2 或 V2V0 右侧的点将为正。 然而,如果我们以逆时针顺序声明顶点,则顶点 A 和 B 定义的边右侧的点仍为正,但它们将位于三角形之外。 换句话说,与三角形重叠的点不是正值而是负值(图 10)。 你仍然可以对边缘函数稍加修改即可使代码处理正数:

总之,根据你使用的边缘排序约定,可能需要使用边缘函数的一个或另一个版本。

4、重心坐标

图11:平行四边形的面积是三角形面积的两倍

无需计算重心坐标(barycentric coordinates)即可使光栅化算法正常工作。 对于渲染技术的简单实现,你所需要的只是投影顶点并使用我们上面描述的边缘函数之类的技术来查找像素是否在三角形内部。 这是生成图像唯一必要的两个步骤。

然而,正如我们上面所解释的,边缘函数的结果可以解释为由向量 A 和 B 定义的平行四边形的面积,可以直接用于计算这些重心坐标。 因此,同时研究边缘函数和重心坐标是有意义的。

在我们进一步讨论之前,让我们解释一下这些重心坐标是什么。 首先,它们以一组三个浮点数形式出现,在本课中,我们将表示为λ0、λ1和λ2。存在许多不同的约定,但维基百科也使用希腊字母 lambda (λ),同时也被其他作者使用(希腊字母 omega有时也使用)。 这并不重要,你可以按照你想要的方式称呼它们。 简而言之,坐标可用于按以下方式定义三角形上的任意点:

通常,V0、V1 和 V2 是三角形的顶点。 这些坐标可以采用任何值,但对于三角形内部(或位于其边缘之一)的点,它们只能在 [0,1] 范围内,并且三个坐标之和等于 1。 也就是说:

如果你愿意的话,这是一种插值形式。 它们有时也被定义为三角形顶点的权重(这就是为什么在代码中我们将用字母 w 表示它们)。 与三角形重叠的点可以定义为“一点点V0加一点点V1加一点点V2”。 请注意,当任何坐标为 1(这意味着在这种情况下其他坐标必然为 0)时,点 P 等于三角形的一个顶点。 例如如果
那么P等于V2。

图12:我们如何找到P的颜色?

插值三角形的顶点来查找三角形内点的位置并不是很有用。 但该方法也可用于在三角形表面上插值在三角形顶点处定义的任何量或变量。 例如,假设你在三角形的每个顶点定义了一种颜色。 假设 V0 为红色,V1 为绿色,V2 为蓝色(图 12)。 你想要做的是找出这三种颜色如何在三角形表面上插值。 如果你知道三角形上点 P 的重心坐标,那么它的颜色Cp(三角形顶点颜色的组合)定义为:

这是一种非常方便的技术,对于给三角形着色非常有用。 与三角形顶点相关的数据称为顶点属性(vertex attribute)。 这是CG中非常常见也非常重要的技术。 最常见的顶点属性是颜色、法线和纹理坐标。 这在实践中意味着,通常当你定义三角形时,不仅将三角形顶点传递给渲染器,而且还将其关联的顶点属性传递给渲染器。 例如,如果要对三角形进行着色,则可能需要颜色和法线顶点属性,这意味着每个三角形将由 3 个点(三角形顶点位置)、3 种颜色(三角形顶点的颜色)和 3 个点定义。 法线(三角形顶点的法线)。 法线也可以在三角形的表面上进行插值。 插值法线用于一种称为平滑着色(smooth shading)的技术,该技术由 Henri Gouraud 首次提出。 稍后我们将在讨论着色时解释这项技术。

我们如何找到这些重心坐标? 事实证明很简单。 如上所述,当我们提出边缘函数时,边缘函数的结果可以解释为由向量 A 和 B 定义的平行四边形的面积。如果你看图 8,你可以很容易地看到三角形的面积 由顶点 V0、V1 和 V2 定义的,只是由向量 A 和 B 定义的平行四边形面积的一半。因此,三角形的面积是平行四边形面积的一半,我们知道可以通过交叉计算 -两个 2D 向量 A 和 B 的乘积:

图13:将P连接到三角形的每个顶点形成三个子三角形

如果点 P 在三角形内部,那么通过图 13 可以看出,我们可以绘制三个子三角形:V0-V1-P(绿色)、V1-V2-P(洋红色)和 V2-V0 -P(青色)。 很明显,这三个子三角形面积之和等于三角形 V0-V1-V2 的面积:

图 14:λ0、λ1 和 λ2 的值取决于 P 在三角形上的位置

让我们首先尝试直观地了解它们是如何工作的。 如果你查看图 14,这将变得更容易。该系列中的每个图像都显示了当点 P(最初位于由顶点 V1-V2 定义的边缘上)向 V0 移动时,子三角形会发生什么情况。 一开始,P 正好位于边 V1-V2 上。 在某种程度上,这类似于两点之间的基本线性插值。 换句话说,我们可以这样写:

由于 λ1+λ2=1,因此 λ2=1-λ1。 在这种特殊情况下更有趣的是,如果使用重心坐标计算 P 位置的通用方程为:

因此,它清楚地表明,在这种特殊情况下,λ0 等于 0:

这很简单。 另请注意,在第一张图像中,红色三角形不可见。 另请注意,P 与 V1 的距离比与 V2 的距离更近。 因此,不知何故,λ1 必然大于 λ2。 另请注意,在第一张图像中,绿色三角形比蓝色三角形大。 因此,如果我们总结一下:当红色三角形不可见时,λ0 等于 0。λ1 大于 λ2,绿色三角形大于蓝色三角形。 因此,在某种程度上,三角形的面积和重心坐标之间似乎存在某种关系。 此外,红色三角形似乎与 λ0 相关,绿色三角形与 λ1 相关,蓝色三角形与 λ2 相关:

图 15:要计算重心坐标之一,请使用由 P 定义的三角形面积以及与需要计算重心坐标的顶点相对的边

还要注意,在这种特殊情况下,蓝色和绿色三角形消失了,并且三角形 V0-V1-V2 的面积与红色三角形的面积相同。 这证实了我们的直觉,即子三角形的面积和重心坐标之间存在关系。 最后,根据上述观察,我们还可以说每个重心坐标与由与重心坐标关联的顶点直接相对的边和点 P 定义的子三角形的面积有某种关系。换句话说( 图15):

  • λ0 与 V0 相关。 V0 相对的边是 V1-V2。 V1-V2-P 定义红色三角形。
  • λ1 与 V1 相关。 V1 相对的边是 V2-V0。 V2-V0-P 定义绿色三角形。
  • λ2 与V2 相关。 V2 相对的边是 V0-V1。 V0-V1-P 定义蓝色三角形。

如果你还没有注意到,红色、绿色和蓝色三角形的面积是由我们之前用来确定 P 是否在三角形内部的相应边缘函数给出的,除以 2(记住边缘函数 本身给出由两个向量 A 和 B 定义的平行四边形的“有符号”区域,其中 A 和 B 可以是三角形的三个边中的任何一个):

重心坐标可以计算为子三角形面积与三角形面积 V0V1V2 之间的比率:

除以三角形面积的作用本质上是标准化坐标。 例如,当 P 与 V0 位置相同时,则三角形 V2V1P(红色三角形)的面积与三角形 V0V1V2 的面积相同。 因此,1 除以 over 得到 1,即坐标 λ0 的值。 由于在这种情况下,绿色和蓝色三角形的面积为 0,因此 λ1 和 λ2 等于 0,我们得到:

这正是我们所期望的。

为了计算三角形的面积,我们可以使用前面提到的边函数。 这适用于子三角形以及主三角形 V0V1V2。 然而,边缘函数返回平行四边形的面积而不是三角形的面积(图 8),但由于重心坐标是根据子三角形面积与主三角形面积之比计算的,因此我们可以忽略除以 2 (分子和分母中的除法相互抵消):

注意:

让我们看看它在代码中的样子。 我们之前已经计算了边函数来测试点是否在三角形内。 只是,在我们之前的实现中,我们只是根据函数的结果是正还是负来返回 true 或 false。 为了计算重心坐标,我们需要边缘函数的实际结果。 我们还可以使用边函数来计算三角形的面积(乘以 2)。 下面是一个实现版本,用于测试点 P 是否在三角形内,如果是,则计算其重心坐标:

float edgeFunction(const Vec2f &a, const Vec3f &b, const Vec2f &c)
{
    return (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x);
}

float area = edgeFunction(v0, v1, v2); // area of the triangle multiplied by 2
float w0 = edgeFunction(v1, v2, p); // signed area of the triangle v1v2p multiplied by 2
float w1 = edgeFunction(v2, v0, p); // signed area of the triangle v2v0p multiplied by 2
float w2 = edgeFunction(v0, v1, p); // signed area of the triangle v0v1p multiplied by 2

// if point p is inside triangles defined by vertices v0, v1, v2
if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
    // barycentric coordinates are the areas of the sub-triangles divided by the area of the main triangle
    w0 /= area;
    w1 /= area;
    w2 /= area;
}

让我们尝试使用此代码来生成实际图像。

5、插值与外推

图 16:内插法与外推法

值得注意的一件事是,重心坐标的计算与其相对于三角形的位置无关。 换句话说,如果该点位于三角形外部,则坐标有效。 当点在内部时,使用重心坐标来评估顶点属性的值称为插值(interpolation),当点在外部时,我们称为外插(extrapolation)。 这是一个重要的细节,因为在某些情况下,我们必须评估可能不与三角形重叠的点的给定顶点属性的值。 更具体地说,例如,需要计算三角形纹理坐标的导数。 这些导数用于正确过滤纹理。 如果你有兴趣了解有关此特定主题的更多信息,可以阅读有关纹理映射的课程。 同时,你需要记住的是,即使该点没有覆盖三角形,重心坐标也是有效的。 你还需要了解顶点属性外插和插值之间的区别。

6、光栅化规则

在某些特殊情况下,一个像素可能与多个三角形重叠。 当像素恰好位于两个三角形共享的边缘上时,就会发生这种情况,如图 17 所示:

图 17:像素可能覆盖两个三角形共享的边缘

这样的像素将通过两个三角形的覆盖测试。 如果它们是半透明的,由于半透明对象的组合方式,像素与两个三角形重叠的地方可能会出现暗边缘(想象一下两个叠加的半透明塑料片。表面更加不透明,并且 看起来比单张纸更暗)。 您将得到类似于图 18 中看到的内容,这是一条较暗的线,两个三角形共享一条边:

图 18:如果几何体是半透明的,则像素与两个三角形重叠的位置可能会出现暗边

这个问题的解决方案是提出某种规则,保证一个像素永远不会与共享一条边的两个三角形重叠两次。 我们该怎么做呢? 大多数图形 API(例如 OpenGL 和 DirectX)都定义了所谓的左上角规则。 我们已经知道,如果一个点位于三角形内部或位于任何三角形边上,则覆盖测试将返回 true。 不过,左上角规则所说的是,如果像素或点位于三角形内部或位于三角形的顶边或任何被视为左边的边上,则该像素或点被视为与三角形重叠。 什么是顶边(top edge)和左边(left edge)? 如果查看图 19,你可以轻松明白顶部和左侧边缘的含义:

图 19:顶部和左侧边缘

  • 顶边是完全水平的边,其定义顶点位于第三个顶点之上。 从技术上讲,这意味着向量 V[(X+1)%3]-V[X] 的 y 坐标等于 0,并且其 x 坐标为正(大于 0)。
  • 左边缘本质上是向上的边缘。 请记住,在我们的例子中,顶点是按顺时针顺序定义的。 如果边缘各自的向量 V[(X+1)%3]-V[X](其中 X 可以是 0、1、2)具有正 y 坐标,则认为边缘向上。

在伪代码中我们有:

// Does it pass the top-left rule?
Vec2f v0 = { ... };
Vec2f v1 = { ... };
Vec2f v2 = { ... };

float w0 = edgeFunction(v1, v2, p); 
float w1 = edgeFunction(v2, v0, p); 
float w2 = edgeFunction(v0, v1, p); 

Vec2f edge0 = v2 - v1;
Vec2f edge1 = v0 - v2;
Vec2f edge2 = v1 - v0;

bool overlaps = true;

// If the point is on the edge, test if it is a top or left edge, 
// otherwise test if  the edge function is positive
overlaps &= (w0 == 0 ? ((edge0.y == 0 && edge0.x > 0) ||  edge0.y > 0) : (w0 > 0));
overlaps &= (w1 == 0 ? ((edge1.y == 0 && edge1.x > 0) ||  edge1.y > 0) : (w1 > 0));
overlaps &= (w1 == 0 ? ((edge2.y == 0 && edge2.x > 0) ||  edge2.y > 0) : (w2 > 0));

if (overlaps) {
    // pixel overlap the triangle
    ...
}

该版本作为概念证明是有效的,但高度未优化。 关键思想是首先检查返回函数返回的值是否等于 0,这意味着该点位于边缘上。 在本例中,我们测试相关边缘是否是左上角边缘。 如果是,则返回 true。 如果edge函数返回的值不等于0,那么如果该值大于0,我们就返回true。本课提供的程序中我们不会实现左上角规则。

7、整合:查找像素是否与三角形重叠

让我们在生成实际图像的程序中测试本章中学到的不同技术。 假设我们已经投影了三角形(查看本课的最后一章以了解光栅化算法的完整实现)。 我们还将为三角形的每个顶点分配一种颜色。 以下是图像的形成方式。 我们将循环图像中的所有像素,并使用边缘函数方法测试它们是否与三角形重叠。 三角形的所有三个边都根据像素的当前位置进行测试,如果边缘函数为所有边返回正数,则像素与三角形重叠。 然后,我们可以计算像素的重心坐标,并通过对三角形每个顶点定义的颜色进行插值,使用这些坐标来对像素进行着色。 帧缓冲区的结果保存到 PPM 文件(您可以使用 Photoshop 读取该文件)。 程序的输出如图 20 所示:

图 20:使用重心坐标进行顶点属性线性插值的示例

请注意,该程序的一种可能的优化是循环遍历三角形边界框中包含的像素。 我们没有在此版本的程序中进行此优化,但如果你愿意,可以自己进行优化。

另请注意,在此版本的程序中,我们将点 P 移动到每个像素的中心。你也可以使用像素整数坐标。 我们将在下一章中介绍有关此主题的更多详细信息。

// c++ -o raster2d raster2d.cpp
// (c) www.scratchapixel.com

#include <cstdio>
#include <cstdlib>
#include <fstream>

typedef float Vec2[2];
typedef float Vec3[3];
typedef unsigned char Rgb[3];

inline
float edgeFunction(const Vec2 &a, const Vec2 &b, const Vec2 &c)
{ return (c[0] - a[0]) * (b[1] - a[1]) - (c[1] - a[1]) * (b[0] - a[0]); }

int main(int argc, char **argv)
{
    Vec2 v0 = {491.407, 411.407};
    Vec2 v1 = {148.593, 68.5928};
    Vec2 v2 = {148.593, 411.407};
    Vec3 c0 = {1, 0, 0};
    Vec3 c1 = {0, 1, 0};
    Vec3 c2 = {0, 0, 1};
    
    const uint32_t w = 512;
    const uint32_t h = 512;
    
    Rgb *framebuffer = new Rgb[w * h];
    memset(framebuffer, 0x0, w * h * 3);
    
    float area = edgeFunction(v0, v1, v2);
    
    for (uint32_t j = 0; j < h; ++j) {
        for (uint32_t i = 0; i < w; ++i) {
            Vec2 p = {i + 0.5f, j + 0.5f};
            float w0 = edgeFunction(v1, v2, p);
            float w1 = edgeFunction(v2, v0, p);
            float w2 = edgeFunction(v0, v1, p);
            if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
                w0 /= area;
                w1 /= area;
                w2 /= area;
                float r = w0 * c0[0] + w1 * c1[0] + w2 * c2[0];
                float g = w0 * c0[1] + w1 * c1[1] + w2 * c2[1];
                float b = w0 * c0[2] + w1 * c1[2] + w2 * c2[2];
                framebuffer[j * w + i][0] = (unsigned char)(r * 255);
                framebuffer[j * w + i][1] = (unsigned char)(g * 255);
                framebuffer[j * w + i][2] = (unsigned char)(b * 255);
            }
        }
    }
    
    std::ofstream ofs;
    ofs.open("./raster2d.ppm");
    ofs << "P6\n" << w << " " << h << "\n255\n";
    ofs.write((char*)framebuffer, w * h * 3);
    ofs.close();
    
    delete [] framebuffer;
    
    return 0; 
}

正如你所看到的,我们可以说光栅化算法本身非常简单(并且该技术的基本实现也非常简单)。

8、下一步是什么?

有许多与重心坐标主题相关的有趣技术和琐事,但本课只是光栅化算法的介绍,因此我们不会再进一步。 不过,有一个有趣的细节是,重心坐标沿着平行于边缘的线是恒定的(如图 21 所示):

图 21:沿着平行于边缘的线,重心坐标是恒定的

在本课中,我们学习了两个重要的方法和各种概念。

首先,我们了解了边缘函数以及如何使用它来查找点 P 是否与三角形重叠。 为三角形的每条边计算边函数,并由边的第一个顶点和另一个点 P 定义第二个向量。如果对于所有三个边,函数均为正,则点 P 与三角形重叠。

此外,我们还了解到,边函数的结果还可以用于计算点 P 的重心坐标。这些坐标可用于在三角形表面上插值顶点数据或顶点属性。 它们可以被解释为各个顶点的权重。 最常见的顶点属性是颜色、法线和纹理坐标。


原文链接:光栅化算法实现 - BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/241637.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++1114新标准——统一初始化(Uniform Initialization)、Initializer_list(初始化列表)、explicit

系列文章目录 C11&14新标准——Variadic templates&#xff08;数量不定的模板参数&#xff09; C11&14新标准——Uniform Initialization&#xff08;统一初始化&#xff09;、Initializer_list&#xff08;初始化列表&#xff09;、explicit 文章目录 系列文章目录1…

Unity_使用FairyGUI搭建登录页面

Unity_使用FairyGUI搭建登录页面 1. 使用FairyGUI准备一个UI界面&#xff0c;例如&#xff1a;以下登录 2. 发布导出&#xff08;发布路径设置为Unity的Asset下任何路径&#xff09; 3. Unity编辑器安装FairyGUI包资源&#xff08;在资源商店找见并存储为我的资源&#xff0c;…

管理的五大过程和十大知识领域

PMBOK五大过程组是什么&#xff1f; PMBOK五大过程组是&#xff1a;启动过程、规划过程、执行过程、监控过程、收尾过程。 各用一句话概括项目管理知识体系五大过程组&#xff1a; 1、启动过程组&#xff1a;作用是设定项目目标&#xff0c;让项目团队有事可做&#xff1b; 2、…

TIGRE: a MATLAB-GPU toolbox for CBCT image reconstruction

TIGRE: 用于CBCT图像重建的MATLAB-GPU工具箱 论文链接&#xff1a;https://iopscience.iop.org/article/10.1088/2057-1976/2/5/055010 项目链接&#xff1a;https://github.com/CERN/TIGRE Abstract 本文介绍了基于层析迭代GPU的重建(TIGRE)工具箱&#xff0c;这是一个用于…

因光而遇·领杭设计,Yeelight易来设计师梦享会在杭州成功举办

12月13日&#xff0c;Yeelight易来设计师梦享会“因光而遇领杭设计”在杭州举办。本次活动邀请《梦想改造家》设计师朱晓鸣参与&#xff0c;与众多易来照明设计师进行了深入对话&#xff0c;分享前沿的设计理念&#xff0c;探讨智能灯光在家装设计中的应用。 易来品牌总监李田…

算法:最长公共前缀(横向扫描和纵向扫描)

横向扫描 时间复杂度 O(m * n)&#xff0c;空间复杂度O(1) /*** param {string[]} strs* return {string}*/ var longestCommonPrefix function(strs) {// 先把第一个字符串拿出来let str strs[0]// 用 startsWith 检查数组中每个字符串是否以当前字符串为前缀while(!strs.e…

高精度电压源的作用是什么

高精度电压源是一种用于提供稳定和精确电压输出的电子设备。它们在实验室研究、工业生产和医疗器械等各种应用中发挥着重要作用。下面西安安泰来为大家详细介绍高精度电压源的作用和用途等内容。 一、高精度电压源的作用 提供准确的电压值&#xff1a;高精度电压源可以提供非常…

银河麒麟重置密码

桌面版银河麒麟重置密码 1.选择界面按e 出现银河麒麟系统选择的页面&#xff0c;我们点击键盘上的“e”键&#xff0c;进入电脑启动项编辑页 2.编辑启动页 在启动项编辑页面&#xff0c;我们将光标移动到linux这一行的最后&#xff0c;然后输入“init/bin/bash consoletty0”…

图片怎么去文字水印?分享三个简单无痕方法

图片怎么去文字水印&#xff1f;大家在遇到好看的图片时&#xff0c;是否曾想过将其用作自己的头像&#xff1f;然而&#xff0c;很多时候从网上保存的图片会带有平台或作者的水印&#xff0c;这会严重影响头像的整体视觉效果&#xff0c;导致我们不得不放弃使用这张图片&#…

雪花算法详细讲解

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 背景一、介绍二、结构三、数据库分表1.垂直分表2.水平分表&#xff08;1&#xff09;主键自增&#xff08;2&#xff09;取模&#xff08;3&#xff09;雪花算法&am…

【Linux】探索Linux进程优先级 | 环境变量 |本地变量 | 内建命令

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 目录 一、进程优先级1.1优先级VS权限1.2为什么要有进程优先级&am…

springboot098基于web的网上摄影工作室的开发与实现

springboot098基于web的网上摄影工作室的开发与实现 源码获取&#xff1a; https://docs.qq.com/doc/DUXdsVlhIdVlsemdX

HarmonyOS4.0从零开始的开发教程12给您的应用添加弹窗

HarmonyOS&#xff08;十&#xff09;给您的应用添加弹窗 概述 在我们日常使用应用的时候&#xff0c;可能会进行一些敏感的操作&#xff0c;比如删除联系人&#xff0c;这时候我们给应用添加弹窗来提示用户是否需要执行该操作&#xff0c;如下图所示&#xff1a; 弹窗是一种…

3分钟,掌握“曲面屏显示屏”

在3分钟内掌握“曲面屏显示屏”的概念和特点&#xff0c;可以按照以下步骤进行&#xff1a; 一、了解曲面屏显示屏的基本概念 曲面屏显示屏是一种采用柔性塑料的显示屏&#xff0c;主要通过OLED面板来实现。相比直面屏幕&#xff0c;曲面屏幕弹性更好&#xff0c;不易破碎。此外…

Java版工程行业管理系统源码-专业的工程管理软件-提供一站式服务—鸿鹄工程管理系统

鸿鹄工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离构建工程项目管理系统 项目背景 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性&#xff0c;公司对内部工程管理的提…

LeetCode(57)合并两个有序链表【链表】【简单】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 合并两个有序链表 1.题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4…

如何将xlsx中的数据通过datagrep导入到mysql数据库表中

在将xlsx数据通过datagrep导入到mysql数据库表中之前需要将xlsx数据导出为csv结尾的格式&#xff0c;因为如果不以csv格式导入会出现乱码。 详细步骤 1、是导入到数据库中没有表 找到对应的数据库&#xff0c;右键点击 选择需要导入的文件&#xff0c;注意一定要选择csv格式…

解决Windows使用CMD控制台打印中文出现乱码问题

问题 分析 查看Cmd当前的编码 修改再运行 chcp 65001中文可正常显示 但是这种方法只能在当前的控制卡生效&#xff0c;新开一个就不行了 解决 打开设置&#xff0c;搜索控制面板 勾选上&#xff0c;确定 注意&#xff01;&#xff01;&#xff01;需要重启才能生效 …

作业11.27

1. 2. def methods(m, n):#创建一个二维数组cp&#xff0c;用于存储到到每个位置的不同走法数量&#xff1b;cp [[0] * n for _ in range(m)]#从第一行和第一列的格子上的走法数量都为1for i in range(m):cp[i][0] 1for j in range(n):cp [0][j] 1#从第二行和第二列开始&…

浅入研究 tcache_perthread_struct

Index 前情提要过程总结 前情提要 tcache_perthread_struct 是GLIBC从2.27开始引入的机制&#xff0c;本质就是链表。 最近我在复现CISCN往年题目&#xff0c;刚好想仔细研究研究劫持等的原理是什么&#xff0c;于是就研究了一会。 过程 找ChatGPT要了一段申请删除堆块的示例…