在Unity中使用Mesh的功能

如您所知,计算机图形学是游戏行业的基础。 在创建图形内容的过程中,我们不可避免地会遇到与在创建环境和应用程序中呈现方式不同有关的困难。 除了这些困难之外,还增加了简单的人类粗心大意的风险。 考虑到游戏开发的规模,此类问题经常出现或大量出现。

为克服这些困难,我们开始考虑自动化并撰写有关该主题的文章。 大多数资料将与Unity 3D一起使用,因为这是Plarium Krasnodar中的主要开发工具。 在下文中,将3D模型和纹理视为图形内容。

在本文中,我们将讨论在Unity中访问表示3D对象的数据的功能。 该材料将主要对初学者以及很少与此类模型的内部表示进行交互的开发人员有用。



关于Unity中的3D模型-最小的模型




在标准方法中, Unity使用MeshFilterMeshRenderer组件渲染模型。 MeshFilter是指代表模型的Mesh资源。 对于大多数着色器,几何信息是在屏幕上渲染模型的必需的最低要求。 如果不涉及纹理扫描数据和动画骨骼,则它们可能不可用。 此类的内部实现方式以及所有内容的存储方式, 七个印章中隐藏第n笔钱的奥秘。

在外部,将网格作为对象可以访问以下数据集:

  • 顶点 -几何顶点在三维空间中具有其自身起点的一组位置;
  • 法线,切线 -顶点的法线和切向量的集合,通常用于计算光照;
  • uv,uv2,uv3,uv4,uv5,uv6,uv7,uv8-用于纹理扫描的坐标集;
  • colors,colors32-顶点的颜色值集,其教科书示例是通过遮罩混合纹理;
  • bindposes-用于相对于骨骼定位顶点的矩阵集;
  • boneWeights-骨骼在顶部的影响系数;
  • 三角形 -一次处理3个顶点索引的集合; 每个这样的三元组代表模型的多边形(在这种情况下为三角形)。

通过相应的属性可以访问有关顶点和多边形的信息,每个属性都返回一个结构数组。 对于不阅读文档的人来说很少会在Unity中使用网格,因此显而易见,每当在内存中访问顶点时,就会以长度等于顶点数量的数组形式创建对应集合的副本。 一小部分文档中考虑了这种细微差别。 上面提到的有关Mesh类的属性的注释也对此有所警告。 出现这种情况的原因是Mono运行时环境中的Unity体系结构功能。 可以用以下方式表示:



引擎核心(UnityEngine(本机))与开发人员脚本隔离,并且可以通过UnityEngine库(C#)实现对其功能的访问。 实际上,它是一个适配器,因为大多数方法都充当从内核接收数据的层。 在这种情况下,内核及其其余部分(包括脚本)会在不同的进程下旋转,并且脚本部分仅知道命令列表。 因此,无法从脚本直接访问内核使用的内存。

关于访问内部数据,或可能有多糟糕


为了演示情况有多糟糕,让我们使用文档中的示例来分析Garbage Collector清除的内存量。 为了简化分析,将相同的代码包装在Update方法中。

public class MemoryTest : MonoBehaviour { public Mesh Mesh; private void Update() { for (int i = 0; i < Mesh.vertexCount; i++) { float x = Mesh.vertices[i].x; float y = Mesh.vertices[i].y; float z = Mesh.vertices[i].z; DoSomething(x, y, z); } } private void DoSomething(float x, float y, float z) { //nothing to do } } 

我们使用标准图元(一个球体(515个顶点))运行此脚本。 使用事件探查器工具,在“ 内存”选项卡中,您可以查看每个帧中为垃圾回收标记了多少内存。 在我们的工作机器上,该值为〜9.2 Mb。



即使对于已加载的应用程序,这也很多。在这里,我们启动了一个场景,其中包含一个对象,该对象上安装了最简单的脚本。

提到.Net编译器功能和代码优化很重要。 遍历调用链,您会发现调用Mesh.vertices需要调用引擎的extern方法。 尽管DoSomething()为空,并且由于这个原因未使用变量x,y,z ,但这仍使编译器无法优化Update()方法中的代码。

现在我们开始缓存位置数组。

 public class MemoryTest : MonoBehaviour { public Mesh Mesh; private Vector3[] _vertices; private void Start() { _vertices = Mesh.vertices; } private void Update() { for (int i = 0; i < _vertices.Length; i++) { float x = _vertices[i].x; float y = _vertices[i].y; float z = _vertices[i].z; DoSomething(x, y, z); } } private void DoSomething(float x, float y, float z) { //nothing to do } } 



平均6 Kb。 另一件事!

此功能成为我们必须实施自己的结构来存储和处理网格数据的原因之一。

我们该怎么做


在大型项目的工作过程中,出现了创建用于分析和编辑导入的图形内容的工具的想法。 我们将在以下文章中讨论分析和转换的方法。 现在,考虑到访问有关网格信息的功能,我们来看一下为方便实现算法而决定编写的数据结构。

最初,此结构如下所示:



在这里, CustomMesh类表示网格本身。 另外,以实用程序的形式我们实现了UntiyEngine.Mesh的转换,反之亦然。 网格由其三角形数组定义。 每个三角形正好包含三个边缘,这些边缘又由两个顶点定义。 我们决定仅将需要分析的信息添加到顶点,即:位置,法线,两个纹理扫描通道(主纹理为uv0 ,照明为uv2 )和颜色。

一段时间后,出现了向上移动层次结构的需求。 例如,从三角形中找出它所属的网格。 另外,从CustomMesh降级Vertex看起来很自以为是,不合理且数量众多的重复值让我感到不安 。 由于这些原因,必须重新设计结构。



CustomMeshPool实现用于方便管理和访问所有已处理CustomMesh的方法 。 由于MeshId字段,每个实体都可以访问整个网格的信息。 该数据结构满足初始任务的要求。 通过将适当的数据集添加到CustomMesh并将必要的方法添加到Vertex ,可以轻松进行扩展。

值得注意的是,这种方法并不是最佳的性能。 同时,我们已经实现的大多数算法都集中在分析Unity编辑器中的内容,这就是为什么您不必经常考虑使用的内存量的原因。 因此,我们从字面上缓存所有可能的内容。 我们首先测试实现的算法,然后重构其方法,在某些情况下,简化数据结构以优化运行时间。

现在就这些了。 在下一篇文章中,我们将讨论如何编辑已经添加到项目中的3D模型,并将使用考虑的数据结构。

Source: https://habr.com/ru/post/zh-CN440690/


All Articles