前言
在最近的工作中开发了一个类似蓝图的功能模块,需要在蓝图中实现连线功能,以连接对应的蓝图面板,起初设想的是这样的:
然而,实际情况是这样的:
其中每个线段都对应了一个 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 中顶点生成部分,只修改和刷新变化部分,复用中间数据和对最终渲染结果进行精确填充,既能使用较少的模型完成渲染又可以尽量少的重刷数据,从而实现较好的优化效果;
优化步骤
- 复用路径和点对象
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 方法中只会对新增的路径计算顶点信息并填充,所以接下来我们还要进一步的修改顶点数据的计算和填充方法,让它可以支持只处理更新过的路径;
- 复用顶点数据,减少顶点计算
画线顶点数据的计算装配是在 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);
},
}
- 优化模型填充
在准备好顶点数据后,我们还差最后一步,就是把准备好的顶点数据填充到子模型再提交;模型填充和提交代码在 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()
}
- 优化顶点结构
除此之外,在实际应用中还发现,填充的每个顶点数据中都包含了颜色信息,除非是每条线段的颜色不一样,否则必定会有颜色信息是重复的(特别是绘制贝塞尔曲线时,一条曲线需要使用大量的顶点来描述,它们必然是使用同一个颜色的);当前的应用只会使用到几个颜色,所以可以通过:删除顶点的颜色信息来减少顶点数据和填充内容的大小,同时修改对应的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 顶点部分不需要修改,省略
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 环境下测试通过,并在实际工作中应用;
示例代码正在整理,稍后更新;