自定义渲染管线、延迟渲染、常见后处理效果

1,654 阅读31分钟

一.常见的RenderPath渲染路径

1.前向渲染Forward

2020年的知乎文章《如何选择 WebGL 框架和引擎?》 zhuanlan.zhihu.com/p/162878354

简单总结,那时候webgl渲染引擎基本都是前向渲染,对自定义渲染管线比较弱。Layaair和Cocos都开始努力搞了。

1-1.背景

(1)绝大部分webgl引擎,使用传统的前向渲染,在渲染时,会对每个物体计算所有光照的贡献。

(2)这种实现的复杂度随着光源和场景中对象的增加而增加,导致光源越多性能越差。这也是为什么在 Web 上看到的三维效果大部分都只使用一个主光源,只有一个太阳光。

1-2.问题~Cluster Lighting 多光源、集群光照

(1)很多光源的效果,比如夜晚场景有很多光源。

(2)用前项forward渲染管线实现,性能非常不好。

(3)解决多光源问题,只能使用Clustered forward render 集群(分簇)前向渲染,或者Deferred render 延迟渲染。

(4)目前主流的桌面游戏引擎都是使用延迟渲染,并配合前向渲染来支持透明物体。

2.集群前向渲染Clustered Forward Render

1.解决多光源问题~集群光照剔除

(1)在二维 (Tile) 和三维 (Cluster) 同时对于空间进行块状分割

(2)最后只计算对这个块状空间有光照贡献的光源,完成无效光源的剔除过程,提高计算效率。

(3)基于 WebGLUniform Buffer 有很多限制,光源数量支持比较少,一般在10个以内。

WebGPU 具备了 Storage Buffer,基本上就是直接对标 GPU 显存的限制。只要做好自身的内存管理和优化,就可以充分利用GPU的能力,实现多光源渲染的场景。

2.Orillusion,WebGPU渲染引擎

www.orillusion.com/guide/

(1)Orillusion是基于浏览器的Web渲染引擎,采用了最新的WebGPU标准来提供底层的图形能力。

(2)引擎的整体框架采用了现代引擎遵循的ECS组件式架构,以更为严谨的TypeScript作为开发语言。

(3)通过 集群前向渲染 和动态全局光照,使得整体的3D场景计算效率和渲染效果得到了质的提升。

(4)目前,Orillusion引擎还处于快速迭代升级阶段,很多桌面级的图形能力,都将因为WebGPU标准的出现而迁移到Web端。

同时,基于浏览器的Web环境,天然地赋予了我们很多应用层面的优势,比如易分享、易协作、易上手、跨平台等等。因此,Web3D领域孕育着巨大的想象空间。

3.layaAir3.0使用

支持webGPU,升级多光源,采用集群前向渲染的方式实现多光源的算法。

4.webgpu的Compute Shader

(1)依靠 WebGPU 的通用计算能力可以更高效的利用 GPU 计算优势,实现非常好的效果。

(2)比如基于 WebGPUcompute shader,orillusion引擎实现了 HDR 泛光屏幕空间反射, 环境光屏蔽 等常用的后处理效果。

(3)资料可以看Babylon.js的computeShader文档

doc.babylonjs.com/features/fe…

(4)webgpu其他细节不再过多介绍。

3.延迟渲染Deferred

(1)Claygl

最早支持延迟着色的webgl框架。是 ECharts 核心开发者 pissang 大神的开发的 WebGL 游戏引擎,它还用在了 ECharts-gl 项目中,使得 ECharts 在三维图表方面远超所有竞品。

(2)layaair也比较早的支持延迟渲染。

(3)Cocos 在22年3.1开始支持延迟渲染管线,23年3.7的版本开始支持自定义渲染管线。

4.CRP 自定义渲染管线(Custom Render Pipeline)

二.延迟渲染

背景:

2004 年的 GDC 上被正式提出,已经过去了近二十年,很多新接触 3D 编程的同学不清楚细节。

网上文章的问题

过于学术,用词高端,却晦涩难懂

没有讲清楚延迟渲染技术的工作边界,让人误以为延迟渲染能解决所有问题

没有结合实际案例,导致看完就忘、学完就废。

本章节大纲

延迟渲染技术参与的渲染环节

延迟渲染技术解决了哪些问题

延迟渲染技术原理浅析

Cocos Cyberpunk 项目中的延迟渲染管线实现细节

延迟渲染技术~内存、兼容性、GPU硬件参数

透明渲染解决办法

一. 延迟渲染技术参与的渲染环节

你以为延迟渲染无所不能,实际上它只参与了一小部分。

3D 渲染大致的流程:阴影贴图渲染 --> 主场景渲染-->屏幕后效渲染/后处理-->2D&UI 渲染

这个是关键的标准渲染流程,某些项目会根据需求进行定制,增加一些特殊用途的渲染流程。

1. 阴影贴图渲染

这个阶段会把场景中所有物体,采用统一的材质渲染一遍,生成阴影贴图,供下一个阶段渲染时使用。

2.场景渲染

这个阶段就是正常的渲染阶段,会渲染场景中所有的物体,会使用阴影贴图产生阴影,会进行光照计算,产生各种质感。

场景中的物体可以分为两大类:不透明物体半透明物体,它们的处理方式不一样。

3. 基于屏幕空间的效果渲染

这个阶段会利用适合的图像算法,对渲染出来的场景内容做一些特殊处理,提升画面效果。

