第一章 渲染流水线
Posted xiegaosen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第一章 渲染流水线相关的知识,希望对你有一定的参考价值。
1 综述
好吧,既然是从最基础开始说起,那我们就有必要了解什么是Shader,即着色器。与之关系非常紧密的就是渲染流水线。可以说如果不了解渲染流水线的工作流程,就无法说自己真正的了解Shader。
1.1 什么是流水线
要想学会怎么使用Shader,我们首先要了解Shader是怎么工作的。实际上,Shader仅仅是渲染流水线的一个环节,想要让我们的Shader发挥出它的作用,我们就需要知道它在流水中扮演了怎样的角色。
理想情况下,如果把一个非流水线系统分成n个流水线阶段,并且每个阶段耗费时间相同的话,会使整个系统得到n倍速度的提升。
1.2 什么是渲染流水线
渲染流水线的工作任务在于由一个三维场景出发、生成(或者说渲染)一张二维图像。换句话说,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。而这个工作通常是由CPU和GPU共同完成的。
《Real-Time Rendering, Third Edition》一书中将一个渲染流程分为了3个阶段:应用阶段、几何阶段、光栅化阶段。
注意,这里仅仅是概念性阶段,每个阶段本身通常也是一个流水线系统,既包含了自流水线阶段。下图显示了三个概念阶段之间的关系。
应用阶段
从这个名字我们可以看出,这个阶段是由我们的应用主导的,因此通常有CPU负责实现,换句话说,我们开发者对这个阶段有着绝对的控制权。
在这个阶段,开发者有3个任务:首先我们要准备好场景数据,例如摄像机的位置、视椎体、场景中包含了哪些模型、使用了哪些光源等等;其次为了提高渲染性能,我们往往要做一个粗粒度剔除工作(culling),可以把那些不可见的物体剔除出去,这样就不用再移交给几何阶段进行处理;最后我们需要设置好每个模型的渲染状态。这些状态包括但不限于它使用的材质(漫反射颜色,高光反射颜色)、使用的纹理、使用的Shader等。这一阶段最重要的输出是渲染所需的几何信息,即渲染图元(rendering primitives)。通俗来讲可以是点、线、三角面等。这些渲染图元会被传递给下一个阶段--几何阶段。
几何阶段
几何阶段用于处理和我们要绘制的几何相关的事情。例如决定要绘制的图元是什么。怎么绘制它们,在哪里绘制它们。这一阶段,通常在GPU上执行。
几何阶段负责和每个渲染图源打交道,进行逐点、逐多边形的操作。这个阶段可以进一步分成更小的流水线阶段,后面我们会更加细说。几何阶段的一个重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一阶段。
光栅化阶段
这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行。光栅化的任务主要是决定每个渲染图元中的哪些像素应该会被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。和上一阶段类似,光栅化阶段也可以分成更小的流水线阶段。
(注意:要把上面的3个流水线阶段和我们要讲到的GPU流水线阶段区分开来。这里的流水线均是概念流水线,是我们为了给一个渲染流程进行基本功能的划分而提出来的。下面要介绍的GPU流水线,则是硬件真正用于实现上述概念的流水线)。
2 CPU和GPU之间的通信
渲染流水线的起点是CPU,即应用阶段,应用阶段大致可分为下面三个阶段:
(1)把数据加载到显存中
(2)设置渲染状态
(3)调用Draw Call(后面慢慢提到)
2.1 把数据加载到显存中
所有渲染所需的数据都需要从硬盘(HDD)中加载到系统内存(RAM)中。然后网格和纹理等数据又会被加载到显卡上的存储空间——显存(VRAM)中。这是因为显卡对显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。下图给出了这样的例子
需要注意的是,真是的渲染中需要加载到显存的数据往往比上图复杂许多,例如顶点的位置信息、法线方向、顶点颜色、纹理坐标等。
当把数据加载到缓存后,RAM中的数据就可以移除了。但对于一些数据来说,CPU仍然需要访问他们(例如,我们希望CPU可以访问网格数据来进行碰撞检测),那么我们可能就不希望这些数据被移除,因为从硬盘加载到RAM的过程是十分耗时的。
在此之后,我们还需要通过CPU来设置渲染状态,从而指导GPU如何进行渲染工作。
2.2 设置渲染状态
什么是渲染状态?一个通俗的解释是,这些状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器(Vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等。如果我们没有更改渲染状态,那么所有网格都将使用同一种渲染状态。下图显示了当使用同一种渲染状态时,渲染三个不同网格的结果。
在准备好上述工作后,CPU就需要调用一个渲染命令来告诉GPU:嘿,老兄我帮你把数据准备好啦,你可以按照我的设置开始渲染啦“!”而这个渲染命令就是Draw Call。
2.3 调用Draw Call
Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息——上一阶段我们已经完成。如下图所示
当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理。着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。而这个计算过程就是GPU流水线。
3 GPU流水线
3.1 概述
GPU渲染的过程就是GPU流水线。
对于概念的后两个阶段,即几何阶段和光栅化阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU。GPU通过实现流水线化,大大加快了渲染速度。虽然我们无法完全控制这两个阶段的实现细节,但GPU向开发者开发了很多控制权。
几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置或可编程性。如下图所示:
从图中可以看出,GPU渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器。
顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。曲面细分着色器(Tessellation Shader)是一个可选着色器,它用于细分图元。几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于逐图元(Per-primitive)的着色操作,或者被用于产生更多的图元。下一个流水线阶段是裁剪(Clipping),这一阶段的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。这个阶段是可配置的。例如,我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。几何概念阶段的最后一个流水线阶段是屏幕映射(Screen Mapping)。这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。
光栅化概念阶段中的三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也都是固定函数(Fixed-Function)的阶段。接下来的片元着色器(Fragment Shader),则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。最后逐片元操作(Per-Fragment Operations)阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。
3.2 顶点着色器
顶点着色器(Vertex Shader)是流水线的第一阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,也就是说输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或销毁任何顶点,而且无法得到顶点与顶点之间的关系。例如我们无法得知两个顶点是否属于同一个三角网格。但正是因为这样的相互独立性,GPU可以利用本身的特性并行化处理每个顶点,这意味着这一阶段处理速度会很快。
顶点着色器需要完成的主要工作有:坐标变换和逐顶点光照。当然,除了这两个主要任务外,顶点着色器还可以输出后续阶段所需的数据。下图展示了在顶点着色器中对顶点位置进行坐标变换并计算顶点颜色的过程。
坐标变换
顾名思义,就是对顶点坐标(即位置)进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。例如,我们可以改变顶点的位置来模拟水面、布料等。但需要注意的是,无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间,就是类似下面的代码,
o.pos = mul (UNITY_MVP, v.position);
类似上面这句代码的功能,就是把顶点坐标转换到齐次裁剪坐标系下,接着通常再由硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates, NDC),具体数学上的实现细节我们后面慢慢说。下图展示了这样的一个过程。
需要注意的是,图给出的坐标范围是OpenGL同时也是Unity使用的NDC,它的Z分量范围在[-1, 1]之间,而在DirectX中,NDC的z分量范围是[0,1]。顶点着色器可以有不同的输出方式。最常见的输出路径是经光栅化后交给片元着色器进行处理。而在现代的Shader Model中,它还可以把数据发送给曲面细分着色器或几何着色器,感兴趣的话可以自行了解。
3.3 裁剪
由于我们的场景会很大,而摄像机的视野范围很有可能不会覆盖所有的场景物体,一个很自然的想法是,那些不在摄像机视野范围内的物体不需要处理。而裁剪(Clipping)就是为了完成这个目的被提出来的。
一个图元和摄像机的视野关系有3种:完全在视野内、部分在视野内、完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要进行一个处理,这就是裁剪。例如,一条线段的一个顶点在视野内,而另一个顶点不在视野内,那么视野外部的顶点应该使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点处。
由于我们已知在NDC下的顶点位置,即顶点位置在一个立方体内,因此裁剪就变得很简单:只需要将图元裁减到单位立方体内。下图展示了这样的一个过程。
和顶点着色器不同,这一步是不可编程的,即我们无法通过编程来控制裁剪的过程,而是硬件上的固定操作,但我们可以自定义一个裁剪操作来对这一步进行配置。
3.4 屏幕映射
这一步输入的坐标仍然是三维坐标系下的坐标(范围在单位立方体内)。屏幕映射(Screen Mapping)的任务是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
假设我们要把场景渲染到一个窗口上,窗口的范围是从最小的窗口坐标(x1,y1)到最大的窗口坐标(x2,y2),其中x1<x2且y1<y2。由于我们输入的坐标范围在-1到1,因此可以想到,这个过程实际是个缩放的过程。如下图所示
也许大家会有疑惑,输入的Z坐标会怎么样。屏幕映射不会对输入的Z坐标进行任何处理。实际上屏幕坐标和Z坐标一起构成了一个坐标系,叫做窗口坐标系(Window Coordinates)。这些值会一起被传递到光栅化阶段。
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
有一个需要注意的地方是,屏幕坐标系在OpenGL和DirectX之间的差异问题。OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义了屏幕左上角为最小的窗口坐标值,下图展示了这样的差异
产生这种差异的原因是,微软的窗口都使用了这样的坐标系统,因为这和我们的阅读方式是一致的:从左到右,从上到下,并且很多图像文件也是这样存储的。
不管原因如何,差异就这么造成了。留给我们开发者的就是,要时刻小心这样的差异,如果你发现得到的图像是倒转的,那么很有可能是这种原因造成的。
3.5 三角形设置
由这一步开始进入了光栅化阶段。从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。光栅化阶段有两个重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。
光栅化的第一个流水线阶段是三角形设置(Triangle Setup)。这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们必须计算每条边上的像素坐标。为了能计算边界像素的坐标信息,我们需要得到三角形边界的表示方式。这样一个计算三角网络表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。
3.6 三角形遍历
三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。
三角形遍历会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值,下图展示了三角形遍历阶段的简化计算过程。
这一步的传出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
3.7 片元着色器
片元着色器(Fragment Shader)是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器(Pixel Shader),但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。
前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。
片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。下图展示了这样的一个过程。
这一阶段可以完成很多重要的渲染技术,其中重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己执行的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息(gradient,或derivative)。
3.8 逐片元操作
终于到了渲染管线的最后一步。逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,这一说法被称为输出合并阶段(Output-Merger)。
这一阶段有几个主要任务
(1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。
(2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
需要指明的是,逐片元操作阶段是高度可配置性的,即我们可以设置每一步的操作细节。我们后面会慢慢讲到。
这个阶段首先需要解决每个片元的可见性问题,这需要进行一系列测试。一个片元只有经过了所有的测试,才能获得最终个GPU谈判的资格,这个资格是说它可以和颜色缓冲区进行合并。如果他没有通过其中的任何一个测试,那么对不起,之前为了产生这个片元所做的所有工作都是白费的,因为这个片元会被舍弃掉。下图给出了简化后的逐片元操作所做的工作。
测试过程实际上是一个比较复杂的过程,而且不同的图形接口(例如OpenGL和DirectX)的实现细节也不尽相同。这里给出两个最基本的测试——深度测试和模板测试的实现过程。
我们先来看模板测试(Stencil Test)。与之相关的是模板缓冲(Stencil Buffer)。实际上。模板缓冲和我们经常听到的颜色缓冲、深度缓冲几乎是一类的东西。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值(reference value)进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果该片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试·,我们都可以通过模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如在失败时,模板缓冲区保持不变,通过时将模板缓冲区中对应的位置加1等。模板缓冲区通常用于限制渲染的区域。另外,模板测试还有一些更高级的用法,如渲染阴影、轮廓渲染等。
如果一个片元幸运的通过了模板测试,那么它会进行下一个测试——深度测试(Depth Test)。这个测试同样也是可以高度配置的。如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可以由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃他,因为,我们总想只显示出离摄像机最近的物体,那些被其他物体遮挡的物体就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和模板测试有些不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。后面我们会发现,透明效果和深度测试以及深度写入的关系非常密切。
如果一个幸运的片元通过了上面的所有测试,那么它可以自豪地来到合并功能面前。
为什么需要合并?我们要知道,这里所讨论的渲染的过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息都被存储在一个名为颜色缓冲的地方。因此当我们执行这次渲染时,颜色缓冲往往已经有了上次渲染之后的颜色结果,那么我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其它处理?这就是合并需要解决的问题。
对于不透明物体,开发者可以关闭混合(Blend)操作。这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。下图是一个简化版的混合操作流程图
从流程图中我们可以发现,混合操作也是可以高度配置的:开发者可以选择开启/关闭混合功能。如果没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,这也是很多初学者无法得到透明效果的原因(没有开启混合功能)。如果开启了混合,GPU就会取出源颜色和目标颜色,将两种颜色进行混合。源颜色指的是片元着色器得到的颜色值,而目标颜色则是已经存在于颜色缓冲区中的颜色值。之后就会用一个混合函数来进行混合操作。这个混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加、相减、相乘等。混合很想Photoshop中对图层的操作:每一图层可以选择混合模式,混合模式决定了该图层和下层图层的混合结果,而我们看到的图片就是混合后的图片。
上面给出的测试顺序并不是唯一的,虽然从逻辑上来说这些测试是在片元着色器之后进行的,但对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试。想象一下,当GPU在片元着色器阶段花了很大的力气终于计算出片元的颜色,却发现这个片元根本没有通过这些检验,也就是说这个片元还是被舍弃了,那么之前花费的计算成本全都浪费了,如下图所示:
作为一个想充分提高性能的GPU,它会希望尽可能早地知道哪些片元是会被舍弃的,对于这些片元就不需要在使用片元着色器来计算它们的颜色,在Unity给出的渲染流水线中,我们也可以发现它给出的深度测试是在片元着色器之前。这种将深度测试提前执行的技术通常也被称为Early-Z技术。
当模型的图元经过了上面层层计算和测试后,就会显示到我们屏幕上。我们的屏幕显示的就是颜色缓冲区的颜色值。但是为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲策略(Double Buffering)。这意味着,对场景的渲染是在幕后发生的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲(Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕中的图像。由此,保证了我们看到的图像是连续的。
3.9 什么是Draw Call
Draw Call本身含义很简单,就是CPU调用图像编程接口,以命令GPU进行渲染的操作。
3.9.1 CPU和GPU是如何实现并行工作的
如果没有流水线化,那么CPU需要等到上一个GPU完成上一个渲染任务才能再一次发送渲染命令。但这种方法显然会造成效率低下。因此我们需要让CPU和GPU可以并行工作。而解决方法就是使用一个命令缓冲区(Command Buffer)。
命令缓冲区包含了一个命令队列,由CPU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是相互独立的。命令缓冲区使得CPU和GPU可以相互独立的工作。当CPU需要渲染一些对象时,它可以向命令缓冲区中添加命令,而当GPU完成了上一次的渲染任务后,它就可以从命令队列中再取出一个命令并执行它。
命令缓冲区的命令有很多种类,而Draw Call是其中一种,其它命令还有改变渲染状态等(例如改变使用的着色器,使用不同的纹理等。)下图显示了这样的一个例子。
3.9.2 为什么Draw Call多了会影响帧率
每次调用Draw Call之前,CPU需向GPU发送很多内容,包括数据、状态和命令等。在这一阶段,CPU需要完成很多工作,例如检查渲染状态等。而一旦CPU完成了这些准备工作,GPU就可以开始本次渲染。GPU渲染能力是很强的,渲染200个或是2000个三角网格通常没有什么区别,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call的数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU过载。如下图所示:
3.9.3 如何减少Draw Call
尽管减少Draw Call的方法很多,但我们这里仅讨论使用批处理(Batching)的方法。
我们说过,提交大量的Draw Call会造成CPU的性能瓶颈,即CPU把时间都花费在准备Draw Call的工作上了。那么一个很显然的优化想法就是把很多小的DrawCall合并成一个大的DrawCall,这就是批处理的思想,下图显示了批处理所做的工作。
需要注意的是,由于我们需要在CPU内存中合并网格,而合并的过程是需要消耗时间的。因此,批处理技术更加适合于那些静态物体,例如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可。当然,我们也可以对动态物体进行批处理。但是,由于这些物体是不断运动的,因此每一帧都需要重新进行合并然后发送给GPU,这对空间和时间都会造成一定影响。
在游戏开发过程中,为了减小Draw Call的开销,有两点需要注意。
(1)避免使用大量很小的网格。当不可避免的需要使用很小的网格结构时,考虑是否可以合并它们。
(2)避免使用过多的材质。尽量在不同的网格之间公用同一个材质。
4 什么是固定管线渲染
固定函数的流水线(Fixed-Function Pipeline),也简称为固定管线,通常是指在较旧的GPU上实现的渲染流水线。这种流水线只给开发者提供一些配置操作,但开发者没有对流水线阶段的完全控制权。
固定管线通常提供了一系列接口,这些接口包含了一个函数入口点(Function Entry Points)集合,这些函数入口点会匹配GPU上的一个特定逻辑功能。开发者们通过这些接口来控制渲染流水线。换句话说,固定渲染管线是只可配置的管线。一个形象的比喻是,我们在使用固定管线进行渲染时,就好像在控制电路上的多个开关,我们可以选择打开或关闭一个开关,但永远无法控制整个电路的排布。
随着时代的发展,GPU的流水线越来越朝着更高的灵活性和可控性方向发展,可编程渲染管线应运而生。具体的可查阅相关文献,但是在这里说明,如果不是为了对较旧的沈北进行兼容,不建议使用固定管线渲染方式。
以上是关于第一章 渲染流水线的主要内容,如果未能解决你的问题,请参考以下文章
flutter 分层数据显示 类似于第一章的第一节显示 网络请求渲染 开源代码