深入理解CocosCreator 3D渲染管线

深入理解CocosCreator3D渲染管线

CocosCreator3D 刚刚发布了 3.0 Preview 版,首次将2D和3D版本合并到了一起,经过多个版本迭代,渲染架构大幅升级与优化,非常值得深入学习和研究,以下是官网的 性能与框架介绍

  • 多渲染后端框架,已支持 WebGL 1.0 和 WebGL 2.0
  • 面向未来的底层渲染 API 设计
  • 基于 Command Buffer 提交渲染数据

目前渲染相关文档并不完善,本文将从源码入手分析C3D多渲染后端框架GFX,引擎默认的前向渲染管线实现,以及如何实现自定义渲染管线

目录

多渲染后端框架GFX

GFX是针对渲染层做的高级抽象和封装,以达到编写一次渲染代码,适配不同渲染后端的目的。
目前源码中可见引擎已经适配了以下几种渲染后端:

  • WebGL
  • WebGL2
  • OpenGL ES2
  • OpenGL ES3
  • Metal
  • Vulkan

GFX可以理解为实现C3D引擎渲染的最基础接口,实现自定义渲染只能通过GFX提供的接口和规则来编写,以往在Cocos引擎中直接编写GL代码的方式已经成为过去。

GFX接口设计更贴近Vulkan等下一代渲染接口,为了说明GFX如何抽象渲染层,我们通过 WebGL 渲染做一个对比,然后再用GFX接口来实现同样的功能。

WebGL渲染过程

下面的示例代码展示了一次简单的 WebGL 渲染,目的是显示一张2D纹理:

function prepare(){
  // texture
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(/**...*/);
  gl.texImage2D(/** fill image data */);

  // shader
  var shader = someCreateShaderFunc("vert...", "frag...");
  gl.useProgram(shader);

  // vertex buffer
  var vb = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vb);
  gl.bufferData(/** fill vert data */;
  // bind attributes
  var attr = gl.getAttribLocation(shader, 'a_position');
  gl.enableVertexAttribArray(attr);
  gl.vertexAttribPointer(attr, /**...*/);
  var attr = gl.getAttribLocation(shader, 'a_texCoord');
  gl.enableVertexAttribArray(attr);
  gl.vertexAttribPointer(attr, /**...*/);

  // indices buffer
  var ib = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib);
  gl.bufferData(/** fill indices data */;
}

prepare();
render();

function render(){
  // begin draw
  //gl.bindFramebuffer(gl.FRAMEBUFFER, /**...*/);

  gl.clearColor(/**...*/);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.viewport(/**...*/);

  /** set states: depth test, stencil test, blend ... */
  gl.useProgram(shader);
  // set uniforms
  gl.uniform(/**...*/);

  // draw
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

  // end draw
  //gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  
  requestAnimationFrame(render);
}

这里可以把渲染过程简单分为2个阶段,数据准备渲染执行渲染执行又可分为数据绑定绘制调用。实际游戏中渲染执行中会反复执行多个 DrawCall,直到完成一帧中的所有绘制。

  • 数据准备:创建和提交数据到GPU
    • 创建纹理
    • 创建并编译Shader,也就是WebGLProgram
    • 准备顶点数据,绑定attributes layout
    • 准备索引数据
  • 渲染执行
    • 准备画布,清除颜色/深度缓冲,设置viewport等
      • 数据绑定:(Set Pass Call)
        • 绑定纹理
        • 设置uniform参数(Shader中的固定变量)
      • 绘制调用:(Draw Call)
        • 调用绘制函数
    • 渲染结束

数据准备:可以理解为模型数据(顶点和纹理等)的上传
渲染执行:每一帧都会调用,刷新游戏画面
数据绑定:一般抽象为材质数据,切换数据绑定则相当于切换不同的材质

GFX中的对象

Device:抽象GFX渲染设备,提供与设备交互的渲染接口,具体实例化为WebGLDeviceVKDevice等。
此外定义了15种渲染对象类型:

export enum ObjectType {
    UNKNOWN,
    BUFFER,
    TEXTURE,
    RENDER_PASS,
    FRAMEBUFFER,
    SAMPLER,
    SHADER,
    DESCRIPTOR_SET_LAYOUT,
    PIPELINE_LAYOUT,
    PIPELINE_STATE,
    DESCRIPTOR_SET,
    INPUT_ASSEMBLER,
    COMMAND_BUFFER,
    FENCE,
    QUEUE,
    WINDOW,
}

