Cocos Creator 3D源码简析

【本文参与征文活动】

Cocos Creator 3D源码简析

为什么要看源码

  • 了解引擎背后的实现。
  • api文档没有说清楚的地方,就可以直接自己看源码一探究竟了。
  • 引擎有bug反馈到论坛可能需要过好几天才能看到pr。自己动手就可以丰衣足食了。
  • 引擎的实现是建立在通用的目标之上的,面对一些极端情况下性能会表现不佳。此时源码在手你就可以做一些定制优化了。

Cocos的核心:Node

Node的api文档:https://docs.cocos.com/creator3d/api/zh/classes/scene_graph.node-1.html


现在我们对这些方法进行归纳梳理
https://app.yinxiang.com/shard/s36/nl/30731409/50e284a4-91c3-4332-a645-4727cb2613cb
!

Node提供的能力大致划分成:父子节点层级关系的管理、组件的管理、坐标变换transform、事件的管理和注册能力、其它。

引擎的主循环

director.ts

渲染流程

场景结构图


渲染代码

我们来看一下camera.update();

可以看到这里主要是更新视图矩阵和投影矩阵
接下来看看 camera.scene!.update(stamp);

第一部分光源的更新,可以看到有平行光、点光、聚光灯,基本就是更新这些光源的世界坐标、朝向、包围盒等信息。
第二部分是模型的更新。model.updateTransform(stamp);中更新了变换矩阵和包围盒。
来详细看一下model.updateUBOs(stamp);

可以看到是在更新模型的世界矩阵,但是这里分了两种情况:情况1是模型开启了GPU instance所做的处理。情况2是没有开启,然后这里的两个矩阵会被传到shader的UBO当中。shader中对应如下定义:

关于什么是UBO可以看这里https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/08%20Advanced%20GLSL/#uniform

接下来终于到了真正的渲染的地方 this._pipeline.render(this._views);
这里就是渲染管线对所有视图进行渲染的地方。其中每一个视图对应一个相机的视野。
下图是原始的renderpipeline的render函数。代码很简洁,渲染管线顺序遍历视图,每个视图里面又顺序遍历渲染流。


forwardpipeline继承renderpipeline,并重写了render函数。可以看到在每一个RenderView渲染执之前都有一个场景剪裁的过程。

好,我们来看看sceneCulling里干了些啥。

export function sceneCulling (pipeline: ForwardPipeline, view: RenderView) {
    const camera = view.camera;
    const scene = camera.scene!;
    const renderObjects = pipeline.renderObjects;
    roPool.freeArray(renderObjects); renderObjects.length = 0;
    const shadowObjects = pipeline.shadowObjects;
    shadowPool.freeArray(shadowObjects); shadowObjects.length = 0;

    const mainLight = scene.mainLight;
    const shadows = pipeline.shadows;
    const shadowSphere = shadows.sphere;
    shadowSphere.center.set(0.0, 0.0, 0.0);
    shadowSphere.radius = 0.01;
    if (mainLight) {
        mainLight.update();
        if (shadows.type === ShadowType.Planar) {
            shadows.updateDirLight(mainLight);
        }
    }

    if (pipeline.skybox.enabled && pipeline.skybox.model && (camera.clearFlag & SKYBOX_FLAG)) {
        renderObjects.push(getRenderObject(pipeline.skybox.model, camera));
    }

    const models = scene.models;

    for (let i = 0; i < models.length; i++) {
        const model = models[i];

        // filter model by view visibility
        if (model.enabled) {
            const vis = view.visibility & Layers.BitMask.UI_2D;
            if (vis) {
                if ((model.node && (view.visibility === model.node.layer)) ||
                    view.visibility === model.visFlags) {
                    renderObjects.push(getRenderObject(model, camera));
                }
            } else {
                if (model.node && ((view.visibility & model.node.layer) === model.node.layer) ||
                    (view.visibility & model.visFlags)) {

                    // shadow render Object
                    if (model.castShadow) {
                        sphere.mergeAABB(shadowSphere, shadowSphere, model.worldBounds!);
                        shadowObjects.push(getCastShadowRenderObject(model, camera));
                    }

                    // frustum culling
                    if (model.worldBounds && !intersect.aabb_frustum(model.worldBounds, camera.frustum)) {
                        continue;
                    }

                    renderObjects.push(getRenderObject(model, camera));
                }
            }
        }
    }

    if (shadows.type === ShadowType.Planar) {
        shadows.updateShadowList(scene, camera.frustum, (camera.visibility & Layers.BitMask.DEFAULT) !== 0);
    }
}

