Skip to content

RenderPipeline

云风 edited this page Jan 25, 2024 · 3 revisions

渲染管线

得益于 ECS 架构和 Lua 动态语言的灵活性,Ant 引擎的渲染管线很容易搭建和修改。限于开发精力和项目的需求,引擎现在只实现了一套渲染管线。以后根据不同的项目需求,可能会扩展更多。

基于物理的渲染 (PBR)

Physically Based Rendering (PBR) 是目前非常流行的渲染方式。它旨在以现实世界中的光学对灯光和表面进行建模的方式来渲染图像,让画面获得贴近现实的真实感。采用 PBR 最大的好处在于统一美术的工作流。以往根据不同的美术表现需求,要定制不同的着色器。过于随意的编写着色器有极大的性能隐患,参数调整缺少统一规范,为了达到理想的效果,需要巨大的工作量。PBR 统一了着色器,用一个着色器实现了过去千奇百怪的效果。着色器参数基于物理,更容易被人理解。同时还简化了渲染管线的实现。

在制作 Ant 引擎需要的美术 Asset 时,需要为模型制作 PBR 材质。有两种常见的 PBR 材质工作流,一种是 金属/粗糙度 工作流,一种是镜面反射/光泽度工作流。Ant 引擎只使用前者。

引擎最多支持为 PBR 材质定义五张贴图,它们均为可选项。如果至少定义一张贴图,就必须在模型 (mesh) 数据中提供贴图的 UV 信息。

  • 颜色。如果不提供颜色图可以在材质中定义单个颜色值。
  • 法线。用于描述模型表现的细节。
  • 粗糙度/金属度。
  • 发光图。
  • 遮挡图。用于环境光遮挡 (Ambient occlusion, AO)。

最低硬件配置

Ant 引擎目前的渲染管线对硬件有一定要求。因为我们使用了 Compute Shader ,所以在手机上需要支持 OpenGL ES 3.0 以上的硬件水平。Apple A11 即 iPhone 8 以上经过测试没有问题。

PC 上没有经过严格的测试,需要至少支持 Shader model 4.0 。

因为采用 PBR ,所以必须使用 High Dynamic Range (HDR) 。着色阶段使用线性色彩空间,使用 RGBA16F 的缓冲区。渲染管线要求显卡的后备缓冲区 (back buffer) 必须支持 sRGB 空间,纹理图也需要支持 sRGB 。

深度缓冲 (depth buffer) 需要支持比较模式 (compare mode) 。

材质

材质 (material) 控制着渲染器将模型 (mesh) 以何种表现方式渲染出来。材质必须定义在 .material 为后缀的文件中,它是 DataList 格式的文本。材质文件会经过 Asset 的转换过程,不支持运行时在内存动态生成材质。

PBR 材质

以下是一个典型的 PBR 材质:

fx:
  shader_type: PBR
  macros:
    "ALPHAMODE_OPAQUE=1"
  varyings: /pkg/ant.resources/materials/lighting.varyings
properties:
  s_basecolor:  # optional
    texture: /pkg/ant.resources/textures/basecolor.texture
  s_metallic_roughness:  # optional
    texture: /pkg/ant.resources/textures/metallic_roughness.texture
  s_normal:  # optional
    texture: /pkg/ant.resources/textures/normal.texture
  s_emissive:  # optional
    texture: /pkg/ant.resources/textures/emissive.texture
  s_occlusion:  # optional
    texture: /pkg/ant.resources/textures/occlusion.texture
  u_pbr_factor: {1, 1, 0, 0}  #metallic, roughness, alpha_cutoff, occlusion strength
  u_emissive_factor: {0, 0, 0, 0}
  u_basecolor_factor: {1, 1, 1, 1}
state: /pkg/ant.resources/materials/states/default.state

fx.shader_type 定义为 PBR 后,表示这是一个 PBR 材质,所以不需要指定定义着色器文件。

fx.macros 可以自定义一些宏,着色器中可以使用这些宏。

fx.varyings 定义了顶点着色器的输入和顶点的布局。

properties 中,s_basecolor s_metallic_roughness s_normal s_emissive s_occlusion 这五张贴图每一张都是可选的。

