什么是Mesh?
Mesh是指的模型的网格,3D模型是由多边形拼接而成,而多边形实际上又是由多个三角形拼接而成的。即一个3D模型的表面其实是由多个彼此相连的三角面构成。三维空间中,构成这些三角形的点和边的集合就是Mesh。
原理
即动态创建一个Mesh,设置三角形和顶点数据,然后赋值给MeshFilter(增加mesh属性),通过MeshRenderer(增加材质并渲染出Mesh)绘制出来
理论基础:
1、左手坐标系和右手坐标系
我们的三维坐标系,在3dmax里是右手坐标系,而在Unity里是左手坐标系。
左手坐标系和右手坐标系的区别
2、三边面如何组成四边面
如图,左边是Unity里的左手坐标系,右边是在此坐标系里生成的一个面以及它的各个点坐标。
012和230这两个三边面就组成了一个四边面。
如果我问这个四边面有几个顶点,想必大家都会回答4个,实际上是6个,012和230这是6个顶点,不同面的顶点不公用。
要组成2个三边面可以有很多种顺序,例如012和320、012和032、023和012等等等
但是我们一般都是按照4个点的顺序来画2个三边面组成四边面,所以可选的只有【012和230、230和012】,以及【032和210、210和032】这两大类
这两类画法有什么区别呢?细心的童鞋应该已经发现,这两种方式前者是逆时针,后者是顺时针。
这种循环的方向会导致面的法线方向不同,而这个法线方向会决定这个面的朝向。
我们要确定这个法线方向其实很简单,上面说了,Unity里是左手坐标系,拿出左手,伸直,拇指与其他四个指头垂直,然后四指弯曲,指尖朝向循环的方向,拇指就指向法线的方向。
由此我们得出结论,要想生成正确的面(法线指向我们),我们只能用【032和210、210和032】
这里需要注意的一点是,我们确定4个点的循环方向,和生成三边面时的循环方向无关,只要生成三边面时,用到的前4个点的index顺序没错就行了。
Mesh的组成部分
1.vertices(顶点数据数组Vector3[])
2.triangles(三角形顶点索引数组,int[])
3.normals(法线向量数组,Vector3[])
4.uv(纹理坐标数组,Vector2[])
顶点坐标:顶点坐标数组存放Mesh的每个顶点的空间坐标,假设某mesh有n个顶点,则vertex的size为n
法线:法线数组存放mesh每个顶点的法线,大小与顶点坐标对应,normal[i]对应顶点vertex[i]的法线
法线详解:
法线就是垂直于面的一条线,它有方向,没有大小。
法线的方向就是面朝外的方向。比如我们现在盯着显示器看,从显示器的正中心会有一条法线垂直于屏幕指向我们。
法线向外的面就是正面,相反的就是背面,一般来讲,从正面看才能看到面,背面看面是看不到的。
纹理坐标:它定义了图片上每个点的位置的信息. 这些点与3D模型是相互联系的, 以决定表面纹理贴图的位置. UV就是将图像上每一个点精确对应到模型物体的表面. uv[i]对应vertex[i]
三角形序列:每个mesh都由多个三角面组成,而三角面的三个点就是顶点坐标里的点,三角形的数组的size = 三角形个数 * 3
三边面和四边面:
三边面就是三条边组成的面,四边面就是四条边组成的面。
三边面在三维空间中是不可扭曲的,而四边面在三维空间中可以扭曲。所以Unity里只支持三边面。其他支持四边面的软件例如3dmax在导出fbx的时候,会把四边面转换成三边面。
创建一个立方体
1.定顶点坐标
一般我们会以立方体几何中心为坐标原点。
代码:
1 //顶点数组
2 Vector3[] _vertices =
3 {
4 // front
5 new Vector3(-5.0f, 10.0f, -5.0f),
6 new Vector3(-5.0f, 0.0f, -5.0f),
7 new Vector3(5.0f, 0.0f, -5.0f),
8 new Vector3(5.0f, 10.0f, -5.0f),
9
10
11 // left
12 new Vector3(-5.0f, 10.0f, -5.0f),
13 new Vector3(-5.0f, 0.0f, -5.0f),
14 new Vector3(-5.0f, 0.0f, 5.0f),//
15 new Vector3(-5.0f, 10.0f, 5.0f),
16
17 // back
18 new Vector3(-5.0f, 10.0f, 5.0f),
19 new Vector3(-5.0f, 0.0f, 5.0f),
20 new Vector3(5.0f, 0.0f, 5.0f),
21 new Vector3(5.0f, 10.0f, 5.0f),
22
23
24 // right
25 new Vector3(5.0f, 10.0f, 5.0f),
26 new Vector3(5.0f, 0.0f, 5.0f),
27 new Vector3(5.0f, 0.0f, -5.0f),
28 new Vector3(5.0f, 10.0f, -5.0f),
29
30
31 // Top
32 new Vector3(-5.0f, 10.0f, 5.0f),
33 new Vector3(5.0f, 10.0f, 5.0f),
34 new Vector3(5.0f, 10.0f, -5.0f),
35 new Vector3(-5.0f, 10.0f, -5.0f),
36
37 // Bottom
38 new Vector3(-5.0f, 0.0f, 5.0f),
39 new Vector3(5.0f, 0.0f, 5.0f),
40 new Vector3(5.0f, 0.0f, -5.0f),
41 new Vector3(-5.0f, 0.0f, -5.0f),
42
43 };
这里有人会有疑问,正方体6个面,每个面由2个三角形组成,所以共需要36个三角形顶点索引。但是正方体只有8个顶点,为什么需要24个顶点坐标数据呢?
答案是:Unity3D的Mesh.triangles是三角形索引数组,不仅依靠这个索引值索引三角形顶点坐标,而且索引纹理坐标,索引法线向量。即正方体的每个顶点都参与了3个平面,而这3个平面的法线向量是不同的,该顶点在渲染这3个平面的时候需要索引到不同的法线向量。而由于顶点坐标和法线向量是由同一个索引值triangles[Index]取得的,例如,有三个点在vertices中索引到的顶点都为(0,0,0),但是在normals中索引到的法向量值各不相同。这就决定了在正方体中一个顶点,需要有3份存储。(如果你需要创建其它模型,需要根据实际情况决定顶点坐标的冗余度。实质上顶点坐标的冗余正是方便了法线坐标、纹理坐标的存取。),一般不共点。还有就是Unity中是左手坐标系,一定记好,因为在绘制三角面时很重要。
2.三角面索引
//索引数组
int[] _triangles =
{
//front
2,1,0,
0,3,2,
//left
4,5,6,
4,6,7,
//back
9,11,8,
9,10,11,
//right
12,13,14,
12,14,15,
////up
//16,17,18,
//16,18,19,
////buttom
//21,23,22,
//21,20,23,
//不可跳跃设置索引值(否则会提示一些索引超出边界顶点 15直接20不可,要连续15-16)
17,19,18,
17,16,19,
};
这里设置的原则时外面被渲染里面剔除掉,顺时针构建(注意里外面的区别),还要注意的一个点,如上所写,比如我想生成5个面,那你的索引值也要是连续的,不可16直接蹦到20。这里立法体面的绘制顺序是(即绘制三角面的面与上面顶点顺序要一致)设置顶点的顺序
3.UV坐标
代码:
1 //UV数组
2 Vector2[] uvs =
3 {
4 // Front
5 new Vector2(1.0f, 0.0f),
6 new Vector2(1.0f, 1.0f),
7 new Vector2(1.0f, 0.0f),
8 new Vector2(0.0f, 0.0f),
9
10
11 // Left
12 new Vector2(1.0f, 1.0f),
13 new Vector2(0.0f, 1.0f),
14 new Vector2(0.0f, 0.0f),
15 new Vector2(1.0f, 0.0f),
16
17
18 // Back
19 new Vector2(1.0f, 0.0f),
20 new Vector2(1.0f, 1.0f),
21 new Vector2(1.0f, 0.0f),
22 new Vector2(0.0f, 0.0f),
23
24
25 // Right
26 new Vector2(1.0f, 1.0f),
27 new Vector2(0.0f, 1.0f),
28 new Vector2(0.0f, 0.0f),
29 new Vector2(1.0f, 0.0f),
30
31 //// Top
32 //new Vector2(0.0f, 0.0f),
33 //new Vector2(1.0f, 0.0f),
34 //new Vector2(1.0f, 1.0f),
35 //new Vector2(0.0f, 1.0f),
36
37
38 // Bottom
39 new Vector2(0.0f, 0.0f),
40 new Vector2(1.0f, 0.0f),
41 new Vector2(1.0f, 1.0f),
42 new Vector2(0.0f, 1.0f),
43
44 };
UV坐标从左上角开始(想象摄像机在立方体内部去判断),
开始的即(0,0),一般是在0-1之间,一些比较大的面为防止纹理被拉伸马赛克,我们会重复贴纹理,会有大于1的情况,这里的点要与顶点坐标一一对应。重复贴纹理时需要将重复帖的贴图的Wrap Mode设为Repeat(重复)。
即:
4.构建mesh
代码:
1 Mesh mesh = new Mesh()
2 {
3 vertices = _vertices,
4 uv = uvs,
5 triangles = _triangles,
6 };
7
8 //重新计算网格的法线
9 //在修改完顶点后,通常会更新法线来反映新的变化。法线是根据共享的顶点计算出来的。
10 //导入到网格有时不共享所有的顶点。例如:一个顶点在一个纹理坐标的接缝处将会被分成两个顶点。
11 //因此这个RecalculateNormals函数将会在纹理坐标接缝处创建一个不光滑的法线。
12 //RecalculateNormals不会自动产生切线,因此bumpmap着色器在调用RecalculateNormals之后不会工作。然而你可以提取你自己的切线。
13 mesh.RecalculateNormals();
给mesh属性赋值。
5.增加MeshFilter组件,网格过滤。以及增加MeshRenderer组件添加材质实现渲染。OK!!!到这基本已经绘制完了,Mesh已经出来了。
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 public class ShaderBase : MonoBehaviour
6 {
7
8 void Start()
9 {
10 GameObject gameObject = new GameObject("Cube");
11 gameObject.transform.position = Vector3.zero;
12
13 //顶点数组
14 Vector3[] _vertices =
15 {
16 // front
17 new Vector3(-5.0f, 10.0f, -5.0f),
18 new Vector3(-5.0f, 0.0f, -5.0f),
19 new Vector3(5.0f, 0.0f, -5.0f),
20 new Vector3(5.0f, 10.0f, -5.0f),
21
22
23 // left
24 new Vector3(-5.0f, 10.0f, -5.0f),
25 new Vector3(-5.0f, 0.0f, -5.0f),
26 new Vector3(-5.0f, 0.0f, 5.0f),//
27 new Vector3(-5.0f, 10.0f, 5.0f),
28
29 // back
30 new Vector3(-5.0f, 10.0f, 5.0f),
31 new Vector3(-5.0f, 0.0f, 5.0f),
32 new Vector3(5.0f, 0.0f, 5.0f),
33 new Vector3(5.0f, 10.0f, 5.0f),
34
35
36 // right
37 new Vector3(5.0f, 10.0f, 5.0f),
38 new Vector3(5.0f, 0.0f, 5.0f),
39 new Vector3(5.0f, 0.0f, -5.0f),
40 new Vector3(5.0f, 10.0f, -5.0f),
41
42
43 // Top
44 new Vector3(-5.0f, 10.0f, 5.0f),
45 new Vector3(5.0f, 10.0f, 5.0f),
46 new Vector3(5.0f, 10.0f, -5.0f),
47 new Vector3(-5.0f, 10.0f, -5.0f),
48
49 // Bottom
50 new Vector3(-5.0f, 0.0f, 5.0f),
51 new Vector3(5.0f, 0.0f, 5.0f),
52 new Vector3(5.0f, 0.0f, -5.0f),
53 new Vector3(-5.0f, 0.0f, -5.0f),
54
55 };
56 //索引数组
57 int[] _triangles =
58 {
59 //front
60 2,1,0,
61 0,3,2,
62 //left
63 4,5,6,
64 4,6,7,
65 //back
66 9,11,8,
67 9,10,11,
68 //right
69 12,13,14,
70 12,14,15,
71 ////up
72 //16,17,18,
73 //16,18,19,
74 ////buttom
75 //21,23,22,
76 //21,20,23,
77
78 //不可跳跃设置索引值(否则会提示一些索引超出边界顶点 15直接20不可,要连续15-16)
79 17,19,18,
80 17,16,19,
81 };
82
83 //UV数组
84 Vector2[] uvs =
85 {
86 // Front
87 new Vector2(1.0f, 0.0f),
88 new Vector2(1.0f, 1.0f),
89 new Vector2(1.0f, 0.0f),
90 new Vector2(0.0f, 0.0f),
91
92
93 // Left
94 new Vector2(1.0f, 1.0f),
95 new Vector2(0.0f, 1.0f),
96 new Vector2(0.0f, 0.0f),
97 new Vector2(1.0f, 0.0f),
98
99
100 // Back
101 new Vector2(1.0f, 0.0f),
102 new Vector2(1.0f, 1.0f),
103 new Vector2(1.0f, 0.0f),
104 new Vector2(0.0f, 0.0f),
105
106
107 // Right
108 new Vector2(1.0f, 1.0f),
109 new Vector2(0.0f, 1.0f),
110 new Vector2(0.0f, 0.0f),
111 new Vector2(1.0f, 0.0f),
112
113 //// Top
114 //new Vector2(0.0f, 0.0f),
115 //new Vector2(1.0f, 0.0f),
116 //new Vector2(1.0f, 1.0f),
117 //new Vector2(0.0f, 1.0f),
118
119
120 // Bottom
121 new Vector2(0.0f, 0.0f),
122 new Vector2(1.0f, 0.0f),
123 new Vector2(1.0f, 1.0f),
124 new Vector2(0.0f, 1.0f),
125
126 };
127
128 Mesh mesh = new Mesh()
129 {
130 vertices = _vertices,
131 uv = uvs,
132 triangles = _triangles,
133 };
134
135 //重新计算网格的法线
136 //在修改完顶点后,通常会更新法线来反映新的变化。法线是根据共享的顶点计算出来的。
137 //导入到网格有时不共享所有的顶点。例如:一个顶点在一个纹理坐标的接缝处将会被分成两个顶点。
138 //因此这个RecalculateNormals函数将会在纹理坐标接缝处创建一个不光滑的法线。
139 //RecalculateNormals不会自动产生切线,因此bumpmap着色器在调用RecalculateNormals之后不会工作。然而你可以提取你自己的切线。
140 mesh.RecalculateNormals();
141 gameObject.AddComponent<MeshFilter>().mesh=mesh;
142 //Material/New Material 1
143 gameObject.AddComponent<MeshRenderer>().material = Resources.Load<Material>("Material/New Material");
144
145 }
146
147 }
这不是上述代码的结果图片,这是动态创建外围盒的图片,做法一样。
最新:这个立方体,我想底面和侧面贴不同贴图,如何实现?
使用 mesh.subMeshCount = X;即subMesh,子网格,具体使用如下:
1 Vector3 contralPos = (maxPos + minPos) / 2;
2 float boxHight = Mathf.Abs(maxPos.y - minPos.y);
3 float boxLength = Mathf.Abs(maxPos.x - minPos.x);
4 float boxWidth = Mathf.Abs(maxPos.z - minPos.z);
5 vertexPosArray = AddVertexPos(1.2f * boxLength, 1.2f * boxWidth, 1.4f * boxHight);
6 vertexIndexList = AddVertexIndex();
7 uvArr = SetUVPos(GetIntValue(boxLength / (textureSizeL * uvNorm)), GetIntValue(boxWidth / (textureSizeL * uvNorm)), GetIntValue(boxHight / (textureSizeW * uvNorm)));
8 Mesh mesh = new Mesh()
9 {
10 vertices = vertexPosArray,
11 uv = uvArr,
12 };
13 mesh.subMeshCount = 2;
14 mesh.SetTriangles(vertexIndexList[0], 0);
15 mesh.SetTriangles(vertexIndexList[1], 1);
16 mesh.RecalculateNormals();
17 GameObject Box = new GameObject(name);
18 // Box.transform.localPosition = contralPos;
19 Box.transform.localPosition = new Vector3(contralPos.x, minPos.y, contralPos.z);
20 Box.AddComponent<MeshFilter>().mesh = mesh;
21 Material[] materials = new Material[2];
22 materials[0] = new Material(Resources.Load<Material>("Materials/Mine/MinefieldTexture_Side"));
23 materials[1] = new Material(Resources.Load<Material>("Materials/Mine/MinefieldTexture_Buttom"));
24 Box.AddComponent<MeshRenderer>().materials = materials;
mesh.subMeshCount = 2;
mesh.SetTriangles(vertexIndexList[0], 0);
mesh.SetTriangles(vertexIndexList[1], 1);
这是指定子网格对应的索引集合,在设置索引时,应该这样分开存储:
1 /// <summary>
2 /// 添加索引
3 /// </summary>
4 private List<int[]> AddVertexIndex()
5 {
6 List<int[]> indexList = new List<int[]>();
7 int[] sideIndexArray =
8 {
9 //front
10 2,1,0,
11 2,0,3,
12
13 //back
14 4,5,6,
15 4,6,7,
16
17 //left
18 8,10,11,
19 8,9,10,
20
21 //right
22 13,15,14,
23 13,12,15,
24 };
25 int[] buttomFaceIndexArray =
26 {
27 //buttom
28 17,16,19,
29 17,19,18
30 };
31 indexList.Add(sideIndexArray);
32 indexList.Add(buttomFaceIndexArray);
33
34 return indexList;
35 }
View Code
即这样完成分开了Mesh,分别使用不同的材质。
欲戴王冠 必承其重