查看原文
其他

硬核引擎魔改!实现 Graphics 2D&3D 带纹理绘制

希格斯玻色子 COCOS 2022-06-10

Cocos Creator 自带的 Graphics 组件给我们提供了一系列绘画接口,不过有时候想实现一些特殊需求,难免就需要自己想办法。今天就和大家分享一下自己继承 graphics 后魔改的一些简单功能。魔改之后将实现:

  • 2D 带纹理画出各种路径等效果;
  • 3D 可用程序高自由度绘画出各种路径、图形等效果;
  • 具体可应用在:游戏中实时绘制角色路径线、魔法画笔可以用特殊纹理画画、3D 游戏中实时生成玩家想要的 3D 物体/根据游戏玩法高自由度生成 3D 物体等等。

2D 效果预览

3D 效果预览

本次所用引擎版本为 Cocos Creator 3.4.1,以下是我的魔改思路,同样的思路也可以用在魔改其它组件上。
一、2D 带纹理

引擎源码虽然简洁整齐但看着依然云里雾里,咋办呢?没有捷径,努力啃下来吧,万一成了呢!而当我顺着文件夹分门别类地看下来后,还真没有想象中的困难(因为以前看过一个超高耦合度项目代码,简直是地狱级阅读难度,后来再复杂的代码逻辑都觉得不太吃力了)。

阅读引擎源码可以知道 graphics 的绘制原理:

  • graphics.ts 中实现绘制组件,通过各种接口收集绘制信息;

  • graphics-assembler.ts 中实现顶点渲染数据组装器;
  • impl.ts 中实现绘制路径点的存储和加工。

那么,该如何进行魔改呢?我想到了继承+重载 graphics 组件的方法:

  • graphics 组件的 _flushAssembler 方法是获得顶点渲染数据组装器的地方,因此可以在这个方法里实现对顶点数据渲染组装器的重写。
  • 想加纹理,则在 shader 里需要线的长度,线长和线宽两个数据即可组成 uv 坐标来获取纹理的像素点。

  • onLoad 方法里,将路径点存储加工器 impl.ts 替换为自己实现的路径点存储加工器。

思路有了,开工!

首先继承 graphics 组件,然后对照着源码重载 _flushAssembler 方法。考虑到 v3.x 版本的 assembler 方法是一个对象不是类不能继承,干脆一不做二不休新建一个对象,很羞耻地命名为 superGraphicsAssembler,将原组装器的方法都赋值给新组装器。

因为我们的目的是给组件的顶点数据加一个线长数据,所以需要在组装器中实现路径数据整理功能的 _flattenPaths 方法里搞事情。

先把它重写了(其实就是将这个方法源码复制过来改改),至于会报错的地方,该导入的导入,导入不了的就用比如 __private._cocos_2d_assembler_graphics_webgl_impl__Impl 这种方式声明它的类型。如果还不行的,就 any 类型。

如果有需要 new 出对象的类型又无法从引擎导入,就重新写这个类,比如 const dPos = new Point(p1.x, p1.y); 这一行,就可以将引擎的 Point 类复制过来改个名就叫 Point2,顺便在这个点类里面加上自己的料 lineLength 线长。然后用 pts[0][“lineLength”] = lineLength; 这种方式,将从初始点到每个点的线长计算出来赋值给路径点数据,到了组装顶点数据的时候用相同方法取到即可。

到这里我们的路径点都带上了线长数据,但是光有路径点也没用啊,还需将这个数据加到顶点数据里传至 shader 中去用。所以我们盯上了组装连线顶点渲染数据 _expandStroke 方法。将它再复制过来改改,将调用设置顶点数据 _vSet 方法的地方都多传一个参数 lineLength——没错,就是我们刚刚从路径点对象里取出的线长。

但紧接着我们发现,_vSet 方法里设置数据是通过设置 buffer 数组里的对应下标的元素值来达成的,因此接下来还需修改一下顶点数据格式,让这个增加新成员后的 buffer 所存储的数据,能被渲染管道下游的 shader 读懂。找一找,它的顶点数据格式是在 graphics.ts 文件里定义的:

const attributes = vfmtPosColor.concat([
new Attribute(‘a_dist’, Format.R32F),
]);

vfmtPosColor 上跳转进去一看,原来是:

export const vfmtPosColor = [
new Attribute(AttributeName.ATTR_POSITION, Format.RGB32F),
new Attribute(AttributeName.ATTR_COLOR, Format.RGBA32F),
];

buffer 数组里每 new 一条都是多加一个数据。a_position 里32位 float 的三个数组元素为一个数据,a_color 里32位 float 的四个数组元素为一个数据,在 graphics 文件中新加的 a_dist 里32位 float 的一个数组元素为一个数据。相信有同学已经发现规律了(卖个关子,请接着往下看)。