4. 2D/UI 渲染

一些不随摄像机变化的 2D 元素,UI元素会在这个阶段渲染。

在如此长的渲染流程中,延迟渲染主要针对的是 **场景渲染流程中的不透明物体** 。

其余部分的工作在前向渲染和延迟渲染管线中几乎用的是同一套,并没有太多区别。

二.延迟渲染解决了哪些问题

解决了前向渲染的多光源问题

在上面的场景中,有以下内容:

  • 10 个模型(1 个平面,9 个立方体)
  • 7 个光源(1个平行光,6个球型光)

如何采用前向渲染的话,流程如下:

  1. 取得一个模型,提交给显卡渲染
  2. 在 Vertex Shader 中处理顶点变换、UV等内容
  3. 在 Fragment Shader 中,依次用 7 个光源进行光照计算
  4. 处理下一个模型

上面的这个流程,要走 10 次(因为有 10 个模型),并且每个模型需要与 7 个光源进行光照计算。

那整个光照运算就需要做 10 * 7 = 70 次。产生两个严重问题

1.shader指令限制

只能支持一定数量的灯光数量,一般是10个以下。当灯光数量超过一定值的时候,要采用多次绘制才能完成光照计算。

2.浪费 GPU 算力

光照计算是渲染中开销最大的部分,特别是 PBR 材质。

在上面的渲染过程中,有一些模型被遮挡的部分最终是看不见的,但依然会参与光照计算,这就造成了浪费。

如果场景很大,有非常多的模型和光源,两个问题会非常严重。

三. 延迟渲染技术原理浅析

1.分为两步

1-1、准备阶段(几何体渲染阶段 / GBuffer阶段)

会先将模型的用于计算光照所需的基本信息,渲染到不同的 RenderTexture 中存储起来,比如:世界空间的位置信息、法线信息、颜色信息、深度信息 等等。

1-2、光照阶段

光照阶段,顾名思义,就是利用上一阶段渲染出来的那些 RenderTexture,结合场景中的光照数据,计算出像素的最终颜色值。

2.命名由来

像素的颜色计算是统一放在第二阶段进行的,这就是延迟渲染中的延迟的由来。

由于像素的颜色计算统一放在了第二阶段,那些被遮挡、挡住的模型部分的像素已经被 深度测试剔除了 ,极大地避免了不必要的运算,从而提升性能。

3.GBuffer

1.概念

GBuffer 是 Geometry Buffer 的简称,意思是存储了几何体信息的缓冲区。

更具体一点,就是上面说的,在几何体渲染阶段用于描述物体位置、法线、颜色等信息的 RenderTexture 组合。

因此,上面提到的几何体渲染阶段,通常也叫:GBuffer 阶段

2.灵活性

延迟渲染只是一种思想,有很多不同版本的实现。GBuffer 中的 RenderTexture 具体存放什么,并不是统一的。引擎工程师在实现延迟渲染管线时,会根据需求选择适合的组合。

3.内容构成

3-1.必须信息

  • 世界空间的位置信息
  • 世界空间的法线信息
  • 物体表面颜色信息

3-2.光照计算

由于 GBuffer 主要为光照计算提供必要的信息,要弄清楚 GBuffer 的内容,就必须了解一下光照计算相关内容。内容较多,可以看游戏引擎入门的第四篇“所有渲染用一个方程表示”

这里简化一下,不管是 Blinn-Phong, 还是 PBR,都遵守最基本的光照公式:

物体最终颜色 = 自发光 + 漫反射+镜面反射+环境光;

也即渲染方程 出射光 = 自发光 + 反射光(上面后三项都属于)。

自发光:由模型材质中的 Emissive 相关参数决定。

漫反射:由模型材质+光照参数决定。

镜面反射:由模型材质参数+光照参数+观察位置决定

环境光(环境漫反射、环境镜面反射) :Blinn-Phong 中是一个常数;PBR 中,受模型材质参数影响。

(1)在 Blinn-Phong 中,采用的公式都非常简单,大部分由常数和简单的向量运算公式构成。

(2)但在 PBR 中,使用的光照计算公式就非常复杂了,比如

漫反射和镜面反射:会用 Cook-Torrance BRDF 模型计算物体的漫反射和镜面反射。

环境光:则被细分为了环境漫反射和环境镜面反射,使用 IBL 来处理。

(3)PBR是现代引擎标配。GBuffer需要额外存一些信息。

3-3.GBuffer的其他信息

1、环境遮蔽(AO)

用于光照计算阶段参与到模型的漫反射。

2、metallic-roughness

PBR 也不只一种模型,为了简单起见,本文以 Cocos Creator 中采用的 MR 模型为例

3、Emissive

物体自发光参数,在光照计算中,只是简单的相加。它更大的作用是可以用来形成物体bloom辉光效果,比如霓虹灯光。

4.多渲染目标(MRT)Mutiple Render Targets

(1)背景:

第一步需要那么多 RenderTexture,我们怎么获取呢?

笨方法,渲染多次,比如第一次渲染输出位置信息到一个texture上,第二次渲染输出法线等等。但是这种方法,DrawCall 翻了几倍,不管是 CPU 还是 GPU 压力都倍增。

(2)MRT是延迟管线技术得以应用的基础。

(3)渲染目标:屏幕帧缓冲区、渲染纹理,都是渲染目标。

