乐趣区

关于后端:光栅化全面解析

光栅化全面解析

0 两种办法

图形学中渲染过程基本上能够合成为两个次要工作:可见性和着色。光栅化能够说是一种解决可见性问题的办法。可见性包含可能分辨三维物体的哪些局部对摄像机是可见的。这些物体的某些局部能够被禁止,因为它们要么在摄像机的可见区域之外,要么被其余物体暗藏。

解决这个问题基本上能够通过两种形式进行。你能够通过图像中的 每个像素追踪一条射线,找出相机与该射线相交的任何物体(如果有的话)之间的间隔。通过该像素可见的物体就是相交间隔最小的物体(个别用 t 示意)。这就是光线追踪中应用的技术。请留神,在这种非凡状况下,你通过在图像中的所有像素上循环,为每个像素追踪一条光线,而后找出这些光线是否与场景中的任何物体相交来创立图像。换句话说,该算法须要两个次要循环。外循环遍历图像中的像素,内循环遍历场景中的物体。

在光线追踪中,咱们追踪一条穿过图像中每个像素核心的光线,而后测试这条光线是否与场景中的任何几何体相交。如果找到相交,咱们将像素色彩设置为与光线相交的对象的色彩。因为一条射线可能与多个对象相交,所以咱们须要跟踪最近的相交间隔。

for (each pixel in image) {Ray R = computeRayPassingThroughPixel(x,y); 
    float tclosest = INFINITY; 
    Triangle triangleClosest = NULL; 
    for (each triangle in scene) { 
        float thit; 
        if (intersect(R, object, thit)) {if (thit < closest) {triangleClosest = triangle;} 
        } 
    } 
    if (triangleClosest) {imageAtPixel(x,y) = triangleColorAtHitPoint(triangle, tclosest); 
    } 
} 

请留神,在这个例子中,对象实际上被认为是由三角形组成的(而且只是三角形)。咱们没有迭代其余对象,而是把对象看作是一个三角形池子,而后迭代其余三角形。三角形常常被用作光线追踪和光栅化的根本渲染基元(GPU 须要对几何体进行三角化)。

光线追踪 是解决 可见性问题 的第一个可能的办法。咱们说这种技术是以图像为核心的,因为咱们将光线从摄像机射入场景(咱们从图像开始),而不是反过来,这是咱们将在光栅化中应用的办法。

光栅化 采取的是相同的办法。为了解决可见性问题,它实际上是将 三角形 “ 投射 “ 到屏幕上,换句话说,咱们应用透视投影,将三角形的三维示意变成二维示意。这能够通过将形成三角形的顶点投射到屏幕上(应用咱们方才解释的透视投影)来轻松实现。算法的下一步是应用一些技术来填满该二维三角形所笼罩的图像的所有像素。这两个步骤如下图所示。从技术角度来看,它们的执行非常简单。投影步骤只须要进行透视宰割,并将所失去的坐标从图像空间从新映射到光栅空间。找出所产生的三角形笼罩了图像中的哪些像素,也非常简单。

与光线追踪办法相比,该算法是什么样子的呢?首先,请留神,在光栅化中,咱们不是先迭代图像中的所有像素,而是在外循环中迭代场景中的所有三角形。而后,在内循环中,咱们迭代图像中的所有像素,并找出以后像素是否 “ 蕴含 “ 在以后三角形的 “ 投影图像 “ 中。换句话说,这两个算法的内循环和外循环是对调的。

光栅化能够被粗略地合成为两个步骤。咱们首先应用透视投影法将形成三角形的三维顶点投射到屏幕上。而后,咱们对图像中的所有像素进行循环,测试它们是否位于所产生的 2D 三角形内。如果是的话,咱们就用三角形的色彩来填充这个像素。

// rasterization algorithm
for (each triangle in scene) { 
    // STEP 1: project vertices of the triangle using perspective projection
    Vec2f v0 = perspectiveProject(triangle[i].v0); 
    Vec2f v1 = perspectiveProject(triangle[i].v1); 
    Vec2f v2 = perspectiveProject(triangle[i].v2); 
    for (each pixel in image) { 
        // STEP 2: is this pixel contained in the projected image of the triangle?
        if (pixelContainedIn2DTriangle(v0, v1, v2, x, y)) {image(x,y) = triangle[i].color; 
        } 
    } 
} 

下文将全面介绍光栅化的实现细节。【本文有较多公式,可增加 jinjun2050 获取 pdf 版本】

1 光栅化简介

渲染管道的最初一个次要阶段称为光栅化。光栅化是采纳屏幕空间几何图形、片段着色器和该着色器的输出并将几何图形理论绘制到低级二维 (2D) 显示设施的操作。再一次,咱们将专一于绘制三角形集,因为它们是三维 (3D) 图形系统中最常见的图元。事实上,在本文的大部分工夫里,咱们将专一于绘制一个独自的三角形。对于 简直所有古代显示设施,这种低级“绘图”操作波及为显示设施上的每个点或像素调配色彩值。

在概念层面,光栅化的整个主题只是一个实现细节。之所以须要光栅化,是因为咱们明天应用的显示设施是基于密集的矩形发光元件或像素 pixels(术语 picture elements 图片元素的缩写)网格,每个像素的色彩和强度在每一帧中都能够独自调整。因为与基于显像管的电视工作形式无关的历史起因,这些显示器被称 为光栅显示器(raster displays)。

就其本质而言,与渲染管道其余阶段相比,光栅化十分耗时。管道的其余阶段通常须要按对象、按三角形或逐顶点计算,而光栅化实质上须要对每个像素进行某种计算。

1,600 像素宽 x1,200 像素高的显示器(屏 幕上大概有 200 万像素)十分风行。除此之外,光栅化实际上通常须要对每个像素进行屡次计算,咱们意识到必须计算的像素数量通常比给定帧中的三角形数量多 10 倍,20 或更多。

从历史上看,在纯正的软件 3D 管道中,多达 80% 到 90% 的渲染工夫花在光栅化上是很常见的。这种级别的计算需要导致了一个事实,即光栅化是第一个通过专门的生产硬件加速的图形化阶段。事实上,到 2000 年代初,大多数 3D 电 脑游戏开始须要某种模式的 3D 硬件。本文不会具体介绍编写软件 3D 光栅化器所需 的办法和代码,因为大多数游戏开发人员不再须要编写它们。对于如何编写一组光栅器的细节,请看 Hecker 在《Game Developer Magazine》中对于透视纹理映射的优良系列文章 [76]。

只管很少有游戏开发者须要在古代游戏中本人实现哪怕是光栅化管道的一个子集,但光栅化的话题依然十分重要,即便在明天也是如此。光 栅化的基本概念引发了对整个渲染管道中一些最乏味和最奥妙的数学和几何问题的 探讨。此外,对这些基本概念的了解能够让游戏开发人员更好地了解为什么以及如何进行鬼斧神工的渲染和性能瓶颈的呈现,即便光栅化实现是在专用硬件中实现的。许多这些基本概念和低级细节简直能够在任何 3D 游戏中产生视觉相干的后果。本文将重点介绍光栅化的一些基本概念,这些概念对于更深刻地了解应用基于图形处理单元 (GPU) 或计算机处理单元 (CPU) 的渲染零碎的过程至关重要。

2 显示和帧缓冲区

每件显示设施硬件,无论是计算机显示器、电视还是其余相似设施,都须要图像数 据源。对于计算机图形系统,这种图像数据源称为帧缓冲区(之所以这么称说,是 因为它是一个数据缓冲区,用于保留帧的图像信息,或屏幕的图像价值)。根本而言,帧缓冲区是 2D 数字图像:一块内存,其中蕴含示意屏幕上每个点的色彩的数值。每个色彩值代表屏幕在给定点的色彩——一个像素。每个像素都有红色、绿色 和蓝色重量。放在一起,这个帧缓冲区代表要在屏幕上绘制的图像。每次须要更新 屏幕上的图像时,显示硬件都会从内存中读取这些色彩,通常每秒至多 30 次,通常每秒 60 次或更屡次。

正如咱们将看到的,帧缓冲区通常蕴含的不仅仅是每个像素的繁多色彩。尽管理论用于设置显示器上每个点收回的光的色彩和强度的是最终的逐像素色彩,但其余逐像素值在光栅化过程中外部应用。从某种意义上说,这些其余值相似于每 个顶点法线和每个三角形的材质色彩。尽管它们从不间接显示,但它们对最终色彩的计算形式有重大影响。

3 概念化光栅化管道

光栅化整个帧所需的步骤如图 1 所示。第一步是从帧缓冲区中革除任何以前的图像。这在某些状况下能够跳过;例如,如果已知场景几何图形笼罩整个屏幕,则无需革除屏幕。在这种状况下,旧图像将被新图像齐全笼罩。但对于大多数应用程序,此步骤波及应用渲染应用程序编程接口 (API) 将帧缓冲区中的所有像素(在单个函数调用中)设置为固定色彩。

第二步是将几何光栅化到帧缓冲区。咱们将在本文的其余部分具体介绍这个阶段,因为它是三个步骤中最简单的步骤(到目前为止)。

第三步是将帧缓冲图像出现 (present) 给物理显示器。这个阶段通常称为替换或缓冲区替换(swapping or buffer swapping),因为从历史上看,它常常波及(并且在许多状况下依然波及)两个缓冲区之间的切换——在显示另一个时绘制到一个,而后在每一帧之后替换两个缓冲区。这是为了防止在渲染期间呈现闪动或其余伪影(特地是为了避 免让用户看到局部渲染的帧)。然而,本文前面形容的其余技术将须要在出现步骤(presentation step)中实现额定的工作。因而,咱们将应用更通用的术语 present 来指代这一步。

3.1 光栅化阶段

即便是一个简略的光栅化管道也有几个阶段。应该留神的是,尽管这些阶段往往存在于光栅化硬件实现中,但硬件简直从不遵循以下列表中概念阶段的程序(甚至构造)。这个简略的管道光栅化单个三角形如下:

  1. 确定三角形笼罩的可见像素。
  2. 计算每个像素处可见三角形的色彩。
  3. 确定每个像素的最终色彩并写入帧缓冲区。

第一阶段进一步合成为两个独立的步骤:

  1. 确定三角形笼罩的像素
  2. 确定在每个像素处可见的三角形

本文的其余部分将具体探讨每个流水线阶段。

4 确定片段(Fragments):三角形笼罩的像素

4.1 片段

为了在渲染的光栅化阶段获得进一步停顿,咱们必须将屏幕空间中的三角形(或更个别地,几何体)分解成更间接匹配帧缓冲区中像素的片段。这波及确定像素矩形或像素中心点与三角形的交点。在光照和暗影中,咱们应用术语片段来示意多边形外表上给定点四周的无限小表面积。片段着色器被形容为在这些渺小的外表上进行求值。

在光栅化级别,片段具备更明确但相干的定义。它们是上述合成屏幕空间三角形以匹配像素的过程的后果。这些片段能够被认为是屏幕空间中像素大小的三角形碎片(pieces)。这些能够被可视化为一个三角形,通过沿着像素边界切割成小块(pieces)。许多这些片段(三角形的外部)将是正方形的,即像素正方形的残缺大小。咱们称这些像素大小的片段为 残缺片段 。然而,沿着三角形的边缘,这些片段可能是搁置在像素正方形外部的被分为多个边的多边形,因而小于像素。咱们称这些较小的片段为 局部片段。在实践中,这些片段可能实际上是在像素核心拍摄的三角形的点样本(相似于咱们在光照和暗影中对片段的概念),但根本的想法是,片段代表了一个三角形的碎片(pieces),这些碎片影响到了一个给定的像素。咱们将把像素看作是目的地或箱子,咱们把笼罩该像素区域的所有碎片放入其中。因而,它不是一个一对一的映射。一个像素可能蕴含来自不同(甚至是雷同)对象的多个片段,或者一个像素可能不蕴含场景的以后视图中的任何片段。

本文的其余部分将应用这个更具体的片段定义。图 2 显示了一个笼罩有像素矩形边界的三角形。图 3 显示雷同的配置被分解成片段,包含残缺的和局部的。图中将片段略微离开,以更好地展现局部片段的形态。

4.2 深度复杂度