里面其实就是在遍历所有的模型,如果模型的包围盒跟当前相机的视锥体相交,就会被丢到renderObjects这个数组里。另外如果模型进行了投影会被丢到shadowObjects数组中。可以看到这个判定是在视锥剪裁之前进行的。这是因为模型本身可能不在视锥体内,但它的投影却可能遮挡视锥体内的模型。在这里也进行了阴影包围球shadowSphere的更新。不过遗憾的是目前v1.2.0版本投影范围并没有根据这个阴影包围球进行投影,而是跟平行光的位置进行了锁定。具体可见如下代码:


现在我们先来捋一下渲染管线、渲染视图、渲染流程之间的关系。
目前引擎只内置了一个渲染管线forward-pipeline,也就是前向渲染管线。
然后前向渲染管线里面有三个渲染流程。
forward-pipeline

  • ShadowFlow 阴影贴图绘制流程
  • ForwardFlow 前向渲染流程。
  • UIFlow UI渲染流程。

这里可以看到不仅渲染管线里有渲染流程,渲染视图里也有渲染流程??
揭开谜底的时候到了


原来渲染视图是有选择的执行渲染管线中的渲染流程!每一个渲染视图对应一个相机,相机分两类:UI相机、3D场景相机。UI相机所对应的渲染视图只有一个渲染流程UIFlow,3D场景相机有两个渲染流程ShadowFlow、ForwardFlow。如下图:

接下来看看渲染流程RenderFlow里干了啥
RenderFlow中的render函数

UIFlow中重写后的render函数

ForwardFlow中重写后的render函数

好吧,可以看到这些flow里面又分了一些阶段。这里应该是为了灵活性和可扩展性。实际上目前为止所有的RenderFlow都只有一个RenderStage:ForwardFlow只有一个ForwardStage、ShadowFlow只有一个ShadowStage、UIFlow只有一个UIStage。

不过在执行flow的这些stage的render之前都进行了渲染管线UBO的更新,代码如下:

