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

2023-10-15 03:40

本文主要是介绍硬核引擎魔改!实现 Graphics 2D3D 带纹理绘制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

  • 2D 带纹理画出各种路径等效果;

  • 3D 可用程序高自由度绘画出各种路径、图形等效果;

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

41265a42fbb764ed9ec5ee08369abcbb.png

2D 效果预览

b4c46a9b9477a62c47c06f73c7b3a44d.gif

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);
}
}
  • 最终效果

49bf0daf2d29bf177708a4df121bcc9e.png

3a0877670feab57adfde0ea04ef39979.png

cce6b827f6f65a13e4fff8fd4742a063.png

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

二、3D 可带可不带纹理

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

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

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

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

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

837a11edd9f015b3c58599bd325990f6.png

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

44e995956d74b42c180f90bc35381f36.png

  • 最终效果

19d6c38347fe9207af28bddbe130aae5.gif

a0934d0b72969438d50cec440f90eb45.gif

0bb82337b90c43cfe016017117e2dff6.gif


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

完整源码

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

论坛专贴

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

往期精彩

0874254254a48c82b7f477a9031a566a.png

55a9bcd9c4a8168ce984a0633a839a0d.png

333c1ce12b447b36af95644921119cfc.png

d83ada0c6e325b3ed746b23c9b033f72.gif

这篇关于硬核引擎魔改!实现 Graphics 2D3D 带纹理绘制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/215101

相关文章

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分