整个场景中的片段数量能够比屏幕上的像素数量少得多,也能够多得多。如果几何图形仅笼罩屏幕的一个子集,则可能有许多像素不蕴含场景中的片段。另一方面,如果许多三角形在屏幕空间中互相重叠,那么屏幕上的许多像素可能蕴含多个片段。给定帧中场景中的片段数与屏幕上像素数的比率称为深度复杂度或适度绘制,因为该比率示意有多少全屏几何体形成场景。通常,具备更高深度复杂度的场景光栅化老本更高。请留神,这是整个视图的总体比率;即便几何图形仅笼罩屏幕的一半,场景的深度复杂度也可能为 2。如果均匀而言,被笼罩的一半屏幕上的几何图形是四个三角形深度,那么深度复杂度将是每个像素两个片段在整个屏幕上摊销。

4.3 将三角形转换为片段

三角形是凸的,无论它们如何通过射影变换进行投影(在某些状况下,三角形可能显示为线或点,但它们依然是凸对象)。这是一个十分有用的属性,因为它意味着任何三角形与程度的像素行相交(也称为扫描线,因为历史起因与基于 CRT 的电视显示器)最多在一个间断的片段中。因而,对于与三角形相交的任何扫描线,
咱们能够仅用最小 x 值和最大 x 值示意交点,称为 跨度(span)。因而,在光栅化期间三角形的示意由一组跨度组成,每条扫描线一个,三角形相交。此外,三角形的凸性还意味着与三角形相交的扫描线汇合在 y 上是间断的;给定三角形有一个最小值和一个最大值 y,其中蕴含所有非空跨度。图 4 显示了三角形跨度集的示例。笼罩在三角形上的暗带代表将用于绘制三角形的相邻片段的跨度。

三角形的 y~min~ 即最小 y 像素坐标就是三个三角形顶点的最小 y 值。相似地,三角形的最大 y 像素坐标 y~max~ 就是三个顶点的最大 y 值。因而,三个顶点之间的简略 min/max 计算定义了必须为三角形生成的 (y~max~−y~min~+1) 的整个跨度范畴。

每个跨度的最右边和最左边的片段可能是局部片段,因为三角形的边缘可能不会齐全落在像素边界上。此外,出于同样的起因,最顶部和最底部的跨度可能蕴含局部片段。三角形的残余片段将是残缺的片段。

生成跨度自身只是波及到将程度扫描线与三角形的边缘相交。因为三角形的凸性,除非扫描线与一个顶点相交,该扫描线将与三角形的两条边恰好相交:一个从三角形外跨入三角形,一个再次来到三角形。这两个交点将定义跨度的最小和最大 x 值。

4.4 解决局部片段

残缺的片段总是持续到光栅化过程的下一个阶段。然而,局部片段的命运取决于特定的渲染零碎。在更高级的零碎中,一个像素处的所有局部片段都作为局部片段传递,最终像素的可见性和色彩可能会受到所有这些局部的影响。然而,更简略的光栅化零碎不解决局部片段,并且必须在生成局部片段时决定是抛弃该片段还是将其晋升为残缺片段。解决此问题的罕用办法是当且仅当它们蕴含像素的中心点时才保留局部片段。这有时称为几何点采样,因为整个片段是基于每个像素内的单点样本生成或不生成的。图 5 显示了与图 3 雷同的三角形,但局部片段被抛弃或晋升为残缺片段,具体取决于片段是否蕴含像素的中心点。

当一个三角形的顶点或边缘正好落在一个像素核心时,这样的图形系统的行为是由与零碎相干的填充常规决定的。它确保如果两个三角形共享一个顶点或一条边,只有一个三角形会为像素奉献一个片段。这一点十分重要,因为如果没有一个明确的填充约定,在三角形之间的共享边缘可能会呈现空洞(两个三角形的局部碎片都被丢掉的像素)或反复绘制的像素(两个三角形的局部碎片都被晋升为残缺的碎片)。沿着共享三角形边缘的孔容许背景色透过原本是间断的、不通明的外表,使外表看起来有裂缝穿过。沿着共享边缘的双重绘制的像素会导致更奥妙的伪影,通常只有在应用通明或其余模式的混合时才会看到(见第 8.1 节)。对于实现点取样填充约定的细节,见 Hecker 的《游戏开发者杂志》系列文章 [76]。

5 确定可见几何

渲染几何图形的总体目标是确保最终渲染的图像令人信服地代表给定场景。在最高级别上,这意味着物体必须看起来被更近的物体正确遮挡,并且不能被更远的物体遮挡。这个过程被称为可见外表测定 (VSD),并且有许多十分不同的办法来实现它。这些办法都波及在一个或另一个粒度级别上比拟外表的深度,并以给定像素处的最小深度对象(即最近的对象)是渲染到屏幕的对象的形式渲染它们。

历史上,有许多不同的办法被用于 VSD。许多晚期的算法都是基于奇妙的排序技巧,包含在光栅化之前将几何体从后往前排序。这是一个低廉的命题,通常在 CPU 上每帧计算一次。到目前为止,明天最罕用的办法是基于光栅化的办法:深度缓冲区。光栅化器是图形管线中最早被减速的局部,有专门的硬件,这意味着基于光栅化器的可见外表确定零碎能够实现高性能。深度缓冲器也被称为 Z 缓冲区(z-buffer)。它实际上是更广泛的深度缓冲的一个具体的、非凡的案例。

5.1 深度缓冲

深度缓冲是基于可见性应以输入为重点的概念。换句话说,因为像素是咱们渲染管道的最终目的地,可见性应该在每个像素(或者更确切地说,每个片段)的根底上计算。如果在每个像素处看到的最终色彩是具备最小深度的片段的色彩(在绘制到该像素的所有片段中),则场景将显示为正确绘制。换句话说,在绘制到一个像素的所有片段中,具备最小深度的片段应该“博得”该像素并抉择该像素的色彩。出于探讨的目标,咱们假如点采样几何(即,没有局部片段)。

因为常见的光栅化办法偏向于一次渲染一个三角形,一个给定的像素可能会在一帧的过程中被来自不同三角形的片段重绘几次。如果咱们心愿防止按深度对三角形进行排序(咱们的确这样做了),那么应该取得给定像素的片段可能不是最初一个绘制到该像素的片段。咱们必须有某种办法来存储以后最近片段在每个像素处的深度以及该片段的色彩。

存储了这些信息后,咱们能够在每次将片段绘制到像素时计算一个简略的测试。如果新片段的深度比该像素以后存储的深度值更靠近,则新片段博得该像素。计算新片段的色彩,并将这个新片段色彩写入像素。片段的深度值替换该像素的现有深度值。如果新片段的深度大于为像素着色的以后片段,则疏忽新片段的色彩和深度,因为片段示意以后像素处最近的已知外表前面的外表。在这种状况下,咱们晓得新片段将在该像素处被遮挡,因为咱们曾经在该像素处看到了比最新片段更近的片段。图 6 示意将片段从两个三角形渲染到一个小的深度缓冲区。请留神更靠近的三角形的片段如何总是博得像素(正确的后果),即便它是先绘制的。

因为该办法是按像素计算的,因而是按片段计算的,因而每个三角形的深度是在每个片段的粒度上计算的,并且该值用于深度比拟。因为这种更精密的子三角形粒度,深度缓冲区会主动解决无奈应用逐三角形排序正确显示的三角形配置。几何图形能够以任何程序传递到深度缓冲区。这种随机程序可能有问题的状况是给定像素的两个片段具备雷同的深度。在这种状况下,程序很重要,具体取决于用于排序深度的确切比拟(即 < 或≤)。然而,这种状况对于简直任何可见外表办法都是有问题的。

深度缓冲区有几个毛病,只管其中大多数在古代 PC 或游戏机上不再重要。深度缓冲办法的历史缺点之一隐含在办法的名称中;它须要一个缓冲区或深度数组值,每个像素一个。这是一大块内存,通常须要与帧缓冲区自身一样多的内存。同样,正如帧缓冲区必须在每帧之前革除为背景色彩一样,深度缓冲区也必须革除为背景深度,通常是可示意的最大深度值。这些问题在 GPU 内存无限的手持和嵌入式 3D 零碎上可能很重大。最初(依然实用于 PC 和控制台),深度缓冲区须要以下工作:

  • 计算片段的深度值
  • 在深度缓冲区中查找现有像素深度
  • 这两个值的比拟
  • (仅实用于新的“获胜者”片段)将新深度写入深度缓冲区

在许多 GPU 上,深度缓冲区存储在分层的压缩数据结构中,容许应用大块像素进行疾速回绝测试。然而,对于根本实现,这是为每个片段计算的。对于大多数软件光栅器,这个额定的逐片段的工作会使深度缓冲不适宜继续应用。全软件 3D 零碎偏向于尽可能应用优化的几何排序,为真正须要它的多数对象保留深度缓冲。例如,晚期的第三人称射击游戏渲染引擎将大量工作投入到环境的专门排序中,从而防止对它们进行任何深度缓冲测试。这留下了足够的 CPU 周期来应用软件深度缓冲渲染动画角色、怪物和小物体(笼罩的像素比风光少得多)。

此外,深度缓冲区并不能解决高深度复杂度场景的潜在性能问题。咱们依然必须计算每个片段的深度并将其与缓冲区进行比拟。然而,在某些状况下,它能够缩小适度绘制的问题,因为没有必要计算或写入任何未通过深度测试的片段的色彩。事实上,一些应用程序会尝试以大抵从近到远的程序渲染它们的深度缓冲场景(同时依然防止在 CPU 上按三角形、按帧排序),这样前面的几何图形可能会使深度缓冲失败测试并且不须要色彩计算。

深度缓冲在硬件加速平台上运行的 3D 应用程序中十分风行,因为它易于应用,须要很少的利用程序代码或主机 CPU 计算,并且能够以高性能生成高质量的图像。

5.1.1 计算每个片段的深度值

应用深度缓冲区计算片段可见性的第一步是计算以后片段的深度值。正如咱们将看到的,${Z_{ndc}}$ 将工作得很好。然而,${Z_{ndc}}$ 工作良好而视图空间值 ${Z_v}$ 不工作的起因是相当乏味的。

为了更好地了解深度值如何在屏幕空间中跨三角形变动的性质,咱们必须可能将屏幕上的点映射到投影到它的三角形中的点。这与拾取十分类似,咱们将应用几个概念。因为透视投影的非线性个性,咱们会发现咱们从屏幕空间像素到给定视图空间点的映射三角有点简单。咱们将通过几个较小的阶段来跟踪此映射。对于本文的探讨,咱们将假如咱们正在应用 OpenGL 款式的矩阵,咱们在视图空间中向下看‑z 轴。

视图空间中的三角形只是视图空间中立体的凸子集。因而,咱们能够通过立体的法向量 $\mathop n\limits^ \wedge=(a,b,c)$ 来定义视图空间中三角形的立体,并且一个常数 d,使得立体上的点 $P = \left({{x_p},{y_p},{z_p}} \right)$ 满足

回顾拾取,2D 归一化设施坐标 $\left({{x_{ndc}},{y_{ndc}}} \right)$ 中的一个点映射到视图空间射线 tr 使得

${\rm{t}}r{\rm{ = t(}}{{\rm{x}}_{{\rm{ndc}}}}{\rm{,}}{{\rm{y}}_{{\rm{ndc}}}}{\rm{, –}}{{\rm{d}}_{{\rm{proj}}}}{\rm{), t }} \ge {\rm{0}}$

其中 ${{\rm{d}}_{{\rm{proj}}}}$ 是投影间隔(从视图空间原点到投影立体的间隔)。投影到 $\left({{x_{ndc}},{y_{ndc}}} \right)$ 处的像素的任何视点空间必须与这条射线相交。通常,咱们不能反转投影变换,因为屏幕上的一个点映射到视图空间中的一条射线。然而,通过晓得三角形的立体,咱们能够将三角形与眼帘相交,如下所示。视图空间中落在三角形立体内的所有点 P 由公式 1 给出。此外,咱们晓得三角形上投影到 $\left({{x_{ndc}},{y_{ndc}}} \right)$ 的点对于某些 t 必须等于 tr。用向量 tr 代替公式 1 中的点 ${\rm{(}}{{\rm{x}}_{\rm{p}}}{\rm{,}}{{\rm{y}}_{\rm{p}}}{\rm{,}}{{\rm{z}}_{\rm{p}}}{\rm{)}}$ 并求解 t,

$\eqalign{
& \mathop {\rm{n}}\limits^ \wedge {\rm{(t}}r{\rm{) + d = 0}} \cr
& {\rm{t(}}\mathop {\rm{n}}\limits^ \wedge {\rm{r) = – d}} \cr
& {\rm{t =}}{{– d} \over {\mathop {\rm{n}}\limits^ \wedge {\rm{r}}}} \cr} $

