【转载】UE4学习笔记:实时渲染原理(三)

1,008 阅读18分钟

文章来源: 博客园
原文链接: UE4学习笔记:实时渲染原理 | U_N_Owen

原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式进行了修整,方便自己日后阅读

渲染结束后,将图像呈现到屏幕上的过程

光栅化过程(Resterization Process)

所谓光栅化过程,就是将 3D 数据(顶点数据)转换为像素的过程。光栅化基本上就是将前几个步骤确立好的模型渲染成图像。光栅化会使用许多像素格来渲染已经准备好的信息,像素格是方形的,这也意味着整张图像实际上是一张很大的网格。渲染多边形的时候,多边形边所代表的线条有时候会斜着跨越这些像素格,计算这些像素格哪些应该被渲染,计算这个结果的数学过程被称为光栅化过程。

一个像素点永远只会同时用于显示一个多边形。 可能是多边形的一部分,也可能是整个多边形。但是同时其表示的多边形只会是一个。

光栅化按照绘制调用逐次进行。

光栅化过程很多步骤你都无法改变,但是过度着色(Overshading)是你可以控制的。假如你有一个 10 万多边形的模型,当你在非常非常远的地方去观察这个模型的时候,它仅仅只会占用一个像素的大小,只会看到一个多边形,但是我们依然需要去计算这 10 万个多边形,这对性能来说是没有任何意义的,这样 “过度” 去计算不需要的多边形并渲染成图像的过程,就叫过度着色。过度着色一共会发生两次。

光栅化的着色过程就是给像素点上色的过程,因为硬件设计的原因,着色的时候并不是逐像素点去着色,而是根据 2x2 的像素格去进行着色,这意味着如果一个多边形非常的细小非常的薄,显示出来的就只有一个像素点有颜色,但是实际上着色器会对其本身及周围共四个像素点进行着色,这就是第一次过度着色。第一次过度着色发生在对可见像素以外的其他像素进行着色的时候。

在上面那个三角形旁边新生成一个三角形,如果这个新三角形需要渲染的像素格(即 2x2 的像素围成的方格)和之前的三角形需要渲染的像素格有重叠,那么之前已经渲染过的像素格 需要再次 被渲染一次,这就是第二次过度着色。第二次过度着色发生在其他多边形和模型覆盖相同的 4 像素区域的时候,它们会再次处理这些相同的像素区域。

通常,引擎会计算如果像素的中点位于三角形的内部,那么整个像素将被着色。

四边形过度绘制(Quad Overdraw)视图模式可以将场景中存在过度绘制的材质可视化,可以点击 “窗口(Window)->性能视图(Optimization Viewmodes)->四边形过度绘制(Quad Overdraw) 来切换。距离摄像机越远的对象渲染的越慢,性能损耗也越高,因为会有更多的多边形重复覆盖更小范围的像素区域(参考上文的第二次过度着色) 。该视图下面展示了什么样的颜色说明了是什么程度的过度绘制,一般对象能达到的最大等级过度绘制为绿色代表的 4 级,比这个等级高的对象只有可能是透明对象。

为了解决过度绘制的问题,可以通过设置 LOD 或剔除,降低多边形数量的方式来解决。LOD 有助于显著地解决多边形带来地过度绘制问题

初始像素着色器通道(Initial Pixel Shader Pass)越复杂,过度着色损耗越大,因为 前向渲染 中像素着色器通道损耗(比起 延迟渲染)更大,因此过度着色对前向渲染效率产生的影响比延迟渲染更大,在延迟渲染中,过度着色通常也会增加损耗,但并不会很大,甚至可以忽略,但是在前向渲染例如 VR 应用、手机应用等就需要着重去注意

经过光栅化的这些步骤之后,引擎将会渲染出来一张张各种各样的图像,这些图像就被称为 G 缓存。

G 缓存(G-Buffer)

从现在开始的步骤我们不再依靠几何体计算结果,我们只用图片

G 缓存用于合成各种内容,包括材质、光照、雾效等等。

G 缓存知道每个像素的材质属性、知道像素颜色、知道像素的朝向、还知道每个像素离开摄像机的距离,这些就是 G 缓存中不同图像各自存储的信息。引擎会使用这些信息进一步渲染。

