Unity 网格模型及优化

news/2024/11/14 2:33:56/
一个模型中可以包含很多网格,一个模型可以由多个网格组成。在Unity3D中一个网格可以由多个子网格(Sub-Mesh)组成。

在渲染引擎的时候,每个子网格都要匹配一个材质球来做渲染,实际上一个子网格本身就是一个个普通的模型,它由很多个三角形构成,也需要材质球支持达成渲染。

为什么要有子网格?

美术人员在制作3D模型时,为什么不打网格编成一个,而要制作成多个子网格?

  1. 模型中的一部分网格用一种材质球来表现效果,另一部分用另一种。因为一个网格只能对应一个材质球所以要拆开。
  2. 模型中的莫部分贴图在众多模型中共同时使用的频率比较高,为了不重复制作以减少重复劳动,原本可以作为一个整体的模型会将公共材质部分单独拆分出来让一部分模型使用同一个材质球。
  3. 制作动画时,由于动画过于复杂,如果使用同一个模型去表现,则骨骼的数量就会成倍的增加,为了更好的表现动画,也为了能降低骨骼的使用数量,要拆分出来一部分模型,让它们单独成为模型动画的一部分。

从以上看多个子网格比单个网格要灵活的多,但它也有缺点,由于每个子网格都有材质球,导致子网格越多,drawcall 也越多。

子网格功能虽然强大,但也需要注意性能开销。 

动态合并3D模型

我们知道每个网格模型都需要有一个材质球支持,导致每个模型都会产生一个drawcall,过多的drawcall,CPU会忙于发送状态数据给GPU,GPU大多数时间处于等待状态,这将导致画面帧率下降及强烈的画面卡顿。

实际项目中通常会遇到这样的问题:场景中会有很多物体,人物、建筑、路标、景观、树木、石头、碎块、花草等等,有的物体可能不只有一个材质球,相同物体共用一个,当然不同的物体也可能使用相同的材质球,这些物体如果不做任何优化处理,就会产生很多drawcall。

合并3D模型的主要目的就是减少drawcall,通过减少材质球的提交次数来完成优化,简单的说就是大拥有相同材质球的模型合并成一个模型和一个材质球,从而减少向GPU提交的drawcall数量,让GPU并行处理数据时更快更顺畅。

在Unity中分为动态合批和静态合批两种,使用它们的前提条件是模型物体必须具有相同的材质球,除了这个必要条件外还有其他条件。

动态批处理

开启动态批处理时,Unity会将场景中的某些物体自动批处理成为同一个drawcall,需要满足:

  1. 顶点数目要少于900个顶点网格。如果你的着色器使用顶点坐标、法线和单独的UV,那么顶点个数就要在300以内,如果着色器使用顶点坐标、法线、UV0、UV1和切线,则顶点要在180个内的网格。
  2. 两个物体的缩放单位要相同,比如A(1,1,1) B(1,1,2)就没法合批了
  3. 使用相同材质球
  4. 多管线(Pipeline)着色器会中断动态合批,支持多个灯光的前置渲染,它们增加了多个渲染通道,因此无法批处理。旧系统中的延迟渲染路径(灯光前置通道)关闭了动态批处理,因为它需要绘制物体两次。多有多个Pass的着色器增加了渲染管道,不会被动态批处理。

从上面看动态批处理的条件非常苛刻,真实的项目里很多模型时不符合动态批处理要求的。另外动态批处理要消耗CPU将所有物体的顶点转换到世界空间,合并网格也会带来开销。一味的减小drawcall并不是万能的,开销取决于很多因素,drawcall的开销主要是由图形API的调用造成的,如果节省的开销小于准备工作的开销,就得不偿失。例如,一个主机设备或流行的API(Apple Metal这样的),drawcall的开销通常很低,那么对于它来说动态批处理就没有什么优势了。

静态批处理

