原文链接:  https://forum.cocos.org/t/demo/95087
作者: GT
排版整理: 白玉无冰

背景

自定义渲染可以实现很多酷炫的 shader 特效,目前常用的有两种方法:

  • 创建自定义材质,给材质增加参数。这个参数会作为 uniform 变量传入 shader 由于渲染合批要求材质参数保持一致,所以如果大量对象使用自定义材质时,并且材质参数各不相同,是无法进行合批渲染的,一个对象占一个 draw call

  • 创建自定义 assembler,在顶点数据输入渲染管道前修改它的值 这种方式比较灵活,如果需要输入更多自定义参数,标准的顶点格式就不够用了

本文介绍另一种方法,即能让shader获得自定义参数,又能让自定义材质合批渲染。这种方法就是自定义顶点格式

Assembler详解

Assembler是实现本文相关功能的核心类,先简单回顾一下官方文档里介绍的内容 https://docs.cocos.com/creator/manual/zh/advanced-topics/custom-render.html

Assembler 中必须要定义 updateRenderDatafillBuffers 方法 前者需要更新准备顶点数据,后者则是将准备好的顶点数据填充进 VetexBufferIndiceBuffer

2D渲染中,Assember2D类是一个重要的基础类,最常用的cc.Sprite的各种模式(Simple,平铺,九宫格)在内部都对应了不同的Assembler派生类。同样是一个四边形的节点,不同的Assembler可以将其转化成不同数量的顶点实现不同的渲染效果

  • Simple模式下是常规的四边形,有4个顶点。(利用这个可以实现渐变色效果)

  • 平铺模式下Assembler根据纹理的重复次数对节点进行“拆碎”,相当于每重复一次就产生1个四边形。(利用这个可以实现顶点动画之水纹旗子)

  • 九宫格模式下Assembler将节点拆分为9个四边形,每个四边形对应纹理上的一个“格子”

fillBuffers源码解读

先看看Assembler2D是如何实现 fillBuffers 的 源码位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/assembler-2d.js

    fillBuffers (comp, renderer) {// 如果节点的世界坐标发生变化,重新从当前节点的世界坐标计算一次顶点数据if (renderer.worldMatDirty) {this.updateWorldVerts(comp);}// 获取准备好的顶点数据// vData包含pos、uv、color数据// iData包含三角剖分后的顶点索引数据let renderData = this._renderData;let vData = renderData.vDatas[0];let iData = renderData.iDatas[0];// 获取顶点缓存// getBuffer()方法后面会被我们重载,以便获得支持自定义顶点格式的缓存let buffer = this.getBuffer(renderer);// 获取当前节点的顶点数据对应最终buffer的偏移量// 可以简单理解为当前节点和其他同格式节点的数据,都将按顺序追加到这个大buffer里let offsetInfo = buffer.request(this.verticesCount, this.indicesCount);// fill verticeslet vertexOffset = offsetInfo.byteOffset >> 2,vbuf = buffer._vData;// 将准备好的vData拷贝到VetexBuffer里。这里会判断如果buffer装不下了,vData会被截断一部分// 通常是因为节点数量太多导致的,从下个节点开始会使用新的buffer,也就是重新开一个合批// 当前节点的数据被截断后,则只能被渲染一部分(推测)if (vData.length + vertexOffset > vbuf.length) {vbuf.set(vData.subarray(0, vbuf.length - vertexOffset), vertexOffset);} else {vbuf.set(vData, vertexOffset);}// 将准备好的iData拷贝到IndiceBuffer里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];}}

思考

Q: 为什么要需要准备顶点数据,而不是在fillBuffer()方法内直接计算后填入buffer?
A: 因为fillBuffer()每帧都会被调用,是热点代码,需要关注效率。但是顶点数据不是每一帧都会更新,可以预先计算

Q: 实现自定义顶点格式需要修改fillBuffer()方法吗?
A: 不需要,fillBuffer()是简单的字节流拷贝,只关心数据长度,不关心数据内容

Q: 顶点数据包含哪些内容?如何计算?
A: 见下文

顶点数据格式描述

最常用的顶点格式是 vfmtPosUvColor ,也是Assembler2D默认使用的格式。https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/webgl/vertex-format.js

var vfmtPosUvColor = new gfx.VertexFormat([// 节点的世界坐标,占2个float32{ name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },// 节点的纹理uv坐标,占2个float32// 如果节点使用了独立的纹理(未合图),这里的uv值通常是0或1// 合图后的纹理,这里的uv对应其在图集里的相对位置,取值范围在[0,1)内{ name: gfx.ATTR_UV0, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },// 节点颜色值,cc.Sprite组件上可以设置。占4个uint8 = 1个float32{ name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true },
]);

顶点格式和shader顶点着色器的attribute变量对应关系如下

CCProgram vs %{precision highp float;#include <cc-global>#include <cc-local>// 对应vfmtPosUvColor结构里的3个字段// 注意这里a_position是vec3类型,但是vfmtPosUvColor对其自定义了2个float长度。所以a_position.z = 0in vec3 a_position;          // gfx.ATTR_POSITIONin vec2 a_uv0;   // gfx.ATTR_UV0in vec4 a_color;   // gfx.ATTR_COLOR// ...void main () {// ...}
}%

看下Assembler2D里的属性和顶点格式的对应关系 源码位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/assembler-2d.js

cc.js.addon(Assembler2D.prototype, {// vfmtPosUvColor 结构占5个float32floatsPerVert: 5,// 一个四边形4个顶点verticesCount: 4,// 一个四边形按照对角拆分成2个三角形,2*3 = 6个顶点索引indicesCount: 6,// uv的值在vfmtPosUvColor结构里下标从2开始算uvOffset: 2,// color的值在vfmtPosUvColor结构里下标从4开始算colorOffset: 4,
});

除了默认属性之外,这里还定义了一批可以使用的attribute变量。https://github.com/cocos-creator/engine/blob/master/cocos2d/renderer/gfx/enums.js

顶点数据计算

了解了上面的顶点格式之后,顶点数据无非就是计算 posuvcolor几个值。在Assembler里分别有 updateVerts() updateUVs()``updateColor() 方法来准备这几个值,并且临时存储在Assembler自己分配的数组里。顶点数据存在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数据// renderData.iDatas用来存储顶点索引数据this._renderData = new RenderData();this._renderData.init(this);this.initData();this.initLocal();}get verticesFloats () {// 当前节点的所有顶点数据总大小return this.verticesCount * this.floatsPerVert;}initData () {let data = this._renderData;// 创建一个足够长的空间用来存储顶点数据 & 顶点索引数据// 这个方法内部会初始化顶点索引数据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) {// 获取当前cc.Sprite组件设置的spriteFrame对应的uv// uv数组长度=8,分别表示4个顶点的uv.x, uv.y// 按照左下、右下、左上、右上的顺序存储,注意这里的顺序和顶点索引的数据需要对应上let uv = sprite._spriteFrame.uv;let uvOffset = this.uvOffset;  // 之前提到过vfmtPosUvColor结构里uvOffset = 2let floatsPerVert = this.floatsPerVert; // floatsPerVert = vfmtPosUvColor结构大小 = 5let verts = this._renderData.vDatas[0];for (let i = 0; i < 4; i++) {// 2个1组取uv数据,写入renderData.vDatas对应位置let srcOffset = i * 2;let dstOffset = floatsPerVert * i + uvOffset;verts[dstOffset] = uv[srcOffset];verts[dstOffset + 1] = uv[srcOffset + 1];}}

updateColor() 和 updateVerts() 的具体实现这里不再分析。

由于上面多次提到了顶点索引,对于不了解它的同学需要再单独解释一下。

理解顶点索引

除了posuvcolor数据之外,为什么还需要计算顶点索引数据?我们发送给GPU的数据,实际上表示的是三角形,而不是四边形。一个四边形需要剖分成2个三角形然后传给GPU。在4个顶点数据的基础上,三角形的描述信息单独存在IndiceBuffer (即renderData.iDatas)里,IndiceBuffer里的每个值表示其对应顶点数据的下标。通过索引可以合并掉多个三角形中相同的顶点数据,减少总数据大小。

常规四边形的索引数据准备,源码位置:https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/webgl/render-data.js

    initQuadIndices(indices) {// 按照上述剖分方式得到的下标: [0,1,2] [1,3,2]// 6个一组(对应1个四边形)生成索引数据let count = indices.length / 6;for (let i = 0, idx = 0; i < count; 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;}}

顶点格式自定义

现在进入正题,基于上面对Assembler以及相关类的解读,顶点格式自定义需要做这么几件事

  1. 定义新的格式

  2. 用新的格式准备足够长的renderData

  3. renderData对应位置写入自定义数据

  4. fillBuffers()方法内将renderData数据正确刷入buffer

// 自定义顶点格式,在vfmtPosUvColor基础上,加入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 }         // 自定义数据
]);const VEC2_ZERO = cc.Vec2.ZERO;export default class PieceMaskAssembler extends GTSimpleSpriteAssembler2D {// 根据自定义顶点格式,调整下述常量verticesCount = 4;indicesCount = 6;uvOffset = 2;uv1Offset = 4;floatsPerVert = 6;// 自定义数据,将被写入uv1的位置public moveSpeed: cc.Vec2 = VEC2_ZERO;initData() {let data = this._renderData;// createFlexData支持创建指定格式的renderDatadata.createFlexData(0, this.verticesCount, this.indicesCount, this.getVfmt());// createFlexData不会填充顶点索引信息,手动补充一下let indices = data.iDatas[0];let count = indices.length / 6;for (let i = 0, idx = 0; i < count; 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()方式提供出去,除了当前assembler,render-flow的其他地方也会用到getVfmt() {return vfmtCustom;}// 重载getBuffer(), 返回一个能容纳自定义顶点数据的buffer// 默认fillBuffers()方法中会调用到getBuffer() {return cc.renderer._handle.getBuffer("mesh", this.getVfmt());}// pos数据没有变化,不用重载// updateVerts(sprite) {// }updateUVs(sprite) {// uv0调用基类方法写入super.updateUVs(sprite);// 填入自己的uv1数据// ...// 方法类似uv0写入,详见Demo// https://github.com/caogtaa/CCBatchingTricks}updateColor(sprite) {// 由于已经去掉了color字段,这里重载原方法,并且不做任何事}
}

上面用到的 GTSimpleSpriteAssembler2D基类代码大部分参考官方cc.Sprite的实现。

双uv坐标shader案例

这里将通过额外的一组uv数据,实现纹理滚动的方向 & 速度控制。

用材质参数的方法同样能够实现这个效果,但是无法做到合批渲染。基于上面给出的Assembler类,继续完善一下其他辅助类

材质

材质只用于关联effect,没有额外逻辑,也不需要新建uniform变量

RenderComponent

RenderComponent这里的角色:

  • 创建对应的Assembler

  • Assembler传参。让业务逻辑可以控制Assembler

直接继承cc.Sprite后可以少些很多代码

@ccclass
export default class PieceMaskSprite extends cc.Sprite {@property(cc.Vec2)bgOffset: cc.Vec2 = cc.Vec2.ZERO;// 参数传递给assembler,在设置完所有参数后调用// 也可以在bgOffset setter方法内主动传值,需要调用setVertsDirty()使顶点数据重算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 <cc-global>#include <cc-local>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组建挂在到对应节点上即可使用。至此,一个简单的自定义顶点格式达到合批目的的功能就实现了!

小结

Demo地址

demo基于Cocos Creator 2.4.0 (2.4会是Cocos Creator 2D的最后一个版本,也是LTS版本,大家赶紧用起来吧!)

如果小伙伴觉得这个Demo对自己有帮助,记得star哦~^_^~

https://github.com/caogtaa/CCBatchingTricks

写在后面

实际项目中可以灵活利用自定义顶点格式,达到给shader传参的目的,同时不会打断合批。当然想要实现合批渲染,还有其他前置条件要满足,包括节点层级关系、合图、纹理状态等,这些在论坛其他帖子有详细讨论。

有错误的地方欢迎指正

GT赞赏码

原文链接:  https://forum.cocos.org/t/demo/95087 作者: GT

更多精彩




【GT】Assembler 源码解读及使用 !Cocos Creator!相关推荐

  1. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

  2. Bert系列(三)——源码解读之Pre-train

    https://www.jianshu.com/p/22e462f01d8c pre-train是迁移学习的基础,虽然Google已经发布了各种预训练好的模型,而且因为资源消耗巨大,自己再预训练也不现 ...

  3. linux下free源码,linux命令free源码解读:Procps free.c

    linux命令free源码解读 linux命令free源码解读:Procps free.c 作者:isayme 发布时间:September 26, 2011 分类:Linux 我们讨论的是linux ...

  4. nodeJS之eventproxy源码解读

    1.源码缩影 !(function (name, definition) { var hasDefine = typeof define === 'function', //检查上下文环境是否为AMD ...

  5. PyTorch 源码解读之即时编译篇

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 作者丨OpenMMLab 来源丨https://zhuanlan.zhihu.com/ ...

  6. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  7. Feflow 源码解读

    Feflow 源码解读 Feflow(Front-end flow)是腾讯IVWEB团队的前端工程化解决方案,致力于改善多类型项目的开发流程中的规范和非业务相关的问题,可以让开发者将绝大部分精力集中在 ...

  8. spring-session源码解读 sesion

    2019独角兽企业重金招聘Python工程师标准>>> spring-session源码解读 sesion 博客分类: java spring 摘要: session通用策略 Ses ...

  9. 前端日报-20160527-underscore 源码解读

    underscore 源码解读 API文档浏览器 JavaScript 中加号操作符细节 抛弃 jQuery,拥抱原生 JS 从 0 开始学习 GitHub 系列之「加入 GitHub」 js实现克隆 ...

  10. php service locator,Yii源码解读-服务定位器(ServiceLocator)

    SL的目的也是解耦,并且非常适合基于服务和组件的应用. Service Locator充当了一个运行时的链接器的角色,可以在运行时动态地修改一个类所要选用的服务, 而不必对类作任何的修改. 一个类可以 ...

最新文章

  1. Nat. Mach. Intell. | 少量数据的生成式分子设计
  2. ubuntu12.04下android开发环境搭建两个注意事项
  3. etcd部署集群的三种方式
  4. bootstrap菜单展开收起_基于bootstrap的后台左侧导航菜单和点击二级菜单刷新二级页面时候菜单展开显示当前菜单...
  5. LeetCode-----二维数组中的查找
  6. Web Audio API
  7. 最优化方法——Newton方法
  8. Apache Jemeter 参数化
  9. reviewboard mysql_ReviewBoard 的安装和使用
  10. 国家码信道顺从表(含数据库和配置)
  11. 「面试必背」Elasticsearch面试题(收藏)
  12. 用Python学《微积分B》(单调性与极值,凸性与拐点)
  13. 如何在充满不确定性的当下探索未来?
  14. Gitea配置文件说明
  15. Excel每页都打印表头
  16. SNS网店软文推广法
  17. “集五福”瓜分20亿!互联网巨头扎堆春节红包大战,暗藏啥玄机?
  18. 2015年3月12日
  19. 报错-Error creating bean with name xxx: Unsatisfied dependency expressed through field xxx
  20. 优化移动端邮件营销效果办法

热门文章

  1. 大唐凌烟阁开国廿四将
  2. HNOI 2015 落忆枫音 题解
  3. 中国新材料产业十四五竞争格局及产销趋势研究报告2021年版
  4. 使用DevOps强化敏捷(上)
  5. 每日一词20190301——比例尺和地图比例尺
  6. 谷粒商城-商城业务-商品详情
  7. uni-App 商品详情轮播图
  8. linux wgei目录,近 100 个 Linux 常用命令大全
  9. 经典小游戏开发思路和算法之拼图(1)
  10. 智能窗帘系统c语言程序,基于51单片机的智能窗帘控制系统设计与实现毕业论文.doc...