G 缓存中另一个重要的概念是 自定义深度(Custom Depth)会更进一步让我们将某个对象与其他对象分开,我们可以为任何模型开启自定义深度属性,只需要勾选 细节面板(Details)->渲染(Rendering)->渲染器自定义深度通道(Render Custom Depth Pass) 就可启用。自定义深度会用单独的渲染目标或者说 G 缓存来包含模型,然后可以用于 各种特效,例如轮廓特效等等。可以在使用 视图设置(View Options)->高分辨率屏幕截图(High Resolution Screenshot) 里勾选使用自定义深度作为蒙版(Use Custom Depth as Mask)就能看到效果。

G 缓存会占用很多内存和带宽,因此我们如果想把引擎扩展到拥有更多 G 缓存,好让引擎拥有更多信息来处理之后的特效,一定要考虑 到这两个硬件(内存和带宽)带来的限制。

纹理(Texture)

压缩纹理的原因主要是为了节省内存和带宽。

纹理在被导入的时候 总会被 压缩,且在导入时候引擎都会自动压缩纹理。压缩格式会根据平台的不同而不同,所有不同类型的硬件都有着各自纹理的压缩格式。

BC(Block Compress,块压缩)格式,或更为熟知的被称为 DXTC 格式、DXT 格式(是 “DirectX 纹理压缩(Direct X Texture Compress)” 的简称)是用于 PC 上的纹理压缩格式,块压缩正如其名,通过生成像素块来压缩纹理,基于它在像素块(即上文中提到的 2x2 范围大小的像素集合)中找到的颜色,它会改变其中一些颜色以简化,然后逐块对纹理进行压缩,这就是块压缩。需要注意的是 法线贴图使用特殊的压缩设置,法线贴图的压缩使用过保存红色和绿色颜色通道,再经过一些额外的过程来计算出蓝色通道的值,而不是通常的计算像素块来进行压缩。

UE4 大多数时候都会在后台处理处理压缩。

纹理的压缩和 JPEG 很相似,如果仔细观察纹理贴图,我们可以发现一些 “块状结构”。

BC3(DXTC5)用于带有阿尔法通道的纹理,BC1(DXTC1)用于不带阿尔法通道的纹理。这两种是我们大多数时候都会用到的、非常重要的纹理压缩格式。任何时候 只要纹理没有阿尔法通道它就会使用BC1(DXTC1)格式,只要有阿尔法通道它就会使用BC3(DXTC5)格式。

在纹理编辑器页面里看到的资源大小(Resource Size)是压缩后的纹理大小而不是纹理原本的大小,格式(Format)也指出了该纹理是以什么格式进行压缩的。

无论纹理有没有被压缩,每个着色器的纹理采样数量存在着最大限制。

上文说纹理分辨率会影响内存和带宽,但它很少影响渲染效率。当你的项目存在延迟和卡顿的问题时(而不是持续性的帧率下降),很可能都是因为你的带宽或内存不够导致的。

为了最大化内存的效率,我们可以使用多级渐进纹理(Mipmap),它是由许多只有原纹理四分之一大小的的纹理组成,它会不停地进行复制,后续纹理都是前者纹理的四分之一。这些多级渐进纹理会被保存到 DDS 文件和纹理本身内,纹理编辑器里的 细节面板(Details)->细节层级(Level Of Detail)->LOD偏移(LOD Bias) 表明了当前纹理编辑器视口里显示的是哪一级渐进纹理。多级渐进纹理是自动生成的,不需要我们去手动设置。使用多级渐进纹理的另一个原因是减少纹理在视口远处产生的噪点,另另一个原因就是为了处理纹理流送。

纹理流送(Texture Streaming)就是 确定引擎在何时需要哪张纹理以及哪些多级渐进纹理的过程,因为你肯定不会想要一次性将所有纹理及所有多级渐进纹理全部导入进项目里,这样的话硬件的内存和带宽很快就会用完了。

为了让纹理能够正确地被流送,我们必须将纹理的边长设置为 2 的指数幂如果你的纹理的某一边长没有 2 的指数幂,那这张纹理将不会生成多级渐进纹理,也不会被正确流送。 某些特殊处理会需要这种“劣势”,例如被 UI 使用到的纹理,因为我们 永远不会 从不同距离观察到这张纹理,也就是说这张纹理实际上是不需要流送和多级渐进纹理的,像这类永远不会从不同距离观察到的纹理可以不遵循“纹理边长必须为 2 的指数幂”这个规定。