静态批处理允许引擎离线的情况下进行模型合并的批处理,减少drawcall。无论模型有多大,只要使用同一个材质球都会被静态批处理优化。它通常比动态批处理更有用(因为不需要实时转换顶点来消耗CPU),缺点是消耗了更多的内存。

Unity中需要对物体设置静态标记,也就是不能移动,缩放,旋转的物体。

如果一些物体之前共用一个模型,那么unity会复制这些物体的模型用来合并,所以使内存增大。个人觉得使用内存换CPU是值得的,然而如果用100GB的内存换千分之一的CPU效率那就不值得了。

静态批处理的具体流程是,将所有静态标记的物体放入世界空间->通过材质球分类标准将它们分别合并->构建成一个大的顶点集合和索引缓存。从而减少drawcall。

从技术上来说,静态批处理并没有节省3D API drawcall的数量,但它能减少因3D API之间的状态改变导致的消耗。大多数平台上,批处理限制在64000个顶点和64000个索引内(OpenGLES上为48000个,MacOS上为32000个),所以如果批处理过多,需要取消一些静态标记。

自己编写合并3D模型程序

Unity3D中:

  • Mesh类有一个CombineMeshes的接口,提供了合并入口。
  • MeshFilter类,是承载数据类。
  • MeshRenderer类,是绘制网格的类

在使用之前我们还需要了解

MeshFilter和MeshRenderer中的mesh与shareMesh、material及shareMaterial的区别

mesh和material都是实例型的变量,对mesh和material执行任何操作都是额外复制一份后重新赋值,即使只是get操作,同样也会执行复制操作。而就是说,对mesh和material进行操作后,就会变成另一个实例。sharedMesh和shareMaterial与前面的两个变量不同,它们是共享型的。多个模型可以共同指定一个sharedMesh和sharedMaterial,当年修改了shareMesh或sharedMaterial里面的参数,指向同一个sharedMaterial和sharedMesh的多个模型就会同时改变效果。

materials和sharedMaterialsd的区别

materials是实例型,sharedMaterials是共享型,两个都是数组。material和sharedMaterial只针对主网格。而就是说material等于materials[0],sharedMaterial等于sharedMaterial[0]。

网格、MeshFilter、MeshRenderer的关系

网格时数据资源,它可以有自己的资源文件,通常时.FBX文件。网格里存储了顶点、UV、顶点颜色、三角形、切线、法线、骨骼、骨骼权重等提供渲染所必要的数据。

MeshFilter是一个承载网格数据的类,网格被实例化后存储在MeshFilter类中。MeshFilter包含两种类型,即实例型(mesh)和共享型变量(sharedMesh)。

MeshRenderer具有渲染功能,它会提取MeshFilter中的网格数据,结合自身的materials或sharedMaterials进行渲染。

Mesh.CombineInstance合并数据实例类

合并时需要为每个将要合并的网格创建一个CombineInstance实例,并往里放入mesh、subMesh的索引,lightmap的缩放和偏移,realtimeLightmap的缩放和偏移,以及世界坐标矩阵。它承载了所有需要需要合并的数据。        

现在我们掌握了关于合并网格的理论知识我们来实现以下,这里我从Unity 商店下载了一个模型,我将角色手中的 剑和盾牌进行了合并,并生成了一个新的GameObject 效果如下:

