魔改 Graphics 实现高性能画线渲染

前言

在最近的工作中开发了一个类似蓝图的功能模块,需要在蓝图中实现连线功能,以连接对应的蓝图面板,起初设想的是这样的:

然而,实际情况是这样的:

其中每个线段都对应了一个 Graphics 组件,每个 Graphics 组件又会生成一个模型(Model)来完成渲染,每个模型则会生成一个子模型并引用一个网格(Mesh)来填充所需要渲染的顶点与索引缓冲数据;当前业务包含2000个左右的线段,也就是说全部刷新一次需要渲染2000个模型;这也使得刷新和拖动变得巨卡无比;

原理分析

曲线段绘制使用的是引擎自带的 Graphics(它还可以绘制其它几种基本的几何体) 组件,它继承于Renderable2D(所有支持渲染的 2D 组件的基类),除了提交装配的渲染数据(GraphicsAssembler)外,曲线绘制所需要实现的路径点绘制、存储和加工都是由(Impl)来完成的;

每一条需要绘制的线段都由二个或多个路径对象(Path)组成,每个路径又是由多个点对象(Point)组成,通常绘制一条线段的代码是:

// 清除准备
graphics.clear()
// 移动到线段起始位置
graphics.moveTo(x1, y1)
// 绘制直线到指定位置
// graphics.lineTo(x2, y2)
// 或者绘制贝塞尔曲线到指定位置(需要指定两个控制点)
graphics.bezierCurveTo(mx1, my1, mx2, my2, x2, y2);
// 提交当前路径,生成渲染数据
graphics.stroke()
// 如果是其它形状(比如矩形),还可以对形状进行进一步的填充操作(本文仅限线段绘制,其它暂时省略)
// graphics.fill()

在执行 moveTo 时会增加一个路径对象(Path)和一个点对象(Point),在执行 bezierCurveTo(lineTo)时会增加一组点对象(Point),到执行 stroke 后,会将点补齐成线段并生成相应渲染的顶点数据,渲染数据生成的步骤为:

相应的每条线段从开始绘制到完成渲染,会生成的数据包括:路径和点对象(Path、Point)、渲染顶点(MeshRenderData)、子模型(Model)三部分;

从上图和引擎源码可知,我们可以采用一个 Graphics 绘制多条线段生成同一个 Model,从而减少模型总量的优化方法;但这又会带来另一个弊端,当其中某一条线段需要重绘时,就会导致该 Graphics 上的全部线段重绘;而且在调用 clear 方法时会把当前 Graphics 生成的数据都当成脏数据全部清除,然后再重新生成;

所以接下来我们将对 Graphics 及相关的路径顶点装配代码进行魔改,总体思路是将所有的路径生成到同一个 Model 的多个 SubModel 上,记录 Path 和 Point 的变化,重载 Impl 的相关方法,然后重写 GraphicsAssembler 中顶点生成部分,只修改和刷新变化部分,复用中间数据和对最终渲染结果进行精确填充,既能使用较少的模型完成渲染又可以尽量少的重刷数据,从而实现较好的优化效果;

优化步骤

  1. 复用路径和点对象

Graphics 中与绘制线段相关的增加路径和点的操作都在 moveTo 和 lineTo 这两个重要方法中,而最终管理是由 Impl 类来完成的,它定义了一个 Path 数组用来存储路径,一个Point 用来存储点;

首先新建一个继承于 Graphics 的类( 因为主要是针对画线的优化,所以给它取名为 GraphicsLine),然后在创建路径数据时为它赋一个 ID 并记录到 Map 中,在需要重绘时根据 ID 找到并重绘指定路径,完成路径对象的复用,具体如下:

let uid: number = 1000           // 路径ID起始值

// 重新定义路径点对象,Point 数据类相对简单,所以直接改个名从引擎代码里复制过来
export class ImplPoint extends Vec2 {...}

// 因为需要复用 Path 数据,所以对路径对象略做修改
export class ImplPath {
    public id: number                        // 新增唯一ID属性,用来标记路径
    public change: boolean = false           // 变化标记,用于表明该路径是否发生变化
    ....                                     // 省略未修改的代码
    constructor() {
        this.reset()
    }
    