properties 中,u_pbr_factor u_emissive_factor u_basecolor_factor 是 PBR 的参数,它们和以上的贴图共同产生作用。

state 定义了渲染管线的状态。这些状态信息是一个字典,可以直接写在材质文件中,也可以单独写在 .state 文件中(如果很多材质的渲染状态相同,方便共享)。下面是 default.state 定义的默认渲染状态:

ALPHA_REF: 0
CULL: CCW
DEPTH_TEST: GREATER
MSAA: true
WRITE_MASK: RGBAZ

自定义材质

有些场合不需要 PBR 材质,可以自定义一个。比如做 2D 游戏时,我们只需要把一张图片不经过任何光照处理原封不动贴在屏幕上,就可以使用这样一个 texquad.material

fx:
  fs: /pkg/ant.resources/shaders/simple/quad/fs_simplequad.sc
  setting:
    lighting: off
  vs: /pkg/ant.resources/shaders/simple/tex/vs_texquad.sc
properties:
  s_tex:
    stage: 0
    texture: /pkg/ant.resources/textures/black.texture
  u_color: {1.0, 1.0, 1.0, 1.0}
state: /pkg/ant.resources/materials/states/quad.state

fs.fs 和 fs.vs 定义了自定义的着色器。着色器语法是由 bgfx 定义的一种 glsl 方言。

fx.setting 是一些开关,控制着着色器的公用库中的宏定义。这里写的 lighting: off 指的是这个材质不计算光照,直接使用材质中定义的颜色值及颜色贴图。

运行时控制材质

材质文件中,写明了 properties 的默认值。这些 properties 可以取任何字符串名字,但一般约定 s_* 表示着色器中的贴图, u_* 表示着色器中的 uniform 。该命名规则并不强制。

程序运行时,可以动态修改材质中的这些 properties 。

材质,在运行时是 entity 的一个组件。开发者不用关心这个组件的细节,使用一个 api 即可修改一个 entity 中材质组件的属性。

local imaterial = ecs.require "ant.asset|material"
imaterial.set_property(entity, name, value)

材质实现细节

材质对象是用 C 实现的,在 /pkg/ant.material 可以找到源码。一个材质对象是 C 封装的 Lua userdata ,由 ant.material|material 模块中 create_instance() 创建返回。这个 Lua 对象提供有方法供 Lua 代码操控。这个对象的生命期不依靠 Lua 的 GC 机制控制,框架会主动调用它的 release 方法。

引擎同时有 Lua 和 C/C++ 编写的代码。在 C/C++ 实现的渲染系统中,我们使用 struct material_instance * 指针,这个指针通过以上对象的方法 instance:ptr() 获得,记录在 C 组件中。该指针不持有材质对象的生命期,C/C++ 渲染系统也不会长期持有它。

光照

引擎目前支持三种直接光:平行光、点光源、聚光灯。

平行光

平行光是最常见的光源。这个特性被内置在 ant.render 中,不是一个独立的特性。平行光源是一个场景对象,光源含有 light 组件,type 为 directional 。Scene 中只能有一个平行光源(可以没有)。平行光源 scene 组件的空间状态 srt 中,只有方向 r 是有意义的,缩放 s 和位置 t 都是无意义的。

通常,我们会把光源定义在 Prefab 中,用 world:create_instance() 实例化带有光源的预制件就能够在场景中设置光源。引擎的编辑器可以编辑光源,生成预制件。下面是一个平行光源的预制件内容:

policy:
  ant.render|light
data:
  scene:
    r: {0.5, 0.0, 0.0, 0.8660253}
    t: {0, 10, 0, 1}
  light:
    color: {1, 1, 1, 1}
    intensity: 120000
    intensity_unit: lux
    type: directional
  make_shadow: true
  visible: true

阴影

平行光源可以让其它场景物件产生阴影。这是目前引擎唯一可以产生阴影的光源。在 light 组件中,注明 make_shadow: true 即可让这个光源产生投影。

控制每个场景物件渲染时的阴影,通过它们的材质中 setting 选项设置。

  • fx.setting.cast_shadow 控制是否产生阴影
  • fx.setting.receive_shadow 控制是否接收阴影