常规渲染流程中,不管我们是把场景内容渲染到屏幕还是渲染到纹理,都只需要渲染到一个渲染目标。

显卡也逐渐支持在渲染内容时,同时渲染到多个渲染目标,即显卡支持MRT技术。

我们就可以在一次绘制时填充完所有 GBuffer 中的 RenderTexture。

5.GBuffer压缩

基于 PBR 的延迟渲染管线中,GBuffer 至少要能包含以下信息

位置:vec3,高精度,分量范围不可预估

法线:vec3,高精度,分量范围 0.0 ~ 1.0

颜色:vec3,低精度,分量范围 0.0 ~ 1.0

AO:float,取值范围 0.0 ~ 1.0

roughness 粗糙度:float,取值范围 0.0 ~ 1.0

metallic 金属度:float,取值范围 0.0 ~ 1.0

Emissive 自发光:vec3,分量范围 不可预估

深度:d24s8, 分量范围 0.0 ~ 1.0 

1.背景原因

(1)依靠图形 API 和显卡特性,深度图可以直接获取,不需要自己再手动处理。

(2)把 AO|roughness|metallic 这三个 PBR 参数装到一张 RenderTexture 里

总共还是需要5张RenderTexture(位置、法线、颜色、PBR~3合一、Emissive)才能满足需求

(3)但在当前的图形 API 标准中,OpenGL ES 3.0 也只规定了 GL_MAX_COLOR_ATTACHMENTS 小于 4。也就是说,在移动端设备上,多重渲染目标(MRT)大于 4 的情况,兼容性会非常不好。所以需要压缩到4张。

2.解决方案

(1)法线是可以归一化的,只要知道归一化的法线的两个分量xy,就能计算出第三个分量z。这样一来,法线就只需要两个通道。

(2)结合每个数据的取值范围,经过整理,我们可以得到下面这样的 GBuffer:

GBuffer_slot0:RGBA8

    xyz -> albedo.rgb 颜色

    w -> 空闲

GBuffer_slot1:RGBA16F

    xy -> normal.xy 法线

    z -> roughness 粗糙度

    w -> metallic 金属度

GBuffer_slot2:RGBA16F

    xyz -> emissive 自发光

    w -> ao 环境光遮蔽

GBuffer_slot3:RGBA16F

    xyz -> position.xyz 位置

    w -> 空闲

    

还有一个隐藏的,用于深度图。但它是深度缓冲区自带的特性,不占用 MRT 数量。

GBuffer_slot4:D24S8

四.延迟渲染管线的优缺点、优化手段

判断渲染技术优缺点的标准:内存开销兼容性、性能开销

且默认以前向渲染作为参考标准。

1.内存消耗~显存

(1)纹理内存计算方式

纹理的内存=宽x高x单个像素内存大小。

1 Bytes = 8 bits

RGBA8:4 * 8btis = 4 Bytes

RGBA16F:4 * 16bits = 8 Bytes

RGBA32F:4 * 32bits = 16 Bytes

D24S8:24bits + 8bits = 4 Bytes

(2)GBuffer 整体开销

Cocos 使用了 4 张 RGBA16F, 一张 D24S8。

当分辨率为 1280 x 720 时,整个 GBuffer 总共占用内存计算如下:

1280 x 720 x ( 4 * 8 + 4 ) Bytes 约等于 31.64MB。

来对比一下其他 16比9 的分辨率下,GBuffer 的内存开销:

  • (720p)1280 x 720 -> 31.64MB
  • (960p)1706 x 960 -> 56.23MB
  • (1k)1920 x 1080 -> 71.19MB
  • (2k)2560 x 1440 -> 126.56MB
  • (4k)3840 x 2160 -> 284.76MB

可以看到,当分辨率达到 1k 以上的时候,内存使用量还是不可小觑。

特别在移动端,应该尽可能控制渲染分辨率大小。

比起内存,我们更应该担心的是,GBuffer 给 GPU 带来压力。

2.GPU参数和性能瓶颈

见游戏引擎入门笔记中的GPU硬件模块,搜索“GPU参数和性能瓶颈”

3.延迟管线对GPU的影响

1.像素填充率(Pixel Fillrate)

(1)前向渲染中,只需要一个渲染目标+一个深度模板缓存,共2个。

(2)而在延迟渲染中,以 GBuffer 包含 4个渲染目标 + 一个深度模板缓存为例,总共需要5个。

从像素填充率开销而言, 延迟渲染的开销是前向渲染的 5/2 = 2.5 倍。

2.纹理填充率

如果模型使用 PBR 材质,在开启 IBL 的情况下,每个像素都会进行 IBL 计算,会进行环境贴图相关的纹理工作。

但在延迟渲染流程中,被挡住的像素不会进行这些运算。总的来说,纹理填充率的开销能省下一些,但不会太多。

3.显存带宽

在前向渲染中,只需要 RGBA8 x1 + D24S8 x1 就能搞定。

但在延迟渲染中,需要 RGBAF16 x 4 + D24S8 x1。

对显存带宽的要求,提升到了 4.5 倍 ->(4x8+4)/(4+4)

4.浮点运算

由于 PBR 主要开销在光照阶段,特别是 BRDF 和 IBL 会进行大量的数学公式运算。

但在延迟渲染流程中,被挡住的像素不会进行这些运算,此处的开销能省下不少。

5.总结

