从WebGL的角度倒推engine渲染实现
其实看完以上,我还是没有明白,不如倒推,我们已经知道如何渲染一个图片了,那么我们只需要关系提交的顶点数据即可,一层一层往上追顶点数据的来源。
最终的渲染都是汇总到这个地方
// drawPrimitives if (next.indexBuffer) { // 大部分情况下都会使用到顶点索引 gl.drawElements( this._next.primitiveType, // 4 代表 gl.TRIANGLES count, // 6 绘制一个矩形需要4个顶点,但是需要2个三角形,一个三角形是3个顶点,这6个顶点是放在索引缓冲区 next.indexBuffer._format, // 5123 为 gl.UNSIGNED_SHORT base * next.indexBuffer._bytesPerIndex// 0*2 ,gl.UNSIGNED_SHORT占2字节,base是上层传递过来的,是inputAssembler._start ); } else { gl.drawArrays( this._next.primitiveType, base, count ); }
void gl.drawElements(mode 图元类型, count 图元数量, type 缓冲区值的类型, offset 缓冲区偏移);
真正的数据是放在buffer里面,我们就要网上找操作buffer的地方,很容易就能找到这部分的逻辑
// commit index-buffer if (cur.indexBuffer !== next.indexBuffer) { // 使用next.indexBuffer,那么我们就得去寻找下indexBuffer里面的数据 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, next.indexBuffer && next.indexBuffer._glID !== -1 ? next.indexBuffer._glID : null); } // 发现有这个接口,那么这个indexBuffer就是通过外部设置的 setIndexBuffer(buffer) { this._next.indexBuffer = buffer; }
其实也很容易理解,buffer对象就像指针一样,这样才能方便外部修改填充数据,底层核心只需要持有这个指针就行了
_drawItems (view, items) { let shadowLights = this._shadowLights; if (shadowLights.length === 0 && this._numLights === 0) { for (let i = 0; i < items.length; ++i) {// items.length被重载了 let item = items.data[i];// items是个RecyclePool回收池 this._draw(item); // 这个item就包含了ia } } }
从以上的逻辑可以看到,数据是放在了RecyclePool里面,可能是出于性能考虑做的优化。接着往上层追数据
_opaqueStage (view, items) { // 这里面仅仅做了参数透传 this._drawItems(view, items); }
下边的逻辑有点重,也是数据根源的地方,需要耐心看
// get all draw items this._drawItemsPools.reset();// 重置drawItemsPools // drawItemsPools数据是长这样子的 this._drawItemsPools = new RecyclePool(() => { return { model: null, node: null, ia: null, effect: null, defines: null, uniforms: null }; }, 100); for (let i = 0; i < scene._models.length; ++i) { // 数据的源头又被这里所控制 let model = scene._models.data[i]; // filter model by view if ((model._cullingMask & view._cullingMask) === 0) { continue; } let drawItem = this._drawItemsPools.add(); model.extractDrawItem(drawItem); // extractDrawItem(out) { // out.model = this; // out.node = this._node; // out.ia = this._inputAssembler; // inputAssember真正是在model身上,所以寻找源头又变成了scene._models // out.effect = this._effect; // } } } for (let i = 0; i < view._stages.length; ++i) { let stage = view._stages[i]; let stageItems = this._stageItemsPools.add(); stageItems.reset(); for (let j = 0; j < this._drawItemsPools.length; ++j) { // 很明显受到了drawItemsPool控制,drawItemsPool同样是一个RecyclePool let drawItem = this._drawItemsPools.data[j]; let passes = drawItem.effect.stagePasses[stage]; if (!passes || passes.length === 0) continue; let stageItem = stageItems.add();// 递增生产stageItem,后续是对生成的item进行数据填充 stageItem.passes = passes; stageItem.model = drawItem.model; stageItem.node = drawItem.node; stageItem.ia = drawItem.ia; // 这里就是InputAssembler的来源啦!,很明显来自drawItem stageItem.effect = drawItem.effect; stageItem.defines = drawItem.defines; stageItem.sortKey = -1; stageItem.uniforms = drawItem.uniforms; } let stageInfo = _stageInfos.add(); stageInfo.stage = stage; stageInfo.items = stageItems; // info.items的具体数据源 } // render stages for (let i = 0; i < _stageInfos.length; ++i) { // 同样的_stageInfos也是RecyclePool类型,所以这个class的源码还是有必要仔细读一下,也不是非常复杂,也就不到100行 let info = _stageInfos.data[i]; let fn = this._stage2fn[info.stage]; fn(view, info.items); // 不同的stage对应不同的函数实现 } // 这个是_stagetInfos的关联数据 let _stageInfos = new RecyclePool(() => { return { stage: null, items: null, }; }, 8);
上边的分析有点长,但是我们最终会发现渲染数据是放在_batcher._renderScene._models
RenderFlow.render = function (rootNode, dt) { _batcher.reset(); _batcher.walking = true; RenderFlow.visitRootNode(rootNode); _batcher.terminate(); _batcher.walking = false; _forward.render(_batcher._renderScene, dt);// 将渲染的场景传递过去 };
而这个_batcher._renderScene
的来源如下:
this._handle = new ModelBatcher(this.device, this.scene); // batcher的来源,同时也看到了scene的来源 this._flow.init(this._handle, this._forward); RenderFlow.init = function (batcher, forwardRenderer) { _batcher = batcher;// 设置_batcher _forward = forwardRenderer; flows[0] = EMPTY_FLOW; for (let i = 1; i < FINAL; i++) { flows[i] = new RenderFlow(); } };
又追到了scene里面,因为我们很关心scene._models
的数据
_add(pool, item) { if (item._poolID !== -1) { return; } pool.push(item); item._poolID = pool.length - 1; } addModel(model) { this._add(this._models, model); }
那么什么时候在触发这个model呢?
model-batcher就是在处理这件事
_flush () { let material = this.material, buffer = this._buffer, // inputAssembler的关联,它是一个meshBuffer indiceCount = buffer.indiceOffset - buffer.indiceStart; if (!this.walking || !material || indiceCount <= 0) { return; } let effect = material.effect; if (!effect) return; // Generate ia(终于追到了根源,心好累) let ia = this._iaPool.add(); // this._iaPool = new RecyclePool(function () { // return new InputAssembler(); // }, 16); // 你会发现数据又来自buffer ia._vertexBuffer = buffer._vb; // this._vb = new gfx.VertexBuffer( // batcher._device, // vertexFormat, // gfx.USAGE_DYNAMIC, // new ArrayBuffer(), // 0 // ); ia._indexBuffer = buffer._ib; // this._ib = new gfx.IndexBuffer( // batcher._device, // gfx.INDEX_FMT_UINT16, // gfx.USAGE_STATIC, // new ArrayBuffer(), // 0 // ); ia._start = buffer.indiceStart; ia._count = indiceCount; // Generate model let model = this._modelPool.add(); // 数据结构 // this._modelPool = new RecyclePool(function () { // return new Model(); // }, 16); this._batchedModels.push(model); model.sortKey = this._sortKey++; model._cullingMask = this.cullingMask; model.setNode(this.node); model.setEffect(effect); model.setInputAssembler(ia); // 设置 inputAssembler this._renderScene.addModel(model); buffer.forwardIndiceStartToOffset(); },
new ModelBatcher
的时候会初始化这件事
this._handle = new ModelBatcher(this.device, this.scene); var ModelBatcher = function (device, renderScene) { // buffers this._quadBuffer = this.getBuffer('quad', vfmtPosUvColor); this._meshBuffer = this.getBuffer('mesh', vfmtPosUvColor); // 统一的 this._quadBuffer3D = this.getBuffer('quad', vfmt3D); this._meshBuffer3D = this.getBuffer('mesh', vfmt3D); this._buffer = this._meshBuffer; }; getBuffer (type, vertextFormat) { let key = type + vertextFormat.getHash(); let buffer = _buffers[key]; if (!buffer) { if (type === 'mesh') { buffer = new MeshBuffer(this, vertextFormat); } _buffers[key] = buffer; } return buffer; }
让我们再回过头看看提交数据时的样子
indexBuffer._glID
indexBuffer我们已经追查到其实对应的就是IndexBuffer类
class IndexBuffer { /** * @constructor * @param {Device} device * @param {INDEX_FMT_*} format * @param {USAGE_*} usage * @param {ArrayBuffer | Uint8Array} data */ constructor(device, format, usage, data) { this._device = device; this._format = format; this._usage = usage; this._bytesPerIndex = BYTES_PER_INDEX[format]; this._bytes = data.byteLength; this._numIndices = this._bytes / this._bytesPerIndex; this._needExpandDataStore = true; // update this._glID = device._gl.createBuffer(); // webgl使用到的参数来源 this.update(0, data); // stats device._stats.ib += this._bytes; } }
追查了这么久,我们只是知道了webgl渲染所需要的数据来源,但是游戏的sprite是如何影响这条链路上的数据呢?
RenderFlow.render = function (rootNode, dt) { _batcher.reset(); _batcher.walking = true; RenderFlow.visitRootNode(rootNode); // 就发生在这个逻辑里面 _batcher.terminate(); _batcher.walking = false; _forward.render(_batcher._renderScene, dt); };
在renderFlow里面,有几个比较重要的flow,
_proto._updateRenderData = function (node) { let comp = node._renderComponent; // Sprite,Label都是继承子cc.RenderComponent comp._assembler.updateRenderData(comp); // RenderComponent需要绑定Assembler node._renderFlag &= ~UPDATE_RENDER_DATA; this._next._func(node); }; _proto._render = function (node) { let comp = node._renderComponent; comp._checkBacth(_batcher, node._cullingMask); comp._assembler.fillBuffers(comp, _batcher);// RenderComponent需要绑定Assembler this._next._func(node); };
fillBuffers的实现,以assembler-2d为例
fillBuffers (comp, renderer) { if (renderer.worldMatDirty) { this.updateWorldVerts(comp); } let renderData = this._renderData; let vData = renderData.vDatas[0]; let iData = renderData.iDatas[0]; // 注意:这里获取到的buffer,就是this._meshBuffer,后续的所有操作都是在操作这个buffer let buffer = this.getBuffer(renderer); // meshBuffer let offsetInfo = buffer.request(this.verticesCount, this.indicesCount); // buffer data may be realloc, need get reference after request. // fill vertices let vertexOffset = offsetInfo.byteOffset >> 2, vbuf = buffer._vData; if (vData.length + vertexOffset > vbuf.length) { vbuf.set(vData.subarray(0, vbuf.length - vertexOffset), vertexOffset); } else { vbuf.set(vData, vertexOffset); } // fill indices let ibuf = buffer._iData, indiceOffset = offsetInfo.indiceOffset, vertexId = offsetInfo.vertexOffset; for (let i = 0, l = iData.length; i < l; i++) { ibuf[indiceOffset++] = vertexId + iData[i]; } } getBuffer () { // 这里使用的buffer是meshBuffer return cc.renderer._handle._meshBuffer; }
里面会反复使用到一个结构RenderData
而文档中也反复提到Assembler里面有2个非常重要的函数updateRenderData
, fillBuffers
在渲染流程中的确是必须的函数
提交顶点
所有的数据都在buffer的_iData
, _vData