最近在工做中愈来愈多地接触到一些3D以及相比常见特性更酷炫的效果,所以萌发了想要本身从0开始打造一个渲染引擎的念头,一方面是为了更好地实现公司业务的需求,另外一方面则是能够学到整个渲染流水线上的方方面面。html
由于以前作的大部分都是在特效这块,对OpenGL会比较熟悉一些,可是放大到渲染引擎上就不少方面不熟悉了,甚至有些是彻底0基础,在给本身今年定下这么一个目标时也是一头雾水,有种瞬间不知道该向哪里迈出第一步的感受。所以这几天基本都在寻找和渲染引擎甚至游戏引擎相关的一些基本信息和资料,本身给本身提问题,再从问题出发去寻找答案,一步步地去理清本身的思路,从而造成了这一份文章。git
文章里面的不少内容都是本身整理各位大神的文章/回答中的摘要而来(全部引用均会注明出处,在此对各位大神表示感谢和仰望),并且只靠一篇文章是确定不足以彻底讲述清楚整个引擎所须要了解到的内容的,只是从我本身的角度来讲,这篇文章之于以后构建渲染引擎之路来讲,无异于拨云见日的做用。github
另外这系列文章假定各位对OpenGL具备必定的基础,所以不会过多地介绍OpenGL的一些基础知识如渲染流程、着色器用法等。web
且不论游戏引擎这样的庞然巨物,就只说渲染引擎所须要涉及的内容就很是,你可能要考虑怎么去封装渲染API,还要考虑怎么去整合各种效果系统,第一版效果出来了,要不要考虑搞搞Vulkan搞搞Metal,什么样的方案才是最佳的等等。编程
所以若是没有一个方向就开始寻找资料的话和大海捞针没有什么区别,也容易由于一时间接收的资讯过多进而打击信心和兴趣,所以在开始找资料以前我给本身定了几个问题,以这个几个问题为方向去找资料为本身解答:canvas
而在寻找答案的过程当中,天然而然就也会接触到其余方面的内容,好比一些本身以前见都没见过的名词,或者是大多数引擎都在用的成熟方案等等,这些也都会一并记录下来。数组
其实这篇文章是有一个检验收获的方式的,这个方式在一开始是没有的,可是在找资料的过程当中看到知乎大佬的回答有所感悟,在这篇文章结束时再回过头来看看下面这句话,若是大体明白了,那么这篇文章就起到做用了:缓存
而这段话正是这一轮资料搜寻下来我的以为最精炼的对渲染流程的描述了,至于里面具体各个名词所指则会在后面的内容进行补充,固然各个渲染引擎的流程在细节之处甚至一些环节上都会作出有所区别,这些则会在后面针对一些参考引擎作分析的时候再逐步整理出来。bash
那么,启程!数据结构
先贴一下找到的 知乎上的回答 :
是否是有种看一眼就想放弃的冲动 ?(╯°Д°)╯︵ ┻━┻
可是正如咱们在平常开发中忽然遇到一个新的技术需求的时候同样,通常咱们也不会把一个技术学到精通来才开始落地,而是会先预研一下方案,作下对比,掌握好基础,对一些风险提早作好功课,保证好大方向上的正确性,而后就会开始逐步落地,在过程当中慢慢打磨。
类比到上面的内容也是如此,按照我本身的想法,我以为首先C++的基本语法、内存管理、标准库的使用等基础内容以及OpenGL的渲染管线和基本的glsl、矩阵这些是在开始以前必需要先打好底子的,不然可能会连参考第三方开源引擎都成问题,更不要说本身去写了,而在本身写项目或学习第三方开源项目的过程当中则能够对遇到的不明白或者不彻底理解的内容进行进一步的学习,不求快但求吃透每一点,以这样的方式去持续扩大或加深本身的技能,在这个过程当中尽可能以点及面,每个细节尽量多去了解一些边边角角涉及到的地方,以便本身可以尽快完成一个又一个的闭环,最终对整个领域的理解愈来愈清晰。
其次就是合理安排出一部分时间来进行理论方面的系统学习,好比计算机图形学等,系统性的学习我的仍是比较推荐经过书籍来进行,虽然周期更长,可是可让你对这个领域的方方面面可以有一个更清晰的认识,从而造成一个更大的闭环。
而至于dx、metal、编译知识这些若是暂时不须要作这方面的需求的话倒不是很是关键,能够放到上面的技能学习以后,甚至可能很长一段时间你都不会使用到,固然也可能你如今是须要用dx而不须要opengl,那么就是dx的基础知识是必须的opengl排到后面,总之因人而异吧。
这部份内容是我在寻找渲染流程过程当中的额外收获,虽说的是演化史,可是里面的内容正好是对 启程
章节里对渲染引擎主要流程的描述的扩展,经过对多种着色方式的了解,能够间接对渲染流程里的几个主要步骤有一个初步的感知。
该章节的内容起源是3D渲染引擎着色方式的演化史,原文对各个着色方式的介绍比较简明扼要,在阅读完这篇文章后我又本身去对里面的各个名词和许多不理解的地方进行了搜索,所以大纲上仍是会按照这篇文章的几大部分进行,在内容里面再补上本身找到的额外的资料,主要来源于 实时渲染中经常使用的几种Rendering Path 和其余针对其中细节讲解的文章。
这部分我的认为在这篇文章的阶段不须要深究里面的实现细节,这个能够在后续分析开源引擎流程以及自主实现的时候去深刻研究,在这里更多地是对这些Render path
有一个印象而且知道他们的大体渲染原理、彼此之间的区别以及能解决什么样的问题,从而能在后面的引擎中因地制宜地使用不一样的着色方式。
Rendering Path
其实指的就是渲染场景中光照的方式。因为场景中的光源可能不少,甚至是动态的光源。因此怎么在速度和效果上达到一个最好的结果确实很困难。以当今的显卡发展为契机,人们才衍生出了这么多的 Rendering Path 来处理各类光照。
在介绍各类光照渲染方式以前,首先必须介绍一下现代的图形渲染管线。这是下面提到的几种 Rendering Path 的技术基础。
现代的渲染管线也称为可编程管线(Programmable Pipeline)
,简单点 说就是将之前固定管线写死的部分(好比顶点的处理,像素颜色的处理等等)变成在 GPU 上能够进行用户自定义编程的部分,好处就是用户能够自由发挥的空间增大,缺点就是必须用户本身实现不少功能。
下面简单介绍下可编程管线的流程。以 OpenGL 绘制一个三角形举例。首先用户指定三 个顶点传给 Vertex Shader
。而后用户能够选择是否进行Tessellation Shader
(曲面细分可能会用到)和 Geometry Shader
(能够在 GPU 上增删几何信息)。紧接着进行光栅化
,再将光栅化后的结果传给 Fragment Shader
进行 pixel
级别的处理。 最后将处理的像素传给 FrameBuffer
并显示到屏幕上。
名词解释
Geometry:即咱们所要渲染的一个几何图形
Vertex Shader:顶点着色器,处理每一个顶点,将顶点的空间位置投影在屏幕上,即计算顶点的二维坐标。
Tessellation Shader:曲面细分着色器,是一个可选的着色器,用于细分图元
Geometry Shader:几何着色器,是一个可选的着色器,用于逐图元的着色,能够产生更多的图元
Fragment Shader:片断着色器,也称为像素着色器(Pixel Shader),用于计算“片断”的颜色和其它属性,此处的“片断”一般是指单独的像素
后面文章内容中出现的VS、TS、GS、FS(PS)即对应上图中的Vertex Shader、Tessellation Shader、Geometry Shader、Fragment Shader
FrameBuffer:帧缓冲存储器,简称帧缓存或显存,它是屏幕所显示画面的一个直接映象,又称为位映射图(Bit Map)或光栅。帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。
这是最初始的渲染方式,原理是以mesh
为单位进行渲染,在光栅化后,对每一个PS
进行计算时,根据光照
进行着色计算,因此这种方式称为前向着色
。
Forward Rendering 是绝大数引擎都含有的一种渲染方式。要使用 Forward Rendering,通常在 Vertex Shader
或 Fragment Shader
阶段对每一个顶点或每一个像素进行光照计算,而且是对每一个光源进行计算产生最终结果。下面是 Forward Rendering 的核心伪代码:
For each light:
For each object affected by the light:
framebuffer += object * light
复制代码
好比在 Unity3D 4.x 引擎中,对于下图中的圆圈(表示一个 Geometry
),进行 Forward Rendering 处理:
将获得下面的处理结果:
也就是说,对于 ABCD 四个光源咱们在 Fragment Shader 中咱们对每一个 pixel 处理光照, 对于 DEFG 光源咱们在 Vertex Shader 中对每一个 vertex 处理光照,而对于 GH 光源,咱们采用球调和(SH)函数进行处理。
这种方式存在如下弊端:
所以,Deferred rendering就应运而生了。
很明显,对于 Forward Rendering,光源数量对计算复杂度影响巨大,因此比较适合户外这种光源较少的场景(通常只有太阳光)。
可是对于多光源,咱们使用 Forward Rendering 的效率会极其低下。由于若是在 Vertex Shader 中计算光照,其复杂度将是
O(num_geometry_vertexes ∗ num_lights)
,而若是在 Fragment Shader 中计算光照,其复杂度为O(num_geometry_fragments ∗ num_lights)
。可见光源数目和复杂度是成线性增加的
。对此,咱们须要进行必要的优化。好比
多在 Vertex Shader 中进行光照处理,由于有一个几何体有 10000 个顶点,那么对于 n 个光源,至少要在 Vertex Shader 中计算 10000n 次。而对于在 Fragment Shader 中进行处理,这种消耗会更多,由于对于一个普通的 1024x768 屏幕,将近有 8 百万的像素 要处理。因此若是顶点数小于像素个数的话,尽可能在 Vertex shader 中进行光照。
若是要在 fragment shader 中处理光照,咱们大可没必要对每一个光源进行计算时,把全部像素都对该光源进行处理一次。由于每一个光源都有其本身的做用区域。好比点光源的做用区域是一个球体,而平行光的做用区域就是整个空间了。对于不在此光照做用区域的像素就不进行处理。可是这样作的话,CPU 端的负担将加剧。
对于某个几何体,光源对其做用的程度是不一样,因此有些做用程度特别小的光源能够不进行考虑。典型的例子就是 Unity 中只考虑重要程度最大的 4 个光源。
名词解释
Mesh:网格,任何一个模型都是由若干网格面组成,而每个面又有若干个三角形组成,也就是说,模型是由若干个三角形面组成的
Deferred Rendering(延迟渲染)顾名思义,就是将光照处理这一步骤延迟一段时间再处理。
具体作法就是将光照放在已经将三维物体生成二维图片以后进行处理。也就是说将物空间
的光照处理放到了像空间
进行处理。要作到这一步,须要一个重要的辅助工具——G-Buffer
。
G-Buffer 主要是用来存储每一个像素对应的 Position,Normal,Diffuse Color 和其余 Material parameters
。根据这些信息,咱们就能够在像空间中对每一个像素进行光照处理。
下面是 Deferred Rendering 的核心伪代码。
For each object:
Render to multiple targets
For each light:
Apply light as a 2D postprocess
复制代码
这种渲染方式相比 Forward rendering 就是在渲染mesh
时,并不进行光照计算,而是按照如下步骤进行:
将深度、法线、Diffuse、Specular等材质属性
分别输出到GBuffer
里(其实就是几张RT
)
而后GBuffer里的深度和法线信息,累加全部光照的强度到一张光照强度RT
上
根据GBuffer里的Diffuse和Specular信息,以及光照强度RT,进行着色计算
名词解释
GBuffer:指Geometry Buffer,亦即“物体缓冲”。区别于普通的仅将颜色渲染到纹理中,G-Buffer指包含颜色、法线、世界空间坐标的缓冲区,亦即指包含颜色、法线、世界空间坐标的纹理。因为G-Buffer须要的向量长度超出一般纹理能包含的向量的长度,一般在游戏开发中,使用多渲染目标技术来生成G-Buffer,即在一次绘制中将颜色、法线、世界空间坐标分别渲染到三张浮点纹理中
下面简单举个例子:
首先咱们用存储各类信息的纹理图。好比下面这张 Depth Buffer
,主要是用来肯定该像 素距离视点的远近的。
根据反射光的密度/强度分度图来计算反射效果。
下图表示法向数据,这个很关键。进行光照计算最重要的一组数据。
下图使用了 Diffuse Color Buffer。
这是使用 Deferred Rendering 最终的结果。
Deferred rendering 的最大的优点就是将光源的数目和场景中物体的数目在复杂度层面上彻底分开,也就是说场景中无论是一个三角形仍是一百万个三角形,最后的复杂度不会随光源数目变化而产生巨大变化。从上面的伪代码能够看出 Deferred rendering 的复杂度为 O(screen_resolution + num_lights)
。
这种渲染方式也有一些弊端:
名词解释
MSAA、FXAA、Temporal AA都是抗锯齿(Anti-Aliasing)技术,锯齿的来源是由于场景的定义在三维空间中是连续的,而最终显示的像素则是一个离散的二维数组。因此判断一个点到底没有被某个像素覆盖的时候单纯是一个“有”或者“没有"问题,丢失了连续性的信息,致使锯齿。
具体区别可见FXAA、FSAA与MSAA有什么区别?
Deferred Rendering 局限性是显而易见的。好比我在 G-Buffer 存储如下数据:
这样的话,对于一个普通的 1024x768 的屏幕分辨率。总共得使用 1024x768x128bit=20MB, 对于目前的动则上 GB 的显卡内存可能不算什么,可是使用 G-Buffer 耗费的显存仍是不少的。一方面,对于低端显卡,这么大的显卡内存确实很耗费资源;另外一方面,若是要渲染更酷的特效,使用的 G-Buffer 大小将增长,而且其增长的幅度也是很可观的;而且存取 G-Buffer 耗费的带宽也是一个不可忽视的缺陷。
对于 Deferred Rendering 的优化也是一个颇有挑战的问题。 下面简单介绍几种下降 Deferred Rendering 存取带宽的方式。最简单也是最容易想到的就是将存取的 G-Buffer 数据结构最小化,这也就衍生除了 Light Pre-Pass
方法。另外一种方式是将多个光照组成一组,而后一块儿处理,这种方法衍生了 Tile-based deferred Rendering
。
这个技术是CryTek这个团队(该团队开发了CryENGINE游戏引擎,即下面简称的CE,若是还不熟悉的话,那么这个团队开发了《孤岛危机》、《孤岛惊魂》等游戏)原创的,由 Wolfgang Engel 在他的 博客 中提到的,也用于解决Deferred rendering
渲染方式里的第一个弊端。原理跟Deferred rendering
差很少,只是有几处不一样:
GBuffer中只有深度(Z)和法线(Normal)数据,对比 Deferred Rendering,少了 Diffuse Color, Specular Color 以及对应位置的材质索引值
在 FS 阶段利用上面的 G-Buffer 计算出所必须的 Light properties,好比 Normal * LightDir, LightColor, Specular 等 Light properties,将这些计算出的光照进行 alpha-blend
并存入 LightBuffer
(就是用来存储 Light properties 的 buffer)
着色过程不是Deferred rendering中相似于后处理的方式,而是渲染mesh,即将结果送到 Forward rendering 渲染方式计算最后的光照效果
相对于传统的 Deferred Render,使用 Light Pre-Pass 能够对每一个不一样的几何体使用不一样 的 Shader 进行渲染,因此每一个物体的 Material properties 将有更多变化。这里咱们能够看出对于传统的 Deferred Rendering,它的第二步是遍历每一个光源,这样就增长了光源设置的灵活性,而 Light Pre-Pass 第三步使用的实际上是 Forward rendering,因此能够对每一个 mesh 设置其材质,这二者是相辅相成的,有利有弊。
另外一个 Light Pre-Pass 的优势是在使用 MSAA 上颇有利。虽然并非 100%使用上了 MSAA(除非使用 DX10/11 的特性),可是因为使用了 Z 值和 Normal 值,就能够很容易找到边缘,并进行采样。
下面这两张图,上边是使用传统 Deferred Render 绘制的,下边是使用 Light Pre-Pass 绘 制的。这两张图在效果上不该该有太大区别。
其实这种方式也有弊端:
因为不透明物体在主视口中被渲染了两次,会大幅增长渲染批次,不过好在CE对状态切换管理的很是好,因此渲染批次的承载力很高
因为某些特殊材质须要对光照进行特殊处理,好比说树叶的背光面也会有必定的光照,因此这种方式也不太完美
印象里貌似CE对主光,例如太阳光,不累加进光照强度RT,而是着色时单独处理,这样的话效果会提高很多,至少室外场景是彻底可以解决问题的;而对于点光源比较多的室内场景,主光着色好看了就会效果很好了,毕竟其余光照的影响占比比较小。
这个方案是对Deferred rendering
渲染方式里的第三个弊端进行优化的。原理就是:
这样就能减小对光照强度RT上某个像素频繁读写的次数。
TBDR 主要思想就是将屏幕分红一个个小块 tile
,而后根据这些 Depth 求得每一个 tile 的 bounding box
。对每一个 tile 的 bounding box 和 light 进行求交,这样就获得了对该 tile 有做用 的 light 的序列。最后根据获得的序列计算所在 tile 的光照效果。
对比 Deferred Render,以前是对每一个光源求取其做用区域 light volume
,而后决定其做用的的 pixel,也就是说每一个光源要求取一次。而使用 TBDR,只要遍历每一个 pixel,让其所属 tile 与光线求交,来计算做用其上的 light,并利用 G-Buffer 进行 Shading。一方面这样作减小 了所需考虑的光源个数,另外一方面与传统的 Deferred Rendering 相比,减小了存取的带宽。
在 一篇文章 中提到目前全部的移动设备都使用的是 Tile-Based Deferred Rendering(TBDR) 的渲染架构,,里面还说起了使用TBDR的一些注意事项,感兴趣的能够看看,以及 针对移动端TBDR架构GPU特性的渲染优化 ,移动GPU渲染原理的流派——IMR、TBR及TBDR
名词解释
tile:区块,即将须要渲染的画面分红一个个的区块
bounding box:边界框,是一个矩形框,能够由矩形左上角的xx和yy轴坐标与右下角的xx和yy轴坐标肯定。从技术上讲,边界框是包含一个物体的最小矩形
light volume:体积光,散射是一种很是美丽的天然现象,在天然界中光穿过潮湿或者含有杂质的介质时产生散射,散射的光线进入人眼,让这些介质看起来像拢住了光线同样,也就是所谓的体积光。可见 游戏开发相关实时渲染技术之体积光
为了解决Deferred lighting
里面的第一个弊端,从CE3的某个版本开始,换成了这种方式。理由是,对于大多数物体来讲,Deferred rendering
的方式就很好了,而对于特殊材质,则使用Deferred lighting
的方式。这样,既能保持很好的渲染效果,又能避免渲染批次激增。
更详细的内容可见 Hybrid-Deferred-Rendering.pdf
有时候,你转了很大一个圈之后,发现又回到了原点。
好,那这就到了终极方式了——前向着色
的改进版。这个方案是ATI(著名显卡生产商,06年被AMD收购)发明的,已经应用于Ogre 2.1(开源的面向对象的3D引擎)。UE4(大名鼎鼎的虚幻引擎)正在针对VR研发前向着色,不知道是否是也是这个。
原理也很简单:
先用Tile-based deferred rendering
里的方式计算好每一个区域受哪些光照影响
而后像传统的前向着色同样渲染每一个mesh——固然,要去光照列表里查找影响当前区域的全部光照,并着色
这种方式只有上述提到的一个缺点,那就是可能和Deferred lighting
同样须要渲染两遍场景,不过之后应该会有优化的方案。优势则有:
渲染效果好
带宽开销低,尤为适用于VR这种每帧须要渲染两遍场景的应用
可使用硬件支持的MSAA,质量最高。
Forward+的优点还有不少,其实大多就是传统 Forward Rendering 自己的优点,因此 Forward+更像一个集各类 Rendering Path 优点于一体的 Rendering Path。
Forward+ = Forward + Light Culling
。Forward+ 很相似 Tiled-based Deferred Rendering。 其具体作法就是先对输入的场景进行 z-prepass,也就是说关闭写入 color,只向 z-buffer 写入 z 值。注意此步骤是 Forward+必须的,而其余渲染方式是可选的。接下来的步骤和 TBDR 很相似,都是划分 tiles,并计算 bounding box。只不过 TBDR 是在 G-Buffer 中完成这一步骤 的,而 Forward+是根据 Z-Buffer。最后一步其实使用的是 Forward rendering 方式,即在 FS 阶段对每一个 pixel 根据其所在 tile 的 light 序列计算光照效果。而 TBDR 使用的是基于 G-Buffer 的 Deferred rendering。实际上,forward+比 deferred 运行的更快。咱们能够看出因为 Forward+只要写深度缓存 就能够,而 Deferred Rendering 除了深度缓存,还要写入法向缓存。而在
Light Culling
步骤, Forward+只须要计算出哪些 light 对该 tile 有影响便可。而 Deferred Rendering 还在这一部分把光照处理给作了。而这一部分,Forward+是放在 Shading 阶段作的。因此 Shading 阶段 Forward+ 耗费更多时间。可是对目前硬件来讲,Shading 耗费的时间没有那么多。
如下是 Forward+ 与 Deferred Rendering 的对比图:
感兴趣的能够再额外看看 forward框架的逆袭:解析forward渲染 这篇文章。
名词解释
Light Culling:剔除光照
渲染引擎属于游戏引擎中的一部分,本章节主要简要整理一下找到的一些渲染引擎和游戏引擎,具体内在区别后续进一步深刻了解的时候再整理补上。
在Wiki上也已经有整理了目前为止市面上已有的大量游戏引擎:Game Engine
Github上统计的开源游戏引擎:game-engines
经过上面的调查咱们发现如今市面上的大小引擎数不胜数,一个个地去看的话时间周期估计要以年为单位,首先咱们要先从自身的需求出发定出一些对参考引擎所须要具有的特性的要求,而后再根据要求来筛选出几个比较贴合咱们需求的深刻研究。
以我自身的角度出发,我列出来了如下一些要求:
我从上面调查后的引擎列表里整理出了如下几个符合语言、使用人数、持续更新、支持效果等方面都比较符合的引擎来优先做为研究的对象,后续的分析系列文章也会先以这些引擎来做为目标:
渲染引擎
游戏引擎
至此咱们完成了在迈出跨平台渲染引擎第一步以前的铺垫工做,咱们梳理了渲染引擎的一个大体流程,以及这个流程里面的关于 Rendering path 等方面的细节信息,对这些内容有了一个初步的印象,同时列举了如下使人望而却步的技能树,可是咱们能够一步一步地吃成胖子,重要地是迈出这第一步,最后咱们整理了一下渲染/游戏引擎列表,并按照自身要求从中梳理了几个引擎来做为下一步分析研究的目标。
接下来就是技术活了,下一篇《跨平台渲染引擎之路:bgfx分析》将针对 bgfx 开始第一步学习研究,分享其内部的渲染流程以及分析思路等。