您需要 登录 才可以下载或查看,没有账号?注册
x
目 录
1.1综述 1.1什么是渲染流水线(概念流水线)1.2渲染的流程
2.2渲染流水线概览2.2.1应用阶段=CPU读取数据并放入显存------准备场景数据、粗粒度剔除、数据加载到缓存(延伸阅读):什么是前向渲染、什么是延迟渲染 (延伸阅读):什么是Draw Call 如何优化Draw Call 2.2.2几何阶段=GPU读取数据并逐顶点变换到屏幕空间中------顶点着色器:(坐标变换、正交投影、透视投影、着色) ------几何着色器 *2.3.3片元着色器Fragment Shader ------Blinn-Phong光照模型
------锯齿和抗锯齿(超级采样、多重采样)
*2.3.4逐片元操作(测试和混合阶段)
------Alpha测试 ------模板测试
------隐藏面消除
------入口剔除 ------Alpha混合 ------颜色空间
------抖动处理 文章正文
本期内容 逐片元操作 终于到了渲染流水线的最后一步。逐片元操作(Per-Fragment Operations)是 OpenGL 中的说法,在 DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。Merger这个词可能更容易让读者明白这一步骤的目的:合并。而OpenGL 中的名字可以让读者明白这个阶段的操作单位,即是对每一个片元进行一些操作。那么问题来了,要合并哪些数据?又要进行哪些操作呢? 这一阶段有几个主要任务。 (1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。 (2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。 需要指明的是,逐片元操作阶段是高度可配置性的,即我们可以设置每一步的操作细节。这在后面会讲到。 这个阶段首先需要解决每个片元的可见性问题。这需要进行一系列测试。这就好比考试,一个片元只有通过了所有的考试,才能最终获得和GPU谈判的资格,这个资格指的是它可以和颜色缓冲区进行合并。如果它没有通过其中的某一个测试,那么对不起,之前为了产生这个片元所做的所有工作都是白费的,因为这个片元会被舍弃掉。Poor fragment!图2.14给出了简化后的逐片元操作所做的操作。
【图2.14逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中】
裁切测试 (Scissor Test)在前面视口变换中我们已经提到裁切测试这种技术了。裁切测试可以避免当视口比屏幕窗口小时造成的渲染浪费问题。通常情况下,我们会让视口的大小和屏幕空间一样大,此时可以不需要使用到裁切测试。但当两者大小不一样大时,我们需要用到裁切测试来避免像glClear()出现的问题。裁切测试默认是不开启的,我们可以通过glEnable(GL_SCISSOR_TEST)来开启裁切测试,通过glScissor()来指定裁切区域。下面通过StackExchange的一组图来简单说明裁切测试的原理:(背景颜色使用glClear()设置为白色)
正常情况下视口窗口和屏幕空间一样大,此时渲染整个屏幕空间,没有多余的渲染,不需要进行裁切。
视口变换后视口比屏幕空间小,我们应用裁切测试将渲染窗口变成和视口一样大的窗口,没有多余的需要,但是需要用到裁切。
视口窗口和屏幕窗口一样大,但是进行了裁切测试,裁切的窗口比视口小,只有位于裁切窗口部分的片段进行了渲染。 从上面的讨论我们知道裁切的区域可以比视口大,但是不能大于设备的渲染目标(Render Rarget)。如果对裁切测试还不是很明白,可以参考StackExchange上面的这个问题:What is the purpose of glScissor?
Alpha测试 (Alpha Test)颜色一般采用RGBA四分量来进行表示,其中颜色的Alpha值用来表示物体本身的不透明度(alpha=1表示完全不透明,alpha=0表示完全透明)。Alpha测试可以根据片段颜色的Alpha值来裁剪片段。OpenGL和DIrectX都有内置的函数进行裁剪,其中HLSL中使用Clip(),GLSL中使用discard()。如果我们想在片段着色器中丢弃alpha值小于0.1的片段,我们可以使用下面的代码进行简单的alpha测试。不过由于alpha测试本身消耗较大,性能较低,所以只有在必要的情况下才会使用alpha测试。我们下面讨论下alpha测试效率较低的原因。
Early-Z Culling 通常情况下,我们是在片段着色器执行后再进行深度/模板测试,来确定到底哪些片段(Fragments)对屏幕显示有贡献,哪些片段需要被丢弃(Discard),只有哪些通过了深度/模板测试的片段才成为屏幕上显示的像素(Pixels),这也就是片段和像素本质的区别,也就是说片段是最终显示在屏幕上像素的候选者! 由于深度/模板测试是在片段着色器之后进行的,所以导致着色器计算资源的浪费,因为这些被遮挡的片段对我们最终的画面是没有任何贡献的,而我们还花费了大量的资源对它们进行了复杂的光照等一系列计算。Early-Z Culling正是在这种情况下出现的,不过我们需要注意的是Early-Z Culling本不是管线标准,只是硬件厂商用来加速渲染的一种优化手段,所以在不同的硬件上会有不同的实现,而且Early-Z Culling并不保证一定有效。只有当满足某些条件的情况下,GPU才会开启Early-Z的优化! 到底需要满足那些条件才会开启呢?我们可以从NVIDIA和AMD的官方文档看到一些说明:只允许硬件使用光栅化插值得到的深度值,也就是说我们不能在片段着色器去修改深度缓冲,否则GPU将无法提前为我们做出优化,因为之前的深度值已经被改变了!这也意味着像alpha测试和texkil (HLSL中为Clip,GLSL中为discard) 这些操作会使得Early-Z Culling失效!这也是alpha测试效率低下的主要原因! 除了Early-Z Culling之外,AMD的官方文档还提到了另一种相关的优化:层次Z(Hierarchical Z,HiZ)。Early-Z Culling和HiZ的主要区别在于处理片段的粒度不一样。HiZ是以块(Tiles)为单位进行相对粗糙的深度测试(rough depth test)的,以便节省带宽的消耗。一般是在HiZ操作后进行Early-Z Culling的测试。 模板测试 (Stencil Test)模板测试默认是不开启的,可以通过glEnable(GL_STENCIL_TEST);开启模板测试。模板测试其实很简单,我们可以把它理解为一个模子mask,通过mask的值来控制那些片段的可见性,无法通过模板测试的片段将被丢弃,下图给出了模板测试的一个例子。模板测试属于流水线中高度可配置的阶段,可以通过glStencilMask来设置一个掩码,该掩码会将要写入缓存区的值进行AND操作,默认情况下掩码值有1,不影响输出。此外,我们还可以利用glStencilFunc和glStencilOp连个函数来设置模板函数,控制在模板测试失败或成功时的行为。我们可以利用模板测试来实现平面镜效果、平面阴影和物体轮廓等功能。
深度测试 (Depth Test)根据我们的日常经验,近处的物体会挡住后面的东西,我们可以通过深度缓冲来实现这样的效果。深度测试的原理很简单:比较当前片段的深度值是否比深度缓冲中预设的值小(默认比较方式),如果是更新深度缓冲和颜色缓冲;否则丢弃片段不更新缓冲区的值。其实我们前面介绍Early-Z Culling也是利用Z-Buffer的技术来进行深度测试的,只不过该测试是在片段着色器之前进行的,而深度测试是在片段着色器之后进行的。的深度测试默认是禁用的,我们可以通过glEnable(GL_DEPTH_TEST)来开启深度测试。深度测试是可配置的阶段,我们可以通过深度函数glDepthFunc()来设置深度比较运算符,默认情况下的深度比较函数是GL_LESS,也就是前面的物体会盖住后面的物体。除了GL_LESS还有其他深度比较函数,比如:GL_ALWAYS、GL_GREATER、GL_NEVER等。我们在渲染半透明物体时,需要开启深度测试而关闭深度写入功能。 理解2:模板测试、深度测试
测试的过程实际上是个比较复杂的过程,而且不同的图形接口(例如OpenGL和DirectX)的实现细节也不尽相同。这里给出两个最基本的测试——深度测试和模板测试的实现过程。能否理解这些测试过程将关乎读者是否可以理解本书后面章节中提到的渲染队列,尤其是处理透明效果时出现的问题。图2.15给出了深度测试和模板测试的简化流程图。
我们先来看模板测试(Stencil Test)。与之相关的是模板缓冲(Stencil Buffer)。实际上,模板缓冲和我们经常听到的颜色缓冲、深度缓冲几乎是一类东西。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值(reference value)进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作, 例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模板测试通常用于限制渲染的区域。另外,模板测试还有一些更高级的用法,如渲染阴影、轮廓渲染等。 如果一个片元幸运地通过了模板测试,那么它会进行下一个测试——深度测试(Depth Test)。相信很多读者都听到过这个测试。这个测试同样是可以高度配置的。如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。这是因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和模板测试有些不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。我们在后面的学习中会发现,透明效果和深度测试以及深度写入的关系非常密切。 Z-Fighting Z-fighting是由于深度缓冲精度不够带来的问题。当同一个位置的片段具有相似的深度值时,由于深度缓冲精度不够无法决定应该显示那个片段,导致片段之间抢占深度的至高点,造成了视觉上的假象, 如下图所示。我们在OpenGL投影矩阵这篇文章已经介绍过了z-fighting的问题,我们发现经过透视除法后的z分量具有了非线性的关系,近平面处有较好的深度精度,而靠近远平面处深度精度较低。
解决z-fighting的一个常见技巧是让物体之间有一些偏移,不要将物体靠的太近;另一种技巧是使用高精度的深度缓冲。比如使用32bits的深度缓冲,然而这样会占用更多的显存资源。关于如何更好避免z-fighting可以参考StackExchange上面的这个问题: Avoiding z-fighting with coincident surfaces。
隐藏面消除 (Hidden Surface Removal, HSR)隐藏面消除也叫作可见面确定。其实隐藏面消除的技术我们来说并不陌生,前面讨论的图元组装的裁剪(Clipping)、背面剔除和Z-Buffer技术其实就是隐藏面消除的一种,只不过剔除的粒度有所不同,其中裁剪操作针对的是图元,而Z-Buffer是针对像素点。不同的隐藏面消除技术的主要区别在于剔除的粒度以及不同的剔除目的,但是最终目的都是相同的:减少到达片段着色器的片段的数量,提高渲染的性能。除了裁剪之外,我们下面还将介绍几种比较常见的HSR技术!
视椎体剔除 (Viewing-Frustum Culling) 视椎体剔除是最常见的一种剔除技术,对于大场景我们根本不可能每帧对每个物体都进行渲染,人们发现我们其实只需要渲染那些摄像机看得到的物体,也就是位于视椎体内的物体,其他位于视椎体外的物体根本不需要渲染,我们可以将其进行剔除,不送入渲染管线,提升我们的渲染效率。一般来说,视椎体剔除是在应用程序阶段完成的,也就是在CPU端进行剔除工作。视椎体剔除利用的是射线检测的方法,根据视椎体的六个平面来检测物体。我们一般利用物体包围盒(Bounding Box)来做交差检测,常见的包围盒有轴对齐包围盒(AABB)和有向包围盒(OBB)两种。由于场景中物体可能非常多,所以一般需要借助高效的数据结构来提升碰撞检测的性能,常见的用于3D场景碰撞检测的数据结构有:八叉树(OcTree)、二分空间划分(Binary Space Partitioning)、四叉树(Quad Tree)、场景图(Scene Graphs)、kd树(K-Dimensional Tree)和层次包围(Bounding Volume Hierarchies)。这些空间数据结构也通常使用在场景管理、光线追踪和相交测试中。
入口剔除 (Portal Culling) 当我们位于室内时,我们就可以使用入口剔除技术进行裁剪优化了。我们可以将室内的门或者窗户看做视椎体来进行裁剪。不过我们其实看到入口剔除有很大的局限性,一般只能在室内环境下使用,无法再室外场景使用该技术,对于室外的大场景我们一般需要使用下面介绍的遮挡剔除技术。
遮挡剔除 (Occlusion Culling) 在城市或者森林这种大场景中,我们很容易发现物体之间有很多的遮挡关系,我们需要遮挡剔除技术去掉那些被挡住的物体,来提升渲染效率,如下图所示,左边的图是遮挡剔除前视椎体示意图,右边的图是进行遮挡剔除后需要渲染的物体,可以说大大减少了需要被渲染的物体数量。遮挡剔除的实现方法有很多,既有基于CPU的,也有基于GPU的,也可以混合使用CPU和GPU进行处理。一般进行遮挡剔除时,我们需要通过离线烘焙的犯法来预先计算出潜在可视集合(Potentially Visible Set,PVS)。PVS记录了每个地形块(Tiles)可能看到的物体的集合,用于运行时查找计算。在计算PVS时我们会将场景划分为小的地形块,在每个块上随机选取N个采样点,以这些采样点为起点发出射线来获取场景中相交的物体,记录下物体的ID,求出每个块对应的ID的集合。在运行时根据摄像机的位置获取每个块可见的物体进行渲染。
提高烘焙的精度通常有两种方法:一是通过减小地形块的大小;二是增加采样点数量。采样的方式有很多,如下面所示。增加采样点的数量可以获得更加精确的结果,减少玩家在块之间切换出现物体闪现或闪失的情况。不过过多的采样点会大大增加离线烘焙的时间,所以需要根据实际情况进行选择采样的数量。我们发现其实基于烘焙的方法缺点还是很明显的:烘焙时间长,需要额外的包体以及无法处理动态的物体。 Alpha混合 (Alpha Blending)讨论完裁切测试、Alpha测试、模板测试和深度测试后,我们接下来看Alpha混合技术。从前面我们已经知道,颜色一般采用RGBA四分量来进行表示,其中颜色的Alpha值用来表示物体本身的不透明度。引入Alpha技术是为了实现半透明的效果,如上图所示。在OpenGL可以通过glEnable(GL_BLEND)来开启混合的功能。Alpha混合也是可配置的阶段,我们根据下面混合方程对混合效果进行调整。
其中四个变量分别表示原颜色、原因子值、目标颜色和目标因子值。通过 glBlendFuncSeparate()、glBlendFunc()和glBlendEquation()来设置各种混合效果,常见的选项有GL_ZERO、GL_ONE、GL_SRC_ALPHA、GL_FUNC_ADD等。我们可以从下图看到两种不同的混合
当场景中既有不透明物体,又有半透明物体时,我们需要先渲染不透明物体,渲染顺序为从前往后(Front-to-Back);然后再渲染半透明物体,渲染顺序为从后往前(Back-to-Front)。我们需要对不透明和半透明物体分开渲染是因为:我们可以透过半透明物体看到半透明物体背后的东西,所以对半透明物体进行渲染时需要后面图层的信息,才能够正确进行混合。
话说我们对不透明物体按从近到远进行渲染是为了减少深度颜色缓冲器的写入操作,提升性能;而半透明物体需要遵循画家算法由远及近进行渲染是为了渲染的正确性。从上面两张图可以看到,半透明的绘制跟物体的顺序有紧密的关系。从前面的讨论我们知道,我们并不一定能够对场景中的物体按照距离远近进行严格排序,所以怎么正确渲染半透明物体呢?接下来我们介绍顺序无关半透明算法 混合 如果一个幸运的片元通过了上面的所有测试,它就可以自豪地来到合并功能的面前。
为什么需要合并?我们要知道,这里所讨论的渲染过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行这次渲染时,颜色缓冲中往往已经有了上次渲染之后的颜色结果,那么,我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理?这就是合并需要解决的问题。
对于不透明物体,开发者可以关闭混合(Blend)操作。这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。图2.16展示了一个简化版的混合操作的流程图。
从流程图中我们可以发现,混合操作也是可以高度配置的:开发者可以选择开启/关闭混合功能。如果没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,而这也是很多初学者发现无法得到透明效果的原因(没有开启混合功能)。如果开启了混合,GPU会取出源颜色和目标颜色,将两种颜色进行混合。源颜色指的是片元着色器得到的颜色值,而目标颜色则是已经存在于颜色缓冲区中的颜色值。之后,就会使用一个混合函数来进行混合操作。这个混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加、相减、相乘等。混合很像Photoshop中对图层的操作:每一层图层可以选择混合模式,混合模式决定了该图层和下层图层的混合结果,而我们看到的图片就是混合后的图片。
上面给出的测试顺序并不是唯一的,而且虽然从逻辑上来说这些测试是在片元着色器之后进行的,但对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试。这是可以理解的,想象一下,当GPU在片元着色器阶段花了很大力气终于计算出片元的颜色后,却发现这个片元根本没有通过这些检验,也就是说这个片元还是被舍弃了,那之前花费的计算成本全都浪费了!图2.17给出了这样一个场景。
【图示场景中包含了两个对象:球和长方体,绘制顺序是先绘制球(在屏幕上显示为圆),再绘制长方体(在屏幕上显示为长方形)。如果深度测试在片元着色器之后执行,那么在渲染长方体时,虽然它的大部分区域都被遮挡在球的后面,即它所覆盖的绝大部分片元根本无法通过深度测试,但是我们仍然需要对这些片元执行片元着色器,造成了很大的性能浪费】
但是,如果将这些测试提前的话,其检验结果可能会与片元着色器中的一些操作冲突。例如,如果我们在片元着色器进行了透明度测试(我们将在8.3节中具体讲到),而这个片元没有通过透明度测试,我们会在着色器中调用API(例如 clip函数)来手动将其舍弃掉。这就导致GPU 无法提前执行各种测试。
因此,现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。但是,这样也会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。
当模型的图元经过了上面层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区中的颜色值。但是,为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。这意味着,对场景的渲染是在幕后发生的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲(Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此,保证了我们看到的图像总是连续的。
顺序无关半透明算法
(Order-independent transparency,OIT)
我们知道半透明物体的渲染比较麻烦,需要考虑渲染的顺序,也就是说需要对半透明物体进行排序,这是个比较耗性能的操作,而且很多时候我们并无法严格对半透明物体进行排序。所以,有了一种顺序无关半透明算法(Order-independent transparency,OIT),它可以简化我们对半透明物体的渲染过程,该技术也被称作深度剥离(Depth Peeling)。该技术的思想就是根据物体距离摄像机的远近进行深度信息的逐层剥离,首先记录离相机最近的一层,然后记录距离第二近的层,以此类推,层层递进,如下图所示。我们一般会对剥离的最大层数进行限制,因为我们每一层剥离都需要一个pass进行处理,太多层会影响到运行的效率,比如最大剥离层数限制为4层,那么我们需要4个pass处理这个过程。关于OIT技术可以参考NVIDIA的这篇文章。后来有人提出了双向剥离的算法(Dual Depth Peeling)。其实该算法就是改进我们刚才说到的效率问题,该算法在每个pass可以剥离两个层的信息,即最近和最远两个层。该算法改进了OIT所需要的pass数量,从N变成了N/2 + 1。关于该算法的具体信息可以参考这篇文章。
颜色空间 (Color Spaces)常见的颜色空间有:RGB空间、CMY空间、HSV(B)空间和HSL(I)空间。其中RGB和CMY空间主要是用于设备的,而HSV和HSL空间是面向用户的。RGB空间由红Red、绿Green和蓝Blue三种颜色定义,是一种加色的颜色空间,我们所使用的液晶显示器使用的就是RGB颜色空间。我们所使用的颜色表示都是基于RGB空间的,一般每个分量都是归一化到[0,1]之间的数字,对于半透明物体还会使用alpha分量。RGB三颜色混合得到的是白色。
与RGB空间不同,CMY空间是一种减色系统,所以CMY三种颜色对应的是RGB三种颜色的补色,分别是青Cyan、品红Magenta和黄Yellow。CMY颜色空间通常用于打印设备中。CMY空间和RGB空间的转换非常简单,只需要通过1-RGB便可得到CMY对应的颜色。CMY三颜色混合得到的是黑色。 上面介绍的RGB和CMY颜色空间其实都是面向设备的,对用户来说并不是很敏感。而接下来介绍的HSV是面向用户的,主要由色调Hue、饱和度Saturation和亮度Value或Brightness三个分量构成。我们通常所使用的调色板其实就是这种颜色模型。其中色调主要用来表征不同的颜色信息,饱和度从来表示颜色的纯度,而亮度表示颜色的明暗程度。
接下来介绍的颜色空间是HSL,和HSV类似,H表示色调,S表示饱和度,L表示物体的亮度(Lightness或者Intensity)。这两者的数学模型其实都是圆柱。除了上面介绍的几种颜色空间外,还有一种颜色空间叫做Lab颜色空间,是由国际照明委员会CIE引入的。自然界中任何一种颜色都可以在Lab空间中表示出来,它的颜色空间比RGB还要大,另外,这种方式是通过数字化的方式来描述人的视觉感觉,跟设备没有关系,比RGB和CMY更具有普适性。CIE色度图是归一化的坐标空间,我们只需要存储XY两个分量的信息,对应的Z分量可以通过1-X-Y来计算得到。我们介绍的这些颜色空间其实都是可以进行相互转换的。 抖动处理 (Dithering)抖动可以用来增加可用的颜色数量,例如我们可以用8-bits的色深来模拟屏幕上百万颜色的显示,利用黑白两种颜色来模拟多级灰度图像,都是利用抖动的技术。不过,抖动需要用空间分辨率来换取灰度级或者颜色的精度。下图利用红色和蓝色来模拟紫色。印刷业中经常利用黑白两色来模拟多种灰度级。利用的是人眼视觉能够把黑色圆点图案聚集起来的特性,对于一个小区域内的像素的平均灰度,其实正比于其中黑色像素与区域内所有像素个数的比值。
下面是一个4x4的1-bit的像素数组,从远处看并不是一个具有黑白两色的图案,而是一个具有一定的灰度值的图案。对于这个4x4的数组,尽管有2^16种不同的黑白像素组合模式,但是只有17中可能的灰度值,对应于该像素数组中黑色像素的个数 (0~16个)。OpenGL中默认是开启抖动功能的,我们可以通过glEnable(GL_DITHER)来控制抖动输出功能。
抖动相关的算法有很多,下面展示了几种算法的效果对比图:
原帖地址:计算机图形渲染管线/逐片元操作-测试混合阶段【TA技术美术必修基础知识篇】第四期/共四期 (qq.com)
|