    public reset(isNewUID: boolean = true) { // 为 reset 方法增加创建参数
        if (isNewUID) {            // 如果是新建路径,则产生一个新ID
            this.id = uid
            uid++
        }
        else {                     // 如果是复用路径,则重置变化标记
            this.change = true
        }
        ....
    }
}

export class GraphicsLine extends Graphics {
    private _pathMap: { [id: number]: ImplPath } = {}       // 以id为索引的路径 MAP
    constructor() {
        super();
        // 因为引擎没有开放 Impl 对象无法继承,所以这里采用动态重载的办法重写接口
        this.overloadGraphicsImpl()  
    }

    // 重载 Graphics.Impl 方法
    protected overloadGraphicsImpl() {
        let pathMap = this._pathMap
        this.impl._addPath = function () {
            ....
            if (!path) {
                path = new ImplPath() // Path();  // 使用新定义的路径对象替换
                this.paths.push(path);
            } else {
                path.reset();
            }
            ....
            pathMap[path.id] = path                // 增加到 map
            return path;
        }

        this.impl.addPoint = function (x: number, y: number, flags: __private._cocos_2d_assembler_graphics_types__PointFlags) {
            ....
            let pt: ImplPoint = new ImplPoint(x, y);    // 使用新定义的路径点对象替换
            ....
        }

        // 新增方法,在路径需要变换时调用它而不是调用 moveTo
        this.impl.changeMoveTo = function (id: number, x: number, y: number) {
            let path = pathMap[id]        // 这里只重置指定ID的路径
            if (path) {
                path.reset(false)
                this._curPath = path
            }
            else {
                this._addPath()
            }
            ....
        }
    }
    // 重载该方法,返回当前使用路径的ID,在首次画线时调用
    public moveTo(x: number, y: number): number {
        ....
        return this.impl._curPath.id
    }
    // 新增方法,重复画线时调用该方法
    public changeMoveTo(id: number, x: number, y: number) {
        if (!this.impl) {
            return;
        }
        this.impl.changeMoveTo(id, x, y);
    }
}

经过上述修改,在首次绘制时调用 moveTo 获得路径ID,后续重绘时可以调用 changeMoveTo 并传入路径ID就可以对路径对象进行复用;这时候还不能直观的看到效果,因为在 stroke 方法中只会对新增的路径计算顶点信息并填充,所以接下来我们还要进一步的修改顶点数据的计算和填充方法,让它可以支持只处理更新过的路径;

  1. 复用顶点数据,减少顶点计算

画线顶点数据的计算装配是在 GraphicsAssembler 装配器中完成的,这个装配器是以实例方式定义,引擎并没有开放出来,所以直接从引擎复制一份代码再改个名(graphicsAssemblerLine)来用;相对应的需要修改一下 GraphicsLine 中的装配器获取方法,以便能获取到我们自定义的装配器实例;以及增加变换刷新及变换刷新结束的对应方法:

export class GraphicsLine extends Graphics {
    protected _flushAssembler () {
        // 使用自定义的装配器代替原来的
        const assembler = graphicsAssemblerLine; // Graphics.Assembler.getAssembler(this);
        if (this._assembler !== assembler) {
                this._assembler = assembler;
        }
    }
    
    // 画线变化时调用该方法刷新(拖动中循环调用)
    public changeStroke() {
        ....
        // 调用自定义方法刷新变化的线段路径
        (this._assembler as IAssembler).changeStroke!(this);
        //@ts-ignore   // 每次刷新完成后,重置路径点 change 标记为 false
        this.impl.clearPointChange()
    }
    
    // 画线变化结束时调用该方法重置path及重刷顶点数据(拖动结束后调用一次)
    public changeStrokeEnd() {
        if (!this.impl) return;
        if (!this.changeRenderData) return;

        if (!this._assembler) {
            this._flushAssembler();
        }

        this.isChangeFirst = true        // 重置拖动刷新首次填充标记
        //@ts-ignore
        this.impl.clearPointChange();    // 重置路径点 change 标记
        //@ts-ignore
        this.impl.removeRenderData(this.changeRenderData);    // 删除自定义的 renderData

        this.changeRenderData = null;
        this.changeRenderIndex = -1;

        this._isDrawing = true;
        this._isNeedUploadData = true;        // 调用自定义方法重置变化的线段路径及顶点信息
        (this._assembler as IAssembler).changeStrokeEnd(this);
    }
}