其中Fence(同步信号)和Window在WebGL实现中并没有用到。TextureSamplerShaderFrameBuffer比较好理解,跟GL对象差异不大,其他9种对象类型分别是

  • Buffer:抽象VB,IB,UB等各种数据缓冲。
  • InputAssembler:集中管理VB,IB,Attributes,IndirectBuffer等各种输入。
  • DescriptorSet:集中管理UB,Texture,Sampler等。
  • DescriptorSetLayout:描述DescriptorSet绑定的布局。(相当于告诉GPU如何读取DescriptorSet数据)
  • CommandBuffer:将每一个渲染动作抽象为命令提交到队列,submit时统一执行,支持异步渲染。
    (为Vulkan等下一代渲染接口准备,WebGL并未实现)
  • Queue:CommandBuffer队列?(WebgGL实现只有一个空的Queue,CommandBuffer只用了一个默认的)
  • RenderPass:存放颜色缓冲区和深度缓冲区,也就是画布。
  • PipelineLayout DescriptorSetLayout和其他扩展信息,如WebGL2实现中有WebGL2PipelineLayout信息。
  • PipelineState:主要由以下状态动态组建
    • PipelineLayout
    • Shader
    • RenderPass
    • RasterizerState:CullMode,PolygonMode等
    • DepthStencilState:DepthTest,StencilTest等
    • BlendTarget:Blend设置
    • BlendState:BlendTarget集合等
    • InputState:Attributes

DescriptorSet参考Vulkan中的概念:

GFX渲染过程

基于上述定义的基础概念,如果使用GFX实现同样的功能,代码如下:

function prepare() {
  // vertex buffer
  const vertexBuffers = device.createBuffer(/** buffer info */);
  vertexBuffers.update(/** fill vert data */);
  // indices buffer
  const indicesBuffers = device.createBuffer(/** buffer info */);
  indicesBuffers.update(/** fill indices data */);
  // bind attributes
  const attributes: Attribute[] = [
    new Attribute('a_position', Format.RG32F),
    new Attribute('a_texCoord', Format.RG32F),
  ];
  const IAInfo = new InputAssemblerInfo(attributes, [vertexBuffers], indicesBuffers);
  const assmebler = device.createInputAssembler(IAInfo);

  // material, texture(sampler), shader, pass
  const material = new Material();
  material.initialize({ effectName: 'some shader name' });
  sampler = device.createSampler(/**samplerInfo*/);
  texture = device.createTexture(/** ...*/);

  const pass = material.passes[0];
  const binding = pass.getBinding('mainTexture');
  pass.bindTexture(bingding, texture);

  shader = ShaderPool.get(pass.getShaderVariant());
  const descriptorSet = DSPool.get(PassPool.get(pass.handle, PassView.DESCRIPTOR_SET));
  descriptorSet.bindSampler(binding, sampler);
  descriptorSet.update();
}
prepare();
render();
function render() {
  device.acquire();

  const cmdBuff = device.cmdBuff;
  const framebuffer = root.framebuffer;
  const renderArea = new Rect(0, 0, device.width, device.height);

  cmdBuff.begin();
  // bind framebuffer, clear, set states ...
  cmdBuff.beginRenderPass(framebuffer.renderPass, framebuffer, renderArea/** */);

  // bind PipelineState
  const pass = material.passes[0];
  const pso = PipelineStateManager.getOrCreatePipelineState(device, pass, shader, framebuffer.renderPass, assmebler);
  cmdBuff.bindPipelineState(pso);
  cmdBuff.bindDescriptorSet(SetIndex.MATERIAL, pass.descriptorSet);
  cmdBuff.bindInputAssembler(assmebler);
  // draw
  cmdBuff.draw(assmebler);

  cmdBuff.endRenderPass();

  cmdBuff.end();
  device.queue.submit([cmdBuff]);
  device.present();

  requestAnimationFrame(render);
}

整理一下GFX渲染流程:

  • 数据准备:创建和提交数据到GPU
    • 创建Material,初始化Effect
    • 创建Texture和Sampler,并绑定Texture到Pass
    • 创建InputAssembler
    • 创建GFX Shader
    • 根据Pass,从对象池获取DescriptorSet, 绑定并更新
  • 提交渲染指令:准备画布beginRenderPass
    • 数据绑定:(Set Pass Call)
      • 获取PSO对象,绑定PSO
      • 绑定DescriptorSet
      • 绑定IA
    • 绘制调用:(Draw Call)
      • 调用绘制函数draw(WebGL直接绘制)
  • 执行渲染队列
    • 提交并执行CommandBuffer

对比WebGL可以发现,渲染流程几乎一模一样,这有利于我们快速学习,但是细节上却有很大的区别,这也是号称面向未来的渲染API设计的原因,这里在GFX之上又封装了一些概念:

  • Effect:C3D独有语法的Shader原始文件,类似Unity的ShaderLab。
  • Pass:包含BlendState,RasterizerState等所有信息,全部按位存于handle里面,非常的高效。
  • Material:对应一个Effect,可以有多个Pass。
  • Shader(GFX Shader):结合pass指定编译宏组合动态创建,非常灵活。

显然GFX抽象的接口使用起来更加方便和灵活,OpenGL状态机只提供最细粒度的状态设置接口,如果渲染状态切换,OpenGL需要设置一大堆标志位,现在可以直接切换Pipeline,并且C3D使用了非常多的对象池来优化性能。

C3D渲染管线

C3D渲染管线基于GFX接口,再次做了一层封装,方便应用层灵活使用,大致的渲染流程如下

其中Camera数量可以有多个,Canvas(内含正交OrthCamera)也可以有多个。Flow和Stage都可以自定义和自由组合,Stage负责执行具体渲染指令。

C3D渲染架构UML

渲染相关类定义非常多,而且关系错综复杂,很多相互引用,这里列举一下几个关键类的含义:

  • Root:可以理解为渲染大总管,集中管理所有渲染相关的对象,包含RenderPipeline,RenderWindow,RenderScene,Cameras,UI
  • RenderPipeline:渲染管线,定义一组RenderFlow队列
  • RenderWindow:渲染窗口,可以是屏幕缓冲也可以是离屏缓冲,可能有多个
  • RenderView:渲染视图,Camera对象的渲染层表示
  • RenderScene:整个Scene场景对应的渲染层对象
  • UI:Scene场景中所有Canvas对应的渲染对象,统一由UI管理,C3D单独为UI创建了一个RenderScene用于存放UI渲染模型UIBatchedModel,所以Root中一共有2个RenderScene
  • RenderFlow:定义一组渲染Stage
  • RenderStage:渲染具体的实现,如ForwardStage,UIStage

C3D前向渲染管线

前向渲染管线是C3D提供的默认渲染管线,实现了3个类型的Flow:

  • ShadowFlow:渲染阴影。
  • ForwardFlow:对应一个3D Camera,可能有多个。
  • UIFlow:对应一个ui_Canvas,可能有多个。

另外橙色步骤比较关键,主要负责性能优化:

  • GenBatchedModel:UI动态合批。
  • SceneCulling:裁剪渲染对象。
  • FillQueue:根据裁剪后的对象,填充Instanced队列,Batched队列 ,不透明队列,透明队列。

C3D自定义染管线

C3D支持自定义渲染管线,我们尝试在ForwPipeline中新建一个后处理Flow,右键依次新建Forward Pipeline Asset,PostRenderFlow和PostRenderStage。

点击刚才创建的Pipeline资源,打开Inspector,设置对应的Flow和Stage,将PostRenderFlow插入ForwardFlow和UIFlow之间。

然后重载PostRenderFlow的activate方法,实现Flow的初始化

public activate(pipeline: ForwardPipeline) {
    super.activate(pipeline);
    // create framebuffer
}

重载PostRenderStage的render方法,实现自定义渲染逻辑

render(view: RenderView) {
  const pipeline = this._pipeline as ForwardPipeline;
  const cmdBuff = pipeline.commandBuffers[0];
  const device = pipeline.device;
  const renderPass = this.frameBuffer!.renderPass;

  cmdBuff.begin();
  cmdBuff.beginRenderPass();
  // insert custom render code here

  cmdBuff.endRenderPass();
  cmdBuff.end();
  bufs[0] = cmdBuff;
  device.queue.submit(bufs);
}

最后打开Project Setting,切换至我们的Pipeline,然后就可以运行了!

结束

Creator的3D功能正在努力完善,后续将会推出更多高级和实用功能,我们将在第一时间体验。
最后十分感谢一直免费开源的Cocos,提供给我们直面源码的机会,祝福Cocos十周年,下一个十年更精彩!

参考文档

1、CocosCreator3D用户手册
2、Mozilla WebGL API
3、learnopengl.com
4、Vulkan 资源绑定和状态管理

17赞

先mark慢慢看

:wink: 又可以慢慢看了

为什么高手这么多

最近简单看了一下这块的代码
对于renderFlow很疑惑
一个pipeline有多个flow,一个flow有多个stage
不太清楚中间加的这层flow有什么作用
默认的是三种UI、shadow、forward flow里面也都只有对应的一个stage
感觉比较多余

这个设计将来在复杂的渲染流程中很容易自由组合,一个Flow里面可能有多个独立的Stage,这也是为什么称为渲染流水线的原因。

mark mark