using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;public class CombineTest : MonoBehaviour
{public List<MeshFilter> combineMeshFilers = new List<MeshFilter>();// Start is called before the first frame updatevoid Start(){CreateCombineModel();}private void CreateCombineModel(){// 提取网格所需的资源List<Material> materials = new List<Material>();CombineInstance[] combines = new CombineInstance[combineMeshFilers.Count];for (int i = 0; i < combineMeshFilers.Count; i++){CombineInstance tmpInstance = new CombineInstance();MeshFilter tmpFilter = combineMeshFilers[i];tmpInstance.mesh = tmpFilter.sharedMesh;tmpInstance.transform = tmpFilter.transform.localToWorldMatrix;combines[i] = tmpInstance;MeshRenderer renderer = tmpFilter.transform.GetComponent<MeshRenderer>();for (int j = 0; j < renderer.sharedMaterials.Length; j++){if (!materials.Contains(renderer.sharedMaterials[j]))materials.Add(renderer.sharedMaterials[j]);}}// 将所有网格合并成一个新的网格Mesh mesh = new Mesh();MeshFilter combineFilter = transform.AddComponent<MeshFilter>();MeshRenderer combineRenderer = transform.AddComponent<MeshRenderer>();mesh.CombineMeshes(combines);combineFilter.mesh = mesh;combineRenderer.materials = materials.ToArray();}
}


http://www.ppmy.cn/news/1546422.html

相关文章

(蓝桥杯C/C++)——搜索

一、回溯法 1.回溯法简介 回溯法一般使用 ** DFS(深度优先搜索) ** 实现&#xff0c;DFS是一种遍历或搜索图、树或图像等数据结构的算法&#xff0c;当然这个图、树未必要存储下来(隐式处理就是回溯法)&#xff0c;常见的是通过某种关系构造出的搜索树&#xff0c;搜索树一般…

Stable Diffusion:照片转视频插件

Stable Diffusion是一款基于人工智能技术开发的作图神器&#xff0c;它支持照片转视频的功能&#xff0c;这通常需要通过安装特定的插件来实现。以下是一些关于Stable Diffusion照片转视频插件的详细信息&#xff1a; 一、插件介绍 AnimateDiff 功能&#xff1a;AnimateDiff是…

蓝桥杯:编程爱好者的试炼场

引言 蓝桥杯全国软件和信息技术专业人才大赛&#xff08;简称 “蓝桥杯”&#xff09;是中国大陆地区一项重要的计算机编程与软件设计赛事&#xff0c;旨在为在校学生提供展示编程能力的平台&#xff0c;以促进信息技术人才的培养和软件开发产业的发展。从2010年首届比赛开始&…

计算机网络(5)

网络层 网关&#xff08;Gateway&#xff09; 网关是连接两个不同网络协议、不同体系结构的计算机网络的设备&#xff0c;可实现不 同网络之间的协议转换&#xff0c;将数据重新分组、包装和转换&#xff0c;使采用不同高层协议 的主机仍然可以互相合作&#xff0c;从而完成各种…

3. JVM 发展历程

整理罗列了一些主流 JVM 的发展历程&#xff0c;方便查阅 名称公司年份简述Sun Classic VMSun1996第一款商用虚拟机只提供解释器JDK1.4 完全淘汰Exact VMSun1998JDK1.2 时出现对 Classic VM 进行优化准确式内存该管理热点探测技术编译器和解释器混合工作很快被 Spot VM 取代了…

通过物流分拣系统来理解RabbitMQ的消息机制

RabbitMQ作为一个消息中间件&#xff0c;通过队列和路由机制&#xff0c;帮助应用程序高效传递消息。而它的消息流转过程&#xff0c;其实可以用物流分拣系统来直观理解。 在一个典型的物流分拣系统中&#xff0c;包裹会经过多个节点&#xff08;比如分拣中心、配送站&#xf…

【vue】封装一个可随时暂停启动无需担心副作用的定时器

【vue】封装一个可随时暂停启动无需担心副作用的定时器 现成轮子&#xff1a;VueUse 库的 useIntervalFn 方法是什么&#xff1f;为什么要用它&#xff1f;怎么用&#xff1f; 分析源码 & 自己手写一个源码自己手写 现成轮子&#xff1a;VueUse 库的 useIntervalFn 方法 是…

【LeetCode】【算法】33. 搜索旋转排序数组

LeetCode 33. 搜索旋转排序数组 题目描述 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转&#xff0c;使数组变为 [nums[k], nums[k…