Cocos Creator 3.0源码漫游
激动人心的3.0预览版出来了,不仅大幅增强了2D的功能支持,在渲染方面也支持了Metal和Vulkan。还等什么赶紧一睹源码为快吧。
在开启本次的迷思破除之旅前,先设定一下目标
- 场景是如何切换的?
- node的destroy到底干了些啥?
- GPU骨骼动画的蒙皮是在哪里实现的?
- 没有使用点光或者聚光灯是否会有额外的性能损耗?多pass是个啥?
- 阴影咋实现的?
节点的destroy
先看一下destroy涉及到的几个函数。
cocos/core/scene-graph/base-node.ts
public destroy () {
if (super.destroy()) {
this.active = false;
return true;
}
return false;
}
cocos/core/data/object.ts
/**
* @en
* Destroy this Object, and release all its own references to other objects.<br/>
* Actual object destruction will delayed until before rendering.
* From the next frame, this object is not usable any more.
* You can use `isValid(obj)` to check whether the object is destroyed before accessing it.
* @zh
* 销毁该对象,并释放所有它对其它对象的引用。<br/>
* 实际销毁操作会延迟到当前帧渲染前执行。从下一帧开始,该对象将不再可用。
* 您可以在访问对象之前使用 `isValid(obj)` 来检查对象是否已被销毁。
* @return whether it is the first time the destroy being called
* @example
* ```
* obj.destroy();
* ```
*/
public destroy (): boolean {
if (this._objFlags & Destroyed) {
warnID(5000);
return false;
}
if (this._objFlags & ToDestroy) {
return false;
}
this._objFlags |= ToDestroy;
objectsToDestroy.push(this);
if (EDITOR && deferredDestroyTimer === null && legacyCC.engine && ! legacyCC.engine._isUpdating) {
// auto destroy immediate in edit mode
// @ts-expect-error
deferredDestroyTimer = setImmediate(CCObject._deferredDestroy);
}
return true;
}
cocos/core/data/object.ts
public static _deferredDestroy () {
const deleteCount = objectsToDestroy.length;
for (let i = 0; i < deleteCount; ++i) {
const obj = objectsToDestroy[i];
if (!(obj._objFlags & Destroyed)) {
obj._destroyImmediate();
}
}
// if we called b.destory() in a.onDestroy(), objectsToDestroy will be resized,
// but we only destroy the objects which called destory in this frame.
if (deleteCount === objectsToDestroy.length) {
objectsToDestroy.length = 0;
}
else {
objectsToDestroy.splice(0, deleteCount);
}
if (EDITOR) {
deferredDestroyTimer = null;
}
}
cocos/core/director.ts
/**
* @en Run main loop of director
* @zh 运行主循环
*/
public mainLoop (time: number) {
// ...
// 省略了部分代码
// Update
if (!this._paused) {
this.emit(Director.EVENT_BEFORE_UPDATE);
// Call start for new added components
this._compScheduler.startPhase();
// Update for components
this._compScheduler.updatePhase(dt);
// Update systems
for (let i = 0; i < this._systems.length; ++i) {
this._systems[i].update(dt);
}
// Late update for components
this._compScheduler.lateUpdatePhase(dt);
// User can use this event to do things after update
this.emit(Director.EVENT_AFTER_UPDATE);
// Destroy entities that have been removed recently
CCObject._deferredDestroy();
// Post update systems
for (let i = 0; i < this._systems.length; ++i) {
this._systems[i].postUpdate(dt);
}
}
// ...
// 省略了部分代码
}
概括一下destroy的几个步骤:
- 将节点丢到待销毁队列
- active置为false
- 在当前帧渲染前执行真正的销毁操作
接下来看一下真正的销毁函数做了些什么。
cocos/core/data/object.ts
public _destroyImmediate () {
if (this._objFlags & Destroyed) {
errorID(5000);
return;
}
// engine internal callback
// @ts-expect-error
if (this._onPreDestroy) {
// @ts-expect-error
this._onPreDestroy();
}
if (!EDITOR || legacyCC.GAME_VIEW) {
this._destruct();
}
this._objFlags |= Destroyed;
}
可以看到真正的销毁函数执行了两个函数:
- this._onPreDestroy()
- this._destruct()
接着看_onPreDestroy
cocos/core/scene-graph/base-node.ts
protected _onPreDestroy () {
this._onPreDestroyBase();
}
protected _onPreDestroyBase () {
// marked as destroying
this._objFlags |= Destroying;
// detach self and children from editor
const parent = this._parent;
const destroyByParent: boolean = (!!parent) && ((parent._objFlags & Destroying) !== 0);
if (!destroyByParent && EDITOR) {
this._registerIfAttached!(false);
}
// remove from persist
if (this._persistNode) {
legacyCC.game.removePersistRootNode(this);
}
if (!destroyByParent) {
// remove from parent
if (parent) {
this.emit(SystemEventType.PARENT_CHANGED, this);
// During destroy process, siblingIndex is not relyable
const childIndex = parent._children.indexOf(this);
parent._children.splice(childIndex, 1);
this._siblingIndex = 0;
if (parent.emit) {
parent.emit(SystemEventType.CHILD_REMOVED, this);
}
}
}
// emit node destroy event (this should before event processor destroy)
this.emit(SystemEventType.NODE_DESTROYED, this);
// Destroy node event processor
this._eventProcessor.destroy();
// destroy children
const children = this._children;
for (let i = 0; i < children.length; ++i) {
// destroy immediate so its _onPreDestroy can be called
children[i]._destroyImmediate();
}
// destroy self components
const comps = this._components;
for (let i = 0; i < comps.length; ++i) {
// destroy immediate so its _onPreDestroy can be called
// TO DO
comps[i]._destroyImmediate();
}
return destroyByParent;
}
可以看到_onPreDestroyBase里执行了:子节点的销毁、组件的销毁、从父节点上移除、、、
再看_destruct做了些什么
public _destruct () {
const ctor: any = this.constructor;
let destruct = ctor.__destruct__;
if (!destruct) {
destruct = compileDestruct(this, ctor);
js.value(ctor, '__destruct__', destruct, true);
}
destruct(this);
}
function compileDestruct (obj, ctor) {
const shouldSkipId = obj instanceof legacyCC._BaseNode || obj instanceof legacyCC.Component;
const idToSkip = shouldSkipId ? '_id' : null;
let key;
const propsToReset = {};
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === idToSkip) {
continue;
}
switch (typeof obj[key]) {
case 'string':
propsToReset[key] = '';
break;
case 'object':
case 'function':
propsToReset[key] = null;
break;
}
}
}
// Overwrite propsToReset according to Class
if (CCClass._isCCClass(ctor)) {
const attrs = legacyCC.Class.Attr.getClassAttrs(ctor);
const propList = ctor.__props__;
for (let i = 0; i < propList.length; i++) {
key = propList[i];
const attrKey = key + legacyCC.Class.Attr.DELIMETER + 'default';
if (attrKey in attrs) {
if (shouldSkipId && key === '_id') {
continue;
}
switch (typeof attrs[attrKey]) {
case 'string':
propsToReset[key] = '';
break;
case 'object':
case 'function':
propsToReset[key] = null;
break;
case 'undefined':
propsToReset[key] = undefined;
break;
}
}
}
}
if (SUPPORT_JIT) {
// compile code
let func = '';
for (key in propsToReset) {
let statement;
if (CCClass.IDENTIFIER_RE.test(key)) {
statement = 'o.' + key + '=';
}
else {
statement = 'o[' + CCClass.escapeForJS(key) + ']=';
}
let val = propsToReset[key];
if (val === '') {
val = '""';
}
func += (statement + val + ';\n');
}
return Function('o', func);
}
else {
return (o) => {
for (const _key in propsToReset) {
o[_key] = propsToReset[_key];
}
};
}
}
可以看到:对象本身的属性会根据类型进行赋值,字符串会被置成空字符串’’,object、函数会被赋值null,数字、布尔则不处理。
另外,这里可以看到一个有意思的代码return Function('o', func);
。运行时动态生成代码,不过需要JIT的支持。
附上节点销毁前后对比图:
场景的切换
先看一下loadScene的代码。
cocos/core/director.ts
/**
* @en Loads the scene by its name.
* @zh 通过场景名称进行加载场景。
*
* @param sceneName - The name of the scene to load.
* @param onLaunched - callback, will be called after scene launched.
* @return if error, return false
*/
public loadScene (sceneName: string, onLaunched?: Director.OnSceneLaunched, onUnloaded?: Director.OnUnload) {
if (this._loadingScene) {
warnID(1208, sceneName, this._loadingScene);
return false;
}
const bundle = legacyCC.assetManager.bundles.find((bundle) => {
return bundle.getSceneInfo(sceneName);
});
if (bundle) {
this.emit(legacyCC.Director.EVENT_BEFORE_SCENE_LOADING, sceneName);
this._loadingScene = sceneName;
console.time('LoadScene ' + sceneName);
bundle.loadScene(sceneName, (err, scene) => {
console.timeEnd('LoadScene ' + sceneName);
this._loadingScene = '';
if (err) {
error(err);
if (onLaunched) {
onLaunched(err);
}
}
else {
this.runSceneImmediate(scene, onUnloaded, onLaunched);
}
});
return true;
}
else {
errorID(1209, sceneName);
return false;
}
}
可以看到逻辑大概是:找到scene所属的bundle,然后开始加载这个bundle中的scene。加载完毕后,调用了runSceneImmediate。好,接下来看一下runSceneImmediate的实现。
/**
* @en
* Run a scene. Replaces the running scene with a new one or enter the first scene.<br>
* The new scene will be launched immediately.
* @zh 运行指定场景。将正在运行的场景替换为(或重入为)新场景。新场景将立即启动。
* @param scene - The need run scene.
* @param onBeforeLoadScene - The function invoked at the scene before loading.
* @param onLaunched - The function invoked at the scene after launch.
*/
public runSceneImmediate (scene: Scene|SceneAsset, onBeforeLoadScene?: Director.OnBeforeLoadScene, onLaunched?: Director.OnSceneLaunched) {
if (scene instanceof SceneAsset) scene = scene.scene!;
assertID(scene instanceof Scene, 1216);
if (BUILD && DEBUG) {
console.time('InitScene');
}
// @ts-expect-error
scene._load(); // ensure scene initialized
if (BUILD && DEBUG) {
console.timeEnd('InitScene');
}
// Re-attach or replace persist nodes
if (BUILD && DEBUG) {
console.time('AttachPersist');
}
const persistNodeList = Object.keys(legacyCC.game._persistRootNodes).map((x) => {
return legacyCC.game._persistRootNodes[x];
});
for (let i = 0; i < persistNodeList.length; i++) {
const node = persistNodeList[i];
node.emit(legacyCC.Node.SCENE_CHANGED_FOR_PERSISTS, scene.renderScene);
const existNode = scene.getChildByUuid(node.uuid);
if (existNode) {
// scene also contains the persist node, select the old one
const index = existNode.getSiblingIndex();
existNode._destroyImmediate();
scene.insertChild(node, index);
}
else {
node.parent = scene;
}
}
if (BUILD && DEBUG) {
console.timeEnd('AttachPersist');
}
const oldScene = this._scene;
// unload scene
if (BUILD && DEBUG) {
console.time('Destroy');
}
if (legacyCC.isValid(oldScene)) {
oldScene!.destroy();
}
if (!EDITOR) {
// auto release assets
if (BUILD && DEBUG) {
console.time('AutoRelease');
}
legacyCC.assetManager._releaseManager._autoRelease(oldScene, scene, persistNodeList);
if (BUILD && DEBUG) {
console.timeEnd('AutoRelease');
}
}
this._scene = null;
// purge destroyed nodes belongs to old scene
CCObject._deferredDestroy();
if (BUILD && DEBUG) { console.timeEnd('Destroy'); }
if (onBeforeLoadScene) {
onBeforeLoadScene();
}
this.emit(legacyCC.Director.EVENT_BEFORE_SCENE_LAUNCH, scene);
// Run an Entity Scene
this._scene = scene;
if (BUILD && DEBUG) {
console.time('Activate');
}
// @ts-expect-error
scene._activate();
if (BUILD && DEBUG) {
console.timeEnd('Activate');
}
// start scene
if (this._root) {
this._root.resetCumulativeTime();
}
this.startAnimation();
if (onLaunched) {
onLaunched(null, scene);
}
this.emit(legacyCC.Director.EVENT_AFTER_SCENE_LAUNCH, scene);
}
大致流程:
- 首先是常驻节点的处理。现有的常驻节点如果在新的场景中重复出现了,需要删除新场景中重复的常驻节点。
- 旧场景的销毁
- 资源的自动释放(preview版本有bug,资源并没有被释放掉)
- 强制执行一次待销毁对象的销毁
- 激活新场景
这里可以看到,切换场景的时候需要先加载好新的场景。也就是说在切换的瞬间,内存当中会同时存在两个场景的资源。原本计划看一下场景资源自动释放的实现,目前preview版本有bug先跳过
GPU骨骼动画蒙皮
蒙皮是了避免刚体动画的接缝,而让一个顶点跟随多根骨骼一起运动。首先从引擎默认标准effect的standard-vs入手。
找到CCVertInput的宏定义
继续找到ccSkin函数
看来这里就是蒙皮的所在了。继续看skinMatrix函数。
mat4 skinMatrix () {
vec4 joints = vec4(a_joints); //simple conversion of a_joints from unsigned integer to float to match function parameter type
return getJointMatrix(joints.x) * a_weights.x
+ getJointMatrix(joints.y) * a_weights.y
+ getJointMatrix(joints.z) * a_weights.z
+ getJointMatrix(joints.w) * a_weights.w;
}
获取四根骨骼的矩阵,并更根据权重得到最终的矩阵。
getJointMatrix则有3个版本的实现,如下
如果没有将骨骼的动画信息烘焙进纹理,则通过uniform的形式传到shader中(对应者uniform变量cc_joints)。如果进行了烘焙,又根据浮点纹理分了两种实现。但都是利用纹理记录了骨骼所有动作在每一帧的矩阵信息,典型的空间换时间。
球面光和聚光灯
首先看一下引擎的标准effect文件
可以看到:对于不透明物体,有3个pass。第一个对应着平行光pass,第二个forward-add则对应着点光和聚光灯pass。第三个则对应着阴影pass。
展开看一下forward-add
深度测试函数改成了相等,并且关闭了深度写入。开启了混合,并使用了叠加模式。
点光、聚光灯的光照计算如下:
上面部分是shader层面的实现,现在来看一下引擎代码层做了哪些处理。
可以看到:前向渲染阶段ForwardStage有一个额外灯光渲染队列_additiveLightQueue
。再继续看一下啊gatherLightPasses的实现。
这一部分主要是对所有点光和聚光灯进行有效性判定,如果光的照射区域跟相机的视锥体没有交集则被排除掉。
绘制调用部分
总结一下:在场景中不透明物体绘制完成之后,再把所有跟点光、聚光灯有相交部分的模型重新绘制一次。为了确保不覆盖场景中已经绘制的其他模型,需要将深度测试函数设置成相等模式,并且不能修改现有的深度信息。又为了跟第一次绘制时候平行光计算出的颜色进行叠加,需要开启混合并修改混合模式为叠加的计算方式(不透明物体第一遍绘制的时候是不开混合的)。
阴影
引擎提供了两套阴影系统:planar和shadowMap。planar产生的阴影只能在一个平面上,优势上性能高。shadowMap则没有必须要在一个平面的限制。这里主要讲shadow Map,因为其更通用灵活。shadowMap的原理可以看这篇文章:https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping。
阴影分成两个部分:shadow Map 的生成,利用阴影map判断当前像素是否在阴影中。
shadow Map的生成
老样子先看一下shader的实现:
同样是利用了一个额外的pass,在vs中将深度值包装成了颜色值。
回顾一下渲染流程图:
找到ShadowFlow
关键代码:
const validLights = lightCollecting(view, shadowInfo.maxReceived);
其作用是收集有效光源,代码如下:
export function lightCollecting (view: RenderView, lightNumber: number) {
_validLights.length = 0;
const camera = view.camera;
const scene = camera.scene!;
_validLights.push(scene.mainLight!);
const spotLights = scene.spotLights;
for (let i = 0; i < spotLights.length; i++) {
const light = spotLights[i];
Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range);
if (intersect.sphereFrustum(_sphere, view.camera.frustum) &&
lightNumber > _validLights.length) {
_validLights.push(light);
}
}
return _validLights;
}
可以看到平行光是一定有效的,聚光灯需要判定光照范围跟相机的视锥体是否相交。没有看到点光源。有可能是因为点光需要6个frameBuffer消耗过大,也可能是因为时间关系来不及实现了。
shadowCollecting(pipeline, view);
找到所有投影的模型,注意这里模型不需要判断是否在相机视锥体内。因为相机之外的物体产生的阴影可以对相机视锥体内的物体产生遮蔽。
this._initShadowFrameBuffer(pipeline, light);
确保每个灯光有一块frame Buffer。因为需要将这些深度信息记录下来。
接着就是
shadowStage.render(view);
。源码如下:
this._additiveShadowQueue.gatherLightPasses(this._light, cmdBuff);
收集光源能照射到的所有投影模型。
经过了这些步骤,也就对平行光和所有的聚光灯生成一个shadow Map。接下来看正常渲染是如何使用shadow Map的。
可以看到,接受投影的模型会在vs计算完pbr的颜色值之后再进行阴影的处理。处理的函数如下:
好吧,这里的计算过程我也莫有看懂~。~
每日小技巧:利用常驻节点可以实现跨场景动画。
更多源码分析
Cocos Creator v3.0 是在Cocos Creator 3D的基础之上合如了2D版本,所以两者在架构层面上是基本一致的。
如何阅读引擎源码
Cocos Creator 3D源码简析
Cocos Creator 3D源码之GFX
更多文章
个人博客: https://blog.youkuaiyun.com/u014560101
公众号:游戏开发之旅