(1)延迟渲染会使像素填充量提升到 2.5 倍,会使显存带宽~数据访问量提升到 4.5 倍,能节省一部分的纹理填充工作和大量的逻辑运算工作。

(2)光栅能力不足的,显存带宽不足的 GPU 会有性能问题

(3)功耗的提升,会带来耗电和发热,移动端项目需要特别注意

4.延迟渲染的优化方法

1、限制GBuffer中的渲染目标分辨率

这是最容易操作的,在保证画面品质的情况下,尽可能降低渲染目标分辨率就可以。

2、减少GBuffer中的渲染目标数量

(1)想要减少渲染目标的数量,可以通过压缩算法,减少数据占用的字节数,合并存入通道。

这样会带来精度损失,但优点是效果得以保存。

(2)另一种就是去掉一些不必要的参数,这样带来的副作用是某些效果无法实现。

3、优先使用 RGBA8

RGBA8 比 RGBA16F 少 1 倍的内存占用,对显卡带宽的提升优化非常大。但副作用就是会带来精度损失,某些效果没有那么精细。

比如在 Cocos Cyberpunk 项目中, fragColor0 是可以使用 RGBA8 的,因为 albedo 原值本来就是 RGBA8 ,不会有精度损失。

5.延迟渲染的兼容性

1.延迟渲染需要三个特性支持

获取深度纹理

多重渲染目标(MRT)

渲染到浮点纹理

2.上面3个特性的正式支持,对应的 API 如下

桌面端

  • Direct3D 9.0c,2004-07-26, 100%
  • OpenGL 2.0,2004-09-07, 100%

移动原生端

  • OpenGL ES 3.0, 2012-08-05, 99.5%
  • Metal 1.0, 2014-06-03, 99.5% (iPhone 6/6 Plus)
  • Vulkan for mobile, 2016-08-22, Android 7.0

Web

  • WebGL 2.0, 2017-04-11,基于 OpenGL ES 3.0
  • WebGPU 1.0,预计 2023 年正式推出

3.总结

(1)在 PC桌面端和原生端,延迟渲染是不用担心兼容性问题的。而在 Web 端需要 WebGL 2.0 的浏览器才可以支持。

(2)2022年2月14日,Khronos Group 重磅宣布当下所有主流浏览器均已实现了对 WebGL 2.0 的支持。包括两个钉子户:Safari 和 Edge。

(3)WebGPU 虽然没有正式推出,许多浏览器也添加了对它的实验性支持。

4.cocos的兼容性处理

(1)在 Cocos Creator 中,你可以根据情况选择 OpenGL ES/Metal/Vulkan/WebGL/WebGPU 中的任何一个来发布到目标平台。

(2)发布 Android 和 Windows 时,你可以选择使用 Vulkan/OpenGL ES 3.0/ OpenGL ES 2.0。

(3)发布 Web Desktop 时,可以选择是否启用 WebGPU。

(4)发布 iOS/Mac 时,由于苹果的限制,你只能用 Metal 作为图形渲染后端。

总的来说,从当前的数据来看,延迟渲染支持的平台非常多。并且,Cocos Creator 的材质系统做了上层隔离,同一套材质,可以在前向渲染和延迟渲染管线中正常渲染。

因此,兼容性问题并不大,实在遇上不能运行的环境,还可以回退到前向渲染,确保用户顺利进入游戏。

五.延迟渲染管线总结

1.优点

(1)能够将光照计算复杂度从 M*N 变为 M+N,减少光照计算开销。

(2)光照计算阶段,仅计算看得见的像素,极大地减轻了 Overdraw 消耗。

(3)能够直接从 GBuffer 中拿到 深度、位置、法线、自发光强度等信息。能够非常方便地实现基于屏幕空间的后处理 高级渲染效果,比如 BloomSSRSSAO 等等。

2.缺点

(1)额外的内存(显存)开销

(2)对 GPU 像素填充率 和 显存带宽 要求高出几倍

(3)功耗提升带来的发热和耗电量比前向渲染高

(4)不支持透明渲染,透明渲染依然需要退回到前向渲染管线处理

(5)由于 MRT 的原因,不能使用显卡自带的 MSAA 硬件抗锯齿方式,需要用 FXAA,TAA 处理。

3.项目高中低端机性能适配策略

1.在高端机上使用延迟渲染,在低端机上关闭高级效果,退回前向渲染。

2.根据不同的画质和性能要求,制定不同的精度的 GBuffer。

比如:使用压缩存储机制,在单张 RGBA16F 渲染纹理中,放入 Normal, Color , Position,再配合一张 RGBA8 或者 RGBA16F 用于传递光照参数。

三.基于Cocos3.7 Cyberpunk项目 中学习自定义渲染管线

Cocos Creator 3.7 提供的全新的自定义管线,并且以extension形式放在了项目中,做到了非常好的隔离性。自己的项目需要的话,可直接复用,也可以作为研究新版自定义管线的学习案例。

  • 实现了前向渲染管线和延迟渲染管线,可以根据项目情况直接复用
  • 实现了后处理管线与框架,提供了Bloom、TAA、FSR等常见技术,也可以非常方便添加自己需要的后处理效果
  • 可视化自定义管线图,可以非常直观地看到管线渲染流程

1.查看渲染管线图 RenderPipeline Graph

1.新建一个空节点

2.将 pipeline/graph/pipeline-graph.ts 组件拖到这个节点上

3.勾选组件上的 Edit 复选框,就会弹出 RenderPipeline Graph 窗口

