cocos creator 自定义渲染

该博客介绍了如何使用自定义渲染组件来创建2D正多边形图片,以避免过多的drawcall导致的性能问题。作者通过创建一个继承自cc.RenderComponent的渲染组件,并实现特定的assembler,详细展示了计算顶点、更新UV坐标、处理颜色等过程,最终达到减少drawcall并实现自定义图形的效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在想虽然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;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值