从这个 t 值,咱们能够计算出沿投影射线的点 ${\rm{(}}{{\rm{x}}_{\rm{v}}}{\rm{,}}{{\rm{y}}_{\rm{v}}}{\rm{,}}{{\rm{z}}_{\rm{v}}}{\rm{) = t}}r$,它是投影到 $\left({{x_{ndc}},{y_{ndc}}} \right)$ 的三角形上的视图空间的点。这相当于发现

然而,咱们当初只对 ${Z_v}$ 感兴趣,因为咱们正在尝试计算深度缓冲的 perfragment 值。公式 2 的 ${Z_v}$ 重量是

作为对已知后果的疾速查看,请留神,在具备恒定深度 ${Z_v} = {Z_{const}}$ 的三角形的非凡状况下,咱们能够替换

代入公式 3,计算结果为预期常数 ${Z_v} = {Z_{const}}$:

正如公式 3 中所定义的,${Z_v}$ 是一个计算每个片段的代价昂扬的值(在个别的十分数深度状况下),因为它是具备十分数分母的分数。

这将须要按片段划分来计算 ${Z_v}$,这比咱们想要的要低廉。然而,深度缓冲只须要可能互相比拟深度值。如果咱们比拟 ${Z_v}$ 值,咱们晓得它们随着深度的减少而减小(因为视图方向是‑z),给出深度测试:

${Z_v}$≥DepthBuffer→新片段可见

${Z_v}$<DepthBuffer→新片段不可见

然而,如果咱们计算并存储 zv 的倒数(乘法逆),那么相似的比拟依然以雷同的形式工作。如果咱们应用所有 zv 值的倒数,咱们失去

${1 \over {{Z_v}}}$≤DepthBuffer→新片段可见

${1 \over {{Z_v}}}$>DepthBuffer→新片段不可见

如果咱们对等式 3 进行倒数,咱们能够看到每个片段的计算变得更简略:

其中所有带括号的项在三角形中都是恒定的。事实上,这造成了 ND 坐标到 $1/{Z_v}$ 的仿射映射。因为咱们晓得存在从像素坐标 (xs,ys) 到 ND 坐标 (xndc,yndc) 的仿射映射(affine mapping),咱们能够将这些仿射映射组合成从屏幕空间像素坐标到 $1/{Z_v}$ 的单个仿射映射。后果,对于给定的投影三角形,

其中 f、g 和 h 是实数值并且是每个三角形的常数。咱们将给定三角形的上述映射定义为

从推导中能够看出 $RecipZ\left({{x_s},{y_s}} \right)$(或任何仿射映射)的一个乏味性质

同样地

换句话说,一旦咱们计算出任何起始片段的 RecipZ 深度缓冲区值,咱们能够通过简略地增加 f 来计算跨度中下一个片段的深度缓冲区值。一旦咱们计算了给定跨度的根本深度缓冲区值,当咱们沿着扫描线后退,填充跨度时,咱们须要做的就是将 f 增加到每个相邻片段之间的以后深度(图 7)。这使得深度值的每片段计算的确十分快。并且,一旦计算了第一个跨度的根底 RecipZ,咱们能够将 g 增加到前一个跨度的根底深度以计算下一个跨度的根底深度。这种技术被称为前向差分,因为咱们应用一个片段的值与下一个片段的值之间的差值(或增量)来逐渐更新以后深度。此办法实用于任何存在来自屏幕空间的仿射映射的值。咱们将这些值称为 屏幕空间中的仿射或屏幕仿射(affine in screen space, or screen affine.)。

事实上,咱们能够应用咱们在投影期间计算的 ${Z_{ndc}}$ 值作为代替对于 RecipZ。在视图和投影中,咱们计算了一个 zndc 值,它在近立体等于‑1,在远立体等于 1,其模式为

这是 RecipZ 的仿射映射。后果,咱们发现咱们现有的值 ${Z_{ndc}}$ 是屏幕仿射的,适宜用作深度缓冲区值。这是咱们后面提到的深度缓冲的非凡状况,通常称为 z 缓冲,因为它间接应用 ${Z_{ndc}}$。

5.5.2 数值精度和 z 缓冲

在实践中,屏幕空间中的深度缓冲有一些数值精度限度,可能会导致视觉伪影。正如后面探讨深度缓冲区时提到的,对象被绘制到深度缓冲零碎的程序(至多在不通明对象的状况下)只有在两个外表(两个片段)的深度值是在给定像素处相等。实践上,除非所探讨的几何对象真正共面,否则这不太可能产生。然而,因为计算机数字示意没有有限的精度,不共面的外表能够映射到雷同的深度值。这可能导致以谬误的程序绘制对象。

如果咱们的深度值被线性映射到视图空间,那么一个 16 位的定点数深度缓冲区将可能正确分类外表深度相差约 160,000 的近立体和远立体间隔之差的任何对象。对于简直任何应用程序来说,这仿佛都入不敷出。例如,对于 1 公里的视距,这将等于大概 1.5 厘米的分辨率。挪动到更高分辨率的深度缓冲区会使这个值变得更小。

然而,在 z 缓冲的状况下,可示意的深度值不是均匀分布的在视图空间中。事实上,正如咱们所见,存储到缓冲区的深度值基本上是 $1/{Z_v}$,这相对不是视图空间 z 的均匀分布。深度缓冲区值在视图空间 z 上的图表如图 8 所示。这是视图空间 z 到深度缓冲区值的双曲线映射——留神深度值随着 Z 向远处立体的变动而变动很小。

对此应用定点值会导致间隔精度非常低,因为 z 的大距离映射到逆 z 的雷同定点值。事实上,一个常见的预计是 z 缓冲区将其 90% 的精度集中在最近的 10% 的视图空间 z 中。这意味着远处物体的碎片常常绝对于彼此被谬误地分类。

一种在 3D 硬件中风行的解决精度问题的办法称为 w‑buffer。w‑buffer 以高精度对深度(通常为 1/w)进行插值的屏幕仿射值,而后在每个像素处计算插值的倒数以产生在视图空间中线性的值(即 1/w)。而后将这个反转值存储在深度缓冲区中。通过量化(升高插值期间应用的额定精度)并在视图空间中存储一个线性值,能够在肯定水平上防止 z 缓冲区的双曲线性质。然而,如前所述,不再反对 w‑buffers。它们还存在一个问题,即每个图元在屏幕空间中存储的值是非线性的,这不适用于某些后处理算法。

另一种解决方案应用浮点深度缓冲区,大多数平台都提供这种缓冲区。联合它们,咱们翻转深度缓冲值,使得深度值在近立体映射到 1.0,在远立体映射到 0.0,并且一个 > 或≥的比拟用于深度测试 [89]。通过这样做,浮点数的天然精度个性最终对消了 z 缓冲区值的一些双曲线个性。靠近 0 的浮点值减少的动静范畴弥补了远距离 z 值范畴的损失,其作用相似于旧的 w 缓冲区。也就是说,浮点深度缓冲区可能存在其余问题,适度校对并使最靠近相机的场景区域精度太低。这在渲染场景中尤其显著,因为最靠近相机的几何图形对观看者来说是最显著的。

最初,防止这些问题的最简略办法是通过将近立体尽可能远地挪动来最大化深度缓冲区的应用,从而不会节约凑近近立体的精度。所有这些办法都有依赖于场景和应用程序的衡量。

5.2 实际中的深度缓冲

在大多数图形系统中应用深度缓冲须要在渲染代码中增加几个点:

  • 创立帧缓冲区时创立深度缓冲区
  • 每帧革除深度缓冲区
  • 启用深度缓冲区测试和写入

第一步是确保应用深度缓冲区创立渲染窗口或设施。这因 API 不同而不同,Iv(IvGraphics 图形 API)在所有状况下都会主动调配深度缓冲区。申请创立深度缓冲区后(在大多数状况下,只是申请深度缓冲区,取决于硬件反对),必须在每帧开始时革除缓冲区。深度缓冲区的革除通常应用与帧缓冲区革除雷同的性能。Iv 应用 IvRenderer 函数 ClearBuffers,但带有新参数 kDepthClear。尽管能够应用独立于帧缓冲区来革除深度缓冲区

renderer->ClearBuffers(kDepthClear);

如果您在帧开始时革除两个缓冲区,则在某些零碎上通过一次调用革除它们可能会更快,这在 Iv 中如下实现:

renderer->ClearBuffers(kColorDepthClear);

要启用或禁用深度测试,咱们只需应用 IvRenderer 函数 SetDepthTest。要禁用测试,请通过 kDisableDepthTest。要启用测试,请通过其余测试模式之一(例如 kLessDepthTest)。默认状况下,深度测试被禁用,因而应用程序应在渲染之前显式启用它。最常见的深度测试模式是 kLessDepthTest 和 kLessEqualDepthTest。如果其深度值小于或等于以后像素深度,则后一种模式会导致应用新片段。

深度值的写入也能够启用或禁用,与深度测试无关。正如咱们将在本文前面看到的那样,在禁用深度缓冲区写入的同时启用深度测试会很有用。调用 IvRenderer 函数 SetDepthWrite 能够启用或禁用写入 z 缓冲区。

6 计算片段着色器输出

光栅化管道的下一个阶段是通过评估以后片段的以后流动片段着色器来计算片段的整体色彩(以及可能的其余着色器输入值)。这反过来又要求在以后片段地位

  • 应用程序设置的每对象对立值(uniform)
  • 顶点着色器从源顶点生成或传递的逐顶点属性
  • 每个片段的间接值,通常来自纹理

请留神,给定片段可能存在许多起源。作为着色器输出源生成的一部分,它们中的每一个都必须对每个片段进行独立评估。在计算了每个片段的源值之后,必须通过运行片段着色器来生成最终的片段色彩。在片段着色器中组合每个片段顶点色彩值、每个顶点光照值和纹理色彩有各种办法。着色器生成最终的片段色彩,该色彩将传递到光栅化管道的最初阶段,即混合(本文稍后将探讨)。

接下来的几节将探讨如何从咱们列出的源中计算每个片段的着色器源值。尽管有许多可能的办法能够应用,咱们将专一于在屏幕空间中疾速计算并且非常适合大多数光栅化软件甚至一些光栅化硬件的以扫描线为核心的个性的办法。

6.1 对立值 Uniform Values

值与管道中的所有其余阶段一样,每个对象的值或色彩最容易光栅化。对于每个片段,恒定的对立值能够间接向下传递给着色器。不须要对每个片段进行评估或计算。因而,对立值对片段着色过程的性能影响最小。

6.2 每顶点属性 Per-Vertex Attributes

正如咱们之前探讨过的,每个顶点的属性是从最初一个顶点解决阶段(在咱们的例子中,从顶点着色器)传递给片段着色器的变量。这些值仅在每个三角形的三个顶点处定义,因而必须进行插值以确定三角形中每个片段核心的值。正如咱们将看到的,在个别状况下,要正确计算三角形的每个片段,这可能是一项低廉的操作。然而,咱们将首先查看等深三角形的非凡状况。这种状况下的映射在计算上一点也不低廉,即便在渲染非恒定深度的三角形时(尤其是在软件渲染器中),它也是一个迷人的近似值。

6.2.1 恒定深度插值 Constant Depth Interpolation

为了剖析恒定深度的状况,咱们将确定恒定深度三角形的映射实质,从像素空间,通过 NDC 空间,到视图空间,通过重心坐标,最初到逐顶点源属性。咱们首先从从像素空间映射到视图空间的非凡状况开始。

整体投影方程(从视图空间映射到 NDC 空间到屏幕空间像素坐标)的所有模式

其中 a,c 不等于零。如果咱们假如一个三角形的顶点都在雷同的深度(即,视图空间 ${Z_v}$ 等于三角形中所有点的常数 ${Z_{const}}$),那么一个点在三角形外部是

留神 a,c 不等于零意味着 a′,c′不等于零,所以咱们能够重写这些使得

因而,对于恒定深度 ${Z_{const}}$ 的三角形,

投影在 ${Z_v} = {Z_{const}}$ 立体上造成从屏幕顶点到视图空间顶点的仿射映射。

重心坐标是视图空间顶点的仿射映射。

顶点属性定义了从重心坐标到属性值的仿射映射(例如,Gouraud 着色)。

如果咱们组合这些仿射映射,咱们最终会失去一个从屏幕空间像素坐标到属性值的仿射映射。例如,咱们能够将这个从像素坐标到色彩的仿射映射写为

其中 ${{\rm{C}}_{\rm{x}}}$、${{\rm{C}}_{\rm{y}}}$ 和 ${{\rm{C}}_{\rm{0}}}$ 都是色彩(每种色彩都可能为正数或大于 1.0)。无关将三个屏幕空间像素地位和相应的三重顶点色彩映射到三种色彩 ${{\rm{C}}_{\rm{x}}}$、${{\rm{C}}_{\rm{y}}}$ 和 ${{\rm{C}}_{\rm{0}}}$ 的公式的推导,请参见 Eberly[35] 的第 126 页。从咱们先前对屏幕空间中逆 z 属性的推导,咱们留神到色彩 $\left({{x_s},{y_s}} \right)$ 是常数 z 三角形的屏幕仿射:

与 1/z 一样,咱们能够简略地通过计算三角形中“根本片段”色彩的前向差别来计算常量三角形的每个顶点属性的每个片段值。

6.2.2 透视改正插值 Perspective-Correct Interpolation

当应用透视投影投影在相机空间中没有恒定深度的三角形时,生成的映射不是屏幕仿射的。从咱们对深度缓冲区值的探讨中,咱们能够看到给定视图空间中的个别(不肯定是恒定深度)三角形,从 NDC 空间到三角形上的视图空间点的映射具备以下模式

这些是射影映射(projective mappings),而不是咱们在恒定深度状况下的仿射映射(affine mappings)。这意味着从屏幕空间到线性插值的每个顶点属性的整体映射也是投影的。为了在透视投影中正确插入三角形的顶点属性,咱们必须应用这种更简单的投影映射。

大多数硬件渲染零碎当初以透视改正的形式插入所有每个顶点的属性。然而,这并不总是通用的,而且对于在低功率平台上运行的旧软件渲染零碎来说,它太低廉了。如果被插值的每个顶点属性是来自每个顶点光照的色彩,例如在 Gouraud 着色的状况下,则能够在精度和速度之间进行衡量。请记住,Gouraud 着色首先是一种近似办法,在“正确性”的根底上应用投影映射的理由有所缩小。此外,Gouraud 暗影色彩的插值往往十分平滑,以至于很难判断插值是否透视正确。事实上,Heckbert 和 Moreton[75] 提到纽约理工学院的离线渲染器在几年前就在透视中谬误地插值了色彩,直到有人留神到!因而,软件图形系统通常防止了低廉的、透视正确的 Gouraud 色彩投影插值,而仅应用仿射映射和前向差分。

也就是说,其余每个顶点的值,例如纹理坐标,并不能容忍透视改正插值中的问题。对纹理进行光栅化的过程首先是对每个顶点的纹理坐标进行插值,以确定每个片段的正确值。实际上,在光栅化器中插值通常是纹理坐标(纹理坐标乘以纹理图像尺寸). 这个过程相似于对其余每个顶点属性进行插值。然而,因为纹理坐标的应用实际上与片段着色器中的顶点色彩有些不同,咱们无奈应用后面形容的屏幕仿射近似。纹理坐标须要正确的透视插值。纹理坐标的间接性质意味着尽管纹理坐标在三角形上平滑而奥妙地变动,但生成的纹理色彩查找不会。

纹理坐标的问题与仿射和投影变换的属性无关。仿射变换将平行线映射到平行线,而射影变换只保障将直线映射到直线。任何已经看过一条又长又直的路线的人都晓得,造成路线边缘的两条线仿佛在远处相遇,即便它们是平行的。透视,作为一种投影映射,不保留平行线。

仿射插值和投影插值之间差别的经典示例是纹理坐标是棋盘格,以透视图绘制。图 9 显示了作为图像的方格纹理,以及利用盘绕到由两个三角形(两个三角形以轮廓或线框显示)造成的正方形的图像。当顶部在透视图中歪斜时,请留神,如果应用投影映射(图 10)映射纹理,则垂直线会按预期汇聚到远处。

如果应用仿射映射对纹理坐标进行插值(图 11),咱们看到两个不同的视觉伪影。首先,在每个三角形内,所有的平行线都放弃平行,垂直线不会像咱们预期的那样汇聚。此外,请留神沿正方形对角线(共享三角形边缘)的线条中显著的“扭结”。这有可能乍一看仿佛是插值代码的一个 bug,但略微剖析一下,它实际上是仿射变换的一个根本属性。仿射变换由三角形的三个点定义。后果,在定义了三角形的三个点及其纹理坐标后,变换就没有更多的自由度了。每个三角形独立于其余三角形定义其变换,后果是应该是一组穿过正方形的线的蜿蜒。

然而,投影变换具备额定的自由度,示意为与每个顶点关联的深度值。这些深度值扭转了纹理坐标在三角形上插值的形式,并容许映射纹理图像中的直线在屏幕上放弃笔挺,即便逾越三角形边界。

侥幸的是,这个解决方案绝对简略,但老本很高。正如咱们从公式 4 中看到的,$1/{Z_v}$ 能够应用屏幕空间地位的仿射映射来计算。因为纹理坐标自身是仿射映射,咱们能够将它们与 $1/{Z_v}$ 仿射映射组合起来,发现 ${u_{texel}}/{z_v}$ 和 ${v_{texel}}/{z_v}$ 是仿射映射。因而,这三个量($1/{Z_v}$、${u_{texel}}/{z_v}$ 和 ${v_{texel}}/{z_v}$)能够应用前向差分在三角形上进行插值。在每个片段中,最终 (${u_{texel}}$,${v_{texel}}$) 值能够通过将 $1/{Z_v}$ 取反失去 zv 来计算,而后将其乘以插值的 ${u_{texel}}/{z_v}$ 和 ${v_{texel}}/{z_v}$。

这种投射式映射的毛病是,它须要对每个片段进行正确的评估。

  1. 更新 $1/{Z_v}$ 的仿射前向差分操作
  2. 一个仿射前向差分操作来更新 ${u_{texel}}/{z_v}$
  3. 一个仿射前向差分操作更新 ${v_{texel}}/{z_v}$
  4. 从 $1/{Z_v}$ 复原透视正确 ${Z_v}$ 的除法
    5.${u_{texel}}/{z_v}$ 乘以 ${Z_v}$ 以复原透视正确的 ${u_{texel}}$
    6.${v_{texel}}/{z_v}$ 乘以 ${Z_v}$ 以复原透视正确的 ${v_{texel}}$

1990 年代的许多 PC 游戏和一些视频游戏机应用较便宜(且不太正确)的实在透视纹理近似值。然而,如前所述,在古代硬件光栅化零碎上,每个片段的透视改正纹理被简略地假如。此外,可编程片段着色器基本上能够容许将任何逐顶点属性用作纹理坐标这一事实进一步影响了硬件供应商以正确的视角插入所有顶点属性。在实践中,许多 GPU 并未针对所有顶点属性遵循上述过程。相同,他们应用透视改正插值计算一组重心坐标,而后应用这些重心坐标将每个属性映射到正确的值。

6.3 每个片段的间接值

每个顶点属性的插值只是每个片段值的一种可能起源。因为古代片段着色器的弱小性能,纹理坐标和其余值不须要间接来自每个顶点的属性。作为波及其余逐顶点属性的计算的后果,能够从片段着色器自身中生成的一组坐标评估纹理查找。

在片段着色器中生成的纹理坐标甚至能够是之前在同一个片段着色器中查找纹理的后果。在这种技术中,第一个纹理中的纹理图像值不是色彩,而是纹理坐标自身。这是一种十分弱小的技术,称为间接纹理。第一个纹理查找造成一个表查找或间接查找,为第二个纹理查找生成一个新的纹理坐标。

间接纹理是更个别的纹理案例的示例,其中评估纹理样本会生成除色彩之外的“值”。显然,并非所有纹理查找都用作色彩。然而,为了在上面的探讨中易于了解,咱们将假如纹理图像的值代表最常见的状况——色彩。

7 光栅化纹理 Rasterizing Textures

上一节形容了如何在片段着色器中插入通用的逐顶点属性,如果这些属性是咱们所须要的,咱们能够简略地评估或运行片段着色器并计算片段的色彩。然而,如果咱们有纹理查找,这只是第一步。在计算或插值给定片段的纹理坐标后,必须将纹理坐标映射到纹理图像自身以产生色彩。

一些最早的着色语言要求纹理只能通过每个顶点的属性来解决,并且在某些状况下,甚至在调用片段着色器之前就理论计算了纹理查找。然而,如上所述,古代着色器容许在片段着色器自身中计算纹理坐标,甚至可能作为纹理查找的后果。此外,着色器中的条件和不同的循环迭代可能会导致某些片段的纹理查找被跳过。因而,咱们将纹理的光栅化视为片段着色器自身的一部分。

事实上,尽管在片段着色器外部实现的数学计算很乏味,但孤立片段着色器评估中最(数学)简单的局部是纹理查找的计算。正如咱们将看到的,纹理查找不仅仅是抓取并返回最近的纹素到片段核心。将纹理映射到几何体,而后将几何体映射到片段的宽泛映射须要一组更大的技术来防止显著的视觉伪影。

7.1 纹理坐标温习

在咱们对光栅化纹理的探讨中,咱们将应用多种不同模式的坐标。这包含应用程序级的、标准化的、与纹理无关的纹理坐标(u、v),以及与纹理大小相干的纹理坐标(${u_{texel}}$、${v_{texel}}$),它们都被认为是实数值。咱们在纹理介绍中应用了这些坐标。

纹理坐标的最终模式是整数纹素坐标,或纹素地址。这些代表对纹理图像数组的间接索引。与其余两种模式的坐标不同,它们(顾名思义)是整数值。从纹素坐标到整数纹素坐标的映射不是通用的,并且取决于纹理过滤模式,这将在上面探讨。

7.2 将坐标映射到纹素 Mapping a Coordinate to a Texe

在对纹理进行光栅化时,咱们会发现——因为透视投影的性质、几何对象的形态以及纹理坐标的生成形式——片段很少间接和准确地对应于一对一映射中的纹素。任何反对纹理的光栅化器都须要解决各种纹素到片段的映射。在软纹理初始探讨中,咱们留神到纹素坐标通常包含精度(通过浮点数或定点数),它比仿佛须要的每纹素值更细粒度。正如咱们将看到的,在某些状况下,咱们将在称为纹理过滤的过程中应用这种所谓的子像素精度来进步渲染图像的品质。

纹理过滤(以其多种形式)通过混合纹理像素坐标映射和后果纹理像素值的组合,来执行从实值纹理像素坐标到最终纹理图像值或色彩的映射。咱们将对纹理过滤的探讨分为两种次要状况:一种是单个纹素映射到一个具备多个片段大小(放大)的区域,另一种是多个纹素映射到单个片段(放大)所笼罩的区域,因为它们的解决形式齐全不同。

7.2.1 放大纹理 Magnifying a Texture

咱们最后的纹理探讨指出,将这些子纹理准确坐标映射到纹理图像色彩的一个常见办法是简略地抉择蕴含片段中心点的纹理,并间接应用它的色彩。这种办法称为最近邻纹理,计算起来非常简单。对于任何 (${u_{texel}}$, ${v_{texel}}$) texel 坐标,整数 texel 坐标 (${u_{{\mathop{\rm int}} }}$, ${v_{{\mathop{\rm int}} }}$) 是最近的整数 texel 核心,通过截断计算:

计算完这个整数纹素坐标后,咱们只需应用函数 Image(),它将整数纹素坐标映射到纹素值,以查找纹素的值。返回的色彩被传递给以后片段的片段着色器。尽管这种办法计算简略疾速,但当纹理以单个纹素笼罩超过 1 个像素的形式映射时,它有一个显著的毛病。在这种状况下,纹理被称为放大,因为屏幕上的多个片段的四边形块齐全被纹理中的单个纹理像素笼罩,如图 12 所示。

应用最近邻纹理,正方形中的所有 (${u_{texel}}$,${v_{texel}}$)texel 坐标

将映射到整数纹理坐标(iint,jint),从而产生一个恒定的片段着色器值。这是纹素空间中高度和宽度为 1 的正方形,以纹素核心为核心。这会产生显著的恒定色彩正方形,这往往会引起人们对低分辨率图像已映射到外表的事实的留神。请参阅图 12 以获取与片段着色器一起应用的最近邻过滤纹理的示例,该片段着色器间接将纹理作为最终输入色彩返回。在大多数状况下,这种块状后果不是所需的视觉印象。

问题在于最近邻纹理示意纹理图像作为 (u,v) 的分段常数函数。生成的片段着色器属性在三角形中的所有片段中放弃不变,直到 ${u_{{\mathop{\rm int}} }}$ 或 ${v_{{\mathop{\rm int}} }}$ 发生变化。因为 floor 操作在整数值处是不间断的,这会导致由三角形外表上的纹理示意的函数中的尖利边缘。

纹素边界不间断色彩问题的常见解决方案是将纹理图像值视为指定了不同类型的函数。咱们不是从离散纹理图像值创立分段常数函数,而是创立 分段平滑色彩函数 。尽管有很多办法能够从一组离散值创立平滑函数,但光栅化硬件中最常见的办法是在二维中每个纹素核心的色彩之间进行 线性插值。该办法首先计算小于 (${u_{texel}}$,${v_{texel}}$) 的最大纹素核心坐标 (${u_{{\mathop{\rm int}} }}$,${v_{{\mathop{\rm int}} }}$),即纹素坐标(即纹素坐标的上限减去半纹素偏移量):

换句话说,(uint,vint) 定义了四个相邻 texel 核心的正方形的最小角(纹理图像空间的左下角),它们“束缚(buond)”了 texel 坐标(图 13)。找到这个正方形后,咱们还能够计算一个小数纹理坐标 0.0≤${{\rm{u}}_{{\rm{frac}}}}$,${v_{{\rm{frac}}}}$<1.0,它定义了 4 纹理正方形内的纹理坐标的地位。

咱们应用 Image() 来查找正方形四个角的纹素色彩。为了便于记号,咱们为正方形四个角的纹理色彩定义了以下简写模式(图 14):

而后,咱们定义了一个平滑的插值的 4 个纹素围绕纹素坐标。咱们将平滑映射定义为两个阶段。首先,咱们基于分数 u 坐标,沿着正方形的最小 v 边在色彩之间线性插值:

并且相似地沿着最大 v 边:

最初,咱们应用分数 v 坐标在这两个值之间进行线性插值

无关这两个步骤的图形示意,请参见图 15。将这些代入一个繁多的间接公式,咱们失去

这被称为双线性纹理过滤(bilinear texture filtering),因为插值波及到二维的线性插值,从四个相邻纹理图像值生成平滑函数。它在硬件 3D 图形系统中十分风行。咱们先沿 u 插值,而后沿 v 插值的事实并不影响后果(除了潜在的精度问题)。疾速替换表明,无论哪种办法,后果都是雷同的。然而,请留神这不是仿射映射。二维仿射映射是由三个不同的点惟一定义的。咱们的双线性纹理映射的第四个源点可能与其余三个点定义的映射不匹配。

应用双线性过滤,整个纹理域的色彩是间断的。一个最近邻过滤和双线性过滤之间视觉差别的示例如图 16 所示。尽管双线性滤波能够通过缩小视觉块状来极大地提高放大纹理的图像品质,但它不会为纹理增加新的细节。如果将纹理放大很多(即 1 纹素映射到许多像素),因为不足细节,图像看起来会很含糊。图 16 所示的纹理被高度放大,导致左图 (a) 中呈现显著的块状,右图 (b) 中呈现含糊。

7.2.2 实际中的纹理放大

IvAPI 应用 IvTexture 函数 SetMagFiltering 来管制纹理放大率。Iv 反对双线性过滤和最近邻抉择。它们别离设置如下:

IvTexture* texture;
// ...
{
// Nearest-neighbor
texture->SetMagFiltering(kNearestTexMagFilter);
// Bilinear interpolationc
texture->SetMagFiltering(kBilerpTexMagFilter);
// ...
7.2.3 纹理放大

到目前为止,在咱们探讨光栅化的过程中,咱们次要通过核心来指代片段——位于正方形片段核心的无穷小点(当初持续假如只有残缺的片段)。然而,片段具备非零区域。片段区域和代表它的点样本之间的这种差别在纹理的常见状况下变得非常明显。

举个例子,设想一个物体离相机很远。场景中的物体通常都具备高细节的纹理。这样做是为了防止含糊(比方咱们在图 16b 中看到的含糊),当一个物体凑近相机时,它利用了一个低分辨率的纹理。当雷同的物体和纹理被挪动到远处时(这是动静场景中的常见状况),因为物体的视角缩放,同样的,具体的纹理将被映射到屏幕上越来越小的区域。这被称为纹理放大,因为它是放大的正比。这导致雷同的对象和纹理笼罩的碎片越来越少。

在一个极其(但实际上很常见)的状况下,整个高细节纹理可能是以这样一种形式映射,它只映射到几个片段。图 17 提供了这样一个例子;在这种状况下,请留神,如果对象略微挪动(甚至小于一个像素),笼罩片段中心点的确切纹素可能会产生巨大变化。事实上,这样的点采样在纹理中简直是随机的,并且会导致用于片段的纹理的点采样色彩随着对象在屏幕上以渺小的亚像素量挪动而在帧与帧之间激烈变动。随着工夫的推移,这可能会导致闪动,这是动画渲染图像中令人分心的伪影。

问题在于,纹理中的大多数纹素对片段简直都有雷同的“要求”,因为它们都投影在片段的矩形区域内。片段纹理样本的整体色彩应代表其外部的所有纹素。一种思考办法是将投影立体上残缺片段的正方形映射到三角形立体上,失去一个(可能是歪斜的)四边形,如图 18 所示。为了偏心地评估该片段的纹理色彩,咱们须要依据每个纹素笼罩的四边形的绝对面积,计算该四边形中所有纹素色彩的加权平均值。给定纹素笼罩的片段越多,该纹素的色彩对片段纹理样本的最终色彩的奉献就越大。

尽管准确的面积加权均匀办法会给出正确的片段色彩和将防止点采样呈现的问题,实际上这不是最适宜实时光栅化的算法。依据纹理的映射形式,一个片段能够笼罩简直有限数量的纹素。在每个片段的根底上查找和求和这些纹素将须要潜在的无限量的每个片段计算,这甚至远远超出了硬件光栅化零碎的能力。须要一种更快(最好是恒定工夫)的办法来迫近这种纹素均匀算法。对于大多数古代图形系统,一种称为 mipmapping 的办法能够满足这些要求。

7.3 Mipmapping

Mipmapping[157] 是一种纹理过滤办法,可防止计算大量纹素的平均值。它通过事后计算和存储每个纹理的附加信息来实现这一点,这比规范纹理须要一些额定的内存。这是每个纹理样本的恒定工夫操作,并且每个纹理须要固定数量的额定存储(实际上,它会将必须存储的纹素数量减少大概三分之一)。Mipmapping 是硬件和软件光栅化器中风行的过滤算法,并且在概念上绝对简略。

要了解 mipmapping 背地的基本概念,请设想一个 2×2 纹素的纹理。如果咱们看一下整个纹理映射到单个片段的状况,咱们能够用 1×1 纹理(繁多色彩)替换 2×2 纹理。一种适合的色彩是 2×2 纹理中 4 个纹素的平均值。咱们能够间接应用这个新纹理。如果咱们在应用程序加载时事后计算 1×1 纹素的纹理,咱们能够依据须要简略地在两个纹理之间进行抉择(图 19)。

当给定的片段以这样一种形式映射,它只笼罩原始 2 × 2 纹素纹理中的 4 个纹素之一,咱们简略地应用放大办法和原始 2 × 2 纹理来确定色彩。如果片段笼罩整个纹理,咱们将间接应用 1×1 纹理,再次对其利用放大算法(只管应用 1×1 纹理,这只是单个纹素色彩)。1×1 纹理充沛代表了单个纹素中 2×2 纹理的整体色彩,但它不包含原始 2×2 纹素纹理的细节。这两个纹理版本都具备另一个版本没有的有用个性。

Mipmapping 采纳这种办法,并将其推广到任何具备二维幂次的纹理。出于探讨的目标,咱们假如纹理是正方形的(算法不须要这样做,咱们稍后
将在实践中对 mipmapping 的探讨中看到)。

生成 mipmap 级别的一种办法是首先取初始纹理图像 Image0(缩写 I0) 的维数为 ${w_{{\rm{texture}}}}$ = ${h_{{\rm{texture}}}}$ = ${{\rm{2}}^{\rm{L}}}$,并通过将四个相邻纹素的每个方格均匀为一个纹素来生成一个新版本的纹理。

这将生成大小为 Image1 的纹理图像

如下:

此时 0 ≤ i, j <${1 \over 2}{{\rm{w}}_{{\rm{texture}}}}$。Image1 中的每个纹素都代表了 Image0 中对应的 4 个纹素块的整体色彩(图 20)。请留神,如果咱们对两个版本的纹理应用雷同的原始纹理坐标,则图像 1 只是显示为图像 0 的含糊版本(具备图像 0 的一半细节)。如果图像 0 中大概四个相邻纹素的块笼罩了一个片段,那么咱们能够在纹理时简略地应用图像 1。然而更极其的放大状况呢?该算法能够递归地持续。对于每个尺寸大于 1 的图像 Imagei,咱们能够定义 Imagei+1,其尺寸是 Imagei 的一半,并将 Imagei 的均匀纹素定义为 Imagei+1。这会生成一整套原始纹理的 L+1 个版本,其中 Imagei 的尺寸等于

这造成了一个图像金字塔,每一个都是金字塔中前一个图像的一半尺寸(并蕴含四分之一的纹素)。图 21 提供了这样一个金字塔的例子。咱们在加载时为场景中的每个纹理计算这个金字塔一次或作为离线预处理,并将每个整个金字塔存储在内存中。

这种计算 mipmap 图像的简略办法称为盒子过滤(正如咱们将一个 2×2“盒子”的纹素均匀为一个纹素)。盒子过滤不会产生十分高质量的 mipmap,因为它往往会使图像过于含糊,同时仍会产生伪影。其余更简单的办法更罕用于将每个 mipmap 级别过滤到下一个较低级别。一个很好的例子是 Lanczos 过滤器。参见 Turkowski[148] 或 Wohlberg[159] 理解其余图像过滤办法的详细信息。在生成每个级别时还必须小心,以确保对线性色彩进行计算;如果原始纹理色彩为 sRGB,则转换为线性,进行任何计算,而后转换回 sRGB 以存储 mipmap 值。

7.3.1 应用 Mipmap 对片段进行纹理解决

应用 mipmap 对片段进行纹理化的最简略、通用的算法能够总结如下:

  1. 通过确定片段角落处的纹理坐标,确定屏幕空间中的片段映射回纹理空间中的四边形。
  2. 将片段正方形映射到纹理空间中的四边形后,抉择最靠近将四边形准确映射到单个纹素的任何 mipmap 级别。
  3. 应用所需的放大算法,应用在上一步中抉择的“最佳匹配”mipmap 级别对片段进行纹理解决。

确定最佳匹配 mipmap 级别的罕用办法有很多,并且有多种办法能够将此 mipmap 级别过滤为最终的片段纹理值。咱们心愿防止必须将片段的角显式映射回纹理空间,因为这计算起来很低廉。咱们能够利用其余光栅化阶段曾经计算的信息。正如咱们在 5.1 和 6.2 节中看到的,在光栅化中通常计算给定片段核心的片段着色器输出值(例如,纹理坐标)与给定片段右侧和下方片段的值之间的差别,以用于前向差分。咱们没有明确示意,这些差别能够示意为导数。上面的清单旨在为这四个偏导数中的每一个调配直观的值。对于不相熟∂的人来说,它是偏导数的符号,是多元微积分的基本概念。∂运算符示意当您更改输出重量之一时,向量值函数的输入的一个重量会产生多少变动。

如果一个片段映射到大概 1 个 texel,那么

换句话说,即便纹理被旋转,如果片段与映射到它的纹素大小大致相同,那么单个片段上纹理坐标的整体变动长度约为 1 纹素。请留神,所有这四个差别都是独立的。这些局部取决于 utexel 和 vtexel,而这又取决于纹理大小。事实上,对于这些差别中的每一个,从 Image i 挪动 Imagei+1 都会导致差别减半。正如咱们将看到的,在计算 mipmapping 值时,这是一个有用的属性。

Heckbert[74] 中形容了一个用于将这些差别转化为像素‑纹素大小比度量的通用公式,该公式定义了一个映射回纹理空间的像素半径的公式。请留神,这实际上是两个半径中的最大值,utexel 中的像素半径和 vtexel 中的像素半径:

咱们能够看到(通过替换∂)每次咱们从 Imagei 挪动到 Imagei+1 时,这个值都会减半(因为所有∂值都会减半)。因而,为了找到将 1 个纹素映射到残缺片段的 mipmap 级别,咱们必须计算 L 使得

其中 size 是应用 Image0 的纹素坐标计算的。求解 L,

L 的这个值就是咱们应该应用的 mipmap 级别的索引。请留神,如果咱们插入对应于准确的一对一纹理到屏幕映射的局部,咱们失去 size=1,这导致 L

咱们失去 size=1,这导致 L=0,这与预期的原始纹理图像绝对应。这为咱们提供了一种关闭模式的办法,能够将现有的局部(用于在扫描线上插入纹
理坐标)转换为特定的 mipmap 级别 L。最初的公式是

请留神,L 的值是实数,而不是整数(稍后咱们将探讨将此值映射到离散 mipmap 金字塔的办法)。后面的性能只有一种可能用于计算 mipmap 级别 L 的选项。图形系统应用这个值的许多简化和近似值(它自身就是一个近似值)甚至其余函数来确定正确的 mipmap 级别。事实上,某些硬件设施应用的 L 的特定近似值是如此不同,以至于一些有教训的 3D 硬件用户实际上能够通过查看渲染的 mipmap 图像来辨认特定的显示硬件。其余 3D 硬件容许开发人员(甚至最终用户)对应用的 L 值进行偏差,因为一些用户更喜爱“清晰”的图像(将 L 偏差负方向,抉择更大、更具体的 mipmap 级别和每个片段),而其他人更喜爱“平滑”图像(将 L 偏差正方向,趋向于不太具体的 mipmap 级别和每个片段的纹素更少)。无关 mipmap 级别抉择的一种状况的具体推导,请参阅 Eberly[35] 的第 106 页。

用于升高 mipmap 的每个片段开销的另一种办法是抉择一个 L 值,因而在每帧中每个三角形的都有单个 mipmap 级别,并应用该 mipmap 级别光栅化整个三角形。尽管这种办法不须要对 L 进行任何每个片段的计算,但它可能会导致重大的视觉伪影,尤其是在三角形的边缘,其中 mipmap 级别可能会急剧变动。反对 mipmapping 的软件光栅化器常常应用这种办法,称为逐三角形 mipmapping(per‑triangle mipmapping)。

请留神,就其本质而言,mipmapping 偏向于在远处的物体上应用较小的纹理。这意味着 mipmapping 实际上能够进步软件光栅化器的性能,因为较小的 mipmap 级别比残缺细节纹理更可能适宜处理器的缓存。在大多数 GPU 上也是如此,因为小型片上纹理缓存存储器 (small, on-chip texture cache memories) 用于保留最近拜访的纹理图像区域。因为 GPU 和软件光栅化器在某种程度上受到读取纹理的内存带宽的限度,因而将纹理保留在缓存中能够显着升高这些带宽需要。此外,如果点采样与未映射的纹理一起应用,则相邻像素可能须要读取纹理中相距较远的局部。通过纹理的这些大的每像素步幅可能会导致可怕的缓存行为,并可能重大妨碍非 mipmapped 光栅化器的性能。这些由缓存未命中引起的处理器管道“进行(stalls)”或期待使得计算 mipmapping 信息的老本(至多在每个三角形的根底上)是值得的,与视觉品质的显着进步无关。

7.3.2 纹理过滤和 Mipmap Texture Filtering and Mipmaps

上述办法的工作原理是,给定片段将有一个繁多的“最佳”mipmap 级别。然而,因为每个 mipmap 级别是每个维度中下一个 mipmap 级别大小的两倍,因而最靠近的 mipmap 级别可能不是准确的片段到纹理映射。线性 mipmap 过滤不是抉择给定的 mipmap 级别作为最佳,而是应用相似于(双)线性纹理过滤的办法。基本上,mipmap 过滤应用实值 L 来查找限度给定片段与纹素比率的相邻 mipmap 级别对和 $\left\lfloor {\rm{L}} \right\rfloor $ 和 $\left\lceil {\rm{L}} \right\rceil $(地板和天花板符号)。残余的小数局部 (L 到 $\left\lceil {\rm{L}} \right\rceil $) 用于在两个 mipmap 级别中找到的纹理色彩之间进行混合。

放在一起,当初有两个独立的过滤轴,每个轴都有两种可能的过滤模式,导致四种可能的 mipmap 过滤模式,如表 1 所示。在这些办法中,最风行的是线性双线性,也称为三线性插值滤波或 trilerp,因为它是双线性插值的准确 3D 模仿。它是这些 mipmap 过滤操作中最低廉的,须要每个片段查找 8 个纹素,以及 7 个线性插值(两个 mipmap 级别中的每一个级别三个,另外一个用于在级别之间进行插值),但它也产生了最平滑的后果。在 mipmap 级别之间进行过滤也会减少应用的纹理内存带宽量,因为每个样本都必须拜访两个 mipmap 级别。因而,多级 mipmap 过滤通常会对消后面提到的 mipmap 在硬件图形设施上的性能劣势。

最初一种更新模式的 mipmap 过滤称为各向异性过滤。到目前为止探讨的 mipmap 过滤办法隐含地假如像素在映射到纹理空间时会产生一个与某个圆十分靠近的四边形——换句话说,纹理空间中的四边形基本上是正方形的状况。在实践中,通常状况并非如此。对于极其透视下的多边形,一个残缺的片段通常会映射到纹理空间中一个很长、很薄的四边形。规范的各向同性过滤模式可能看起来太含糊(依据四边形的长轴抉择了 mipmap 级别)或太尖利(依据四边形的短轴抉择了 mipmap 级别)。各向异性纹理过滤在对 mipmap 进行采样时会思考纹理空间四边形的纵横比,并且可能过滤 mipmap 中的非正方形区域以生成精确示意歪斜多边形纹理的后果。

7.3.3 实际中的 Mipmapping

的默认 CreateTexture 接口仅调配根本级别的纹理数据,而不调配其余 mipmap 级别。要应用 mipmaps 创立纹理,咱们应用 CreateMipmappedTexture,如下所示:

IvResourceManager* manager;
// image data
const int numLevels = 5;
void* data[numLevels];
// ...
{
IvTexture* texture = manager->CreateMipmappedTexture(kRGBA32TexFmt,
width, height,
data, numLevels, kImmutableUsage);
}

请留神,咱们当初传入了一个图像数据数组——每个数组条目都是一个 mipmap 级别。咱们还必须指定级别数。如果应用动静或默认应用创立 mipmapped 纹理,咱们还能够应用 IvTexture 函数 BeginLoadData 和 EndLoadData。然而,在 mipmap 的状况下,咱们应用这些函数的参数 unsignedintlevel(以前默认为 0),它指定 mipmap 级别。最高分辨率图像的 mipmap 级别为 0。每个后续级别编号(1、2、3、…)示意 mipmap 金字塔图像,其尺寸为前一级别的一半。一些 API 要求指定一个“残缺的”金字塔(始终到 1×1 纹素)能力使 mipmapping 失常工作。在实践中,为所有 mipmap 纹理提供残缺的金字塔是一个好主见。残缺金字塔中的 mipmap 层数等于

请留神,mipmap 级别的数量基于纹理的较大尺寸。一旦一个维度降落到 1 纹素,它就会放弃在 1 纹素,而更大的维度持续减小。因而,对于 32×8‑纹素纹理,mipmap 级别如表 2 所示。

请留神,数组中提供的 mipmap 级别图像的纹素传递给 CreateMipmappedTexture 或 BeginLoadData 返回的数组中的设置必须由应用程序计算。Iv 只是承受这些图像作为 mipmap 级别并间接应用它们。一旦指定了纹理的所有 mipmap 级别,就能够通过将纹理采样器附加为着色器对立变量,将纹理用于 mipmap 渲染。指定整个金字塔的示例如下:

IvTexture* texture;
// ...
{for (unsigned int level = 0; level < texture->GetLevels(); level++) {unsigned int width = texture->GetWidth(level);
        unsigned int height = texture->GetHeight(level);
        IvTexColorRGBA* texels
        = (IvTexColorRGBA*)texture->BeginLoadData(level);
       for (unsigned int y = 0; y < height; y++) {for (unsigned int x = 0; x < width; x++) {IvTexColorRGBA& texel = texels[x+y* width];
               // Set the texel color, based on
              // filtering the previous level...
           }
    }
    texture->EndLoadData(level);
}

为了设置放大过滤器,应用了 IvTexture 函数 SetMinFiltering。Iv 反对非 mipmapped 模式(双线性过滤和最近邻抉择)和所有四种 mipmapped 模式。品质最好的 mipmapped 模式(如前所述)是三线性过滤,应用

IvTexture* texture;
// ...
texture->SetMinFiltering(kBilerpMipmapLerpTexMinFilter);
// ...

8 从片段到像素 From Fragments to Pixels

到目前为止,本章曾经探讨了生成片段,计算片段着色器的每个片段源值,以及评估片段着色器(纹理查找)的更简单方面的一些细节。然而,本章的前几节概述了所有这些每片段工作的真正指标:在场景的渲染视图中生成像素的最终色彩。回忆一下,像素是形成矩形网格屏幕(或帧缓冲区)的目标值。像素是“箱”,咱们在其中搁置对该像素区域有影响的的外表碎片。片段代表这些像素大小的外表碎片。最初,咱们必须将所有落入给定像素的箱子中的片段转换为该像素的繁多色彩和深度。到目前为止,咱们在本章中做了两个重要的简化假如:

  • 所有片段都是不通明的;也就是说,近处的片段会覆盖更远的片段。
  • 所有片段都是残缺的;也就是说,一个片段笼罩了整个像素。

综上所述,这两个假如导致了一个重要的整体简化:给定像素处最近的片段齐全决定了该像素的色彩。在这样的零碎中,咱们须要做的就是在一个像素处找到最近的片段,对该片段进行着色,而后将后果写入帧缓冲区。在探讨可见外表确定和纹理时,这是一个有用的简化假如。然而,它限度了示意某些常见类型的外表资料的能力。它还可能导致屏幕上对象边缘呈现锯齿状的视觉伪影。因而,古代图形系统中的两个附加性能打消了这些简化假如:像素混合容许片段局部通明,抗锯齿解决蕴含多个局部片段的像素。咱们将通过对两者探讨来完结本章。

8.1 像素混合 Pixel Blending

像素混合是一个以片段为单位的非几何函数,它的输出是以后片段的着色色彩(咱们称之为 Csrc),片段的 alpha 值(它是片段色彩的一个适当的组成部分,但为了不便,咱们将其称为 Asrc),帧缓冲区中像素的以后色彩(Cdst),以及有时帧缓冲区中该像素的现有 alpha 值(Adst)。这些输出,加上一对混合函数 Fsrc 和 Fdst,定义了将被写入帧缓冲器中的像素的后果色彩(以及可能的 alpha 值),即 CP。请留神,CP 一旦写入,在当前波及同一像素的混合操作中就会变成 Cdst。混合的个别模式是

其中⊕能够示意 +、−、min() 或 max()。咱们还能够有第二对只影响 A 的函数。然而,在游戏中的大多数状况下,咱们应用下面的公式并将⊕设置为 +。

源和指标的 alpha 值通常被解释为不透明度(咱们将在上面探讨 alpha 混合时理解起因)。然而,alpha 也能够解释为色彩对像素的局部笼罩——在这种状况下,alpha 是色彩笼罩的像素的百分比。一般来说,这种解释在游戏中很少应用,除非可能在界面(游戏面板)中。它在应用像素混合进行 2D 合成或图像分层(也称为 alpha 合成)时更常应用。咱们将在 8.2 节中更具体地探讨像素笼罩。无关 alpha 作为覆盖率的更多信息,请参阅 [123]。

像素混合的最简略模式是齐全禁用混合(“源替换”模式),其中片段替换现有像素。这相当于

像素混合通常以其最常见的非凡状况的名称来指代:alpha 混合。Alpha 混合波及应用源 Alpha 值 Asrc 作为新片段的不透明度,以在 Csrc 和 Cdst 之间进行线性插值:

Alpha 混合须要 Cdst 作为操作数。因为 Cdst 是像素色彩(通常存储在帧缓冲区中),所以 alpha 混合能够(取决于硬件)要求从帧缓冲区中读取每个混合片段的像素色彩。这种减少的内存带宽意味着 alpha 混合会影响某些零碎的性能(以相似于深度缓冲的形式)。此外,阿尔法混合还有其余几个个性,使其在实践中的应用有些挑战。

Alpha 混合旨在计算新的像素色彩,基于新的片段色彩示意可能半透明的外表,其不透明度由 Asrc 给出。

Alpha 混合仅应用片段 Alpha 值,而不是指标像素的 Alpha 值。假如现有像素色彩代表比以后片段更远的像素处的整个现有场景,半透明片段搁置在该像素的后面。对于上面的探讨,咱们将 alpha 混合示意为

多个 alpha 混合操作的后果取决于程序。每个 alpha 混合操作都假设 Cdst 示意比新片段更远的所有对象的最终色彩。如果咱们将两个可能半透明的片段 (C1,A1) 和 (C2,A2) 混合到背景色彩 C0 上作为两个混合的序列,咱们能够很快看到,一般来说,扭转程序混合扭转后果。例如,如果咱们比拟两个可能的混合程序,设置 A1=1.0,并开展函数,咱们失去

这两个方面简直素来都不是平等的。两种混合程序通常会产生不同的后果。在大多数状况下,两个外表与背景色彩的 alpha 混合取决于程序。

8.1.1 像素混合和深度缓冲 Pixel Blending and Depth Buffering

在实践中,alpha 混合的这种程序依赖性使深度缓冲复杂化。深度缓冲区基于这样的假如,即给定深度的片段将齐全遮蔽任何深度更大的片段,这仅实用于不通明对象。在存在 alpha 混合的状况下,咱们必须以十分特定的程序计算像素色彩。咱们能够对所有三角形进行深度排序,但如上所述,这很低廉,并且对许多数据集存在重大的正确性问题。相同,一种抉择是假如对于大多数场景,半透明三角形的数量远小于不通明三角形的数量。给定一组三角形,尝试正确计算混合像素色彩的一种办法如下:

  1. 将场景中的不通明三角形收集到一个列表 O 中。
  2. 将场景中的半透明三角形收集到另一个列表 T 中。
  3. 应用深度缓冲失常渲染 O 中的三角形。
  4. 将 T 中的三角形按深度排序为从远到近的程序。
  5. 应用深度缓冲,通过混合渲染排序列表 T。

这仿佛能够解决问题。然而,在大多数状况下,每个三角形的深度排序依然是一项低廉的操作,必须在主机 CPU 上实现。此外,每个三角形排序无奈解决所有差别,因为存在无奈正确排序的三角形的常见配置。曾经提出了其余办法来防止这两个问题。一种这样的办法是在每个对象级别进行深度排序以防止粗略的无序混合,而后应用更简单的办法,例如深度剥离 [44],它应用高级可编程着色和对象的屡次渲染来“剥离”更近的外表(应用深度缓冲区)并生成深度排序的色彩。尽管相当简单,但该办法齐全在 GPU 上运行,并专一于让最靠近的图层正确,依据实践是越来越深的透明度取得递加的回报(因为它们对最终色彩的奉献越来越少)。

在某些特定利用的状况下,能够防止像素混合三角形的深度排序或深度剥离。另外两种常见的像素混合模式是可替换的,因而是程序无关的。这两种混合模式称为 add 和 modulate。add 混合产生“发光”物体的成果,定义如下:

modulate 混合实现色彩过滤。它被定义为

请留神,这些成果都不波及源色彩或指标色彩的 alpha 重量。add 和 modulate 混合模式依然须要先绘制不通明对象,而后是混合对象,但都不须要将混合对象分类为深度排序。因而,这些混合模式在粒子系统成果中十分风行,其中应用了数千个渺小的混合三角形来模仿前期的烟雾、蒸汽、灰尘或水。其余更简单的与程序无关的透明度解决方案是可能的;一个例子见 [107]。

请留神,如果深度缓冲用于未排序的混合对象,则必须在禁用深度缓冲区写入的状况下绘制混合对象,否则两个混合对象的任何无序(从前到后)渲染将导致更多远处的物体没有被绘制。从某种意义上说,混合对象不存在于深度缓冲区中,因为它们不会遮挡其余对象。

8.1.2 预乘 Alpha Premultiplied Alpha

在下面的探讨中,咱们假如咱们的色彩与间接的 RGB 值和关联的 alpha 值一起存储。RGB 值代表咱们的基色,alpha 代表它的透明度或覆盖率;例如,(1,0,0,12) 示意半透明红色。然而,更好的混合格局是咱们将根本 RGB 值乘以 alpha 值 A,或者

这称为预乘 alpha,当初咱们的 RGB 值代表对最终后果的奉献。这里的假如是咱们的 alpha 值位于 [0,1] 范畴内。如果为每个通道应用 8 位值,则须要在乘法后除以 255。在应用 sRGB 时,请务必将线性到 sRGB 的转换利用于预乘色彩——不要将其利用于基色,而后乘以 alpha。应用这个公式,阿尔法混合变成

src 是 AsrcCsrc 这仿佛并没有给咱们带来太多益处,除了可能节俭了一个乘法。但预乘 alpha 具备许多显着劣势。首先,思考咱们应用带有双线性采样的纹理的状况。假如咱们在通明 (A=0) 绿色纹素旁边有一个纯色 (A=1) 红色纹素。应用规范色彩,如果咱们在它们之间进行两头解决,咱们失去

只管咱们从纯红色插值到齐全通明的色彩,但不知何故咱们最终失去了黄色的半透明色彩。如果咱们改用预乘 alpha,通明色的色彩值全副变为 0,所以咱们有

这是咱们想要的半透明红色的预乘 alpha 版本。

即便咱们不应用齐全通明的色彩,咱们依然会失去奇怪的后果,比如说

这又比咱们预期的要黄得多。应用预乘的 alpha 色彩,咱们失去:

并适当升高了第二种色彩的奉献。因而,预乘 alpha 的第一个也是最重要的长处是它能够从纹理采样中失去正确的后果。

第二个长处是它容许咱们将简略的混合方程扩大到更大的混合操作集,称为 Porter‑Duff 混合模式 [123]。这些不罕用于渲染 3D 世界,但它们在混合 2D 元素中十分常见,因而理解如何为游戏内 UI 复制这些成果对于某些成果很有用。一个例子是“Src In”运算符,它将目的地的任何奉献替换为源的比例分数,或者:

这最终成为:

这是规范混合模式和纯色无奈实现的。

第三个长处容许咱们创立色彩值大于 1 的通明值,这对于创立照明成果很有用。例如,咱们能够应用 (1,1,1,12) 的预乘 alpha 色彩,它具备 (2,2,2,12) 的间接色彩等价物。最终后果将对场景产生两倍的奉献,从而产生发射成果。Forsyth[49] 在粒子成果中提供了一个很好的用处。通常咱们心愿粒子以添加剂(即火花)开始,而后变成 alpha 混合(烟灰和烟雾)。通过应用预乘 Alpha,咱们能够创立一个具备子区域的纹理,该子区域代表不同粒子类型的色彩,并将其与繁多混合模式一起应用。火花粒子能够应用具备非零 RGB 值或 (R,G,B,0) 的零 alpha 色彩。烟雾粒子能够应用规范的预乘 alpha 色彩,或 (RA,GA,BA,A)。通过将这些与预乘 alpha 混合方程一起应用,火花区域被增加到场景中,烟雾区域将被 alpha 混合。对于单个粒子的生命周期,咱们须要做的就是将其纹理坐标从纹理的不同区域映射到地图,其可见示意将从火花缓缓变为烟雾。并且所有粒子都将与单个纹理和单个绘制调用正确合成,而无需在 add 和 alpha 混合模式之间切换。

最初,当应用纯色时,混合图层不是关联的,因而为了取得混合图层 A‑D 的正确后果,您必须将 A 与 B 混合,而后将后果与 C 混合,而后将后果与 D 混合。然而,有有时您可能想先混合 B 和 C,例如某种基于屏幕的后处理成果。预乘 alpha 容许您这样做并稍后增加奉献 A 和 D。再次留神,为混合操作设置的程序必须雷同——您不能将 C 与 B 混合并冀望失去与将 B 与 C 混合的雷同后果。

预乘 Alpha 的惟一毛病是,当应用 8 位或更小的色彩通道时,最终会失去精度。如果您打算在某些能够放大它们的操作中仅应用 RGB 色彩,这只是一个问题。否则,为了获得最佳后果,强烈建议应用预乘 alpha。

8.1.3 在实践中混合

只管在 Iv 反对的模式之外还有许多选项,但在大多数图形系统中都能够非常简单地启用和管制混合。通过 IvRenderer 函数 SetBlendFunc 设置混合模式,该函数在单个函数调用中设置 Fsrc、Fdst 和运算符⊕。要应用经典的 alpha 混合(没有预乘 alpha),函数调用是

renderer->SetBlendFunc(kSrcAlphaBlendFunc, kOneMinusSrcAlphaBlendFunc,
kAddBlendOp);

应用调用设置 Add 模式

renderer->SetBlendFunc(kOneBlendFunc, kOneBlendFunc, kAddBlendOp);

Modulate 混合能够通过调用应用

renderer->SetBlendFunc(kZeroBlendFunc, kSrcColorBlendFunc, kAddBlendOp);

还有更多的混合性能和操作可用;无关更多详细信息,请参阅源代码。

回忆一下,在渲染混合对象时禁用 z 缓冲区写入通常很有用。这是通过深度缓冲屏蔽实现的,后面在深度缓冲局部中进行了形容。

8.2 抗锯齿 Antialiasing

咱们之前提出的另一个简化光栅化假如,即局部片段被疏忽或“晋升”为残缺片段的想法,引发了它本人的一系列问题。将所有片段转换为全有或全无状况的想法是容许咱们假如单个片段将“博得”一个像素并确定其色彩。咱们应用这个假如将每个片段的计算缩小到单点样本。

如果咱们将像素视为纯点样本,没有区域,这是正当的。然而,在咱们对片段的初步探讨和对 mipmapped 纹理的具体探讨中,咱们发现状况并非如此;每个像素代表屏幕上一个具备非零面积的矩形区域。因而,在像素的矩形区域内可能会看到多个(局部)片段。图 22 提供了这种多片段像素的示例。

应用探讨的点采样办法,咱们将抉择单个片段的色彩来示意像素的整个区域。然而,如图 23 所示,这个像素中心点样本可能无奈代表整个像素的色彩。在图中,咱们看到像素的大部分区域是深灰色的,只有核心的一个很小的正方形是亮红色的。后果,抉择亮红色的像素色彩并不能精确地示意整个像素矩形的色彩。咱们对矩形色彩的感知与矩形中每种色彩的绝对面积无关,这是单点采样办法无奈示意的。

图 24 使这一点更加显著。在这种状况下,咱们看到两个怪异像素的例子(每个 9 像素 3×3 网格中的核心像素)。在两种核心像素配置中(图左侧的顶部和底部),绝大多数外表区域都是深灰色。在这两种状况下,核心像素都蕴含一个小的红色片段。在这两种状况下,红色片段的大小雷同,但在两种状况下,它们绝对于核心像素的地位略有不同。在第一个(顶部)示例中,红色片段恰好蕴含像素核心,而在底部示例中,红色片段不蕴含像素核心。右列显示在每种状况下将调配给核心像素的色彩。只管它们的几何配置简直雷同,但为这两个像素调配了十分不同的色彩。这证实了一个事实,即对像素色彩进行单点采样可能会导致一些随便的后果。事实上,如果咱们设想红色碎片随着工夫的推移在屏幕上挪动,当红色碎片穿过每个像素的核心时,整行像素会在红色和灰色之间闪动。

能够为图中的 2 个像素确定更精确的色彩。如果图形系统应用像素矩形内每个片段的绝对面积来加权像素的色彩,后果会好得多。在图 25 中,咱们能够看到红色片段笼罩了大概 10% 的像素区域,留下了其余 90% 为深灰色。通过绝对区域加权色彩,咱们失去一个像素色彩

请留神,此计算与红色片段在像素内的地位无关;只有碎片的大小和色彩很重要。这种基于区域的办法防止了咱们看到的点采样谬误。该零碎能够扩大到给定像素内的任意数量的不同色彩的片段。给定一个面积为 a 的像素和一组 n 个不相交的片段,每个片段在像素内都有一个面积 ai 和色彩 Ci,则该像素的最终色彩为

其中 Fi 是给定片段笼罩的像素的分数,或片段的覆盖范围。这种办法称为区域抽样。事实上,这的确是一个更个别的定积分的特例。如果咱们设想咱们有一个屏幕空间函数,它示意屏幕上每个地位的色彩(与像素或像素核心无关)C(x,y),那么像素的色彩定义为区域 l≤x≤r,t≤y≤b(像素的左、右、上、下屏幕坐标),应用这种区域采样办法,等价于

这是像素面积上色彩的积分,除以像素的总面积。公式 5 的求和版本是这个更个别的积分的简化,假如像素齐全由分段恒定色彩的区域组成,即笼罩像素的片段。
作为对该办法的验证,咱们将假如像素齐全被一个色彩为 C(x,y)=CT 的残缺片段笼罩,给出下式,这是咱们在这种状况下所冀望的色彩。

尽管区域采样的确防止了齐全失落或过分强调任何单个样本,但它不是惟一应用的办法,也不是代表显示设施事实的最佳办法(物理像素的强度实际上在像素矩形内可能不是恒定的))。公式 5 中显示的区域采样隐含地对像素的所有区域进行均匀加权,使像素核心的权重等于边缘的权重。因而,它通常被称为未加权区域采样。另一方面,加权区域采样增加了一个加权函数,能够依据须要对像素的任何区域中色彩的重要性进行偏差。如果咱们简化等式 5 原始像素边界和相干函数,使得像素的边界为 0≤x,y≤1,则等式 5 变为

将等式 5 简化为等式 7,咱们定义了一个加权函数 W(x,y),它容许依据须要对像素区域进行加权:

在这种状况下,分母被设计为依据加权面积进行归一化。对等式 6 的相似替换表明,像素上的恒定色彩映射到给定色彩。还要留神(与未加权区域采样不同)像素内图元的地位当初很重要。从公式 8 能够看出,未加权区域抽样只是加权区域抽样的一种非凡状况。对于未加权的区域采样,W(x,y)=1,给出:

Hughes 等人对加权区域采样、其背地的实践以及许多常见的加权函数进行了全面探讨。[82]。对于那些心愿取得更多深度的人,Glassner[54] 和 Wohlberg[159] 具体介绍了宽泛的采样实践。

8.2.1 超采样抗锯齿 Supersampled Antialiasing

到目前为止探讨的办法显示了计算基于区域的像素色彩的实践办法。这些办法要求每个片段计算像素笼罩值。计算三角形的剖析(准确)像素笼罩值可能既简单又低廉。在实践中,纯基于区域的办法不会间接导致简略、疾速的硬件抗锯齿实现。

概念上最简略、最风行的抗锯齿办法称为过采样、超级采样或超采样抗锯齿 (SSAA)。在 SSAA 中,基于区域的采样通过在每个像素多个点对场景进行点采样来近似。在 SSAA 中,片段不是在每个像素级别生成,而是在每个样本级别生成。从某种意义上说,SSAA 在概念上只不过是将整个场景渲染为更大的(更高分辨率)帧缓冲区,而后将高分辨率帧缓冲区中的像素块过滤到最终帧缓冲区的分辨率。例如,超采样帧缓冲区的宽度和高度可能是屏幕上最终目标帧缓冲区的 N 倍。在这种状况下,超采样帧缓冲区中的每个 N×N 像素块将被过滤为屏幕帧缓冲区中的单个像素。

超级样本通过加权(或在某些案例未加权)平均值。这些采样模式的加权区域版本应用的地位和权重因制造商而异;示例地位的常见示例如图 26 所示。请留神,每个像素的超样本数量从少至 2 到多至 16 不等。M 样本 SSAA 将像素示意为 M 元素分段常数函数。局部片段将仅笼罩像素中的一些点样本,因而在后果像素中的权重会升高。

一些 N×N 样本网格也有旋转版本。这样做的起因是程度和垂直线的呈现频率很高,并且也与像素布局自身相干。通过以正确的角度旋转样本,所有 N2 个样本都位于不同的程度和垂直地位。因而,通过像素从左到右或从上到下迟缓挪动的程度或垂直边缘将别离与每个样本相交,因而将具备以 1/${{\rm{N}}^2}$ 增量变动的覆盖率值。应用屏幕对齐的 N×N 样本模式,雷同的挪动程度和垂直边缘会同时与整行或整列样本相交,从而导致笼罩值以 1/N 的增量变动。旋转模式能够更好地利用可用样本的数量。

M-sample SSAA 每个像素生成 M 倍(如前所述,个别为 2-16 倍)的片段。每个这样的(较小的)片段都有本人的色彩,通过评估每个顶点的属性、纹理值和片段着色器自身,每帧的频率比失常渲染高 M 倍。每个样本的残缺渲染管道十分弱小,因为每个样本都实在地代表了该样本的几何色彩。它也十分低廉,须要每个样本调用整个光栅化管道,从而将光栅化开销减少 2‑16 倍。即便是功能强大的 3D 硬件零碎,这也太低廉了。

8.2.2 多重采样抗锯齿 Multisampled Antialiasing

超级采样抗锯齿最低廉的方面是为每个样本创立独自的片段以及每个样本产生的纹理和片段着色。另一种抗锯齿模式意识到,3D 渲染中锯齿最可能的起因是对象边缘的局部片段,其中像素将蕴含来自不同对象的多个局部片段,通常具备十分不同的色彩。多重采样抗锯齿 (MSAA) 试图在不像 SSAA 那样进步渲染老本的状况下解决这个问题。MSAA 的工作形式与失常渲染相似,因为它以最终像素大小生成片段(包含局部片段)。它 只对每个片段评估一次片段着色器,因而与 SSAA 相比,片段着色器调用的数量显着缩小。

MSAA 增加的信息是每个样本的片段覆盖率。当一个片段被渲染时,它的色彩会被评估一次,而后为片段笼罩的每个可见样本存储雷同的色彩。样本中的现有色彩(来自较早的片段)可能会被新片段的色彩替换。但这是在每个样本级别实现的。在帧完结时,依然须要“解析”来从多个样本中计算像素的最终色彩。然而,每个样本、每个片段仅计算一个笼罩值(一个简略的几何运算)和可能的一个深度值。计算片段色彩的低廉步骤依然为每个片段实现一次。与 SSAA 相比,这大大降低了 MSAA 的费用。

MSAA 有两个奥妙之处值得一提。首先,因为 MSAA 是 基于笼罩 的,因而不会对残缺片段计算抗锯齿。渲染残缺的片段,就如同没有应用抗锯齿一样。另一方面,SSAA 通过在每个像素中屡次调用片段的着色器来打消每个像素的锯齿。一个要害的察看后果是,在单采样残缺片段中最有可能导致混叠(aliasing)的我的项目可能是纹理(因为它是片段中的最高频率值)。纹理曾经利用了一种抗锯齿模式:mipmapping。因而,在大多数状况下,这对 MSAA 来说不是问题。

另一个问题是抉择像素中的地位来评估局部片段上的着色器的问题。通常,咱们在像素核心评估片段着色器。然而,局部片段甚至可能无奈笼罩像素核心。如果咱们在像素核心对片段着色器进行采样,咱们实际上会将顶点属性外推到预期值之外。这在纹理中尤其显著,因为咱们将在三角形中可能未映射的地位读取纹理。这会导致显著的视觉伪影。大多数 3DMSAA 硬件中的解决方案是抉择片段笼罩的样本的重心。因为片段是凸的,因而重心将始终落在片段外部。这的确给零碎减少了一些复杂性,然而不包含像素核心的片段的可能配置数量是无限的。凸性和核心样本未被涉及的事实意味着可能存在一组十分无限的笼罩样本配置。甚至能够在构建硬件之前事后计算出一组可能的地位。然而,重心采样必须按一一属性申请。否则,硬件将默认应用像素核心采样。

8.2.3 实际中的抗锯齿

对于大多数渲染 API,应用 MSAA 最重要的一步是创立与该技术兼容的渲染帧缓冲区。深度缓冲须要帧缓冲旁边的附加缓冲区来存储深度值,而 MSAA 须要非凡的帧缓冲格局,其中包含每个像素内每个样本的附加色彩、深度和笼罩值。不同的渲染 API 甚至雷同 API 上的不同渲染硬件通常有不同的办法来显式申请 MSAA 兼容的帧缓冲区。一些出现 API 容许应用程序以像素格局指定样本的数量和事件布局,而另一些则仅应用单个标记来启用单个(未指定)级别的 MSAA。

最初,在向屏幕出现 MSAA 帧缓冲区时,某些出现 API 可能须要非凡标记或限度。例如,有时必须应用非凡模式将 MSAA 帧缓冲区出现给屏幕,该模式在出现后将帧缓冲区的内容标记为有效。这思考到帧缓冲区必须在出现过程中从其每像素多样本格局“解析”为每像素繁多色彩的事实,从而在此过程中毁坏多样本信息。

9 总结

光栅化为咱们提供了整个管道中一些最低级别但数学上最乏味的概念。咱们曾经探讨了数学概念(如投影变换)和渲染办法(如透视正确纹理)之间的分割。此外,咱们在探讨深度缓冲区时解决了数学精度问题。最初,点采样与区域采样的概念呈现了两次,与 mipmapping 和抗锯齿无关。无论是在硬件、软件还是两者的混合中实现,整个图形管道最终都被设计为只为光栅化器提供数据,这使得光栅化器成为最重要但最鲜为人知的渲染技术之一。

因为在各种平台上都能够应用高质量、低成本的 3D 硬件,因而必须实现本人的光栅化器的读者比例当初曾经微不足道了。然而,即便对于那些永远不须要编写光栅化器的人来说,理解光栅化器的性能也很重要。例如,即便是对深度缓冲零碎的根本理论了解也能够帮忙程序员构建一个场景,以防止在可见外表确定期间呈现视觉伪影。理解光栅化器的外部工作原理能够帮忙 3D 程序员疾速调试几何管道中的问题。最初,这些常识能够领导程序员更好地优化他们的几何管道,为他们的光栅化器“提供”高性能数据集。

参考文献

[35] David H. Eberly. 3D Game Engine Design. Morgan Kaufmann Publishers, San Francisco, 2001.

[44] Cass Everitt. Interactive order-independent transparency. Technical report, NVIDIA, 2001.

[49] Tom Forsyth. Premultiplied alpha. http://home.comcast.net/∼tom_forsyth/blog.wiki.html.

[54] Andrew S. Glassner. Principles of Digital Image Synthesis. Morgan Kaufmann Publishers, San Francisco, 1994.

[74] Paul Heckbert. Texture mapping polygons in perspective. Technical report, New Institute of Technology, 1983.

[75] Paul Heckbert and Henry Moreton. Interpolation for polygon texture mapping and shading. In David Rogers and Rae Earnshaw, editors, State of the Art in Computer Graphics: Visualization and Modeling, pages 101–111. Springer-Verlag, Berlin,1991.

[76] Chris Hecker. Under the hood/behind the screen: Perspective texture mapping(series). Game Developer Magazine, 1995–1996.

[82] John F. Hughes, Andries van Dam, Morgan McGuire, David F. Sklar, James D. Foley, Steven K. Feiner, and Kurt Akeley. Computer Graphics: Principles and Practice. Addison-Wesley, Reading, MA, 3rd edition, 2013.

[89] Brano Kamen. Maximizing depth buffer range and precision. http://outerra.blogspot.com/2…

[107] Morgan McGuire and Louis Bavoil. Weighted blended order-independent transparency. Journal of Computer Graphics Techniques (JCGT), 2(2):122 -141,2013.

[123] Thomas Porter and Tom Duff. Compositing digital images. In Proceedings of the 11th Annual Conference on Computer Graphics and Interactive Techniques (SIGGRAPH ’84), pages 253–259, 1984.

[148] Ken Turkowski. Filters for common resampling tasks. In Andrew S. Glassner, editor,Graphics Gems, pages 147–165. Academic Press Professional, San Diego, 1990.

[157] Lance Williams. Pyramidal parametrics. In Computer Graphics (SIGGRAPH ’83 Proceedings), 1983.

[159] George Wohlberg. Digital Image Warping. IEEE Computer Society Press, Los Alamitos, CA, 1990.

本文由 mdnice 多平台公布

退出移动版