creator源码阅读系列第二篇《渲染流程详解》

【本文参与征文活动】

大家好,皮皮哥又来了,上次的源码分析系列文章已经过去好久,最近比较闲,可以这段时间写一些东西,附上上次的第一篇

creator源码阅读系列之第一篇源码总览

渲染还是引擎的重中之重,通过本系列你可以学习到,引擎如何检测合批,2.0系列的渲染相比老的2dx有了哪些大的变化,渲染的核心代码讲解,本人不才,可能会有疏漏和错误的地方,希望大家认真指出,当然本篇文章也会由于我的理解的深入,逐步进行修改

我们先来看看渲染的代码在哪里

一 渲染流程中用到的核心类

  1. Assembler : 处理渲染组件顶点数据的方法,为 RenderComponent 使用以处理不同的顶点数据数量以及不同的填充规则。

    核心方法有:updateRenderData 用以准备顶点数据,fillBuffers 用以将准备好的顶点数据填充进VetexBuffer 和 IndiceBuffer 中。

    子类有:2d图片使用的:SimpleSpriteAssembler 普通渲染使用,SlicedAssembler 九宫格填充,TiledAssembler 平铺模式,BarFilledAssembler 和 RadialFilledAssembler是横向和径向的填充模式。

  2. RenderComponent: 所有直接渲染组件的基类,如cc.Sprite,cc.Label, cc.Graphics等。渲染组件的 _assembler 为其对应关联的 Assembler 来进行渲染数据的更新和提交。

  3. RenderFlow : 渲染流,用以遍历场景下所有节点,根据每个节点的_renderFlag , 处理节点的位置,颜色,透明度,更新并渲染。

  4. ModelBatcher : 用以管理渲染数据model,渲染批次合并,从而减少drawcall,提升性能。

    5.ForwardRenderer : 持有的device是真正的使用gl函数将顶点数据,纹理等绘制到屏幕上。在 cc.render 下的 initWebGL 和 initCanvas 是根据不同设备情况下,创建ForwardRenderer的入口。

  5. RenderData :Assembler 中持有的渲染数据,用以保存顶点数据,顶点索引数据。

  6. Material : 材质,RenderComponent 中用于控制渲染组件的视觉效果,有Effect属性。

  7. Effect : 可定义GLSL脚本。

首先分析Assembler,主要代码如下:
3

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 的运行逻辑

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 方法

看一最终渲染调用栈:


渲染核心逻辑见上图

4

这次先讲这么多,想了想还是分成两部分去写,下次讲介绍ModelBatcher数据合批,材质系统和ForwardRender

28赞

你的这个 博客 打不开了 pipijun.cn/

哦哦,我看下,阿里云太贵换地方了,稍等下

大佬能否解析一下3.x的渲染流程呀

3d的部分我们公司都用u3d,没有太多实际经验,不好意思

皮皮哥,你写的很好,但是,但是,但是,唉。。。。。。。。。。。。。。。。

但是什么,有什么问题可以问的

不是不想问- -水平有限,看不懂。。。。。。。。不过这不耽搁我给皮皮哥喊6666

毕竟皮皮他哥

看不懂的话别急,可以看我第一篇,慢慢看,项目碰到问题,去看对应的代码,另外给楼上大佬打个广告

2赞

皮哥威武,学习下

前排,混脸熟

大佬gkd,收获颇多,还有RenderFlow的性能优化的链接在哪呀 :kissing_closed_eyes:

最近项目比较敢,中秋找个时间写个续篇

膜拜大佬,牛

我先插个眼。。

赞赞赞赞赞赞赞赞赞

mark mark

mark~

大佬牛逼,太需要这个。
写了三四年的业务代码,对于引擎的实现真的是说不出来什么。现在面试小公司都问这些东西,来补补课!