(1)实现了三个渲染管线:forwardmainreflection probe

(2)管线图中每一个方块叫做一个stage,具体每个Stage的实现细节,会在后面的文章中逐一讲解。

2.forward 管线

forward 管线主要用于实现 UI 渲染,它非常简单,只有两个 Stage

  • ForwardStatge:使用前向渲染方式渲染 3D 场景
  • ForwordPostStage:用于前向渲染管线的后期着色阶段

3.main 管线(自定义的延迟渲染管线)

Cocos Cyberpunk 项目,主要的管线,它一共由 8 个 Stage 构成:

  • CustomShaowStage: 阴影贴图阶段
  • DeferredGBufferStage:延迟渲染 GBuffer 阶段
  • DeferredLightingStage:延迟渲染光照阶段
  • custom.BloomStage:全屏泛光、辉光
  • TAAStage:TAA抗锯齿
  • FSRStage:超分技术
  • ZoomScreenStage:屏幕缩放
  • DeferredPostStage:用于延迟渲染管线的后期着色阶段

4.reflection probe 管线

这个管线原本用来烘焙反射探针的时候用的,但目前烘焙反射探针的时候也用的 main 管线,目前没有使用。它一共包含了三个 Stage

  • DeferredGBufferStage
  • DeferredLightingStage
  • DeferredPostStage

5.管线选择

5-1.区分一下两种摄像机。

  • 运行时摄像机:就是我们自己在场景上创建的摄像机节点,它用来在项目运行时,渲染场景中的节点。render Camera
  • 编辑器内置摄像机:这是编辑器自带的摄像机,用于渲染场景编辑器中的预览,它有一个固定的名称叫:Editor Camera

5-2.renderCamera

pipeline/pipeline-manager.tsrenderCamera函数中

if (!pipelineName) {

  pipelineName = 'forward';

  if (cameraSetting) {

      pipelineName = cameraSetting.pipeline;

  }

  else if (camera.name === 'Editor Camera') {

      if (camera.projectionType === renderer.scene.CameraProjection.ORTHO) {

          pipelineName = 'forward';

      }

      else {

          pipelineName = 'main';

      }

  }

}

(1)当场景中的运行时摄像机render Camera没有挂接 CameraSetting 组件时,会按照 forward 方式执行渲染,否则会按 CameraSetting 组件指定的方式渲染。

(2)当编辑器摄像机在渲染 UI 时,会采用 forward 管线渲染,其它情况则使用 main 管线渲染。这就是为什么在场景编辑器中能实时看到Bloom/TAA等效果的原因。

5-3.三个运行时摄像机render Camera

(1)UI摄像机

打开 scene-game-start 场景,定位到 init/canvas/Camera 摄像机节点,这个摄像机用于渲染项目中的 UI。有一个 CameraSetting 组件,Pipeline 属性的值为 forward

(2)漫游摄像机

打开 scene 场景,定位到 Main Camera/Camera,可以看到它有一个 CameraSetting 组件,Pipeline 属性的值为 main

这个摄像机就是当游戏开始时,玩家没有点 START 前,用于漫游场景的那个摄像机。

(3)主角摄像机

打开 assets/resources/obj/player-tps.prefab, 定位到 player-tps/camera_root/camera_player, 可以看到它有一个 CameraSetting 组件, Pipeline 属性值为 main。这个摄像机就是挂在主角身上,用来操控游戏视角的摄像机。

(4)小结:

运行时摄像机renderCamera也是:UI 使用 forward 管线,3D 场景使用 main 管线。

6.目录和代码结构

cocos编辑器的资源窗口(Assets)中可以看到三个文件夹,assets、internal、pipeline。其中 pipeline 就是我们的自定义管线内容。

1.目录

pipeline-manager.ts:主入口文件

components:自定义渲染管线中需要用到的脚本代码组件

graph:自定义渲染管线图相关内容

lib:第三方库,目前用到的是 detect-gpu,用于检查 GPU 参数

resources:自定义渲染管线需要用到的 effect 文件、材质文件、生成的 CRP Graph文件,pipeline 预制体

settings:硬件相关的设置

stages:自定义渲染管线各个 Stage 的具体实现

utils:一些工具类函数,如:mathdebug、event 等

2.入口文件

(1)在 pipeline-manager.ts 中定义了一个类:CustomPipelineBuilder, 这就是我们的主入口。

在文件的底部,有两行代码,如下。

 //将自定义渲染管线注册到引擎渲染器

if (director.root && director.root.device && director.root.device.gfxAPI !== gfx.API.WEBGL) {

  rendering.setCustomPipeline('Deferred', new CustomPipelineBuilder)

}



 //设置 CC_PIPELINE_TYPE 为延迟渲染

game.on(Game.EVENT_RENDERER_INITED, () => {

  director.root.pipeline.setMacroInt('CC_PIPELINE_TYPE', 1);

})

(2)rendering.setCustomPipeline 的原型如下:

setCustomPipeline(name: string, builder: PipelineBuilder): void;

参数

  • name:自定义渲染管线名字
  • builder:自定义渲染管线实例

(3)PipelineBuilder 的原型为:

export interface PipelineBuilder {

  setup(cameras: renderer.scene.Camera[], pipeline: Pipeline): void;

}

它只要求用户实现 setup 函数,这个函数在渲染的时候,每帧都会被调用。你可以把它看作 render,会更容易理解。

7.把 CRP 搬到自己的项目中,实现bloom光晕/泛光/glow辉光效果

1.细节略,参考这一篇

mp.weixin.qq.com/s/S6Wvk0fEA…

2.工程文件目录

study/learn-cocos/test-CRP

3.有几个注意点

(1)copy之后,需要重启编辑器

用command+R没生效,resource面板没有看到pipeline,最后是通过关闭项目重新打开才生效的。

(2)新建两个材质,一个正方体一个,effect选custom-surface,不透明

(3)albedo反射率,黑色rgb为0,scale默认1就好

(4)emissive自发光,scale设置100,然后

最终效果

8.自定义管线,实现shader后处理~变灰的效果

9.基于自定义渲染管线实现“定制版延迟渲染”的实现细节

按照前面讲的方法,查看管线图,在 main 管线中,找到 DeferredGBufferStageDeferredLightingStage

9-1.GBuffer构造

打开 deferred-gbuffer-stage.ts 文件,找到 DeferredGBufferStage 类。

const colFormat = Format.RGBA16F;

let posFormat = colFormat;

if (!sys.isMobile) {

    posFormat = Format.RGBA32F

}



const slot0 = this.slotName(camera, 0);

const slot1 = this.slotName(camera, 1);

const slot2 = this.slotName(camera, 2);

const slot3 = this.slotName(camera, 3);

const slot4 = this.slotName(camera, 4);



passUtils.addRasterPass(width, height, 'default', `${slot0}_Pass`)

    .setViewport(area.x, area.y, width, height)

    .addRasterView(slot0, colFormat, true)

    .addRasterView(slot1, colFormat, true)

    .addRasterView(slot2, colFormat, true)

    .addRasterView(slot3, posFormat, true)

    .addRasterView(slot4, Format.DEPTH_STENCIL, true)

    .version()

(1)addRasterPass 方法的作用是添加一个绘制过程。

(2)addRasterView 方法的作用是添加一个渲染目标。这里一共创建了 5 个渲染目标, slot3 用于存储位置,slot4 用于深度图。slot0,slot1,slot2 在这里看不出来作用。

(3)再看看它们的格式。

slot4 由于是深度图,所以使用了专用的格式 Format.DEPTH_STENCIL。

slot3 用来存储位置,在非移动端,为了提高精度,还使用 Format.RGBA32F 高精度格式。其余渲染目标均使用 Format.RGBA16F。

注意有个优化点, fragColor0 是可以使用 RGBA8 的,因为 albedo 原值本来就是 RGBA8 ,不会有精度损失。

9-2.GBuffer 填充

该阶段主要的工作是,调用的模型绘制, 打开 pipeline/resources/surface/custom-surface.effect

(1)渲染目标

这段代码声明了四个渲染目标 fragColor0~3,layout(location = 数字) 用于指定渲染目标索引。

#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED   

    layout(location = 0) out vec4 fragColor0; 

    layout(location = 1) out vec4 fragColor1;           

    layout(location = 2) out vec4 fragColor2;

    layout(location = 3) out vec4 fragColor3;

...

#endif

(2)main函数

Cocos Creator PBR Shader 的主函数

Cocos Creator 中的 PBR 渲染流程被分为了主要的两步:获取材质参数 光照计算

这样划分的好处,能够很好统一 Shader 的编写,使开发者不用去处理前向渲染和延迟渲染的区别。

void main () {

  StandardSurface s; surf(s);



  #if CC_FORCE_FORWARD_SHADING

    fragColor0 = CCStandardShadingBase(s, CC_SHADOW_POSITION);

    return;

  #endif



  if (cc_fogBase.x == 0.) {       // forward

    fragColor0 = CCStandardShadingBase(s, CC_SHADOW_POSITION);

  }

  else if (cc_fogBase.x == 1.) {  // deferred

    vec3 diffuse = s.albedo.rgb * (1.0 - s.metallic);

    vec3 lightmapColor = diffuse * s.lightmap.rgb;

    float occlusion = s.occlusion * s.lightmap_test;



    fragColor0 = s.albedo;

    fragColor1 = vec4(float32x3_to_oct(s.normal), s.roughness, s.metallic);

    fragColor2 = vec4(s.emissive + lightmapColor, occlusion);

    fragColor3 = vec4(s.position, 1.);

  }

}    

(1)第一阶段:获取材质参数

main 函数的第一句,调用的是 surf 函数,它的功能就是获取物体的PBR材质,比如 albedo,roughnes,metallic,emissive 等等。

StandardSurface s; surf(s);

(2)第二阶段:光照计算

前向渲染和延迟渲染有一点区别。

1)前向渲染会直接调用 Shading 相关函数(CCStandardShadingBase),完成计算。

2)延迟渲染会先将物体材质参数渲染到 GBuffer 中,再在后面统一计算。

(3)填充内容到 GBuffer

fragColor0 = s.albedo;

fragColor1 = vec4(float32x3_to_oct(s.normal), s.roughness, s.metallic);

fragColor2 = vec4(s.emissive + lightmapColor, occlusion);

fragColor3 = vec4(s.position, 1.);

一目了然,跟第三节GBuffer压缩的解决方案是一样

fragColor0:RGBA16F

    xyzw -> albedo.rgba

fragColor1:RGBA16F

    xy -> normal.xy

    z -> roughness

    w -> metallic

