自定义渲染管线

Cocos Creator 3.8 中正式开放了新的 自定义渲染管线

自定义渲染管线 的接口位于 cocos/core/pipeline/custom/pipeline.ts

介绍

对于游戏引擎来说,渲染是最重要的功能之一。而引擎的渲染效果,由 渲染管线(RenderPipeline)决定,取决于不同的艺术风格、运行平台、硬件设备、渲染技术等等。

如何在引擎中实现各种各样的画面表现,是一个非常复杂的问题。

这需要引擎提供足够的灵活性,让用户可以自由定制渲染管线,以实现各种各样的效果。

Cocos Creator 自定义渲染管线 能够在不同的平台、不同的硬件设备上,编写最优的渲染管线,以达到最佳的画面表现。

也能够在不同的平台、不同的硬件设备上,编写最通用的渲染管线,以达到最佳的性能表现以及跨平台性。

启用自定义管线

勾选 自定义渲染管线

通过填写 自定义管线 的名字,选择注册好的 自定义渲染管线

  • 目前支持 前向渲染管线(名字为 Custom 或 Forward)和 后向渲染管线(名字为 Deferred)两种。

编写自定义渲染管线

新建 Typescript 文件,编写一个类,例如 MyPipeline,让该类实现 rendering.PipelineBuilder 接口,通过 rendering.setCustomPipeline 方法把该 pipeline 注册到系统中。

概念

自定义渲染管线以数据流(Dataflow)的形式概括了渲染的整个流程,用渲染图(RenderGraph)描述。

渲染图由不同的渲染节点组成,比如渲染通道(RenderPass),计算通道(ComputePass)等。每个渲染通道有数据输入与输出,比如渲染目标(RenderTarget)、深度模板(DepthStencil)、贴图(Texture)等。这些输入与输出会在计算节点间构成链接关系,形成数据流。

渲染通道计算通道节点下,可以有渲染队列(RenderQueue)子节点,用于控制渲染的顺序。渲染队列节点下,可以有渲染内容(RenderContent)子节点,用于控制渲染的内容。

渲染内容 可以是 场景、屏幕 矩形,也可以是计算任务的 分发(Dispatch),不同的通道支持不同的渲染内容。

自定义渲染管线 的【渲染通道渲染队列渲染内容】构成一个森林:

自定义渲染管线的【渲染通道渲染资源】构成一个有向无圈图(DAG):

我们可以层叠(Stack)以上两张图,得到 渲染流程图(RenderGraph)。渲染流程图 描述了 自定义渲染管线 的全部流程,引擎会按照用户定制的流程图进行资源分配、流程优化、渲染执行。

渲染管线类型

引擎的渲染管线根据硬件能力,分为了两种

  • 基础渲染管线
  • 标准渲染管线

基础渲染管线(BasicPipeline)

基础渲染管线提供跨平台的基础渲染功能,适用一切平台。

接口主要分为两类

  • 注册资源。在管线中注册的资源,可以在渲染通道中使用,资源读写状态由管线托管。

      interface BasicPipeline {
          addRenderWindow (name: string, format: Format, width: number, height: number,
              renderWindow: RenderWindow): number;
          addRenderTarget (name: string, format: Format, width: number, height: number,
              residency?: ResourceResidency): number;
          addDepthStencil (name: string, format: Format, width: number, height: number,
              residency?: ResourceResidency): number;
      }
    
  • 添加渲染通道

      interface BasicPipeline {
          addRenderPass (width: number, height: number, passName?: string): BasicRenderPassBuilder;
      }
    

    渲染通道 代表了一次光栅化过程,将物件渲染至渲染目标以及深度模板缓冲

可以通过BasicRenderPassBuilder构建。

interface BasicRenderPassBuilder extends Setter {
    addRenderTarget (name: string, loadOp?: LoadOp, storeOp?: StoreOp, color?: Color): void;
    addDepthStencil (name: string, loadOp?: LoadOp, storeOp?: StoreOp,
        depth?: number, stencil?: number, clearFlags?: ClearFlagBit): void;
    addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void;
    addQueue (hint?: QueueHint, phaseName?: string): RenderQueueBuilder;
}

标准渲染管线(Pipeline)

标准渲染具备更丰富的管线功能,目前支持GLES3、Vulkan、Metal三个后端。

标准渲染管线在基础渲染管线的基础上,提供了计算通道(ComputePass),渲染子通道(RenderSubpass)等功能。 并且可以使用 StorageBufferStorageTexture 等资源。

export interface Pipeline extends BasicPipeline {
    addStorageBuffer (name: string, format: Format, size: number,
        residency?: ResourceResidency): number;
    addStorageTexture (name: string, format: Format, width: number, height: number,
        residency?: ResourceResidency): number;
    addRenderPass (width: number, height: number, passName: string): RenderPassBuilder;
    addComputePass (passName: string): ComputePassBuilder;
    addUploadPass (uploadPairs: UploadPair[]): void;
    addMovePass (movePairs: MovePair[]): void;
}
export interface RenderPassBuilder extends BasicRenderPassBuilder {
    addStorageBuffer (name: string, accessType: AccessType, slotName: string): void;
    addStorageImage (name: string, accessType: AccessType, slotName: string): void;
    addRenderSubpass (subpassName: string): RenderSubpassBuilder;
}
export interface ComputePassBuilder extends Setter {
    addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void;
    addStorageBuffer (name: string, accessType: AccessType, slotName: string): void;
    addStorageImage (name: string, accessType: AccessType, slotName: string): void;
    addQueue (phaseName?: string): ComputeQueueBuilder;
}