我们复制过来给它多加一条数据:

const attributes2 = UIVertexFormat.vfmtPosColor.concat([
new gfx.Attribute(‘a_dist’, gfx.Format.R32F),
new gfx.Attribute(‘a_line’,gfx.Format.R32F),
]);

对,就是线长,一个32位 float 元素就够用了,再多浪费。然后我们将源码中用到 attributes 的代码都赋值过来改为自己定义的 attributes2,并且将用到这俩的代码也这样做:

const componentPerVertex = getComponentPerVertex(attributes);
const stride = getAttributeStride(attributes);

至于这俩是个啥?在源码中跳进去生成函数看看就知道是单个顶点数据的总占用元素个数和总字节长度。

现在我们回到 _vSet 函数里。此时我们发现修改了顶点数据格式后,就有空位可以放线长数据进 buffer 里了,于是在 vData[dataOffset++] = distance; 下面再加一行 vData[dataOffset++] = lineLong;

除此之外,_vSet 函数改了后所有用到 _vSet 函数的地方都要改一下以加上线长数据,所以我们将源码中所有用到 _vSet 函数的方法都复制过来加上线长参数。

这回是真完美了!

现在可以试试效果了吧?不,别着急,只改了渲染管道的上游让管子更粗,下游的管子还没兼容要爆管呢。本着尽职尽责的原则将下游的 shader 管子也复制 graphics 的默认 shader 新建一个「材质和 Effect」),随意命名为 pathLine,在 shader 的顶点函数里效仿:

in float a_dist;
out float v_dist;

也写一个:

in float a_line;
out float v_line;

这个 a_line 就是 shader 管道承接上游渲染数据组装器里的那个 a_line 线长数据(就像水管一样接过来),out 的意思是让它流入下个水管(片元着色函数),当然这两个水管中间也有两截水管承接(顶点数据连三角、光栅化将每个三角切割成无数像素格子),这中间两截水管不用理会只要知道它俩的作用就行。然后就在片元着色水管里将线宽和线长组成 uv 坐标来取纹理的像素:

vec2 uv0 = vec2(v_line,(v_dist + 1.)/2.);
uv0.x = fract(uv0.x);
uv0.y = fract(uv0.y);
o *= CCSampleWithAlphaSeparated(texture1,uv0);

这纹理哪来的,现在就加上:

properties:
texture1: { value: white }

在片元着色水管里加上 uniform sampler2D texture1;,然后在自己定义的 SuperGraphics 里加上设置材质和纹理的地方:

@ccclass(‘SuperGraphics’)
export class SuperGraphics extends Graphics {
@property(Texture2D)
lineTexture:Texture2D = null;
@property(Material)
myMat:Material = null;

onLoad(){
if (this.lineTexture){
this.lineWidth = this.lineTexture.height;
lineC = this.lineWidth/ (this.lineTexture.height * 2 * this.lineTexture.width);
}
if (this.myMat){
this.setMaterial(this.myMat,0);
if (this.lineTexture)
this.getMaterial(0).setProperty(“texture1”,this.lineTexture);
}

super.onLoad();
}

onEnable(){
if (this.myMat){
this.setMaterial(this.myMat,0);
if (this.lineTexture)
this.getMaterial(0).setProperty(“texture1”,this.lineTexture);
}
}

  • 最终效果

注:当前代码如果绘制使用 close 会导致显示异常,偷懒方法可以不用 close

二、3D 可带可不带纹理

有了之前的经验,接下来升级实验一下将 graphics 魔改为 3D 的。

我们需要给它加一个 z 坐标,那就在之前的基础上给 graphics 加上 moveTo3dlineTo3d 等等接口,然后模仿源码将路径点存储加工类 impl.ts 复制过来重写一下,将有 2D 坐标的地方都照猫画虎的加上 z 坐标。

在我们 Graphics3D 组件的 onLoad 里将原 impl 对象的数据赋值到新 G3DImpl 对象里,然后将源码中所有用到 impl 对象的代码都复制过来改为用自己的 G3DImpl 对象。

由于顶点数据结构里 a_position 一直都有 z 坐标存储位置,所以就用上面加线长后的顶点数据结构了。最后就可以得到用程序来高自由度 3D 画图的快乐!

  • 3D 绘制组件附带的材质可勾选深度写入和深度测试,效果更好。

  • 3D 绘制组件可带纹理可不带纹理

  • 最终效果





欢迎点击文末【阅读原文】前往论坛专贴一起交流讨论,项目完整源码放在开源仓库供各位下载,希望能对大家有所帮助!


完整源码

https://gitee.com/XiGeSiBoSeZi/study.git


论坛专贴

https://forum.cocos.org/t/topic/131608


往期精彩

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存