每次 moveTo 生成一组 Path 路径后,stroke 再根据路径生成一个或多个 MeshRenderData,只要不调用 clear 方法,这两组数据都是增量递增的过程,如下图:

当调用 changeMoveTo 来改变部分路径时(拖动变换),这块的优化思路是:新建一个 MeshRenderData 然后遍历所有的 path 找出 change 标记为 true 的路径,重算顶点信息并保存到新建的 renderData 中,最后再生成新的子模型并填充提交(因为每个 renderData 对应一个 subModel 子模型);

这里需要注意的是,在重新计算改变的 path 路径前,需要在原有的 renderData 中先清除该 path 原来的顶点信息,然后在重新填充对应的子模型(相当于清除已修改的画线内容);

当调用 changeStrokeEnd 来结束改变时(拖动结束),需要把标记过的 path 重算顶点信息并增量保存到默认的 renderDataList 中,同时释放新建的 MeshRenderData,最后重置 path 标记,让绘制渲染走回到原有流程;

这里有一个问题,就是每次拖动结束后变化所涉及的 path,对应的顶点数据并不是保存在原有的位置;这是因为拖动前路径点的数据长度与拖动后的并不相同(贝塞尔曲线生成的路径点不同),无法写回原有位置只能走原流程写到 _renderDataList 最后的 renderData 中,所以当拖动次数增多后 _renderDataList 中的 renderData 也会出现一定的冗余;此时需要重调用 clear 全部清除数据重新生成;

此时需要再给 ImplPath 和 GraphicsLine 增加几个标记:

export class ImplPath {
    // 增加刷新标记,用于标记 changeMoveTo 到 changeStrokeEnd 之间的所有刷新过的路径
    public refresh: boolean = false    
    // 保存该路径对应的顶点数据所在的位置和对象(在 _expandStroke 方法中记录)
    public pathVertexInfo = { start: 0, end: 0 }
    public pathIndexInfo = { start: 0, end: 0 }
    public pathMeshBuffer: MeshRenderData = null
}

export class GraphicsLine extends Graphics {
    // 增加 renderData 变量和索引,用于保存每次拖动产生的顶点信息
    public changeRenderData: MeshRenderData = null
    public changeRenderIndex: number = -1
    // 用于标记是否每次拖动的第一次刷新(因为每次拖动前需要先清除原有的路径,详见前文)
    private isChangeFirst: boolean = true
}

为了不影响原有的画线流程,除了新增的(上图标红的方法名称)和部分方法需要修改的,我们把所有与画线相关的方法全部复制一份,方法名称加上change前缀,用来处理需要重刷的顶点计算;
export const graphicsAssemblerLine: IAssembler = {
useModel: true,

    // ************************** 这里方法直接复制不修改 *********************/
    // updateRenderData、fillBuffers、renderIA、fill、end、_expandFill、_calculateJoins、
    // _flattenPaths、_chooseBevel、_buttCapStart、_buttCapEnd、_roundCapStart、_roundCapEnd、
    // _roundJoin、_bevelJoin、stroke、getRenderData
    
    // ************************** 修改原来的方法 *********************/
    // 在走引擎标准的刷新流程时,记录下 path 与顶点数据对象的对应关系
    _expandStroke(graphics: Graphics, isChange: boolean = false) {
       ....
       // 可以看到,循环是从 _impl.pathOffset 开始
        for (let i = _impl.pathOffset, l = _impl.pathLength; i < l; i++) {
            ....
            // 保存该路径在顶点数据中的起始值
            //@ts-ignore
            path.pathVertexInfo.start = meshBuffer.vertexStart;
            //@ts-ignore
            path.pathIndexInfo.start = meshBuffer.indexStart;
            ....
            meshBuffer.indexStart = indicesOffset;
            // 保存该路径在顶点数据中的结束值,主要是用于在变换开始时清空原有的顶点数据
            //@ts-ignore
            path.pathVertexInfo.end = meshBuffer.vertexStart;
            //@ts-ignore
            path.pathIndexInfo.end = meshBuffer.indexStart;
            //@ts-ignore
            path.pathMeshBuffer = meshBuffer        // 保存该路径对应的 meshBuffer
        }
        _renderData = null;
        _impl = null;
    },
    
    // ************************** 新增的方法 *********************/
    // 变换刷新基本上是复制了 stroke 的流程
    // 只是把顶点生成的过程由: 增量生成顶点数据 改成了: 从全部的 path 中找变换过的生成顶点数据
    changeStroke(graphics: Graphics) {
        Color.copy(_curColor, graphics.strokeColor);
        if (!graphics.impl) {
            return;
        }

        // 如果是变换的首次刷新,把原来变换的顶点数据给清除
        //@ts-ignore
        let icClear = (graphics.isChangeFirst) ? this._clearChangeOld(graphics) : true;
        if (icClear) {
            // 全部用复制修改的方法替换原流程
            this._flattenChangePaths!(graphics.impl);
            this._expandChangeStroke!(graphics);

            this.end(graphics);
        }
    },

    _clearChangeOld(graphics: Graphics): boolean {
        let ret = false
        const paths = graphics.impl.paths;

        // 查找所有路径,找出变换的路径清空顶点数据
        for (let i = 0, l = graphics.impl.pathLength; i < l; i++) {
            const path = paths[i];
            //@ts-ignore
            if (path.change && path.pathMeshBuffer) {
                ret = true

                //@ts-ignore
                let meshBuffer = path.pathMeshBuffer
                //@ts-ignore
                if (meshBuffer === graphics.changeRenderData) continue

                // 清除数据
                const vData = meshBuffer.vData;
                //@ts-ignore
                for (let j = path.pathVertexInfo.start * attrBytes; j < path.pathVertexInfo.end * attrBytes; j++) {
                    vData[j] = 0;
                }

                const iData = meshBuffer.iData
                //@ts-ignore
                for (let j = path.pathIndexInfo.start; j < path.pathIndexInfo.end; j++) {
                    iData[j] = 0
                }
                
                // 因为顶点数据已改变,所以需要重置该标记,让 _render 时可以重新填充数据到子模型
                meshBuffer.lastFilledVertex = 0
            }
        }

        return ret
    },
    
    // 复制于原流程的 _flattenPaths 方法
    _flattenChangePaths(impl: __private._cocos_2d_assembler_graphics_webgl_impl__Impl) {
        ....
        // 原流程循环是从增量位置 _impl.pathOffset 开始,因为我们是需要在所有路径中查找,所以从0开始
        for (let i = 0, l = impl.pathLength; i < l; i++) {
            const path = paths[i];
            const pts = path.points;

            //@ts-ignore
            if (!path.change) continue   // 只重算改变过的路径
            .... 
        }
    },
    
    // 复制于原流程的 _expandStroke 方法
    _expandChangeStroke(graphics: Graphics) {
        ....
        // 计算连接区域
        this._calculateChangeJoins(_impl, w, lineJoin, miterLimit);

        // 该方法原代码太长,所以拆分成了两个方法 _calculateChangeVertex 和 _doChangeMashBuffer
        // Calculate max vertex usage.
        let vertexCount = this._calculateChangeVertex(graphics, nCap, paths);
        if (vertexCount > 0) {
            // 因为后续流程都是操作 _renderData ,所以我们可以使用自已新建变化数据来代替
            let meshBuffer = _renderData = this.getChangeRenderData(graphics, vertexCount)
            if (meshBuffer) {
                for (let i = 0, l = _impl.pathLength; i < l; i++) {
                    //@ts-ignore
                    if (paths[i].change) {
                        this._doChangeMashBuffer(graphics, nCap, paths[i], meshBuffer)
                    }
                }
            }
        }

        _renderData = null;
        _impl = null;
    },

    // 复制于原流程的 _calculateJoins 方法
    _calculateChangeJoins(impl: __private._cocos_2d_assembler_graphics_webgl_impl__Impl, w: number, lineJoin: __private._cocos_2d_assembler_graphics_types__LineJoin, miterLimit: number) {
        ....
        // Calculate which joins needs extra vertices to append, and gather vertex count.
        const paths = impl.paths;
        for (let i = 0, l = impl.pathLength; i < l; i++) {    // 从0开始查找全部路径
            const path = paths[i];

            //@ts-ignore
            if (!path.change) continue        // 只重算改变过的路径
        ....
        }
    },

    // 将原本从 _impl.getRenderDataList 获取 renderData 的过程调整为获取 graphics.changeRenderData
    getChangeRenderData(graphics: Graphics, vertexCount: number) {
        if (!_impl) {
            return null;
        }

        // 定义自定义的 renderData 保存路径变换后的顶点信息(不影响原数据)
        //@ts-ignore
        let renderData = graphics.changeRenderData;    
        if (!renderData) {
            renderData = _impl.requestRenderData();     // _renderData 重新赋给新的渲染对象
            //@ts-ignore
            graphics.changeRenderData = renderData      // 保存到变量 changeRenderData
            //@ts-ignore
            graphics.changeRenderIndex = _impl.getRenderDataList().length - 1
            //@ts-ignore
            renderData.name = "changeData"
        }

        let meshBuffer = renderData;
        const maxVertexCount = meshBuffer ? meshBuffer.vertexStart + vertexCount : 0;
        if (meshBuffer && meshBuffer.vertexCount < maxVertexCount) {
            meshBuffer.request(vertexCount, vertexCount * 3);
        }
        
        if (meshBuffer) {        // 每次变换都清空
            if (vertexCount <= MAX_VERTEX && vertexCount * 3 <= MAX_INDICES) {
                // 清除数据
                const vData = meshBuffer.vData;
                for (let j = 0; j < meshBuffer.vertexStart * attrBytes; j++) {
                    vData[j] = 0;
                }
                const iData = meshBuffer.iData
                for (let j = 0; j < meshBuffer.indexStart; j++) {
                    iData[j] = 0
                }
                
                meshBuffer.vertexStart = 0
                meshBuffer.indexStart = 0
            }
        }

        return renderData;
    },

    // 计算修改路径的顶点长度,复制于原流程的 _expandStroke 方法的顶点计算部分
    _calculateChangeVertex(graphics: Graphics, nCap: number, paths: __private._cocos_2d_assembler_graphics_webgl_impl__Path[], isRefresh: boolean = false): number {
        ....
        // 重新计算变换的顶点数据
        for (let i = 0, l = _impl.pathLength; i < l; i++) {
            const path = paths[i];
            const pointsLength = path.points.length;

            // 只计算改变过的路径
            //@ts-ignore
            isCalculate = (isRefresh) ? path.refresh : path.change
            if (!isCalculate) continue
        ....
        }
        return vertexCount
    },

    // 生成修改路径的顶点数据,复制于原流程的 _expandStroke 方法的顶点生成部分
    _doChangeMashBuffer(graphics: Graphics, nCap: number, path: __private._cocos_2d_assembler_graphics_webgl_impl__Path, meshBuffer: MeshRenderData) {
        ....
    },
    
    // 变换刷新结束,需要把已经改变过的 path 重新计算并保存到原顶点数据对象中(把变换过的路径数据还回去)
    changeStrokeEnd(graphics: Graphics) {
        _impl = graphics.impl;
        if (!_impl) return;

        const w = graphics.lineWidth * 0.5;
        const nCap = curveDivs(w, PI, _impl.tessTol);
        const paths = graphics.impl.paths;
        // 计算变换的顶点数
        let vertexCount = this._calculateChangeVertex(graphics, nCap, paths, true);
        // 走原流程去申请顶点数据对象
        const meshBuffer: MeshRenderData | null = _renderData = this.getRenderData!(graphics, vertexCount);
        if (meshBuffer) {
            for (let i = 0, l = _impl.pathLength; i < l; i++) {
                const path = paths[i];

                // change 标记每次刷新都会重置,refresh 只有变换结束才会重置
                //@ts-ignore 
                if (path.refresh) {     
                    // 路径已经刷新,所以需要重新保存路径的顶点位置和对象,以便下次变换开始时能找到正确的位置
                    //@ts-ignore
                    path.pathVertexInfo.start = meshBuffer.vertexStart;
                    //@ts-ignore
                    path.pathIndexInfo.start = meshBuffer.indexStart;

                    this._doChangeMashBuffer(graphics, nCap, path, meshBuffer, true)

                    //@ts-ignore
                    path.pathVertexInfo.end = meshBuffer.vertexStart;
                    //@ts-ignore
                    path.pathIndexInfo.end = meshBuffer.indexStart;
                    //@ts-ignore
                    path.pathMeshBuffer = meshBuffer        // 保存该路径对应的 meshBuffer
                    //@ts-ignore
                    path.refresh = false    // 清理刷新标记
                }
            }
        }
        _renderData = null;
        _impl = null;

        this.end(graphics);
    },
}
  1. 优化模型填充