根据不同的平台,用户可以针对性构建不同的渲染管线。

比如在移动平台上,用户可以通过渲染子通道(RenderSubpass)利用芯片上的高速缓存,减少内存读写来降低发热。

在桌面平台上,用户则可以使用计算通道(ComputePass)编写复杂的图形算法。充分利用平台特性。

渲染子通道 RenderSubpass (实验性质)

渲染子通道表示渲染的一个阶段,该阶段读取和写入渲染通道中的一部分附件(Attachment)。

渲染命令(Render commands)被记录到渲染通道实例的特定子通道中。

export interface RenderSubpassBuilder extends Setter {
    addRenderTarget (
        name: string,
        accessType: AccessType,
        slotName?: string,
        loadOp?: LoadOp,
        storeOp?: StoreOp,
        color?: Color): void;
    addDepthStencil (
        name: string,
        accessType: AccessType,
        depthSlotName?: string,
        stencilSlotName?: string,
        loadOp?: LoadOp,
        storeOp?: StoreOp,
        depth?: number,
        stencil?: number,
        clearFlags?: ClearFlagBit): void;
    addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void;
    addStorageBuffer (name: string, accessType: AccessType, slotName: string): void;
    addStorageImage (name: string, accessType: AccessType, slotName: string): void;
    addQueue (hint?: QueueHint, phaseName?: string): RenderQueueBuilder;
}

渲染子通道支持输入附件(Input Attachment),可以通过 slotNamedepthSlotNamestencilSlotName 指定,这个名字需要与 effect 中的注册的输入附件名字一致。

// .effect
#pragma subpass
#pragma subpassColor in albedoMap
#pragma subpassColor in normalMap
#pragma subpassColor in emissiveMap
#pragma subpassDepth in depthBuffer
#pragma isubpassStencil in stencilBuffer // isubpass 的 i 表示输入附件类型为 int

void main () {
    vec4 albedo = subpassLoad(albedoMap);
    vec4 normal = subpassLoad(normalMap);
    vec4 emissive = subpassLoad(emissiveMap);
    float depth = subpassLoad(depthBuffer).x;
    int stencil = subpassLoad(stencilBuffer).x;
    ...
}

计算通道(ComputePass)

计算通道是对一次计算任务的抽象,可以调用Compute Shader,执行计算任务。

export interface ComputePassBuilder extends Setter {
    addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void;
    addStorageBuffer (name: string, accessType: AccessType, slotName: string): void;
    addStorageImage (name: string, accessType: AccessType, slotName: string): void;
    addQueue (phaseName?: string): ComputeQueueBuilder;
}

export interface ComputeQueueBuilder extends Setter {
    addDispatch (
        threadGroupCountX: number,
        threadGroupCountY: number,
        threadGroupCountZ: number,
        material?: Material,
        passID?: number): void;
}

更多内容见计算着色器

渲染队列(RenderQueue)

渲染队列渲染通道/子通道 的子节点,有严格的渲染先后顺序。只有一个 渲染队列 的内容完全绘制后,才会绘制下一个 渲染队列 的内容。

可以通过 RenderQueueBuilder 添加绘制内容,渲染队列 内对象的渲染顺序是未定义的,可能是任何顺序。

export interface RenderQueueBuilder extends Setter {
    addScene (camera: Camera, sceneFlags: SceneFlags): void;
    addFullscreenQuad (material: Material, passID: number, sceneFlags?: SceneFlags): void;
}

可以通过 SceneFlags 标记场景渲染特性,比如渲染不透明物件、渲染投影物件等。

export enum SceneFlags {
    NONE = 0,
    OPAQUE = 0x1,
    MASK = 0x2,
    BLEND = 0x4,
    SHADOW_CASTER = 0x8,
    UI = 0x10,
}

渲染数据设置

我们可以通过 Setter 设置 Shader 里用到的数据和只读资源,名字是 Shader 里的变量名。

注意这里的 Shader 是管线相关的,而不是普通表面材质的 Shader。

export interface Setter extends RenderNode {
    setMat4 (name: string, mat: Mat4): void;
    setQuaternion (name: string, quat: Quat): void;
    setColor (name: string, color: Color): void;
    setVec4 (name: string, vec: Vec4): void;
    setVec2 (name: string, vec: Vec2): void;
    setFloat (name: string, v: number): void;
    setArrayBuffer (name: string, arrayBuffer: ArrayBuffer): void;
    setBuffer (name: string, buffer: Buffer): void;
    setTexture (name: string, texture: Texture): void;
    setSampler (name: string, sampler: Sampler): void;
}

这里用到的数据和资源是管线相关的。材质相关的,需要设置到材质上,不应重复设置。

资源必须是只读的。如果需要读写数据,需要注册到管线中,由管线进行管理。

数据更新频率

Effect中,不同的变量有不同的更新频率。由低到高大致分为:

  • pass
  • phase
  • batch
  • instance

effect中需要在变量声明前加上#pragma rate指定更新频率。

  • batch为缺省值
  • instance暂不支持自定义

例子:

// copy-pass.effect

precision highp float;
in vec2 v_uv;

#pragma rate outputResultMap pass
uniform sampler2D outputResultMap;

layout(location = 0) out vec4 fragColor;

void main () {
    fragColor = texture(outputResultMap, v_uv);
}

RenderGraph中的每个节点描述符集的更新频率,由节点的类型决定。

节点类型 更新频率
渲染通道 pass
渲染子通道 pass
计算通道 pass
渲染队列 phase
计算队列 phase

条与 "" 相匹配的结果

    没有与 "" 匹配的结果