纹理大小相关的问题主要是延迟和卡帧(指在正常运行下 突然 卡顿,过一会儿又恢复正常运行的情况),而不是 帧率丢失(指持续性的实际帧率小于目标帧率的情况)。当你启动游戏的时候看到的是一张模糊的纹理,过一阵子才会看到完整的纹理,这就是 “延迟”,会发生这种情况是因为电脑没有足够的带宽或内存来快速传输完整分辨率的纹理,进而使用一张低分辨率并且很模糊的多级渐进纹理。纹理池 是供纹理使用的一段内存,它会被游戏中的纹理塞满,如果这些纹理的大小超出了纹理池的大小,则会发生部分纹理不能显示完整分辨率,而只能显示低分辨率的情况。

材质(Material)

引擎使用像素着色器来完成整个材质系统。

像素着色器(Pixel Shader)

渲染流程当中的每一个步骤(从几何体渲染开始,直到渲染完成)几乎都是由用像素着色器来完成的:修改输入值->重新计算->输出结果,可用于实现实时光照、所有着色、材质、雾、反射、后期处理特效等等。

例如当需要生成雾效的时候,像素着色器会得到屏幕中某个像素的输入值,获得该输入值之后就会根据该像素值距离摄像机的距离来计算出这个距离下的像素在产生雾效的时候的最终结果。

像素着色器会遍历所有像素并运行计算。

像素着色器并不 一定会 改变所有像素,例如我们可以设置在遍历过程中,当前像素如果是红色的话,就将其更改为绿色,这也是为什么我们有遮罩图像

像素着色器使用像素着色器语言进行编写,着色器语言因平台而异,不同平台使用不同的着色器语言编码,在 DirectX 中使用(也是在 UE4 中使用)高级着色器语言(High Level Shader Language,HLSL)。通过在材质编辑器界面点击 窗口(Window)->着色器代码(Shader Code)->HLSL代码(HLSL Code)即可浏览该材质实际的着色器代码内容。

  • 通常情况下,编写好的着色器代码会存在很多未定义的变量,例如要使用哪些纹理、定义光源如何与纹理互相影响、反射有多强烈,诸如此类,这些未定义的变量需要从外部输入,在外部指定好变量需要的内容之后,最终会在模型上渲染出来。这样的过程可以概述为:编写着色器代码->指定代码需要用到的外部资源->渲染到模型。
  • UE4 的方法会更为复杂,因为它的设置更多,意味着自由度更大。首先也是编写着色器代码,也同样存在未定义变量,然后这些代码会被编译成为材质编辑器里面的节点或表达式,通过对节点和表达式进行赋值,这些节点和表达式又会组合形成一个更加复杂的着色器,通过这种层层叠加组合,最后才渲染到模型上。
  • 自从UE 4.17 以后,我们可以直接在项目里面创建着色器代码以实现我们自定义的效果。

每个材质都会有自己的模板,每个模板都会有不同的输入需求,这也是为什么在材质编辑器里面最终节点上会有部分输入是激活状态,部分输入是灰色的未激活状态,激活状态就是当前着色器代码模板需要输入的内容。

每个材质并不只会为自己生成一个着色器,而是会 生成很多个,因为材质需要根据使用情况来生成不同的着色器,“使用情况” 指的是在材质编辑器的细节面板里 用途(Usage) 分类里勾选的属性,一个属性(例如 “与静态照明一起使用(Used With Static lighting)”、“与网格体粒子一起使用(Used With Mesh Particles)”)都会生成一个对应的着色器,之所以需要这样分类去勾选实际的用途,是因为如果我们默认让单一的着色器能够对应所有的情况,那么我们会生成一个特别复杂的着色器,会在一些不需要的地方损耗性能。

UE4 中材质使用的是基于物理的渲染(Physically Based Rendering,PBR),PBR 使用高光、金属色和粗糙度作为输入,它被用来计算环境中几乎所有的着色。

  • PBR 属于同一着色,我们把它称之为统一着色因为所有图像 —— 所有模型和材质 —— 在底层中都建立在相同的 PBR 着色器系统上,这样做是为了获得最佳效率,因为如果我们知道我们预期的是什么样子、知道我们底层使用的是统一一套着色器,那我们就能够针对该着色器进行优化、设计。
  • G 缓存包含了渲染一帧图片所需要的大部分信息,但不是全部信息,借助 PBR 提供的信息,结合 G 缓冲就能实现更好的渲染效果。

着色模型(Shading Model,可在材质编辑器细节面板中材质分类里看到该属性)是由 G 缓冲生成的纹理,是一系列遮罩图像,可以识别哪些像素使用 PBR 之外的其他着色模型,这些(使用 PBR 之外的着色模型的像素)会采用另外一种渲染路径。

现在我们已经拥有了渲染材质所需要的全部信息,但是还缺少材质必须的一部分 —— 反射。

