更多笔记,请关注公众号:
感谢群内大佬 honmono 的分享,也欢迎同学们入群交流
QQ群:521643513
Cocos引擎源码位于CocosCreator.app/Contents/Resources/engine/cocos2d/(Mac版本), 以下使用CocosEngine代替路径
效果展示
1, 自定义渲染组件—TexturePlus
2, 实现自定义多边形渲染范围
===========
3, 图片切割效果
===========
4, 图片破碎效果
===========
5, 碎片掉落效果
===========
浅析Assember
源码路径位于 CocosEngine/core/renderer/assembler.js
对于Assembler的个人理解, Assembler中的核心是顶点数据, 每个顶点都有位置,uv信息, 通过改变顶点的位置和uv信息, 就可以实现一些例如 只显示图片的一部分区域,图片部分区域拉伸(九宫格也是基于这个实现的). 且修改顶点数据并不会打断合批,性能有保障.
而在Cocos中每一个渲染组件,例如cc.Sprite,cc.Label,cc.Graphics等, 它们都继承于RenderComponent, 且都有一个对应的Assembler从而实现不同的渲染效果.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QSLuvXr-1609931515309)(./cocos-dir-1.png)];
Assembler提供两个静态方法, register和init. register方法将渲染组件和Assembler绑定,init方法用于初始化Assembler.
自定义Assembler
自定义Assembler的核心就是将顶点数据填充到renderdata中
Assembler的实现可以参考 Assembler2D, 源码位于CocosEngine/core/renderer/assembler-2d.js
// 将这5个属性注入Assembler2D.prototype内
cc.js.addon(Assembler2D.prototype, {
floatsPerVert: 5, // 一个顶点所需的空间 xy两个,uv两个,color一个
verticesCount: 4, // 顶点个数
indicesCount: 6, // 三角形顶点个数
uvOffset: 2, // uv在buffer中的偏移量,
colorOffset: 4, // color在buffer中的偏移量
// 格式如 x|y|u|v|color|x|y|u|v|color|x|y|u|v|color|......
// 当然也可以自定义格式
});
export default class Assembler2D extends Assembler {
constructor () {
super();
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);
}
// 更新顶点颜色信息
updateColor (comp, color) {}
// 更新顶点坐标信息
updateWorldVerts (comp) {}
// 将renderdata中的数据填充到buffer中, 也计算填充了三角形顶点索引
fillBuffers (comp, renderer) {}
}
cc.Assembler2D = Assembler2D;
简单概况一下就是: 计算每个顶点的position,uv,color(可以不要),以及三角形顶点索引,然后赋值到buffer内就行了,提供三角形顶点索引 是因为gpu绘制图像都是绘制了一个个三角形而成, 换而言之三角形是最小绘制单元而顶点内的信息只需要在顶点发生变化时才需要更新
编辑器内可编辑的多边形区域的实现, 可以看之前那篇MaskPlus, 里面实现了如何实现一个自定义多边形遮罩以及自定义Gizmo.
计算顶点的世界坐标
用上图显示图片的自定义多边形区域为例, 首先计算好多边形顶点数组polygon, polygon是基于结点坐标的, 且按逆时针排序.
计算过程可以直接参考我之前写的maskplus
protected updateWorldVerts(comp: TexturePlus) {
if (CC_NATIVERENDERER) {
this.updateWorldVertsNative(comp);
} else {
this.updateWorldVertsWebGL(comp);
}
}
protected updateWorldVertsWebGL(comp: TexturePlus) {
let verts = this._renderData.vDatas[0];
let matrix: cc.Mat4 = comp.node['_worldMatrix'];
let matrixm = matrix.m,
a = matrixm[0], b = matrixm[1], c = matrixm[4], d = matrixm[5],
tx = matrixm[12], ty = matrixm[13];
let justTranslate = a === 1 && b === 0 && c === 0 && d === 1;
let floatsPerVert = this.floatsPerVert;
if (justTranslate) {
let polygon = comp.polygon;
for(let i=0; i<polygon.length; i++) {
verts[i * floatsPerVert] = polygon[i].x + tx;
verts[i * floatsPerVert+1] = polygon[i].y + ty;
}
} else {
let polygon = comp.polygon;
for(let i=0; i<polygon.length; i++) {
verts[i * floatsPerVert] = a * polygon[i].x + c * polygon[i].y + tx;
verts[i * floatsPerVert+1] = b * polygon[i].x + d * polygon[i].y + ty;
}
}
}
代码很简单, 其中tx, ty是结点对应世界坐标的偏移量, 代码中polygon是更具结点坐标得到的, 这里进行了一次计算
a,b,c,d是cocos为Node计算的旋转值
计算顶点的uv坐标
uv坐标的计算可以有几种方式, 可以做成局部拉伸的效果, 也可以做成裁剪效果, 这里就以裁剪效果为例.
uv坐标取值区间是0~1, 对应的是texture的宽和高, 按比例取的.
取texture的高是反着取的, 因为cocos的世界坐标原点在左下角.
/** 计算uv, 锚点都是中心 */
public static computeUv(points: cc.Vec2[], width: number, height: number) {
let uvs: cc.Vec2[] = [];
for(const p of points) {
let x = MathUtils.clamp(0, 1, (p.x + width/2) / width);
let y = MathUtils.clamp(0, 1, 1. - (p.y + height/2) / height);
uvs.push(cc.v2(x, y));
}
return uvs;
}
将uv填充到renderdata内
/** 更新uv */
protected updateUVs(comp: TexturePlus) {
let uvOffset = this.uvOffset;
let floatsPerVert = this.floatsPerVert;
let verts = this._renderData.vDatas[0];
let uvs = [];
if(comp.texture) {
uvs = CommonUtils.computeUv(comp.polygon, comp.texture.width, comp.texture.height)
}
let polygon = comp.polygon;
for(let i=0; i<polygon.length; i++) {
let dstOffset = floatsPerVert * i + uvOffset;
verts[dstOffset] = uvs[i].x;
verts[dstOffset + 1] = uvs[i].y;
}
}
计算顶点color
/** 填充顶点的color */
public updateColor(comp: TexturePlus, color: number) {
let uintVerts = this._renderData.uintVDatas[0];
if(!uintVerts) return ;
color = color != null ? color : comp.node.color['_val'];
let floatsPerVert = this.floatsPerVert;
let colorOffset = this.colorOffset;
let polygon = comp.polygon;
for(let i=0; i<polygon.length; i++) {
uintVerts[colorOffset + i * floatsPerVert] = color;
}
}
这里可能会造成疑惑的是color填充进的是uintVDatas, 而之前的uv和position都是填充进的vDatas
阅读render-data源码可以知道, uintVerts和vDatas是共享的同一段buffer
/** render-data.js */
updateMesh (index, vertices, indices) {
this.vDatas[index] = vertices;
// 将vertices.buffer当成参数传入, 他们共享同一段buffer
this.uintVDatas[index] = new Uint32Array(vertices.buffer, 0, vertices.length);
this.iDatas[index] = indices;
this.meshCount = this.vDatas.length;
},
createData (index, verticesFloats, indicesCount) {
let vertices = new Float32Array(verticesFloats);
let indices = new Uint16Array(indicesCount);
this.updateMesh(index, vertices, indices);
},
计算三角形顶点索引
因为三角形是最小的绘制单元, 所以需要将多边形转换为一个个三角形让gpu渲染.
计算三角形我这里选择的方式是耳切法, 针对耳切法的实现网上已经有有很多了,我这里也不再赘叙.
ps: 我也是看了白玉无冰大佬的帖子才了解的
链接地址: https://forum.cocos.org/t/mask-mesh-gizmo/88288
代码也不复杂, 需要注意的是points是有序的, 且是逆时针方向排列, 所以只需要循环判断是不是耳朵且三角形内没有包含其他点就行, 找到后切掉在继续判断即可
// 将多边形分解为多个三角形
public static splitPolygonByTriangle(points: cc.Vec2[]): number[] {
if(points.length <= 3) return [0, 1, 2];
let pointMap: {[key: string]: number} = {}; // point与idx的映射
for(let i=0; i<points.length; i++) {
let p = points[i];
pointMap[`${p.x}-${p.y}`] = i;
}
const getIdx = (p: cc.Vec2) => {
return pointMap[`${p.x}-${p.y}`]
}
points = points.concat([]);
let idxs: number[] = [];
let index = 0;
while(points.length > 3) {
let p1 = points[(index) % points.length]
, p2 = points[(index+1) % points.length]
, p3 = points[(index+2) % points.length];
let splitPoint = (index+1) % points.length;
let v1 = p2.sub(p1);
let v2 = p3.sub(p2);
if(v1.cross(v2) < 0) { // 是一个凹角, 寻找下一个
index = (index + 1) % points.length;
continue;
}
let hasPoint = false;
for(const p of points) {
if(p != p1 && p != p2 && p != p3 && this.isInTriangle(p, p1, p2 ,p3)) {
hasPoint = true;
break;
}
}
if(hasPoint) { // 当前三角形包含其他点, 寻找下一个
index = (index + 1) % points.length;
continue;
}
// 找到了耳朵, 切掉
idxs.push(getIdx(p1), getIdx(p2), getIdx(p3));
points.splice(splitPoint, 1);
}
for(const p of points) {
idxs.push(getIdx(p));
}
return idxs;
}
// 判断一个点是否在三角形内
public static isInTriangle(point: cc.Vec2, triA: cc.Vec2, triB: cc.Vec2, triC: cc.Vec2) {
let AB = triB.sub(triA), AC = triC.sub(triA), BC = triC.sub(triB), AD = point.sub(triA), BD = point.sub(triB);
//@ts-ignore
return (AB.cross(AC) >= 0 ^ AB.cross(AD) < 0) && (AB.cross(AC) >= 0 ^ AC.cross(AD) >= 0) && (BC.cross(AB) > 0 ^ BC.cross(BD) >= 0);
}
在assembler中的使用
将这一步的计算不要放到fillBuffer内, 因为并不需要每帧计算, 只需要在修改顶点时计算即可
this.indicesArr = CommonUtils.splitPolygonByTriangle(comp.polygon);
----------------------------------------------------------------------------
let ins = this.indicesArr;
for(let i=0; i<iData.length; i++) {
ibuf[indiceOffset++] = vertexId + ins[i];
}
修改顶点后重新分配render-data
public initData() {
let data = this._renderData;
data.createQuadData(0, this.verticesFloats, this.indicesCount);
}
public resetData(comp: TexturePlus) {
let points = comp.polygon;
if(!points || points.length < 3) return ;
this.verticesCount = points.length;
this.indicesCount = this.verticesCount + (this.verticesCount - 3) * 2;
this._renderData.clear();
this.initData();
}
填充buffer
//每帧都会被调用
fillBuffers(comp: TexturePlus, renderer) {
if (renderer.worldMatDirty) {
this.updateWorldVerts(comp);
}
let renderData = this._renderData;
// vData里包含 pos, uv, color数据, iData中包含三角形顶点索引
let vData = renderData.vDatas[0];
let iData = renderData.iDatas[0];
let buffer = this.getBuffer();
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; // vertexId是已经在buffer里的顶点数,也是当前顶点序号的基数
let ins = this.indicesArr;
for(let i=0; i<iData.length; i++) {
ibuf[indiceOffset++] = vertexId + ins[i];
}
}
图片切割实现
图片切割其实就是做了线段和多边形的切割计算, 原理就不多说了, 直接上代码.
https://github.com/kirikayakazuto/CocosCreator_UIFrameWork/blob/SplitTexture/assets/Script/test/UISplitTexture.ts
源码地址:
TexturePlus: https://github.com/kirikayakazuto/CocosCreator_UIFrameWork/blob/SplitTexture/assets/Script/Common/Components/TexturePlus.ts
TextureAssembler: https://github.com/kirikayakazuto/CocosCreator_UIFrameWork/blob/SplitTexture/assets/Script/Common/Assemblers/TextureAssembler.ts