大数跨境
0
0

(附源码)2000线段秒变丝滑|Graphics渲染性能深度优化

(附源码)2000线段秒变丝滑|Graphics渲染性能深度优化 EcoCosy优可丝
2025-06-06
0
导读:据说,很多爆款小游戏都需要它!

小编:这篇硬核文章发表于 2022 年,至今受用。由于最近社区里对 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 数据类相对简单,所以直接改个名从引擎代码里复制过来
    exportclass ImplPoint extends Vec2 {...}

    // 因为需要复用 Path 数据,所以对路径对象略做修改
    exportclass 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
            }
            ....
        }
    }

    exportclass 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 {
            ....
            returnthis.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
    }

    exportclass 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) {
                returnnull;
            }

            // 定义自定义的 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) {
            ....    // 原流程不变
            elseif (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(thisthis.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++;
        },
    }

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

修改后:

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

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顶点部分不需要修改,省略

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

最终呈现

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

示例代码请前往原文下载。

关注 Cocos 引擎官方公众号

第一时间掌握干货信息


【声明】内容源于网络
0
0
EcoCosy优可丝
1234
内容 703
粉丝 0
EcoCosy优可丝 1234
总阅读52
粉丝0
内容703