材质或着色器能查看的纹理采样器拥有 最大数量上限,通常最大值为 16 个,并且只有 13 个可用,但是 你可以使用共享采样器(Shared Samples),这能使你使用多达 128 张不同的纹理, 仅限于 DX11 或 DX12。在材质编辑器界面下方的位置可以看到当前材质使用的纹理采样器个数,例如是 Texture Samplers: 2/16 样式的字段,说明该材质里最大允许 16 个纹理采样器,已使用了 2 个,当材质为空材质时候也会有采样器被占用的情况,是因为存在一些间接纹理(如光照纹理、阴影纹理等),所以该字段显示的采样器占用数不一定等于该材质正在使用的采样器数。

材质编辑器界面下方的字段 Base pase shader:XXX instructions,说明了该材质有 XXX(整数值)条指令,该指令的数量通常在 100 到 200 次之间属于正常范围,高于这个范围的值属于性能损耗特别大的材质,需要对该材质进行优化。

反射(Reflection)

实时反射非常难以实现,这可能是除光照外最难实现的效果之一,因为每出现一次反射都需要重新渲染整个场景,因此 UE4 使用三种不同的反射系统,彼此互相协同,将它们混合在一起使用来解决面临的挑战和难题,多数时候你看到的效果至少使用了三种(2D/立体场景捕获不算在内)中的两种作为组合。这三种系统是按顺序执行的,最先执行的系统优先级最低

反射捕获(Reflection Captures)

  • 反射捕获会在一个特定位置(一般是反射捕获 Actor 所在的位置)捕获一张 静态立方体贴图 ,这是预先计算出来的,这样的反射本质上只是一张混合到材质上的贴图,结果就是它非常快速,但不太精确,并且是只在固定范围内存在的局部效果。
  • 当摄像机位置和反射捕获 Actor 所在位置重合的时候,反射才是最精确的,当摄像机位置不再和反射捕获 Actor 重合的时候,反射会依然存在,但是会出现 位置偏差
  • 反射捕获 Actor 会在你打开关卡时重新捕获,点击 “构建(Build)->构建反射捕获(Build Reflection Captures)” 也能够进行捕获。运行游戏的时候它也会在加载关卡时更新捕获,但是当你打包游戏之后,反射捕获 Actor 实际上会将纹理烘焙进游戏,而不再会在关卡开始时更新捕获。

平面反射(Planar Reflections)

  • 类似于反射捕获,都是从给定位置捕获内容,但是平面反射 仅仅用平面 捕获内容,因此反射仅限于在那个平面上(同样为局部效果)。
  • 平面反射在某些设置下损耗很大,但非常适合需要精确反射效果的平面,但是也只适合平滑表面的反射。

屏幕空间反射(Screen Space Reflections,SSR)

  • 默认的反射系统。
  • 可在后期处理体积的 细节面板(Details)->渲染特性(Rendering Features)->屏幕空间反射(Screen Space Reflection)->强度(Intensity) 设置里更改反射效果的强弱,将该属性设置为 0 即可关闭 SSR。

当一个项目未经过烘焙时,反射捕获会在关卡加载时进行捕获,因此当你在关卡中添加过多反射捕获可能会导致加载变慢。

当反射捕获发生重叠的时候,项目的性能损耗会变大,因为像素着色器会一遍遍地进行重复计算。

在关卡中可以放置一个非常大的大型反射捕获 Actor,再在一些需要反射程度高或者需要精确反射的表面附近放置小型的反射捕获 Actor,使用这种方法来实现较高质量的反射效果。

除非必要否则不要使用平面反射。

如果硬件性能有限,请关闭三种反射系统中性能损耗 最大 的屏幕空间反射。

如果硬件性能允许,可以将屏幕空间反射质量提升到标准之上以此来减少噪点。

除了这三种反射系统外,还可以使用 “天空光照(Sky Light)” 提供的低成本备用反射捕获方案。天空光照根据 “天空距离阈值(Sky Distance Threshold)”,裁剪掉该属性指定距离及距离内从而形成一个裁剪面,然后捕获场景剩余内容从而为 整个 游戏场景捕获一张立方体贴图,游戏场景里任何附近没有反射捕获 Actor 的对象都会转而使用天空光照反射,这对开发大型户外场景很理想,因为我们不需要在场景中 到处 放置反射捕获 Actor。

相关文章

【转载】UE4学习笔记:实时渲染原理(二)

【转载】UE4学习笔记:实时渲染原理(四)