阴影系统本身有许多配置参数,这些参数配置在 全局设置项 中。

目前,引擎使用 Cascaded Shadow Maps (CSM) 方法产生阴影用的深度图。对于大型场景,CSM 可以将相机的视锥体分割成若干部分,然后为分割的每一部分生成独立的深度图,为不同远近的物件提供不同的阴影精度。我们需要在全局设置项中配置它们。

  • graphic.shadow.size 配置每级阴影图的大小,默认为 1024 。
  • graphic.shadow.split_ratios 配置 0 到 1 之间分割区块的区段,默认不分级,配置为 { 0, 1.0 } 。

引擎目前提供了两种降低阴影边缘锯齿的方案:Percentage-Closer Filtering (PCF) 或 Virtual Shadow Map (VSM) ,通过 graphic.shadow.soft_shadow 选择。没有这一设置项,就不做处理。

  • graphic.shadow.pcf 配置 pcf 的相关参数。
  • graphic.shadow.vsm 配置 vsm 的相关参数。

点光源

点光源作用于一个球形空间,光照强度随距离的平方衰减。你可以在场景添加不只一个点光源,理论上数量没有上限。但每增加一个点光源都有一定的开销,整体开销是 O(n) 的,n 为点光源的数量。点光源也是一个场景对象,光源含有 light 组件,type 为 point 。平行光源 scene 组件的空间状态 srt 中,只有位置 t 是有意义的,缩放 s 和方向 r 都是无意义的。

通常,我们会用引擎编辑器制作点光源,储存在预制件中。预制件里的数据大约是这样:

policy:
  ant.render|light
data:
  scene:
    t: {0, 10, 0, 1}
  light:
    color: {0.8, 0.8, 0.8, 1}
    intensity: 12000
    intensity_unit: candela
    range: 15
    type: point
  visible: true

和平行光源相比,它多出了 range 属性,描述了球体半径,即点光源影响的最远距离。其光强单位使用的 candela , 和平行光使用的 lux 不同。

聚光灯

聚光灯和点光源非常类型,区别在于它的影响范围是一个锥体。光源 type 为 spot,场景对象中 scene 的 srt 会考虑位置 t 和方向 r 。通常也是使用引擎编辑器制作聚光灯的预制件:

policy:
  ant.render|light
data:
  scene:
    t: {0, 10, 0, 1}
    r: {0.5, 0.0, 0.0}
  light:
    color: {0.8, 0.8, 0.8, 1}
    intensity: 12000
    intensity_unit: candela
    range: 15
    inner_radian:  0.8
    outter_radian: 0.9
    type: spot
  visible: true

和点光源相比,增加了 inner_radianoutter_radian 两个配置参数。

环境光

Ant 引擎支持 Image Based Lghting (IBL) 环境光照产生的间接光。使用它需要在场景中创建一个包含 ibl 组件的 entity 。这样的 entity 同一场景只能存在一个。

policy:
  ant.render|ibl
data:
  ibl:
    LUT:
      size: 256
    intensity: 30000
    prefilter:
      size: 128
    source:
      tex_name: /pkg/ant.resources.test/sky/colorcube2x2.texture
      facesize: 512

它需要一个 cubemap 类型的 texture ,这张贴图用来产生环境光。

后处理

引擎渲染完整个场景后,会进行后处理 (Post Processing) 。PBR 至少需要进行两个后处理流程:

  • Bloom 会影响到 PBR 中定义的发光材质
  • 将 HDR 转换到 LDR 用于最终的图像显示

还有一些可选开启的后处理流程

  • 屏幕空间上的环境光遮蔽 Ambient Occlusion (AO)
  • 全屏抗锯齿

所有的后处理流程配置参数,都由 全局设置项 控制。

渲染管线的实现

渲染管线的实现细节通常引擎的使用者不必关心。如果只是想了解怎么让引擎绘制出物件,应该了解一下 Scene , Animation , Effect 等即可。

以下内容供希望了解细节的开发者参考。

渲染队列

一个画面帧是由若干渲染步骤构成。例如:

  • 深度预处理 (PreZ)
  • 生成阴影图
  • 水面反射
  • 主画面

引擎依据功能特性的组合,开启或关闭这些渲染步骤。每个渲染步骤都需要按次序处理全部场景对象的一个子集。所以我们把每个渲染步骤称为一个渲染队列。

在 Ant 引擎中,构建队列是通过 Lua 完成的;处于性能考虑,把渲染队列翻译成图形指令提交给显卡的过程完全在 C/C++ 中完成的。为了做到这一点,设计了一个名为 render_object 的 C 组件,在 Lua 系统中填入它的值,在 C/C++ 系统中读取这些值,翻译成图形指令,通过 bgfx api 提交给显卡。

render_object 组件的定义在 /pkg/ant.render/render_system/render_object.ecs 文件中。它由 render 系统的在初始化 stage 为每个场景对象创建出来。

大部分属性值都是初始化就会确定,整个运行过程中都不会改变的。少数属性例外:

  • worldmat 世界矩阵,由 Scene 系统在该场景对象或它的祖先的空间状态发生改变时更新。
  • rm_idx 记录了该场景对象的材质对象的索引,索引对应了上面介绍过的材质对象指针。材质对象的引用在运行时是不可变的,但可以通过 Lua api 修改对应材质对象中的属性。
  • visible_idx 记录了对象的可见性。运行时可以通过 Lua api 修改。
  • cull_idx 记录了空间裁剪器的裁剪结果。裁剪系统也是由 C/C++ 实现的,最终会被 C/C++ 代码直接修改。

运行时材质

场景对象的材质组件,是从 .material 材质文件实例化得来的。实际上,材质组件中包含的其实是一组运行时材质的集合。这是因为,每个对象在不同的渲染队列中有不同的渲染方式。例如:在 PreZ 阶段,是不需要任何和色彩有关的材质属性的;生成阴影图用到的材质和在主游戏画面用到的材质明显不同。

注:Asset 的编译过程,会为每个材质文件生成多个不同的着色器绑定在不同的运行时材质对象上。

在初始化阶段,渲染系统会根据材质组件为 render_object 生成多个运行时材质组成的材质集,放在 rm_idx 索引的 C 内存中。不同的运行时材质对应了不同的渲染队列。材质集记录了它们到底对应哪些渲染队列。而 visible_idxcull_idx 共同决定了,这个 render_object 在当前帧是否暂时不属于该队列。

所有的 world 中所有的 render_object 共同组成了全部渲染队列的数据集。

渲染队列 (renderqueue) 在 ECS world 中,是以一个 entity 的形态存在的。它的定义在 /pkg/ant.render/renderqueue.ecs 中。如果想扩展其它渲染管线,需要构造 renderqueue 来实现新的渲染步骤。

它需要定义 queue_name 组件给这个队列命名;定义 render_target 组件记录渲染结果放在哪个渲染目标中(这是图形指令的概念),以及 bgfx 的 viewid ;camera_ref 组件记录了关联的摄像机 id ;还需要定义一个 C 组件 render_args ,用来组装它。

组装渲染队列

引擎支持的渲染队列总数是有限的,默认为 64 个,最多可以扩展到 256 。在默认设置下,我们使用一个 64bit 整数 mask 来表示渲染队列的开关集合。每个材质集中把不同运行时材质也分为了最多 256 类。渲染队列和运行时材质类型是多对一的关系。它们在 render_args 中建立起映射关系。同时在 render_args 中还记录了这个渲染队列将提交到 bgfx 的哪个 viewid 上。

渲染队列在渲染系统的初始化阶段组装完毕。

render_args 是 C 组件,所以可以在 C/C++ 中访问它。ecs 按持续管理这些组件,所以这些渲染队列在 C/C++ 系统中看到的次序是固定的。每个后处理流程也是一个渲染队列,放在一同管理。

最终,在 C/C++ 系统 (:system.render) 中实现了 pipeline 中的 render_submit stage 。它是一个 C 函数,它依次处理所有的渲染队列 render_args 。处理每个队列时,都从 render_object 组件中筛选出对应队列所属的对象,翻译成图形指令,调用 bgfx api 提交给显卡。

整个提交过程,不涉及任何 Lua 代码,也不需要任何 Lua 虚拟机中的数据结构。

Clone this wiki locally