在准备好顶点数据后,我们还差最后一步,就是把准备好的顶点数据填充到子模型再提交;模型填充和提交代码在 GraphicsLine 的基类中,所以我们需要重写对应 _render 方法:

export class GraphicsLine extends Graphics {
    private _isNeedUploadChangeData: boolean = false    // 重刷改变数据标记
    // 重载基类 _render 方法
    protected _render(render: __private._cocos_2d_renderer_i_batcher__IBatcher) {
        ....    // 原流程不变
        else if (this._isNeedUploadChangeData && this.changeRenderData) {  // 填充新变换顶点
            const len = this.model!.subModels.length;
            const idx = this.changeRenderIndex   // 取到之前 impl.getChangeRenderData 保存的顶点数据索引
            if (idx >= len) {
                this.activeSubModel(idx);        // 激活该顶点对应的子模型
            }
            // 如果是变换的首次刷新,则需要重新填充一次已修改的renderData 到对应子模型
            //(因为需要对变换路径的原有顶点数据进行清除)
            if (this.isChangeFirst) {        
                this._uploadData()        
            }
            else {
                this._uploadChangeData(idx)    // 只填充变换顶点
            }
            // 重置标记
            this._isNeedUploadChangeData = false        
            this.isChangeFirst = false
        }

        render.commitModel(this, this.model, this.getMaterialInstance(0));
    }

