【GT】Assembler 源碼解讀及使用 !Cocos Creator!
背景原文鏈接: ?https://forum.cocos.org/t/demo/95087
作者: GT
排版整理: 白玉無冰
自定義渲染可以實現(xiàn)很多酷炫的 shader 特效,目前常用的有兩種方法:
- 創(chuàng)建自定義材質(zhì),給材質(zhì)增加參數(shù)。這個參數(shù)會作為 uniform 變量傳入 shader 由于渲染合批要求材質(zhì)參數(shù)保持一致,所以如果大量對象使用自定義材質(zhì)時,并且材質(zhì)參數(shù)各不相同,是無法進(jìn)行合批渲染的,一個對象占一個 draw call
- 創(chuàng)建自定義 assembler,在頂點(diǎn)數(shù)據(jù)輸入渲染管道前修改它的值 這種方式比較靈活,如果需要輸入更多自定義參數(shù),標(biāo)準(zhǔn)的頂點(diǎn)格式就不夠用了
本文介紹另一種方法,即能讓shader獲得自定義參數(shù),又能讓自定義材質(zhì)合批渲染。這種方法就是自定義頂點(diǎn)格式 。
Assembler詳解Assembler是實現(xiàn)本文相關(guān)功能的核心類,先簡單回顧一下官方文檔里介紹的內(nèi)容?https://docs.cocos.com/creator/manual/zh/advanced-topics/custom-render.html

Assembler中必須要定義updateRenderData及fillBuffers方法 前者需要更新準(zhǔn)備頂點(diǎn)數(shù)據(jù),后者則是將準(zhǔn)備好的頂點(diǎn)數(shù)據(jù)填充進(jìn)VetexBuffer和IndiceBuffer中
2D渲染中,Assember2D類是一個重要的基礎(chǔ)類,最常用的cc.Sprite的各種模式(Simple,平鋪,九宮格)在內(nèi)部都對應(yīng)了不同的Assembler派生類。同樣是一個四邊形的節(jié)點(diǎn),不同的Assembler可以將其轉(zhuǎn)化成不同數(shù)量的頂點(diǎn)實現(xiàn)不同的渲染效果
- Simple模式下是常規(guī)的四邊形,有4個頂點(diǎn)。(利用這個可以實現(xiàn)漸變色效果)
- 平鋪模式下Assembler根據(jù)紋理的重復(fù)次數(shù)對節(jié)點(diǎn)進(jìn)行“拆碎”,相當(dāng)于每重復(fù)一次就產(chǎn)生1個四邊形。(利用這個可以實現(xiàn)頂點(diǎn)動畫之水紋旗子)
- 九宮格模式下Assembler將節(jié)點(diǎn)拆分為9個四邊形,每個四邊形對應(yīng)紋理上的一個“格子”
fillBuffers源碼解讀
先看看Assembler2D是如何實現(xiàn)?fillBuffers?的
源碼位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/assembler-2d.js
????fillBuffers?(comp,?renderer)?{
???????//?如果節(jié)點(diǎn)的世界坐標(biāo)發(fā)生變化,重新從當(dāng)前節(jié)點(diǎn)的世界坐標(biāo)計算一次頂點(diǎn)數(shù)據(jù)
????????if?(renderer.worldMatDirty)?{
????????????this.updateWorldVerts(comp);
????????}
???????//?獲取準(zhǔn)備好的頂點(diǎn)數(shù)據(jù)
???????//?vData包含pos、uv、color數(shù)據(jù)
???????//?iData包含三角剖分后的頂點(diǎn)索引數(shù)據(jù)
????????let?renderData?=?this._renderData;
????????let?vData?=?renderData.vDatas[0];
????????let?iData?=?renderData.iDatas[0];
???????//?獲取頂點(diǎn)緩存
???????//?getBuffer()方法后面會被我們重載,以便獲得支持自定義頂點(diǎn)格式的緩存
????????let?buffer?=?this.getBuffer(renderer);
???????//?獲取當(dāng)前節(jié)點(diǎn)的頂點(diǎn)數(shù)據(jù)對應(yīng)最終buffer的偏移量
???????//?可以簡單理解為當(dāng)前節(jié)點(diǎn)和其他同格式節(jié)點(diǎn)的數(shù)據(jù),都將按順序追加到這個大buffer里
????????let?offsetInfo?=?buffer.request(this.verticesCount,?this.indicesCount);
????????//?fill?vertices
????????let?vertexOffset?=?offsetInfo.byteOffset?>>?2,
????????????vbuf?=?buffer._vData;
???????//?將準(zhǔn)備好的vData拷貝到VetexBuffer里。這里會判斷如果buffer裝不下了,vData會被截斷一部分
???????//?通常是因為節(jié)點(diǎn)數(shù)量太多導(dǎo)致的,從下個節(jié)點(diǎn)開始會使用新的buffer,也就是重新開一個合批
???????//?當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)被截斷后,則只能被渲染一部分(推測)
????????if?(vData.length?+?vertexOffset?>?vbuf.length)?{
????????????vbuf.set(vData.subarray(0,?vbuf.length?-?vertexOffset),?vertexOffset);
????????}?else?{
????????????vbuf.set(vData,?vertexOffset);
????????}
????????//?將準(zhǔn)備好的iData拷貝到IndiceBuffer里
????????let?ibuf?=?buffer._iData,
????????????indiceOffset?=?offsetInfo.indiceOffset,
????????????vertexId?=?offsetInfo.vertexOffset;
????????for?(let?i?=?0,?l?=?iData.length;?i?????????????ibuf[indiceOffset++]?=?vertexId?+?iData[i];
????????}
????}
思考
Q: 為什么要需要準(zhǔn)備頂點(diǎn)數(shù)據(jù),而不是在fillBuffer()方法內(nèi)直接計算后填入buffer?
A: 因為fillBuffer()每幀都會被調(diào)用,是熱點(diǎn)代碼,需要關(guān)注效率。但是頂點(diǎn)數(shù)據(jù)不是每一幀都會更新,可以預(yù)先計算
Q: 實現(xiàn)自定義頂點(diǎn)格式需要修改fillBuffer()方法嗎?
A: 不需要,fillBuffer()是簡單的字節(jié)流拷貝,只關(guān)心數(shù)據(jù)長度,不關(guān)心數(shù)據(jù)內(nèi)容
Q: 頂點(diǎn)數(shù)據(jù)包含哪些內(nèi)容?如何計算?
A: 見下文
頂點(diǎn)數(shù)據(jù)格式描述
最常用的頂點(diǎn)格式是?vfmtPosUvColor?,也是Assembler2D默認(rèn)使用的格式。https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/webgl/vertex-format.js
var?vfmtPosUvColor?=?new?gfx.VertexFormat([
????//?節(jié)點(diǎn)的世界坐標(biāo),占2個float32
????{?name:?gfx.ATTR_POSITION,?type:?gfx.ATTR_TYPE_FLOAT32,?num:?2?},
????//?節(jié)點(diǎn)的紋理uv坐標(biāo),占2個float32
????//?如果節(jié)點(diǎn)使用了獨(dú)立的紋理(未合圖),這里的uv值通常是0或1
????//?合圖后的紋理,這里的uv對應(yīng)其在圖集里的相對位置,取值范圍在[0,1)內(nèi)
????{?name:?gfx.ATTR_UV0,?type:?gfx.ATTR_TYPE_FLOAT32,?num:?2?},
????//?節(jié)點(diǎn)顏色值,cc.Sprite組件上可以設(shè)置。占4個uint8 = 1個float32
????{?name:?gfx.ATTR_COLOR,?type:?gfx.ATTR_TYPE_UINT8,?num:?4,?normalize:?true?},
]);
頂點(diǎn)格式和shader頂點(diǎn)著色器的attribute變量對應(yīng)關(guān)系如下
CCProgram?vs?%{
??precision?highp?float;
??#include?
??#include?
??//?對應(yīng)vfmtPosUvColor結(jié)構(gòu)里的3個字段
??//?注意這里a_position是vec3類型,但是vfmtPosUvColor對其自定義了2個float長度。所以a_position.z =?0
??in?vec3?a_position;??????????//?gfx.ATTR_POSITION
??in?vec2?a_uv0;???//?gfx.ATTR_UV0
??in?vec4?a_color;???//?gfx.ATTR_COLOR
??//?...
??void?main?()?{
??????//?...
??}
}%
看下Assembler2D里的屬性和頂點(diǎn)格式的對應(yīng)關(guān)系
源碼位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/assembler-2d.js
cc.js.addon(Assembler2D.prototype,?{
????//?vfmtPosUvColor?結(jié)構(gòu)占5個float32
????floatsPerVert:?5,
????//?一個四邊形4個頂點(diǎn)
????verticesCount:?4,
????//?一個四邊形按照對角拆分成2個三角形,2*3?=?6個頂點(diǎn)索引
????indicesCount:?6,
????//?uv的值在vfmtPosUvColor結(jié)構(gòu)里下標(biāo)從2開始算
????uvOffset:?2,
????//?color的值在vfmtPosUvColor結(jié)構(gòu)里下標(biāo)從4開始算
????colorOffset:?4,
});
除了默認(rèn)屬性之外,這里還定義了一批可以使用的attribute變量。https://github.com/cocos-creator/engine/blob/master/cocos2d/renderer/gfx/enums.js

頂點(diǎn)數(shù)據(jù)計算

了解了上面的頂點(diǎn)格式之后,頂點(diǎn)數(shù)據(jù)無非就是計算 pos、uv、color幾個值。在Assembler里分別有?updateVerts()?updateUVs()``updateColor()?方法來準(zhǔn)備這幾個值,并且臨時存儲在Assembler自己分配的數(shù)組里。頂點(diǎn)數(shù)據(jù)存在RenderData中,源碼位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/assembler-2d.js
export?default?class?Assembler2D?extends?Assembler?{
????constructor?()?{
????????super();
???????//?renderData.vDatas用來存儲pos、uv、color數(shù)據(jù)
????????//?renderData.iDatas用來存儲頂點(diǎn)索引數(shù)據(jù)
????????this._renderData?=?new?RenderData();
????????this._renderData.init(this);
????????this.initData();
????????this.initLocal();
????}
????get?verticesFloats?()?{
???????//?當(dāng)前節(jié)點(diǎn)的所有頂點(diǎn)數(shù)據(jù)總大小
????????return?this.verticesCount?*?this.floatsPerVert;
????}
????initData?()?{
????????let?data?=?this._renderData;
???????//?創(chuàng)建一個足夠長的空間用來存儲頂點(diǎn)數(shù)據(jù)?&?頂點(diǎn)索引數(shù)據(jù)
???????//?這個方法內(nèi)部會初始化頂點(diǎn)索引數(shù)據(jù)
????????data.createQuadData(0,?this.verticesFloats,?this.indicesCount);
????}
???//?...
}
updateUVs()?方法解讀
源碼位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/webgl/assemblers/sprite/2d/simple.js
????updateUVs?(sprite)?{
???????//?獲取當(dāng)前cc.Sprite組件設(shè)置的spriteFrame對應(yīng)的uv
???????//?uv數(shù)組長度=8,分別表示4個頂點(diǎn)的uv.x,?uv.y
???????//?按照左下、右下、左上、右上的順序存儲,注意這里的順序和頂點(diǎn)索引的數(shù)據(jù)需要對應(yīng)上
????????let?uv?=?sprite._spriteFrame.uv;
????????let?uvOffset?=?this.uvOffset;??//?之前提到過vfmtPosUvColor結(jié)構(gòu)里uvOffset?=?2
????????let?floatsPerVert?=?this.floatsPerVert;?//?floatsPerVert?=?vfmtPosUvColor結(jié)構(gòu)大小?=?5
????????let?verts?=?this._renderData.vDatas[0];
????????for?(let?i?=?0;?i?4;?i++)?{
????????????// 2個1組取uv數(shù)據(jù),寫入renderData.vDatas對應(yīng)位置
????????????let?srcOffset?=?i?*?2;
????????????let?dstOffset?=?floatsPerVert?*?i?+?uvOffset;
????????????verts[dstOffset]?=?uv[srcOffset];
????????????verts[dstOffset?+?1]?=?uv[srcOffset?+?1];
????????}
????}
updateColor()?和?updateVerts()?的具體實現(xiàn)這里不再分析。
由于上面多次提到了頂點(diǎn)索引,對于不了解它的同學(xué)需要再單獨(dú)解釋一下。
理解頂點(diǎn)索引
除了pos、uv、color數(shù)據(jù)之外,為什么還需要計算頂點(diǎn)索引數(shù)據(jù)?我們發(fā)送給GPU的數(shù)據(jù),實際上表示的是三角形,而不是四邊形。一個四邊形需要剖分成2個三角形然后傳給GPU。在4個頂點(diǎn)數(shù)據(jù)的基礎(chǔ)上,三角形的描述信息單獨(dú)存在IndiceBuffer (即renderData.iDatas)里,IndiceBuffer里的每個值表示其對應(yīng)頂點(diǎn)數(shù)據(jù)的下標(biāo)。通過索引可以合并掉多個三角形中相同的頂點(diǎn)數(shù)據(jù),減少總數(shù)據(jù)大小。

常規(guī)四邊形的索引數(shù)據(jù)準(zhǔn)備,源碼位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/webgl/render-data.js
????initQuadIndices(indices)?{
???????//?按照上述剖分方式得到的下標(biāo):?[0,1,2]?[1,3,2]
????????// 6個一組(對應(yīng)1個四邊形)生成索引數(shù)據(jù)
????????let?count?=?indices.length?/?6;
????????for?(let?i?=?0,?idx?=?0;?i?????????????let?vertextID?=?i?*?4;
????????????indices[idx++]?=?vertextID;
????????????indices[idx++]?=?vertextID+1;
????????????indices[idx++]?=?vertextID+2;
????????????indices[idx++]?=?vertextID+1;
????????????indices[idx++]?=?vertextID+3;
????????????indices[idx++]?=?vertextID+2;
????????}
????}
頂點(diǎn)格式自定義
現(xiàn)在進(jìn)入正題,基于上面對Assembler以及相關(guān)類的解讀,頂點(diǎn)格式自定義需要做這么幾件事
- 定義新的格式
- 用新的格式準(zhǔn)備足夠長的
renderData - 在
renderData對應(yīng)位置寫入自定義數(shù)據(jù) - 在
fillBuffers()方法內(nèi)將renderData數(shù)據(jù)正確刷入buffer
//?自定義頂點(diǎn)格式,在vfmtPosUvColor基礎(chǔ)上,加入gfx.ATTR_UV1,去掉gfx.ATTR_COLOR
let?gfx?=?cc.gfx;
var?vfmtCustom?=?new?gfx.VertexFormat([
????{?name:?gfx.ATTR_POSITION,?type:?gfx.ATTR_TYPE_FLOAT32,?num:?2?},
????{?name:?gfx.ATTR_UV0,?type:?gfx.ATTR_TYPE_FLOAT32,?num:?2?},????????//?texture紋理uv
????{?name:?gfx.ATTR_UV1,?type:?gfx.ATTR_TYPE_FLOAT32,?num:?2?}?????????//?自定義數(shù)據(jù)
]);
const?VEC2_ZERO?=?cc.Vec2.ZERO;
export?default?class?PieceMaskAssembler?extends?GTSimpleSpriteAssembler2D?{
????//?根據(jù)自定義頂點(diǎn)格式,調(diào)整下述常量
????verticesCount?=?4;
????indicesCount?=?6;
????uvOffset?=?2;
????uv1Offset?=?4;
????floatsPerVert?=?6;
????//?自定義數(shù)據(jù),將被寫入uv1的位置
????public?moveSpeed:?cc.Vec2?=?VEC2_ZERO;
????initData()?{
????????let?data?=?this._renderData;
????????//?createFlexData支持創(chuàng)建指定格式的renderData
????????data.createFlexData(0,?this.verticesCount,?this.indicesCount,?this.getVfmt());
????????//?createFlexData不會填充頂點(diǎn)索引信息,手動補(bǔ)充一下
????????let?indices?=?data.iDatas[0];
????????let?count?=?indices.length?/?6;
????????for?(let?i?=?0,?idx?=?0;?i?????????????let?vertextID?=?i?*?4;
????????????indices[idx++]?=?vertextID;
????????????indices[idx++]?=?vertextID+1;
????????????indices[idx++]?=?vertextID+2;
????????????indices[idx++]?=?vertextID+1;
????????????indices[idx++]?=?vertextID+3;
????????????indices[idx++]?=?vertextID+2;
????????}
????}
????//?自定義格式以getVfmt()方式提供出去,除了當(dāng)前assembler,render-flow的其他地方也會用到
????getVfmt()?{
????????return?vfmtCustom;
????}
????//?重載getBuffer(),?返回一個能容納自定義頂點(diǎn)數(shù)據(jù)的buffer
????//?默認(rèn)fillBuffers()方法中會調(diào)用到
????getBuffer()?{
????????return?cc.renderer._handle.getBuffer("mesh",?this.getVfmt());
????}
????//?pos數(shù)據(jù)沒有變化,不用重載
????//?updateVerts(sprite)?{
????//?}
????updateUVs(sprite)?{
????????//?uv0調(diào)用基類方法寫入
????????super.updateUVs(sprite);
????????//?填入自己的uv1數(shù)據(jù)
????????//?...
????????//?方法類似uv0寫入,詳見Demo
???????//?https://github.com/caogtaa/CCBatchingTricks
????}
????updateColor(sprite)?{
????????//?由于已經(jīng)去掉了color字段,這里重載原方法,并且不做任何事
????}
}
上面用到的?GTSimpleSpriteAssembler2D基類代碼大部分參考官方cc.Sprite的實現(xiàn)。
這里將通過額外的一組uv數(shù)據(jù),實現(xiàn)紋理滾動的方向 & 速度控制。

用材質(zhì)參數(shù)的方法同樣能夠?qū)崿F(xiàn)這個效果,但是無法做到合批渲染?;谏厦娼o出的Assembler類,繼續(xù)完善一下其他輔助類
材質(zhì)
材質(zhì)只用于關(guān)聯(lián)effect,沒有額外邏輯,也不需要新建uniform變量
RenderComponent
RenderComponent這里的角色:
- 創(chuàng)建對應(yīng)的
Assembler類 - 給
Assembler傳參。讓業(yè)務(wù)邏輯可以控制Assembler
直接繼承cc.Sprite后可以少些很多代碼
@ccclass
export?default?class?PieceMaskSprite?extends?cc.Sprite?{
????@property(cc.Vec2)
????bgOffset:?cc.Vec2?=?cc.Vec2.ZERO;
????//?參數(shù)傳遞給assembler,在設(shè)置完所有參數(shù)后調(diào)用
????//?也可以在bgOffset?setter方法內(nèi)主動傳值,需要調(diào)用setVertsDirty()使頂點(diǎn)數(shù)據(jù)重算
????public?FlushProperties()?{
????????let?assembler:?PieceMaskAssembler?=?this._assembler;
????????if?(!assembler)
????????????return;
????????assembler.bgOffset?=?this.bgOffset;
????????this.setVertsDirty();
????}
????_resetAssembler?()?{
????????this.setVertsDirty();
????????let?assembler?=?this._assembler?=?new?PieceMaskAssembler();
????????this.FlushProperties();
????????assembler.init(this);
????}
}
Effect (shader)
滾動效果非常簡單,這里只貼出片元著色器代碼 紋理滾動通過v_uv1.xy控制方向和速度
CCProgram?fs?%{
??precision?highp?float;
??#include?
??#include?
??in?vec2?v_uv0;
??in?vec2?v_uv1;
??uniform?sampler2D?texture;
??void?main()
??{
????vec2?uv?=?v_uv0.xy;
????float?tx?=?cc_time.x?*?v_uv1.x;
????float?ty?=?cc_time.x?*?v_uv1.y;
????uv.x?=?fract(uv.x?-?tx);
????uv.y?=?fract(uv.y?+?ty);
????vec4?col?=?texture(texture,?uv);
????gl_FragColor?=?col;
??}
}%
將RenderComponent組建掛在到對應(yīng)節(jié)點(diǎn)上即可使用。至此,一個簡單的自定義頂點(diǎn)格式達(dá)到合批目的的功能就實現(xiàn)了!
Demo地址
demo基于Cocos Creator 2.4.0 (2.4會是Cocos Creator 2D的最后一個版本,也是LTS版本,大家趕緊用起來吧?。?/p>
如果小伙伴覺得這個Demo對自己有幫助,記得star哦~^_^~
https://github.com/caogtaa/CCBatchingTricks
寫在后面
實際項目中可以靈活利用自定義頂點(diǎn)格式,達(dá)到給shader傳參的目的,同時不會打斷合批。當(dāng)然想要實現(xiàn)合批渲染,還有其他前置條件要滿足,包括節(jié)點(diǎn)層級關(guān)系、合圖、紋理狀態(tài)等,這些在論壇其他帖子有詳細(xì)討論。
有錯誤的地方歡迎指正
GT贊賞碼更多精彩原文鏈接: ?https://forum.cocos.org/t/demo/95087 作者: GT