fragColor2:RGBA16F

    xyz -> emissive

    w -> ao

fragColor3:RGBA16F

    xyz -> position.xyz

    w -> 1.0

对比一下

9-3.光照计算

(1)打开 deferred-lighting-stage.ts 文件

setPassInput 会将对应的渲染目标与 gbuffer_****Map 绑定。

passUtils.addRasterPass(width, height, 'deferred-lighting', `LightingShader${cameraID}`)

    .setViewport(area.x, area.y, width, height)

    .setPassInput(this.lastStage.slotName(camera, 0), 'gbuffer_albedoMap')

    .setPassInput(this.lastStage.slotName(camera, 1), 'gbuffer_normalMap')

    .setPassInput(this.lastStage.slotName(camera, 2), 'gbuffer_emissiveMap')

    .setPassInput(this.lastStage.slotName(camera, 3), 'gbuffer_posMap');

(2)打开 deferred-lighting.effect

void main(){

  StandardSurface s;

  vec4 albedoMap = texture(gbuffer_albedoMap,v_uv);

  vec4 normalMap = texture(gbuffer_normalMap,v_uv);

  vec4 emissiveMap = texture(gbuffer_emissiveMap,v_uv);

  vec3 position = texture(gbuffer_posMap, v_uv).xyz;

  s.albedo = albedoMap;

  s.position = position;

  s.roughness = normalMap.z;

  s.normal = oct_to_float32x3(normalMap.xy);

  s.specularIntensity = 0.5;

  s.metallic = normalMap.w;

  s.emissive = emissiveMap.xyz;

  ...

}

它从 GBuffer 中获取材质参数,并重建 StandardSurface 信息,供后面的光照计算。

后续光照计算流程和前向渲染中一致,就不再赘述了。

9-4.半透明渲染

回到 deferred-lighting-stage.ts 中,在文件的尾部,你会发现下面这样的代码

 // render transparent

if

 (HrefSetting.transparent) {

  ...

}

在基于光栅化的图形管线中,半透明物体需要用从远到近的顺序来绘制,才能保证混合结果正确。而在延迟渲染管线中,几何体绘制阶段不会进行光照计算,绘制入颜色缓冲区的效果不是最终效果,如果在这个阶段进行透明渲染,得到的结果是不正确的,并且还会干扰到后面的光照计算。

所以需要使用前向渲染的方式来绘制场景中的半透明物体,上面代码就是做这个工作。

四.后处理效果 PostProcessEffects

也叫屏幕后处理,图像处理。

1.概念

在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作加工,实现各种特效。

2.资料

(1)unity官方文档的后处理:效果列表 | Universal RP | 12.1.1

(2)UE官方文档的后处理:Post Process Effects

(3)游戏中常见的后处理:zhuanlan.zhihu.com/p/142777086

1.bloom泛光/光晕, HDRBloom

(1)概念:也叫高光溢出,是一种光学效果。屏幕颜色内亮度高于阈值的部分会表现出扩散到周围像素中,并且随距离的增加而递减,形成一种发光朦胧的效果。

(2)例子:灯泡的光晕效果,cocos3.7加入了。

2.AA抗锯齿Antialiasing

2-1.锯齿原因:

3D渲染的栅格化过程将显示对象按二维数组点阵的形式存储起来,得到的原始图像中物体边缘难免会有锯齿样。

2-2.分类

(1)硬件AA,SSAA、MSAA、EQAA和CSAA都属于硬件抗锯齿

(2)软件后处理的AA

threejs等库提供的抗锯齿功能,比如threejs的render接口直接提供了抗锯齿开关,原理是用下面的算法。

2-3.软件后处理AA

(1)FXAA,物体边缘有虚影,去虚影的优化方法

(2)TAA

按照一定策略轻微的给相机设置一些 偏移值,让物体在栅格化时会因不同的相机偏移值得到略微不同的结果。特别是在边缘的地方更为明显。最终输出到屏幕的颜色采用插值历史帧和当前帧的作为结果,且该结果用于下一次的插值。

(3)SMAA

(4)DLAA 2.0(Deep Learning Super Sampling),利用人工智能和深度学习的方法来进行AA,可以同时兼顾画质和性能,属实非常震撼。

3.Lut 查找表

(1)Lookup tables (LUTs),查找表

developer.nvidia.com/gpugems/gpu…

(2)视频剪辑软件,很多这个词,比如什么冷色调、暖色调、日系风格lut等。

4.Vignette 渐晕

unity叫做渐晕。

图像从中心往边缘,变暗和/或去饱和。简单说就是中心亮外边暗。

5.DoF~DepthOfField 景深

概念:景深效果规定了距离相机最近、最远阈值,在相机和最近距离范围内的物体都为清晰的;

而一旦超出了最近距离,物体随距离增加会呈现越来越模糊的效果。直到最远达到最大模糊程度。

6. Blur 各种各样的模糊效果

(1)Motion blur, 运动模糊

(2)box blur 均值模糊

(3)Gaussian Blur 高斯模糊。

(4)Radial Blur 径向模糊。用户呈现速度感。

(5)Bilateral Blur 双边模糊。

(6)Bokeh Blur 散景模糊。

7.SSR 屏幕空间反射 (Screen Space Reflection)

(1)概念:

一种基于屏幕空间大小实现的反射效果,模拟光滑物体表面可以实时反射其周边物体影像的视觉效果。

(2)原理:

1)首先,屏幕空间物体的每个像素需要计算其反射向量。

2)然后,需要判断屏幕空间的 Ray Marching 坐标的深度和深度缓存中存储的物体深度是否相交。

3)最后,适当的调节粗糙度,把交点的颜色做为反射颜色完成着色。

4)整个计算过程,可通过 WebGPUCompute Shader 来实现,避免了 CPU 的消耗。在浏览器中可以呈现出非常良好的反射效果。

(3)优点:

1)相比平面反射,可以实现场景任意表面反射,而且不需要额外的 DrawCall,是非常流行的实时反射技术。

2)为实时渲染,某一个物体发生移动,反射画面中物体也会发生移动;能精确的从每个像素反射。

(4)缺点:

为不能够反射出物体的背面,且屏幕范围外的物体也不能够被反射到其他的物体上。

8.AO环境光遮蔽Ambient Occlusion

(1)概念:AO 是用来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果

(2)效果:

可以解决或改善漏光、飘和阴影不实等问题,

解决或改善场景中缝隙、褶皱与墙角、角线以及细小物体等的表现不清晰问题,

综合改善细节尤其是暗部阴影,增强空间的层次感、真实感,

同时加强和改善画面明暗对比,增强画面的艺术性。

引擎内部通过采样指定屏幕范围内,指定距离范围内的像素点,求积分用于赋值当前像素 AO 系数。

8-1.SSAO

最常见的屏幕空间环境光遮蔽 (Screen Space AO)

8-2. 其他

HBAO,GTAO等等

9.FSR

baijiahao.baidu.com/s?id=174509…

AMD宣布AMD FidelityFX Super Resolution (超级分辨率锐画技术,简称FSR) 

它是一种升级技术,旨在在不损失图像质量和图形细节的情况下提高游戏性能。

10. Outline轮廓描边

为指定的物体,描绘未被遮挡的部分轮廓的功能。

10-1.后处理实现,性能损耗相对大

1.高斯模糊产生边缘

(1)在后处理阶段先渲染产生一张RenderTexture,包含要被描边的物体,使用描边色渲染。

(2)高斯模糊RenderTexture,会产生边缘。

(3)用高斯模糊的图片反向剔除未模糊的图,这样只保留模糊多出的部分。

(4)此时RenderTexture上为只有边缘的图,将此边缘图与渲染结果图进行混合并输出屏幕,即可。

2.边缘检测

边缘检测的原理其实就是用一个特定的卷积核去对一张图像卷积,得到梯度值,再根据梯度值的大小去判断是否为边界。

10-2.shader实现

1.基于观察角度和表面法线

(1)原理:通过视角方向和表面法线点乘结果来得到轮廓线信息。

(2)优点:简单快速。缺点:局限性大。

2.模板测试描边

2-1.原理

模板测试发生在逐片元操作阶段。

(1)在第一个pass中正常渲染,把每个片元的参考值 Ref 都设置为1,Comp Always 总是通过模板测试, 并且 Pass Replace (不写的话默认是 Pass Keep),即把当前的 Ref 写入模板缓冲。

(2)第二个Pass中,把每个顶点按法线方向去进行一个扩张。

选择先把顶点和法线变换到视角空间下,是为了让描边可以在观察空间达到最好的效果。

随后设置法线的z分量,对其归一化后再将顶点沿其方向扩张,得到扩张后的顶点坐标。

对法线的处理是为了尽可能避免背面扩张后的顶点挡住正面的面片。

最后,我们把顶点从视角空间变换到裁剪空间。

在这个Pass中,同样把每个片元的参考值 Ref 都设置为1,Comp NotEqual 即只有当前参考值 Ref 和当前模板缓冲区的值不相等的时候才去渲染片元。

注意到,Unity的模板缓冲区的默认值是0,因此在外轮廓线之内的片元,我们在第一个Pass中写入到模板缓冲区的值为1,因此第二次Pass中相等,就不会去选择渲染。

而外轮廓线向外扩张出来的顶点所形成的那些片元,由于第一个Pass并未渲染,模板缓冲区的值为0,因此不相等,就会按第二个Pass的方法得到结果。

3.过程式几何轮廓线渲染

(1)就是把前面的模板测试换成了剔除操作。

(2)正常渲染的时候剔除背面、渲染正面,第二次顶点扩张之后剔除正面渲染背面,

(3)这样渲染背面时由于顶点外扩的那一部分就将被我们所看见,而原来的部分则由于是背面且不透明所以不会被看见,形成轮廓线渲染原理。

(4)因此从原理上也能看出,这里得到的轮廓线不单单是外轮廓线。

4.SDF 有向距离场/带符号距离场(signed distance field)

(1)概念:对每个像素都记录自己与距离自己最近物体之间的距离,如果在物体内,则距离为正,正好在物体边界上则为0。

(2)原理

SDF值在边界处接近0,于是可通过SDF的fwidth值与当前像素的SDF值去判断

因为fwidth是相邻像素的SDF的差值和,那么必然是很小的值。

所以判断的结果用于lerp,就可以检测哪里的SDF值接近0,亦即检测到轮廓。

11.GlobalFog 屏幕空间雾化

(1)概念:模拟现实环境中人眼观察物体,会被一层具有特定颜色、浓度、体积的半透明的雾覆盖后朦胧的感觉。

(2)常见的3D场景雾效,有三种衰减函数,分别是Linear、Exp、Exp2。