大家好,皮皮哥又来了,上次的源码分析系列文章已经过去好久,最近比较闲,可以这段时间写一些东西,附上上次的第一篇
creator源码阅读系列之第一篇源码总览 23
渲染还是引擎的重中之重,通过本系列你可以学习到,引擎如何检测合批,2.0系列的渲染相比老的2dx有了哪些大的变化,渲染的核心代码讲解,本人不才,可能会有疏漏和错误的地方,希望大家认真指出,当然本篇文章也会由于我的理解的深入,逐步进行修改
我们先来看看渲染的代码在哪里
1754×1136 74.1 KB
一 渲染流程中用到的核心类
- Assembler : 处理渲染组件顶点数据的方法,为 RenderComponent 使用以处理不同的顶点数据数量以及不同的填充规则。
核心方法有:updateRenderData 用以准备顶点数据,fillBuffers 用以将准备好的顶点数据填充进VetexBuffer 和 IndiceBuffer 中。
子类有:2d图片使用的:SimpleSpriteAssembler 普通渲染使用,SlicedAssembler 九宫格填充,TiledAssembler 平铺模式,BarFilledAssembler 和 RadialFilledAssembler是横向和径向的填充模式。 - RenderComponent: 所有直接渲染组件的基类,如cc.Sprite,cc.Label, cc.Graphics等。渲染组件的 _assembler 为其对应关联的 Assembler 来进行渲染数据的更新和提交。
- RenderFlow :渲染流,用以遍历场景下所有节点,根据每个节点的_renderFlag , 处理节点的位置,颜色,透明度,更新并渲染。
- ModelBatcher : 用以管理渲染数据model,渲染批次合并,从而减少drawcall,提升性能。
5.ForwardRenderer :持有的device是真正的使用gl函数将顶点数据,纹理等绘制到屏幕上。在 cc.render 下的 initWebGL 和 initCanvas 是根据不同设备情况下,创建ForwardRenderer的入口。 - RenderData :Assembler 中持有的渲染数据,用以保存顶点数据,顶点索引数据。
- Material :材质,RenderComponent 中用于控制渲染组件的视觉效果,有Effect属性。
- Effect :可定义GLSL脚本。
首先分析Assembler,主要代码如下:
Assembler.register(Label, { getConstructor(label) { let ctor = TTF; if (label.font instanceof cc.BitmapFont) { ctor = Bmfont; } else if (label.cacheMode === Label.CacheMode.CHAR) { cc.warn('sorry, canvas mode does not support CHAR mode currently!'); } return ctor; }, TTF, Bmfont }); export default class Assembler { constructor () { this._extendNative && this._extendNative(); } // 负责初始化渲染数据及一些局部参数 init (renderComp) { this._renderComp = renderComp; } // 负责在渲染组件的顶点数据有变化时进行更新修改 updateRenderData (comp) { } // 负责在渲染时进行顶点数据的 Buffer 填充 // 将顶点坐标数据、uv数据和颜色数据添加到buffer后。核心点!此处是_assembler、_renderData和InputAssembler三者发生关系的地方 fillBuffers (comp, renderer) { } getVfmt () { return vfmtPosUvColor; } } // renderCompCtor 渲染组件类//assembler 组件装配类Assembler.register = function (renderCompCtor, assembler) { renderCompCtor.__assembler__ = assembler;// 注册组件装配类到渲染组件类 }; Assembler.init = function (renderComp) { let renderCompCtor = renderComp.constructor; let assemblerCtor = renderCompCtor.__assembler__;//取得组件装配类 while (!assemblerCtor) { renderCompCtor = renderCompCtor.$super; if (!renderCompCtor) { cc.warn(`Can not find assembler for render component : [${cc.js.getClassName(renderComp)}]`); return; } assemblerCtor = renderCompCtor.__assembler__; } if (assemblerCtor.getConstructor) { assemblerCtor = assemblerCtor.getConstructor(renderComp); } if (!renderComp._assembler || renderComp._assembler.constructor !== assemblerCtor) { let assembler = assemblerPool.get(assemblerCtor); assembler.init(renderComp);// 调用具体组件装配实例对组件实例进行初始化 renderComp._assembler = assembler; } };
渲染组件通过 Assembler.register注册到引擎中,比如图形渲染组件的注册代码为 Assembler.register(cc.Graphics, GraphicsAssembler),cc.Graphics为图形类,GraphicsAssembler继承自Assembler类,渲染组件持有_assembler,_assembler持有_renderData,_renderData和InputAssembler都是数据容器,_assembler是数据操作,_assembler可以创建和updateRenderData,更新verts,InputAssembler是在渲染时用到的,用于组织传入GPU的数据.
二渲染代码流程详解
2.1 初始入口
渲染流程会在每帧调用,所以可以在 CCDirector 的 mainLoop 中找到渲染的入口:
(CCDirector之前的我就不提了,大家可以调试了看堆栈)
// Render this.emit(cc.Director.EVENT_BEFORE_DRAW); renderer.render(this._scene, this._deltaTime); // After draw this.emit(cc.Director.EVENT_AFTER_DRAW);
renderer的定义在 \cocos2d\core\renderer\index.js 中。
2.2 cc.renderer.render()
render (ecScene, dt) { this.device.resetDrawCalls(); if (ecScene) { // walk entity component scene to generate models this._flow.render(ecScene, dt); this.drawCalls = this.device.getDrawCalls(); } },
关于 _flow, 在 cc.renderer 的 initWebGL 和 initCanvas 中可以看到:
this._flow = cc.RenderFlow;
所以下一步进入到 cc.RenderFlow.render()。
2.3 cc.RenderFlow.render()
render方法定义在 cocos2d\core\renderer\render-flow.js 中。这10行代码包含了渲染的整个流程:1.遍历节点获取数据,2.渲染到屏幕。代码如下
RenderFlow.render = function (rootNode, dt) { _batcher.reset(); _batcher.walking = true; // 遍历渲染场景节点的所有子节点 RenderFlow.visitRootNode(rootNode); _batcher.terminate(); _batcher.walking = false; // 将batcher中的渲染数据,渲染到屏幕 _forward.render(_batcher._renderScene, dt); };
这个方法里有_batcher 和 _forward 2个变量, 是在方法 RenderFlow.init 中初始化。而 RenderFlow.init 也是在在 cc.renderer 的 initWebGL 和 initCanvas 中调用,代码如下。
this._forward = new ForwardRenderer(this.device, builtins); this._handle = new ModelBatcher(this.device, this.scene); // 调用了 cc.RenderFlow.init this._flow.init(this._handle, this._forward);
由此可知,变量名字和变量类型有关联性,方便我们能快速了解各个变量的类型:
_batcher 类型是 ModelBatcher,用以渲染合批。
_forward 类型是 ForwardRenderer,用以渲染数据到设备的屏幕中。
搞清楚各个变量的定义类型后,下面会逐步了解,如何获取到各个节点和组件上需要渲染的数据。
三 RenderFlow 的运行逻辑
2 1200×895 105 KB
RenderFlow :渲染流,用以遍历场景下所有节点,根据每个节点的_renderFlag , 处理节点的位置,颜色,透明度,更新并渲染。
3.1 性能优化
在v1.x版本中,每次渲染都会进行很多动态判断,需要去判断每个节点是否需要更新位置矩阵,是否需要渲染,在这些过程中会有很多无用分支判断,消耗性能。
所以在v2.x版本中,RenderFlow根据渲染过程中调用的频繁度划分出多个渲染状态,比如 Transform,Render,Children 等,而每个渲染状态都对应了一个函数。在 RenderFlow 的初始化过程中,会预先根据这些状态创建好对应的渲染分支,这些分支会把对应的状态依次链接在一起。在渲染前会更新该节点的_renderFlag ,在渲染该节点时就可以直接根据 _renderFlag的值,进行相应分支的处理,不用进行多余的状态判断。
例如一个节点在当前帧需要更新矩阵,以及需要渲染自己,那么这个节点会更新他的 flag 为
node._renderFlag = RenderFlow.FLAG_TRANSFORM | RenderFlow.FLAG_RENDER。
更加详细的内容可见文末的相关链接中 : RenderFlow的性能优化.
3.2 RenderFlow 内的链式方法的创建与调用
RenderFlow中根据 _renderFlag 获取渲染流的代码如下:
function getFlow (flag) { let flow = null; let tFlag = FINAL; while (tFlag > 0) { if (tFlag & flag)// 如果flag标识匹配,则添加新的渲染流 flow = createFlow(tFlag, flow);// 需要把上一步创建flow传入,作为子流 tFlag = tFlag >> 1;// 标志右移一位 } return flow; }
createFlow() 中会根据flag创建对应的渲染流,并加入链中,代码如下:
function createFlow (flag, next) { let flow = new RenderFlow(); flow._next = next || EMPTY_FLOW;// 将本次创建的flow加入链表首部 // 根据不同的flag设置不同的处理方法 switch (flag) { case DONOTHING: flow._func = flow._doNothing; break; case BREAK_FLOW: flow._func = flow._doNothing; break; case LOCAL_TRANSFORM: flow._func = flow._localTransform; break; case WORLD_TRANSFORM: flow._func = flow._worldTransform; break; case OPACITY: flow._func = flow._opacity; break; case COLOR: flow._func = flow._color; break; case UPDATE_RENDER_DATA: flow._func = flow._updateRenderData; break; case RENDER: flow._func = flow._render; break; case CHILDREN: flow._func = flow._children; break; case POST_RENDER: flow._func = flow._postRender; break; } return flow; }
RenderFlow是根据node节点上的_renderFlag 来进行不同的渲染流程,所以当node节点上的位置,颜色,透明度等参数改变后,需要同步修改_renderFlag。这样在渲染时会去根据flag处理对应的流程。
3.3 详解 RenderFlow 的不同操作
RenderFlow根据 _renderFlag 创建了链式渲染流,但各个不同的FLAG对应的方法,都做了些什么,下面会详细说明。
_localTransform 方法
更新本地坐标矩阵。(Tips:节点的位置通过本地坐标矩阵和世界坐标矩阵管理,通过矩阵叉乘来进行高效的坐标转换,具体内容待继续学习了解。。。)
_worldTransform 方法
更新世界坐标矩阵。
_opacity 方法
处理透明度。
_color 方法
更新 renderCompent 的颜色
_updateRenderData 方法
更新渲染数据,调用 Assembler 里的 updateRenderData 方法,主要是更新uv和顶点数据。
_render 方法
调用 RenderComponent 的 _checkBacth 检测合批。
调用 Assembler 的 fillBuffers 填充数据。
_children 方法
遍历子节点进行子节点的渲染流程。
_postRender 方法
看一最终渲染调用栈:
1631506369138 2570×1184 362 KB
渲染核心逻辑见上图
这次先讲这么多,想了想还是分成两部分去写,下次讲介绍ModelBatcher数据合批,材质系统和ForwardRender