最近在想虽然shader可以解决2d中的很多特效需求但是会存在一个问题就是drawcall会增加,这样会带来性能隐患,所以看了一下自定义渲染,整了一个正多边形图片的实现:效果如下图:
具体实现是创建一个渲染组件继承自cc.RenderComponent,并且需要一个自定义的assembler,集成的RenderComponent需要 重写updateRenaderData,updateUvs,updateVerts,updateWorldVerts,fillBuffers,其实可以参考 sprite源码里面的simple.js文件,我也是抄了一部分:
先看看 mySprite.ts:
import MyAssembler from "./myAssembler";
const {ccclass, property,mixins,executeInEditMode} = cc._decorator;
@ccclass
@mixins(cc.BlendFunc)
@executeInEditMode
export default class MySprite extends cc.RenderComponent {
@property(cc.Texture2D)
_texture: cc.Texture2D = null;
@property(cc.Integer)
_eadgeCount: number = 6;
@property(cc.Integer)
public get eadgeCount() {
return this._eadgeCount;
}
public set eadgeCount(value: number) {
this._eadgeCount = value;
this.calculatePoints();
this.setVertsDirty();
}
private _assembler: cc.Assembler = null;
public spriteFrame: cc.SpriteFrame = null;
public points: cc.Vec2[] = [];
@property(cc.Texture2D)
public get texture() {
return this._texture;
}
public set texture(tex: cc.Texture2D) {
this._texture = tex;
this.spriteFrame = new cc.SpriteFrame();
this.spriteFrame.setTexture(tex);
this.node.width = this._texture.width;
this.node.height = this._texture.height;
if(CC_EDITOR) {
Editor.log('farme is ',this.spriteFrame);
}
this._updateMaterial();
}
public calculatePoints() {
const points = [];
const r = this.node.width > this.node.height ? this.node.height / 2 : this.node.width / 2;
const angle = Math.PI * 2 / this.eadgeCount;
let startPoint = cc.v2(-r * Math.cos(angle),-r * Math.sin(angle));
points.push(startPoint);
let startp = startPoint
for(let i = 1; i < this.eadgeCount; i++) {
let point = startp.rotate(angle * i);
points.push(point);
}
this.points = points;
}
@property({type: cc.Enum(cc.macro.BlendFactor), override: true})
srcBlendFactor: cc.macro.BlendFactor = cc.macro.BlendFactor.SRC_ALPHA;
@property({type: cc.Enum(cc.macro.BlendFactor), override: true})
dstBlendFactor: cc.macro.BlendFactor = cc.macro.BlendFactor.ONE_MINUS_SRC_ALPHA;
// __preload会调用
public _resetAssembler() {
let assembler = this._assembler = new MyAssembler();
assembler.init(this);
this._updateColor();
this.setVertsDirty();
}
public _updateMaterial() {
let texture = this._texture;
let material = this.getMaterial(0);
if(material) {
if(material.getDefine("USE_TEXTURE") !== undefined) {
material.define("USE_TEXTURE", true);
}
material.setProperty("texture", texture);
}
// this['__proto__']._updateBlendFunc.call(this);
cc.BlendFunc.prototype._updateMaterial.call(this);
this.setVertsDirty();
}
start () {
let p1 = cc.v2(-100,200);
let p3 = cc.v2(300,200);
let p2 = cc.v2(0,0);
let p4 = cc.v2(300,0);
const isIn = this.isInTriangle(p4,p1,p2,p3);
console.log('isIn is ',isIn);
}
/**
* 监测一个点是否在三角形内
* @param {cc.Vec2} point
* @param {cc.Vec2} a
* @param {cc.Vec2} b
* @param {cc.Vec2} c
*/
isInTriangle(point: cc.Vec2,a: cc.Vec2,b: cc.Vec2,c: cc.Vec2) {
const isEqualSign = (n1,n2) => {
return (n1 >= 0 && n2 >= 0) || (n1 <= 0 && n2 <= 0);
}
const ca = a.sub(c);
const cb = b.sub(c);
const cd = point.sub(c);
const ac = c.sub(a);
const ab = b.sub(a);
const ad = point.sub(a);
const ba = a.sub(b);
const bc = c.sub(b);
const bd = point.sub(b);
const cCross = isEqualSign(ca.cross(cb),ca.cross(cd));
const aCross = isEqualSign(ab.cross(ac),ab.cross(ad));
const bCross = isEqualSign(bc.cross(ba),bc.cross(bd));
return cCross && aCross && bCross;
}
update (dt) {
// this._updateMaterial();
}
}
myAssembler.ts:
import MySprite from "./mySprite";
const {ccclass, property} = cc._decorator;
const gfx = cc['gfx'];
// 顶点格式 -> 位置 UV, 颜色
let vfmtPosUvColor = 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 },
{ name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true },
]);
@ccclass
export default class MyAssembler extends cc.Assembler {
public floatsPerVert = 5;
public verticesCount = 4;
public indicesCount = 6;
public uvOffset = 2;
public colorOffset = 4;
public _local = [];
public _renderData: cc.RenderData = null;
public calculatePoints(comp: MySprite) {
const points = [];
const r = comp.node.width > comp.node.height ? comp.node.height / 2 : comp.node.width / 2;
const angle = Math.PI * 2 / comp.eadgeCount;
let startPoint = cc.v2(-r * Math.cos(angle),-r * Math.sin(angle));
let startp = startPoint;
points.push(startp);
for(let i = 1; i < comp.eadgeCount; i++) {
let point = startp.rotate(angle);
points.push(point);
startp = point;
}
console.log('points is ',points);
// this.points = points;
return points;
}
constructor() {
super();
this._renderData = new cc.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);
}
initLocal() {
this._local = [];
this._local.length = 4;
}
updateUVs(comp: MySprite) {
const points = this.calculatePoints(comp);
const uvs = this.computeUv(points,comp.node.width,comp.node.height);
// const uv = [
// 0,1,
// 1,1,
// 0,0,
// 1,0,
// ];
// 更新uv数据
console.log('uvs is ',uvs);
const verts = this._renderData.vDatas[0];
for(let i = 0; i < uvs.length; i++) {
let dstOffset = this.floatsPerVert * i + this.uvOffset;
verts[dstOffset] = uvs[i].x;
verts[dstOffset + 1] = uvs[i].y;
}
}
computeUv(points: cc.Vec2[],width: number,height: number) {
let uvs: cc.Vec2[] = [];
for(let p of points) {
let x = this.clamp(0,1,(p.x + width / 2) / width);
let y = this.clamp(0,1,1 - (p.y + height / 2) / height);
uvs.push(cc.v2(x,y));
}
return uvs;
}
clamp(min,max,value) {
if(value < min) {
return min;
} else if(value > max) {
return max;
}
return value;
}
getIndicesByPoints(points: cc.Vec2[]) {
if(points.length === 3) return [0,1,2];
let pointIndexMap: {[key: string]: number} = {};
for(let i = 0; i < points.length; i++) {
let p = points[i];
pointIndexMap[`${p.x.toFixed(2)}-${p.y.toFixed(2)}`] = i;
}
let index = 0;
let indexs = [];
points = points.concat([]);
while(points.length > 3) {
// 点的长度大于3继续分割
let p1 = points[index % points.length];
let p2 = points[(index + 1) % points.length];
let p3 = points[(index + 2) % points.length];
let p21 = p1.sub(p2);
let p23 = p3.sub(p2);
// 分割点
let splitPoint = p2;
if(p23.cross(p21) < 0) {
// p2点是凹点
index = (index + 1) % points.length;
continue;
}
let isIn = false;
for(let p of points) {
if(p !== p1 && p !== p2 && p !== p3 && this.isInTriangle(p,p1,p2,p3)) {
isIn = true;
break;
}
}
if(isIn) {
index = (index + 1) % points.length;
continue;
}
const splitPointIndex = (index + 1) % points.length;
// 切耳
points.splice(splitPointIndex,1);
indexs.push(pointIndexMap[`${p1.x.toFixed(2)}-${p1.y.toFixed(2)}`]);
indexs.push(pointIndexMap[`${p2.x.toFixed(2)}-${p2.y.toFixed(2)}`]);
indexs.push(pointIndexMap[`${p3.x.toFixed(2)}-${p3.y.toFixed(2)}`]);
}
for(let i = 0; i < points.length; i++) {
let p = points[i];
indexs.push(pointIndexMap[`${p.x.toFixed(2)}-${p.y.toFixed(2)}`]);
}
return indexs;
}
/**
* 监测一个点是否在三角形内
* @param {cc.Vec2} point
* @param {cc.Vec2} a
* @param {cc.Vec2} b
* @param {cc.Vec2} c
*/
isInTriangle(point: cc.Vec2,a: cc.Vec2,b: cc.Vec2,c: cc.Vec2) {
const isEqualSign = (n1,n2) => {
return (n1 > 0 && n2 > 0) || (n1 < 0 && n2 < 0);
}
const ca = a.sub(c);
const cb = b.sub(c);
const cd = point.sub(c);
const ac = c.sub(a);
const ab = b.sub(a);
const ad = point.sub(a);
const ba = a.sub(b);
const bc = c.sub(b);
const bd = point.sub(b);
const cCross = isEqualSign(ca.cross(cb),ca.cross(cd));
const aCross = isEqualSign(ab.cross(ac),ab.cross(ad));
const bCross = isEqualSign(bc.cross(ba),bc.cross(bd));
return cCross && aCross && bCross;
}
public initQuadIndices(indices: number[], arr: number[]) {
for(let i = 0; i < arr.length; i++) {
indices[i] = arr[i];
}
}
updateVerts(comp: MySprite) {
const points = this.calculatePoints(comp);
const indicesArr = this.getIndicesByPoints(points);
console.log('indicesArr is ',indicesArr);
this.initQuadIndices(this._renderData.iDatas[0], indicesArr);
this.updateWorldVerts(comp);
}
updateColor (comp, color) {
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;
for (let i = colorOffset, l = uintVerts.length; i < l; i += floatsPerVert) {
uintVerts[i] = color;
}
}
updateWorldVerts (comp: MySprite) {
let verts = this._renderData.vDatas[0];
let matrix = 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 floatsPerVert = this.floatsPerVert;
let vertexOffset = 0;
let justTranslate = a === 1 && b === 0 && c === 0 && d === 1;
const points = this.calculatePoints(comp);
console.log('points is ',points);
if (justTranslate) {
for(let i = 0; i < points.length; i++) {
verts[i * floatsPerVert] = points[i].x + tx;
verts[i * floatsPerVert + 1] = points[i].y + ty;
}
} else {
for(let i = 0; i < points.length; i++) {
verts[i * floatsPerVert] = a * points[i].x + c * points[i].y + tx;
verts[i * floatsPerVert + 1] = b * points[i].x + d * points[i].y + ty;
}
}
}
getBuffer () {
return cc.renderer._handle.getBuffer('mesh',vfmtPosUvColor);
}
fillBuffers (comp, renderer) {
if (renderer.worldMatDirty) {
this.updateWorldVerts(comp);
}
let renderData = this._renderData;
let vData = renderData.vDatas[0];
let iData = renderData.iDatas[0];
let buffer = this.getBuffer(renderer);
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];
}
}
public resetData(comp: MySprite) {
let points = this.calculatePoints(comp);
if(!points || points.length < 3) return;
// 顶点个数根据points的长度
this.verticesCount = points.length;
// 索引个数
this.indicesCount = this.verticesCount + (this.verticesCount - 3) * 2;
this._renderData['clear']();
this.initData();
}
packToDynamicAtlas(comp: MySprite, frame: any) {
if (CC_TEST) return;
if (!frame._original && cc.dynamicAtlasManager && frame._texture.packable) {
let packedFrame = cc.dynamicAtlasManager.insertSpriteFrame(frame);
if (packedFrame) {
frame._setDynamicAtlasFrame(packedFrame);
}
}
let material = comp['_materials'][0];
if (!material) return;
if (material.getProperty('texture') !== frame._texture) {
// texture was packed to dynamic atlas, should update uvs
comp._vertsDirty = true;
comp._updateMaterial();
}
}
updateRenderData (comp) {
if(comp._vertsDirty) {
this.resetData(comp);
this.updateUVs(comp);
this.updateVerts(comp);
this.updateColor(comp,null);
comp._vertsDirty = false;
}
}
}