    // 填充变化的顶点数据到子模型
    private _uploadChangeData(idx: number) {
        const subModelList = this.model.subModels;
        // 指定填充的顶点数据为路径变换的顶点对象
        const renderData = this.changeRenderData;
        // 指定与顶点对象索引对应的子模型,idx = this.changeRenderIndex
        const ia = subModelList[idx].inputAssembler;
        if (renderData.vertexStart !== 0) {
            ....   // 与 _uploadData 中填充方法相同
        }
    }
}

以上,我们已经改造完成了画线的整个流程,在实际应用时按如下方式调用接口,就可以在拖动时实现流畅的画线刷新:

private _lineId: number
start() {
    graphicsLine.clear()
    this._lineId = graphicsLine.moveTo(x1, y1)
    // graphicsLine.lineTo(x2, y2)
    graphicsLine.bezierCurveTo(mx1, my1, mx2, my2, x2, y2);
    graphicsLine.stroke()
}

onTouchMove() {
    graphicsLine.changeMoveTo(this._lineId, x1, y1)
    graphicsLine.bezierCurveTo(mx1, my1, mx2, my2, x2, y2);
    graphicsLine.changeStroke()
}

onTouchEnd() {
    graphicsLine.changeStrokeEnd()
}
  1. 优化顶点结构

除此之外,在实际应用中还发现,填充的每个顶点数据中都包含了颜色信息,除非是每条线段的颜色不一样,否则必定会有颜色信息是重复的(特别是绘制贝塞尔曲线时,一条曲线需要使用大量的顶点来描述,它们必然是使用同一个颜色的);当前的应用只会使用到几个颜色,所以可以通过:删除顶点的颜色信息来减少顶点数据和填充内容的大小,同时修改对应的shader增加固定颜色来代替顶点中的颜色完成渲染:

