硬核引擎魔改!实现 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

相关文章

使用Python实现表格字段智能去重

《使用Python实现表格字段智能去重》在数据分析和处理过程中,数据清洗是一个至关重要的步骤,其中字段去重是一个常见且关键的任务,下面我们看看如何使用Python进行表格字段智能去重吧... 目录一、引言二、数据重复问题的常见场景与影响三、python在数据清洗中的优势四、基于Python的表格字段智能去重

Spring AI集成DeepSeek实现流式输出的操作方法

《SpringAI集成DeepSeek实现流式输出的操作方法》本文介绍了如何在SpringBoot中使用Sse(Server-SentEvents)技术实现流式输出,后端使用SpringMVC中的S... 目录一、后端代码二、前端代码三、运行项目小天有话说题外话参考资料前面一篇文章我们实现了《Spring

Nginx中location实现多条件匹配的方法详解

《Nginx中location实现多条件匹配的方法详解》在Nginx中,location指令用于匹配请求的URI,虽然location本身是基于单一匹配规则的,但可以通过多种方式实现多个条件的匹配逻辑... 目录1. 概述2. 实现多条件匹配的方式2.1 使用多个 location 块2.2 使用正则表达式

使用Apache POI在Java中实现Excel单元格的合并

《使用ApachePOI在Java中实现Excel单元格的合并》在日常工作中,Excel是一个不可或缺的工具,尤其是在处理大量数据时,本文将介绍如何使用ApachePOI库在Java中实现Excel... 目录工具类介绍工具类代码调用示例依赖配置总结在日常工作中,Excel 是一个不可或缺的工http://

SpringBoot实现导出复杂对象到Excel文件

《SpringBoot实现导出复杂对象到Excel文件》这篇文章主要为大家详细介绍了如何使用Hutool和EasyExcel两种方式来实现在SpringBoot项目中导出复杂对象到Excel文件,需要... 在Spring Boot项目中导出复杂对象到Excel文件,可以利用Hutool或EasyExcel

Python如何实现读取csv文件时忽略文件的编码格式

《Python如何实现读取csv文件时忽略文件的编码格式》我们再日常读取csv文件的时候经常会发现csv文件的格式有多种,所以这篇文章为大家介绍了Python如何实现读取csv文件时忽略文件的编码格式... 目录1、背景介绍2、库的安装3、核心代码4、完整代码1、背景介绍我们再日常读取csv文件的时候经常

Golang中map缩容的实现

《Golang中map缩容的实现》本文主要介绍了Go语言中map的扩缩容机制,包括grow和hashGrow方法的处理,具有一定的参考价值,感兴趣的可以了解一下... 目录基本分析带来的隐患为什么不支持缩容基本分析在 Go 底层源码 src/runtime/map.go 中,扩缩容的处理方法是 grow

Go 1.23中Timer无buffer的实现方式详解

《Go1.23中Timer无buffer的实现方式详解》在Go1.23中,Timer的实现通常是通过time包提供的time.Timer类型来实现的,本文主要介绍了Go1.23中Timer无buff... 目录Timer 的基本实现无缓冲区的实现自定义无缓冲 Timer 实现更复杂的 Timer 实现总结在

基于Python实现多语言朗读与单词选择测验

《基于Python实现多语言朗读与单词选择测验》在数字化教育日益普及的今天,开发一款能够支持多语言朗读和单词选择测验的程序,对于语言学习者来说无疑是一个巨大的福音,下面我们就来用Python实现一个这... 目录一、项目概述二、环境准备三、实现朗读功能四、实现单词选择测验五、创建图形用户界面六、运行程序七、

Vue中动态权限到按钮的完整实现方案详解

《Vue中动态权限到按钮的完整实现方案详解》这篇文章主要为大家详细介绍了Vue如何在现有方案的基础上加入对路由的增、删、改、查权限控制,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、数据库设计扩展1.1 修改路由表(routes)1.2 修改角色与路由权限表(role_routes)二、后端接口设计