从WebGL的角度倒推engine渲染实现
其实看完以上,我还是没有明白,不如倒推,我们已经知道如何渲染一个图片了,那么我们只需要关系提交的顶点数据即可,一层一层往上追顶点数据的来源。
最终的渲染都是汇总到这个地方
js
// 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
);
}
js
void gl.drawElements(mode 图元类型, count 图元数量, type 缓冲区值的类型, offset 缓冲区偏移);
真正的数据是放在buffer里面,我们就要网上找操作buffer的地方,很容易就能找到这部分的逻辑
js
// 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;
}
顺着setIndexBuffer的接口,我们找到base-render,发现Buffer数据都是来自InputAssembler
js
_draw (item) {
const device = this._device;
const programLib = this._programLib;
const { node, ia, passes, effect } = item; // 很明显,ia来自item参数,我们还需要往上层追
/// ...
// set vertex buffer
if (ia._vertexBuffer) {
device.setVertexBuffer(0, ia._vertexBuffer);
}
// set index buffer
if (ia._indexBuffer) {
device.setIndexBuffer(ia._indexBuffer);
}
// set primitive type
device.setPrimitiveType(ia._primitiveType);
}
其实也很容易理解,buffer对象就像指针一样,这样才能方便外部修改填充数据,底层核心只需要持有这个指针就行了
js
_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里面,可能是出于性能考虑做的优化。接着往上层追数据
js
_opaqueStage (view, items) {
// 这里面仅仅做了参数透传
this._drawItems(view, items);
}
下边的逻辑有点重,也是数据根源的地方,需要耐心看
js
// 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
js
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
的来源如下:
js
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
的数据
js
_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就是在处理这件事
js
_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
的时候会初始化这件事
js
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;
}
让我们再回过头看看提交数据时的样子
diff
indexBuffer._glID
indexBuffer我们已经追查到其实对应的就是IndexBuffer类
js
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是如何影响这条链路上的数据呢?
js
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,
js
_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为例
js
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