游戏开发进阶Unity网格(Mesh\动态合批\骨骼动画\蒙皮)
目录
- 一、前言
- 二、Hello Mesh
- 三、萌新初识Mesh
- 1、引擎内置的Mesh
- 2、Mesh是什么
- 三、Mesh的创建方式
- 1、第三方建模软件
- 2、Unity建模插件:ProBuilder
- 3、程序动态生成网格
- 四、Unity中如何显示网格
- 1、MeshFilter:网格过滤器
- 2、MeshRenderer:网格渲染器
- 3、SkinnedMeshRenderer:蒙皮网格渲染器
- 3.1 骨骼动画
- 3.2 SkinnedMeshRenderer组件
- 3.2 使用BakeMesh进行优化
- 五、纯代码动态创建网格
- 1、创建Mesh对象
- 2、顶点坐标
- 3、UV坐标
- 4、三角形序列
- 5、重新计算法线和包围体
- 6、完整版代码
- 7、测试
- 8、项目源码
- 六、网格相关的开源项目
- 1、2D网格涂鸦
- 2、3D网格涂鸦
- 3、网格体素化
- 4、网格平滑算法
- 5、网格切割
- 6、网格合并
- 七、未完的探险
一、前言
嗨,大家好,我是新发。
有同学私信我让我写一篇Unity网格相关的教程,
那我就带大家来一次UnITy的网格探险之旅吧~
二、Hello Mesh
我背着旅行背包走在Unity的场景中,突然眼前出现了一棵树,
我走近一看,这棵树身上挂着MeshFilter和Meshrenderer组件,根据Unity探险手册记载,这个MeshFilter是网格过滤器,它会引用一个网格资源,我顺腾摸瓜,找到了对应的网格,
实在太美了,我久久伫立,这就是网格啊!
正当我欣赏着网格三角形时,突然世界暗了下来,眼前出现了一团火,
我又拿出了Unity探险手册,啊,这一定就是粒子系统了!它可以动态生成网格。
天外传来一阵打字声,场景中出现了一行看起来像文字的网格,作为一个具有多年Hello World经验的程序员,我看出了第一个单词应该是Hello,第二个单词…我知道了,
是Hello Mesh!
(此处为震撼人心的入场音乐)
三、萌新初识Mesh
@H_512_191@1、引擎内置的Mesh网格的英文名是Mesh,Unity萌新最先接触的网格应该就是引擎内置的Cube(正方体)、Capsule(胶囊体)、Cylinder(圆柱体)、Plane(平面)、Sphere(球体)、Quad(四边形),如下
事实上,我们在Unity场景中,所有能被渲染出来的物体都会带有网格,比如3D模型、粒子特效、UI、文字等等。
2、Mesh是什么
从概念上讲,网格是图形硬件用来绘制复杂内容的构造。它至少包含一组定义3D空间中点的顶点,以及一组连接这些点的三角形,实际上还包含法线、顶点颜色纹理坐标等信息,这些三角形构成了网格所代表的任何表面。
我们可以看下Unity的Mesh类,Mesh的属性和方法很多,我这里列举几个比较常用的,如下
// 顶点坐标数组public Vector3[] vertices {
get;
set;
}
// 法线向量数组public Vector3[] normals {
get;
set;
}
// 顶点颜色数组public Color[] colors {
get;
set;
}
// 三角形序列数组,每三个数字为一组public int[] triangles {
get;
set;
}
// uv坐标数组public Vector2[] uv {
get;
set;
}
// 重新计算法线,在修改完顶点后,通常会更新法线来反映新的变化,注意,法线是根据共享的顶点计算出来的。public void RecalculateNormals();
// 从法线和纹理坐标重新计算网格的切线。修改网格的顶点和法线之后,如果网格使用引用法线贴图的着色器进行渲染,则切线需要更新。 public void RecalculateTangents();
// 重新计算从网格包围体的顶点, 在修改顶点后需要这个函数以确保包围体是正确的,赋值三角形将自动重新计算这个包围体。public void RecalculateBounds();
画个图,方便大家有个直观印象,
三、Mesh的创建方式
1、第三方建模软件
建模本质上就是建网格,我们可以事先通过第三方建模软件来创建模型网格,
常见的建模软件比如
3DS MAX官网:https://www.autodesk.com/products/3ds-max/overview
MAYA官网:https://www.autodesk.com/products/maya/overview
blender官网:https://www.blender.org/
2、Unity建模插件:PRoBuilder
Unity官方提供了一个可以用来创建和自定义几何体的工具ProBuilder,我们可以在Unity的Package Manager中下载到这个插件,
使用ProBuilder我们可以直接在Unity中创建或编辑简单的几何体,不用通过第三方建模软件,提升了效率,方便快速搭建场景原型,
3、程序动态生成网格
网格也可以是程序动态生成的,比如粒子系统的网格就是动态生成的,
又比如文字,也是程序动态生成网格,
文章后面我还会手把手教你如何使用纯代码来构建网格,这里先不急着写代码,我们继续探寻网格的秘密先~
四、Unity中如何显示网格
在Unity中,我们要显示一个网格,需要用到两个组件:MeshFilter和MeshRenderer。
注:你也可以直接使用SkinnedMeshRenderer组件,与MeshFilter和MeshRenderer的区别我下文会讲。
1、MeshFilter:网格过滤器
MeshFilter是网格过滤器,我们需要通过它设置引用的网格资源,比如这里引用的是一个Cube(正方体)网格。
我们可以看下MeshFilter.cs的源码,
[Requirecomponent(tyPEof(Transform))][NativeHeader("Runtime/Graphics/Mesh/MeshFilter.h")]public sealed partial class MeshFilter : Component{
[RequiredByNativeCode] // MeshFilter is used in the VR Splash screen. private void DontStripMeshFilter() {
}
extern public Mesh sharedMesh {
get;
set;
}
extern public Mesh mesh {
[NativeName("GetInstantiatedMeshFromScript")] get;
[NativeName("SetInstantiatedMesh")] set;
}
}
MeshFilter只有两个属性:mesh和sharedMesh,
我们查看Unity的官方手册,看看mesh与sharedMesh的区别:https://docs.unity3d.com/ScriptReference/MeshFilter.html
我来解读一下,mesh访问的是一个Mesh资源的实例(副本),这意味着我们修改这个mesh并不会修改到原始资源本身,改的只是Mesh的实例(副本)。
而sharedMesh是原始资源的引用,如果修改了sharedMesh,比如修改顶点坐标,那么原始资源也会被修改。
画成图大概是这样子:
这里我顺手写个随机修改Mesh顶点坐标的脚本,如下,将下面这个RandoMeshmVertices脚本挂到MeshFilter组件所在的物体上即可,
// RandoMeshmVertices.cs// 随机修改Mesh顶点坐标using UnityEngine;
public class RandoMeshmVertices: MonoBehaviour{
// Mesh的实例 MeshFilter meshFilter;
// 顶点的原始坐标 Vector3[] originalVertices;
void Start() {
meshFilter = GetComponentmeshFilter>
();
originalVertices = meshFilter.mesh.vertices;
}
void Update() {
// 随机修改顶点坐标 Vector3[] vertices = meshFilter.mesh.vertices;
for (int i = 0, len = originalVertices.Length;
i len;
++i) {
VAR v = originalVertices[i];
vertices[i] = v + Random.Range(-0.1F, 0.1F) * Vector3.one;
}
meshFilter.mesh.vertices = vertices;
meshFilter.mesh.RecalculateNormals();
}
}
运行效果如下,网格顶点坐标发生了随机偏移,
关于mesh属性的访问需要特别注意一下,我们先看看Unity官方手册的说明,https://docs.unity3d.com/ScriptReference/MeshFilter-mesh.html
翻译一下就是,如果一个Mesh资源已经被分配给MeshFilter的mesh属性,那么当我们在代码中第一次访问mesh属性时才正真创建了Mesh的实例;再次访问mesh属性时则直接返回这个实例,并且一旦mesh属性被访问,则与原始共享网格的链接会丢失,此时sharedMesh变成mesh的别名,如果我们想避免这种自动生成Mesh实例,可以使用sharedMesh代替。
写成伪代码的话大致是这样子:
public class MeshFilter ...{
... private Mesh _mesh;
public Mesh mesh {
get {
if (_mesh == null) {
_mesh = new Mesh();
Copy(sharedMeh, _mesh);
}
return _mesh;
}
}
...}
还有,如果我们访问了mesh属性而导致自动创建了Mesh实例,则需要在代码中主动调用Resources.UnloadUnusedAssets来销毁没有引用的Mesh实例,建议是在场景切换时调用Resources.UnloadUnusedAssets。
2、MeshRenderer:网格渲染器
MeshRenderer,顾名思义,网格渲染器。我们依旧先来看看官方手册的介绍:
https://docs.unity3d.com/Manual/class-MeshRenderer.html
翻译过来就是MeshRenderer会从MeshFilter那里拿到网格数据并在所在物体的位置处将其渲染出来。
如果没有MeshRenderer,我们就看不见网格了,如下
另外,我们还需要在MeshRenderer的MATErials中指定一个材质球,这样才能正常显示,否则模型表面就是紫色的。
3、SkinnedMeshRenderer:蒙皮网格渲染器
SkinnedMeshRenderer是蒙皮网格渲染器,可能有小伙伴就会问了,上面使用MeshFilter和MeshRenderer已经可以显示模型网格了,为什么又弄了一个SkinnedMeshRenderer呢?
看下Unity官方手册的介绍:https://docs.unity3d.com/Manual/class-SkinnedMeshRenderer.html
可以看到SkinnedMeshRenderer其实是针对带 骨骼动画 的模型的渲染的。
3.1 骨骼动画
为什么需要做骨骼动画呢?
就好比我们人一样,我们的骨骼会随着我们肌肉的伸缩而动,骨骼又可以带动它管辖的身体部位发生形变和移动,骨骼还会影响它所连接的其他骨骼一起发生联动。对应到模型动作上,想想一个简单的举手动作要牵涉到多少网格顶点的移动,如果没有骨骼,那动画师要每帧挨个网格顶点进行调整,即使动画做出来了,这个动画也不能复用到其他模型上,因为不同模型的顶点信息都不一样,这么低效的动画制作肯定是不行的,于是,就有了骨骼动画。
骨骼动画的原理
就是将模型分为骨骼(Bone)和蒙皮(Mesh)两个部分,骨骼可分为多层父子骨骼,每个骨骼都附加到周围网格的一些顶点上,在动画关键帧数据的驱动下,计算出各个父子骨骼的位置,基于骨骼的控制通过顶点混合动态计算出蒙皮网格的顶点。
动画师可以在MAYA软件上给模型绑定骨骼,绑定骨骼不是本文的重点,这里就不展讲开具体操作了,感兴趣的同学可以自行百科学习。
制作好导出为fbx格式,
将fbx文件导入到Unity中,选中它,
在Inspector视图中点击Rig按钮,
我们可以看到动画类型Animation Type有None、Legacy、Generic和Humanoid四个,
具体选项可以参见Unity官方手册:https://docs.unity3d.com/Manual/FBXImporter-Rig.html
我这里演示一下人形骨骼动画,选择Humanoid类型,Avatar Definition选择Create From This Model,然后点击configure,
在Inspector视图中我们就可以看到对应的骨骼绑定信息了,
如下,绿色的线段就是一根根骨骼,
我们调整一根骨骼,对应的网格也会跟着一起动,如下
这样做出来的人形动画是可以进行复用了,有请妹子上场,
骨骼动画资源的话,我在之前的文章中也介绍过一个宝藏网站Mixamo:https://www.mixamo.com/,上面有很多做好的人形骨骼动画,
看,是不是挺好玩的,
我们可以把它的动作直接复用到我们自己的人形模型上,效果如下:
3.2 SkinnedMeshRenderer组件
骨骼动画可以正常播放,要归功于SkinnedMeshRenderer组件,制作好骨骼动画的fbx文件导入Unity中,Unity会自动帮我们挂上SkinnedMeshRenderer组件,
其中几个重要的属性我讲一下,Bounds:骨骼数据;Mesh:要渲染的网格;Root Bone:根骨骼,其他骨骼都是相对根骨骼移动的;BlendShapes:一般用于制作表情融合,我之前写过一篇文章讲过BlendShapes:
Unity通过BlendShape实现面部表情过渡切换Animation教程
我们再来看看SkinnedMeshRenderer脚本的属性和方法:
需要讲的应该就是这个BakeMesh方法了,下面我就单独拎出来讲下BakeMesh。
3.2 使用BakeMesh进行优化
假设现在场景中有100只皮卡丘,每只皮卡丘的网格、贴图、动作相同,
如果每只皮卡丘身上都挂SkinnedMeshRenderer,那就是100个SkinnedMeshRenderer在计算蒙皮,
由于SkinnedMeshRenderer是根据骨骼动画动态计算网格顶点坐标,这个运算开销还是不小的,有没有办法优化呢?
SkinnedMeshRenderer提供了一个BakeMesh方法,可以将一个蒙皮动画的某个时间点上的动作,Bake成一个不带蒙皮的Mesh,我们统一使用这个Mesh来显示其余的皮卡丘,这样就可以大大减少了SkinnedMeshRenderer的计算了,
画成图大概是这样子:
不过,上面这种方案的局限性是每只皮卡丘的动画是相同的,如果突然某一只皮卡丘要播放与其他皮卡丘不同的动画,那就不行了。
另一种Bake方案可以是这样:
对皮卡丘的每个动画进行遍历采样,把采样到的Mesh存到数组中,因为这里要Bake很多网格,比较耗时,建议在加载场景时时就完成采样过程;后面要播放某个动画时直接从这个Mesh数组中获取Mesh来显示,此时直接使用MeshFilter加MeshRenderer的方式来显示网格就好了。
贴个BakeMesh的示例脚本:
using UnityEngine;
using System.Collections.Generic;
/// summary>
/// Bake Mesh 示例/// /summary>
public class BakeMeshtest : MonoBehaviour{
[SerializeField] Animation m_animation;
[SerializeField] SkinnedMeshRenderer m_skinnedMeshRenderer;
[SerializeField] string m_clipToBake = "idle";
ListMesh>
m_bakedMeshList = new ListMesh>
();
/// summary>
/// 采样帧数 /// /summary>
[SerializeField] int m_numFramesToBake = 30;
void Start() {
// 获取要Bake的动画片段 Animationstate clipState = m_animation[m_clipToBake];
if (clipState == null) {
Debug.LOGError(string.Format("Unable to get clip '{
0}
'", m_clipToBake), this);
return;
}
// 开始播放动画 m_animation.play(m_clipToBake, PlayMode.StopAll);
// 设置动画初始时间戳 clipState.time = 0.0f;
// 采样帧间隔 float deltaTime = clipState.length / (float)(m_numFramesToBake - 1);
for (int frameindex = 0;
frameIndex m_numFramesToBake;
++frameIndex) {
string frameName = string.Format("BakeDFrame{
0}
", frameIndex);
// 创建Mesh Mesh frameMesh = new Mesh();
frameMesh.name = frameName;
// 动画采样 m_animation.Sample();
// 执行BakeMesh m_skinnedMeshRenderer.BakeMesh(frameMesh);
m_bakedMeshList.Add(frameMesh);
// 设置动画时间戳 clipState.time += deltaTime;
}
// 停止播放动画 m_animation.Stop();
}
}
需要提醒的是,这个方案是利用空间换时间,如果模型顶点数据特别多或动画时长特别长的时候,这时就会遇到内存瓶颈。
五、纯代码动态创建网格
一般情况下,网格是事先制作好的资源,但也有一些特殊的需求需要在代码中动态创建网格。
比如我之前写的一篇牙齿碎了的文章:
游戏开发Unity2D图片任意形状破碎裂片效果展示
现在我来教大家如何使用代码从零创建网格并将网格渲染出来,下文我以创建一个正方形网格为例进行讲解。
1、创建Mesh对象
第一步最简单,就是直接new一个Mesh,
var mesh = new Mesh();
2、顶点坐标
首先分析一下,一个四边形有四个顶点,假设正方形边长为1,四个点的坐标如下,
写成代码就是这样:
// 构建顶点坐标var vertices = new ListVector3>
();
vertices.Add(new Vector3(-0.5f, -0.5f, 0));
vertices.Add(new Vector3(-0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, -0.5f, 0));
// 将顶点坐标设置给Meshmesh.SetVertices(vertices);
3、UV坐标
UV坐标就是纹理贴图坐标,它将纹理上每一个点精确对应到模型物体的表面上,注意U和V的取值范围是0~1。UV坐标系原点在左下角,U轴是水平轴,V轴是竖直轴,如下:
对应到我们的上面那个正方向网格的话,四个点的UV坐标如下:
写成代码就是这样:
// 构建UV坐标var uvs = new ListVector2>
();
uvs.Add(new Vector2(0, 0));
uvs.Add(new Vector2(0, 1));
uvs.Add(new Vector2(1, 1));
uvs.Add(new Vector2(1, 0));
// 将UV坐标设置给Meshmesh.SetUVs(0, uvs);
4、三角形序列
网格需要切分成三角形,我们可以这样切分,
当然也可以这样切分,
两种切分方法对应不同的三角形序列,假设 法线方向 是垂直于屏幕从内指向屏幕外的话,第一种切分方式的三角形序列如下:
注:法线的方向就决定了表面正面,如果你的材质是单面渲染的话,那么只有从正面看才能看到网格被渲染。
即三角形序列为:{
0, 1, 2, 0, 2, 3 }
,注意序号是从0开始的。
为什么是这样的顺序呢?我教大家一个技巧,伸出你的左手,竖起大拇指,像这样子,
大拇指指向法线的方向,那么此时你的其余四根手指头环绕的方向就是三角形的序号的顺序,三个序号为一组按顺序塞入数组中即可,即得到的数组就是:{ 0, 1, 2, 0, 2,3} 当然,以下数组最终的效果都是等价的,只要顺序一致即可:
{
0, 1, 2, 0, 2, 3 }
,
{
1, 2, 0, 0, 2, 3 }
,
{
0, 2, 3, 1, 2, 0 }
,
…
我们现在写成代码,
// 重新计算法线,注意,法线是根据共享的顶点计算出来的。mesh.RecalculateNormals();
// 重新计算包围体,在修改顶点后需要这个函数以确保包围体是正确的mesh.RecalculateBounds();
// 从法线和纹理坐标重新计算网格的切线(如果网格使用引用法线贴图的着色器进行渲染,则切线需要更新)// 因为我们这里不使用法线贴图,所以就不调用它了// mesh.RecalculateTangents();
@L_126_65@
5、重新计算法线和包围体
当我们设置或修改了顶点数据后,需要调用Mesh的Recalculate方法来重新计算一些必要的信息,比如重新计算法线、包围体,代码如下
// 重新计算法线,注意,法线是根据共享的顶点计算出来的。mesh.RecalculateNormals();
// 重新计算包围体,在修改顶点后需要这个函数以确保包围体是正确的mesh.RecalculateBounds();
// 从法线和纹理坐标重新计算网格的切线(如果网格使用引用法线贴图的着色器进行渲染,则切线需要更新)// 因为我们这里不使用法线贴图,所以就不调用它了// mesh.RecalculateTangents();
6、完整版代码
以上代码封装成GenQuadMesh.cs脚本,完整代码如下:
// 使用代码生成四边形网格using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))][RequireComponent(typeof(MeshRenderer))]public class GenQuadMesh : MonoBehaviour{
public MeshFilter mf;
private void Start() {
mf.mesh = Build();
}
public static Mesh Build() {
var mesh = new Mesh();
// 构建顶点坐标 var vertices = new ListVector3>
();
vertices.Add(new Vector3(-0.5f, -0.5f, 0));
vertices.Add(new Vector3(-0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, -0.5f, 0));
// 将顶点坐标设置给Mesh mesh.SetVertices(vertices);
// 构建UV坐标 var uvs = new ListVector2>
();
uvs.Add(new Vector2(0, 0));
uvs.Add(new Vector2(0, 1));
uvs.Add(new Vector2(1, 1));
uvs.Add(new Vector2(1, 0));
// 将UV坐标设置给Mesh mesh.SetUVs(0, uvs);
// 设置三角形序列 var triangles = new int[] {
0, 1, 2, 0, 2, 3 }
;
mesh.SetTriangles(triangles, 0);
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
}
7、测试
创建一个空物体,挂上MeshFilter和MeshRenderer组件。
再挂上我们上面写的GenQuadMesh脚本,赋值mf变量为MeshFilter对象,如下
运行Unity,看到一个紫色快,
将Scene视图的模式设置为Wireframe,如下
现在我们可以看到我们动态创建的网格啦,
上面之所以显示紫色块,是因为我们没有给MeshFilter设置材质球,顺手做一个炮姐的材质球吧,
给MeshRenderer设置材质球对象,
重新运行Unity,效果如下,
8、项目源码
要用代码动态创建一个Mesh,就是new一个Mesh,给它塞入顶点坐标、UV坐标和三角形序列即可。再复杂的网格也可以通过这些步骤创建出来~
下面这些就是使用纯代码创建出来的几何体网格,感兴趣的同学可以下载项目源码下来学习。
项目源码:https://codechina.csdn.net/linxinfa/unity-mesh-builder
六、网格相关的开源项目
我再推荐一些网格相关的开源项目给大家~
1、2D网格涂鸦
项目地址:https://github.com/mattatz/unity-triangulation2D
2、3D网格涂鸦
项目地址:https://github.com/mattatz/unity-teddy
3、网格体素化
项目地址:https://github.COM/Scrawk/Mesh-Voxelization
4、网格平滑算法
项目地址:https://github.com/mattatz/unity-mesh-smoothing
5、网格切割
项目地址:https://github.com/hugoscurti/mesh-cutter
6、网格合并
项目地址:https://github.com/sanukin39/UniMeshCombiner
七、未完的探险
好了,这次探险之旅就暂时到这里吧,还有很多内容需要探索,先保持体力,我们下次再见,更多关于Unity网格(Mesh\动态合批\骨骼动画\蒙皮)的资料请关注其它相关文章!
您可能感兴趣的文章:- unity自带寻路(导航)系统 Nav Mesh导航网格
- unity通过Mesh网格绘制图形球体
- unity通过Mesh网格绘制图形(三角形、正方体、圆柱)
- Unity通过脚本创建网格Mesh的方法
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: 游戏开发进阶Unity网格(Mesh\动态合批\骨骼动画\蒙皮)
本文地址: https://pptw.com/jishu/609004.html