// 原有顶点信息定义 3 position + 4 color + 1 dist = 8 (Float32)
const attributes = vfmtPosColor.concat([
    new Attribute('a_dist', Format.R32F),
]);

// 新的顶点信息定义,去除颜色信息后 3 position + 1 dist = 4 (Float32)
const vfmtCustom = UIVertexFormat.vfmt.concat([ 
    new gfx.Attribute('a_dist', gfx.Format.R32F),
]);

// 同时对应的常量也需要重新定义
const componentPerVertexEx = UIVertexFormat.getComponentPerVertex(vfmtCustom);
const strideEx = UIVertexFormat.getAttributeStride(vfmtCustom);

// graphicsAssemblerLine 中的顶点长度常量也需要修改一下
const attrBytes = 4; // 8;

由于顶点长度发生变化,所以对应涉及到的顶点计算和填充部分代码都需要对应调整一下:

// 涉及到顶点格式定义,需要调整的方法有:
// impl.requestRenderData
// GraphicsLine.activeSubModel
// GraphicsLine._uploadData

// 涉及写入顶点的操作,都是通过 graphicsAssemblerLine._vSet 方法去实现的,所以这个方法也需要修改一下
graphicsAssemblerLine: IAssembler = {
    _vSet(x: number, y: number, distance = 0) {
        ....
        vData[dataOffset++] = x;
        vData[dataOffset++] = y;
        vData[dataOffset++] = 0;
        // Color.toArray(vData, _curColor, dataOffset);    // 注释颜色信息的写入填充
        // dataOffset += 4;
        vData[dataOffset++] = distance;
        meshBuffer.vertexStart++;
    },
}

从调试的顶点数据中可以看到填充内容的变化(大小节省一半):
修改前:
image

修改后:
image

最后,从引擎目录复制一份 builtin-graphics.effect 到当前工程,修改对应 shader 为它增加一个 LineColor 的颜色参数做为线段渲染的当前颜色,如果需要多显示几个颜色可以多定义几个参数,可以给顶点数据格式增加一个颜色索引,通过颜色索引取指定的颜色来完成;
image

shader 的内容具体修改如下(该示例只定义了一个颜色):
// Effect Syntax Guide: https://github.com/cocos-creator/docs-3d/blob/master/zh/material-system/effect-syntax.md
// 实现自定义连线渲染

CCEffect %{
    ....   # 省略
      properties:   # 新增固定颜色参数
        lineColor: { value: [0.28, 0.28, 0.28, 1.0], editor: { tooltip: "背景颜色", type: color } }
}%

// vs 顶点部分不需要修改,省略

CCProgram fs %{
  .... 
  uniform Constant {          // 开放参数
    vec4 lineColor;           // 背景色
  };
  .... 
  vec4 frag () {
    vec4 o = lineColor; // v_color;    // 使用 shader 设定颜色代替
    // 这行是抄的,加上这行会让线段产生颜色渐变效果
    // vec4 o =  lineColor + (lineEndColor-lineColor)*(sin(1.77*cc_time.x));
    ....
  }
}%

最终呈现

当前优化在 CocosCreator 3.5.2 版本 h5 环境下测试通过,并在实际工作中应用;


示例代码正在整理,稍后更新;

46赞

666我的宝贝

mark,大佬666

大佬大佬,带带我

真大佬,收藏了,肯定用得到

点赞,收藏

这么好的机制,官方为啥默认不支持呢~~~

感觉好厉害的样子,mark

牛逼牛逼牛逼,插眼插眼插眼

满血复活了? :smiley:

建议 cc.Graphics 改名为 cc.Geometry2D,因为它与其它引擎的 Graphics 完全不是一回事。

3赞

mark 等大佬示例代码学习一下

mark 等大佬分享

插眼!!!!!

6666,先插个眼

六六六啊!

出色的。 謝謝!

mark
支持.呢.

戳个眼!戳个眼!戳个眼!戳个眼!

请问大佬有源码分享嘛?