private _updateUBO (view: RenderView) {
        this._descriptorSet.update();

        const root = legacyCC.director.root;

        const camera = view.camera;
        const scene = camera.scene!;

        const mainLight = scene.mainLight;
        const ambient = this.ambient;
        const fog = this.fog;
        const fv = this._globalUBO;
        const device = this.device;

        const shadingWidth = Math.floor(device.width);
        const shadingHeight = Math.floor(device.height);

        // update UBOGlobal
        fv[UBOGlobal.TIME_OFFSET] = root.cumulativeTime;
        fv[UBOGlobal.TIME_OFFSET + 1] = root.frameTime;
        fv[UBOGlobal.TIME_OFFSET + 2] = legacyCC.director.getTotalFrames();

        fv[UBOGlobal.SCREEN_SIZE_OFFSET] = device.width;
        fv[UBOGlobal.SCREEN_SIZE_OFFSET + 1] = device.height;
        fv[UBOGlobal.SCREEN_SIZE_OFFSET + 2] = 1.0 / fv[UBOGlobal.SCREEN_SIZE_OFFSET];
        fv[UBOGlobal.SCREEN_SIZE_OFFSET + 3] = 1.0 / fv[UBOGlobal.SCREEN_SIZE_OFFSET + 1];

        fv[UBOGlobal.SCREEN_SCALE_OFFSET] = camera.width / shadingWidth * this.shadingScale;
        fv[UBOGlobal.SCREEN_SCALE_OFFSET + 1] = camera.height / shadingHeight * this.shadingScale;
        fv[UBOGlobal.SCREEN_SCALE_OFFSET + 2] = 1.0 / fv[UBOGlobal.SCREEN_SCALE_OFFSET];
        fv[UBOGlobal.SCREEN_SCALE_OFFSET + 3] = 1.0 / fv[UBOGlobal.SCREEN_SCALE_OFFSET + 1];

        fv[UBOGlobal.NATIVE_SIZE_OFFSET] = shadingWidth;
        fv[UBOGlobal.NATIVE_SIZE_OFFSET + 1] = shadingHeight;
        fv[UBOGlobal.NATIVE_SIZE_OFFSET + 2] = 1.0 / fv[UBOGlobal.NATIVE_SIZE_OFFSET];
        fv[UBOGlobal.NATIVE_SIZE_OFFSET + 3] = 1.0 / fv[UBOGlobal.NATIVE_SIZE_OFFSET + 1];

        Mat4.toArray(fv, camera.matView, UBOGlobal.MAT_VIEW_OFFSET);
        Mat4.toArray(fv, camera.node.worldMatrix, UBOGlobal.MAT_VIEW_INV_OFFSET);
        Mat4.toArray(fv, camera.matProj, UBOGlobal.MAT_PROJ_OFFSET);
        Mat4.toArray(fv, camera.matProjInv, UBOGlobal.MAT_PROJ_INV_OFFSET);
        Mat4.toArray(fv, camera.matViewProj, UBOGlobal.MAT_VIEW_PROJ_OFFSET);
        Mat4.toArray(fv, camera.matViewProjInv, UBOGlobal.MAT_VIEW_PROJ_INV_OFFSET);
        Vec3.toArray(fv, camera.position, UBOGlobal.CAMERA_POS_OFFSET);
        let projectionSignY = device.screenSpaceSignY;
        if (view.window.hasOffScreenAttachments) {
            projectionSignY *= device.UVSpaceSignY; // need flipping if drawing on render targets
        }
        fv[UBOGlobal.CAMERA_POS_OFFSET + 3] = projectionSignY;

        const exposure = camera.exposure;
        fv[UBOGlobal.EXPOSURE_OFFSET] = exposure;
        fv[UBOGlobal.EXPOSURE_OFFSET + 1] = 1.0 / exposure;
        fv[UBOGlobal.EXPOSURE_OFFSET + 2] = this._isHDR ? 1.0 : 0.0;
        fv[UBOGlobal.EXPOSURE_OFFSET + 3] = this._fpScale / exposure;

        if (mainLight) {
            Vec3.toArray(fv, mainLight.direction, UBOGlobal.MAIN_LIT_DIR_OFFSET);
            Vec3.toArray(fv, mainLight.color, UBOGlobal.MAIN_LIT_COLOR_OFFSET);
            if (mainLight.useColorTemperature) {
                const colorTempRGB = mainLight.colorTemperatureRGB;
                fv[UBOGlobal.MAIN_LIT_COLOR_OFFSET] *= colorTempRGB.x;
                fv[UBOGlobal.MAIN_LIT_COLOR_OFFSET + 1] *= colorTempRGB.y;
                fv[UBOGlobal.MAIN_LIT_COLOR_OFFSET + 2] *= colorTempRGB.z;
            }

            if (this._isHDR) {
                fv[UBOGlobal.MAIN_LIT_COLOR_OFFSET + 3] = mainLight.illuminance * this._fpScale;
            } else {
                fv[UBOGlobal.MAIN_LIT_COLOR_OFFSET + 3] = mainLight.illuminance * exposure;
            }
        } else {
            Vec3.toArray(fv, Vec3.UNIT_Z, UBOGlobal.MAIN_LIT_DIR_OFFSET);
            Vec4.toArray(fv, Vec4.ZERO, UBOGlobal.MAIN_LIT_COLOR_OFFSET);
        }

        const skyColor = ambient.colorArray;
        if (this._isHDR) {
            skyColor[3] = ambient.skyIllum * this._fpScale;
        } else {
            skyColor[3] = ambient.skyIllum * exposure;
        }
        fv.set(skyColor, UBOGlobal.AMBIENT_SKY_OFFSET);
        fv.set(ambient.albedoArray, UBOGlobal.AMBIENT_GROUND_OFFSET);

        if (fog.enabled) {
            fv.set(fog.colorArray, UBOGlobal.GLOBAL_FOG_COLOR_OFFSET);

            fv[UBOGlobal.GLOBAL_FOG_BASE_OFFSET] = fog.fogStart;
            fv[UBOGlobal.GLOBAL_FOG_BASE_OFFSET + 1] = fog.fogEnd;
            fv[UBOGlobal.GLOBAL_FOG_BASE_OFFSET + 2] = fog.fogDensity;

            fv[UBOGlobal.GLOBAL_FOG_ADD_OFFSET] = fog.fogTop;
            fv[UBOGlobal.GLOBAL_FOG_ADD_OFFSET + 1] = fog.fogRange;
            fv[UBOGlobal.GLOBAL_FOG_ADD_OFFSET + 2] = fog.fogAtten;
        }
    

里面更新了两个UBO(Uniform Buffer Object):阴影的UBO、全局UBO。对应shader中的定义如下图:



我们继续往下走,看看RenderFlow中的RenderStage是个什么样。
ForwardStage 前向渲染阶段

可以看到几个渲染队列:

  • _batchedQueue 动态合批队列
  • _instancedQueue 开启了GPU Instance的队列
  • _additiveLightQueue 光照相关的队列?
  • 不透明物体渲染队列,队列的排序是FRONT_TO_BACK。透明物体渲染队列,排序方式是BACK_TO_FRONT。排序函数的实现如下:

    其中depth的计算是根据物体中心点在相机前方向投影的距离,代码如下:

    接着看ForwardStage的render部分的实现,代码以及注释如下图:


    接下来看一看RenderQueue的recordCommandBuffer做了些什么

    加一下注释
public recordCommandBuffer (device: GFXDevice, renderPass: GFXRenderPass, cmdBuff: GFXCommandBuffer) {
  for (let i = 0; i < this.queue.length; ++i) {
            const { subModel, passIdx } = this.queue.array[i];
            const { inputAssembler, handle: hSubModel } = subModel;
            const hPass = SubModelPool.get(hSubModel, SubModelView.PASS_0 + passIdx) as PassHandle;
            const shader = ShaderPool.get(SubModelPool.get(hSubModel, SubModelView.SHADER_0 + passIdx) as ShaderHandle);
            const pso = PipelineStateManager.getOrCreatePipelineState(device, hPass, shader, renderPass, inputAssembler)
            
            // 渲染管线状态的设置,例如:混合模式、模板剪裁、深度测试、正反面的剔除等。
            cmdBuff.bindPipelineState(pso);
            
            // uniform的更新
            cmdBuff.bindDescriptorSet(SetIndex.MATERIAL, DSPool.get(PassPool.get(hPass, PassView.DESCRIPTOR_SET)));
            cmdBuff.bindDescriptorSet(SetIndex.LOCAL, DSPool.get(SubModelPool.get(hSubModel, SubModelView.DESCRIPTOR_SET)));
            
            // 顶点数据装配器的绑定
            cmdBuff.bindInputAssembler(inputAssembler);
            
            // 绘制命令真正执行的地方
            cmdBuff.draw(inputAssembler);
        }
    }

接来下看一看cmdBuff.draw(inputAssembler);


进入WebGL2CmdFuncDraw

在这里我们才真正看到WebGL命令的调用。
其它命令的执行看这里:

UI渲染流程

可以看到在构造函数里监听了Director.EVENT_BEFORE_DRAW


可以看到事件是在渲染管线开始渲染之前发出的。接来下看看update里的实现


可以看到_renderScreens()其实就是遍历所有的画布,再递归遍历画布所有子节点并调用子节点的渲染组件的updateAssembler。
接着看render.updateAssembler(this);这里以sprite为render继续解析后续的流程。

autoMergeBatches

最后剩下的this.render();

可以看到:UI里的每一个批次都会被构造成一个只会被UI相机看到的模型Model放到场景中。而UI的自动合批无非就是将相同渲染条件的顶点数据集中放到一个buff中,注意这里填充的顶点坐标需要是世界坐标系下。


更多文章
个人博客: https://blog.csdn.net/u014560101
公众号:

38赞

第三篇了,:是不是可以 :trophy::trophy::trophy: :sunglasses:

1赞

写这么长点个赞吧:+1:

更新一下

1赞

插眼,学习学习

插眼,学习学习

可以可以可以

下一篇文章想写一下Cocos的GFX

学习学习。。

写的不错,期待下一篇

这是真正的好帖子

MARK!

mark!

1赞

mark!

赞一个
太喜欢这种解析了
给楼主打call

赞一个,战略性插眼

更深入的看这里

官方API真不咋样。
感觉还不如查他的文档和论坛……
或者直接查源码……

这个creator 3.0和3D数据结构变了很多,要不也写一个实战的教程

看了一下差别还好,核心结构变化不大