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
公众号: