一些造出来的轮子,是否好用另说,能是能用了。基础知识是Unity的EditorWindow类和Editor类的使用,有一说一,官网讲了跟没讲差不多,但好在网上有很多资料可以学习。

模型处理系列-法线平滑/法线重建

与其说是法线平滑不如说是法线的重建,因为法线的重建实际上只比法线的平滑多一个步骤……
首先遍历所有的三角形,根据三角形的三个顶点组成的两个向量进行叉积可以得到一个面的法线(这一步骤仅针对于不自带法线数据或者法线数据错误的模型,还是比较少见的)。然后通过把一个点对应的所有面的法线取均值得到一个顶点的法线,代码非常简单,但是跑起来效率比较低下,面数极大的模型运行速度会很慢。
可以考虑在ComputeShader里跑,或者放到多线程里跑,面数低的话就无所谓了。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public void execution(GameObject obj)
{
if (obj == null) return;
process = 0;
Mesh mesh = null;
if (obj.GetComponent<MeshFilter>() != null) mesh = obj.GetComponent<MeshFilter>().sharedMesh;
if (obj.GetComponent<SkinnedMeshRenderer>() != null) mesh = obj.GetComponent<SkinnedMeshRenderer>().sharedMesh;
if (mesh == null) return;
for (var j = 0; j < mesh.triangles.Length; j+=3)
{
Vector3 dot1 = mesh.vertices[mesh.triangles[j]];
Vector3 dot2 = mesh.vertices[mesh.triangles[j+1]];
Vector3 dot3 = mesh.vertices[mesh.triangles[j+2]];
Vector3 vec1 = dot2 - dot1;
Vector3 vec2 = dot3 - dot1;
Vector3 normal = Vector3.Cross(vec1, vec2);
mesh.normals[mesh.triangles[j]] = normal;
mesh.normals[mesh.triangles[j+1]] = normal;
mesh.normals[mesh.triangles[j+2]] = normal;
}
SaveMesh(mesh, obj.name);

}

然后进行平滑法线的计算,这个过程同样很慢。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13

var averageNormal = new Dictionary<Vector3, Vector3>();
for (var j = 0; j < mesh.vertexCount; j++)
{
if (!averageNormal.ContainsKey(mesh.vertices[j]))
{
averageNormal.Add(mesh.vertices[j], mesh.normals[j]);
}
else
{
averageNormal[mesh.vertices[j]] += mesh.normals[j];
}

然后输出生成的Mesh到文件中,新Mesh就是进行过法线处理的模型。

代码
1
2
3
4
5
6
7
8
9
10
11

public static void SaveMesh(Mesh mesh, string name)
{
string path = EditorUtility.SaveFilePanel("Save Meshes", "Assets/", name, "asset");
if (string.IsNullOrEmpty(path)) return;
path = FileUtil.GetProjectRelativePath(path);
Mesh meshToSave = Object.Instantiate(mesh) as Mesh;
AssetDatabase.CreateAsset(meshToSave, path);
AssetDatabase.SaveAssets();
}

模型处理系列-模型合并

模型合并是一个经常需要用到的功能,但如果只是有合并需求的话又用不到MeshBaker这种比较大量级的插件,所以可以考虑自己造一个简单的Mesh合并。
UnityEngine中有自带的CombineInstance类可以完成这个功能,用法也比较直观。
照着别人的写了一个。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

public void OnGUI()
{
GUILayout.Label("��ǰѡ���������", EditorStyles.boldLabel);
Object[] lst =Selection.objects;
GUILayout.Label(string.Format("{0}����", lst.Length));
if (GUILayout.Button("Combine"))
{
List<Material> materials = new List<Material>();
List<CombineInstance> combineInstances = new List<CombineInstance>();
for(int i = 0; i < Selection.objects.Length; i++)
{
Debug.Log(i);
GameObject g = Selection.objects[i] as GameObject;
if (g == null) continue;
Debug.Log(-i);
MeshFilter filter = g.GetComponent<MeshFilter>();
if (filter == null || filter.sharedMesh == null) continue;
EditorUtility.DisplayProgressBar("Mesh Combine", "Combine " + filter.sharedMesh.name, (float)i + 1 / Selection.objects.Length);
combineInstances.Add(new CombineInstance()
{
mesh = filter.sharedMesh,
transform = filter.transform.localToWorldMatrix
});
MeshRenderer renderer = g.GetComponent<MeshRenderer>();
materials.AddRange(renderer.sharedMaterials);
}
EditorUtility.ClearProgressBar();
GameObject combine = new GameObject("Combined Mesh");
MeshFilter meshFilter = combine.AddComponent<MeshFilter>();
meshFilter.mesh = new Mesh { name = $"Combined Mesh" };
meshFilter.sharedMesh.CombineMeshes(combineInstances.ToArray(),true);
MeshRenderer meshRenderer = combine.AddComponent<MeshRenderer>();
meshRenderer.materials = materials.ToArray();

}
}

图像处理

如果有opencv,为什么我要用unity来做图像处理呢?
主要是记录一下Unity保存图像的方式,示例是将一些图像按照固定的灰度值进行离散化,将模糊的边缘二值化,输出二值化后的结果图像。但实际上由于图像的压缩算法原因,真正的二值化是无法做到的,如果真的有需要用二值化的图像来进行一些非线性运算的话(例如之前的碎屏效果),请使用滤波算法来去噪。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void SaveTex(Texture2D tex, string name)
{
if (tex == null) return;
Debug.Log(string.Format("Out Image, width:{0}, height:{1},format:{2}", tex.width, tex.height, tex.graphicsFormat));
string path = EditorUtility.SaveFilePanel("Save Image", "Assets/", name, "png");
if (string.IsNullOrEmpty(path)) return;
byte[] dataBytes = tex.EncodeToPNG();
FileStream fileStream = File.Open(path, FileMode.OpenOrCreate);
fileStream.Write(dataBytes, 0, dataBytes.Length);
fileStream.Close();
UnityEditor.AssetDatabase.SaveAssets();
UnityEditor.AssetDatabase.Refresh();
}
public void execution(Texture2D obj)
{
//SaveTex(obj, "wocaonidema");
//return;
Texture2D newtex = new Texture2D(obj.width, obj.height) ;
float[] discre = {0,18,32,43,61,82,98,123,140,162,186,205,231,999 };
for(int i = 0; i < obj.height; i++)
{
for(int j = 0; j < obj.width; j++)
{
Color formerColor = obj.GetPixel(i, j);
float val = formerColor.r * 256f;
float res = 0;
for(int k = 0; k < discre.Length; k++)
{
if (discre[k] > val)
{
res=discre[k-1]/256f;
break;
}
}
newtex.SetPixel(i, j, new Color(res,res,res));
}
}
newtex.Apply();
SaveTex(newtex,obj.name);
}

地形编辑工具-树刷草刷

树刷主要实现一个在任意物体上通过鼠标划动画线来放置其他物体的功能,严格来讲可以被编辑的不止是地形,那刷的也不止是树。
首先,因为是在unity端使用的,这次我们的确会需要一个可以交互的工具窗口UI,基础而言,这个窗口至少要实现如下信息:
①可以选择刷上去的物体;
②可以调整笔刷的参数;
③可以更换手上的工具类型(笔刷、橡皮等);
UI的编写就是EditorWindow的配置,选择刷上去的物体可以用EditorGUILayout.ObjectField()函数来实现,其余也没有什么可以介绍的。
借鉴了网上的一个UI,在此基础上稍微改了改。

图片

接下来是实际效果部分,首先,我们希望在当前工具为“笔刷”或“橡皮”的时候,左键拖拽Scene中被刷物体的时候后者不会被移动等默认的左键效果所影响,这部分可以在OnSceneGUI()中使用HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));函数来实现,当工具处于笔刷、橡皮等状态时调用此函数,可以使得这一次刷新Scene的时候鼠标失去原本的效果。
然后就是刷树的过程,暂且挂个MeshCollider并依靠射线检测来找到鼠标在地形上的坐标不必多说。我们希望刷出来的树只会出现在被刷的地形上,而不会出现树上加树的情况,因此,在射线中需要使用到LayerMask,先把地形放到一个指定Layer层里,然后再进行检测。
用Event来进行鼠标事件的判断,仅当鼠标左键拖拽的时候会触发刷树效果:
e.type==EventType.MouseDrag&&e.button==0
之后是笔刷方面的实现,笔刷笔刷,刷过的地方那肯定就不会再刷了,毕竟再怎么刷都是同样的颜色。因此制作的第一件事就是判定一下笔刷当前的位置是否已经有树了。那怎么知道位置上是否有树呢?用Physics.OverlapSphereNonAlloc()函数来判定范围内是否有足够的标签为树的物体,根据参数中设定的区域范围内参数中设定的最大容量和当前容量的差值,决定这一笔刷多少树上去。
然后就是生成预制体以及调整方向。大部分的资产在导入unity的时候都会设定Z轴朝上,因此我们只需要让生成的预制体面向光线和地形交点的法线方向即可。别忘了给生成的物体打上树标签。

代码
1
2
3
4
5
6
7
8
9
10
11
12
Vector3 bp = new Vector3(pos.x,pos.y+100,pos.z);
Ray ray = new Ray(bp, new Vector3(0, -1, 0));
RaycastHit hit2;
if (!Physics.Raycast(ray, out hit2, 100f, LayerMask.GetMask("TERRAIN")))
{
continue;
}
GameObject g= Instantiate(tree, hit2.point, Quaternion.identity);
tree.tag = "PLANTS";
g.transform.localScale = TreePainter.Instance.scaleBase * g.transform.localScale;
g.transform.LookAt(g.transform.position + hit2.normal);

橡皮就非常简单,直接删掉范围内标签为树标签的物体即可。
无论是树刷还是草刷,生成的预制体太多显然是不能接受的,尤其是大面积生成的时候以及物体的材质无法合批的时候,一旦预制体占的内存太多unity直接就卡死了。因此我们希望当生成的物体面数太多时自动进行一个合批,这里就要用到之间介绍的Mesh合并工具。在草工具中做个实验,我们希望在鼠标左键松开(Event.MouseUp)时之前刷的草自动合并到一个大Mesh,用同样的原理就能完成。
值得一提的是当面数超过一定数量的时候CombineInstance是无法合并的,因此需要注意一下面数,当然也可以在工具里对超过一定面数的Mesh自动合并。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

void MergeMesh()
{
Debug.Log("Merging Mesh");
List<Material> materials = new List<Material>();
List<CombineInstance> combineInstances = new List<CombineInstance>();
for (int i = 0; i < grasses.Count; i++)
{
GameObject g = grasses[i];
if (g == null) continue;
MeshFilter filter = g.GetComponent<MeshFilter>();
if (filter == null || filter.sharedMesh == null) continue;
EditorUtility.DisplayProgressBar("Mesh Combine", "Combine " + filter.sharedMesh.name, (float)i + 1 / Selection.objects.Length);
combineInstances.Add(new CombineInstance()
{
mesh = filter.sharedMesh,
transform = filter.transform.localToWorldMatrix
});
MeshRenderer renderer = g.GetComponent<MeshRenderer>();
materials.AddRange(renderer.sharedMaterials);
}
EditorUtility.ClearProgressBar();
GameObject combine = new GameObject("Combined Mesh");
//if (grasses[0]!=null)
// combine.transform.position = grasses[0].transform.position;
MeshFilter meshFilter = combine.AddComponent<MeshFilter>();
meshFilter.mesh = new Mesh { name = $"Combined Mesh" };
meshFilter.sharedMesh.CombineMeshes(combineInstances.ToArray(), true);
MeshRenderer meshRenderer = combine.AddComponent<MeshRenderer>();
meshRenderer.materials = materials.ToArray();
}
void MergingPlants()
{
MergeMesh();
foreach(GameObject g in grasses)
{
DestroyImmediate(g);
}
grasses.Clear();
}

附上草刷的Editor类完整代码

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.Antlr3.Runtime.Tree;
using UnityEditor;
using UnityEngine;
using UnityEngine.SocialPlatforms;
using static UnityEditor.PlayerSettings;

[CustomEditor(typeof(TreePainterBase))]
public class TreePainterExample : Editor
{
private GameObject grassObject=null;
MeshFilter grassFilter=null;
private Collider[] colliders=new Collider[1005];
Dictionary<Vector3,int> havVectors;
List<GameObject> grasses=new List<GameObject>();
//int operationType = -1;
//int index = 0;
//struct PLANTSinfo
//{
// public GameObject objectType;
// public Vector3 position;
// public Quaternion rotation;
//}
//private PLANTSinfo[] lastOperation = new PLANTSinfo[1005];
private void OnSceneGUI()
{
if (TreePainter.Instance == null) return;
if (TreePainter.Instance.enabled)
{
HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
Planting();

}
}
public override void OnInspectorGUI()
{
//Debug.Log("I am here,not as always");
base.OnInspectorGUI();
EditorGUILayout.BeginVertical();
if (TreePainter.Instance != null)
{
float radius = TreePainter.Instance.brushSize;
float randomness = TreePainter.Instance.scaleRandom;
float density = TreePainter.Instance.density;
GUILayout.Label(string.Format("Radius is :{0}", radius));
GUILayout.Label(string.Format("Randomness is :{0}", randomness));
GUILayout.Label(string.Format("Density is :{0}", density));
}
else
{
GUILayout.Label("There's No TreePainter Opening");
}
EditorGUILayout.EndVertical();
}
void SpawningPlants(Vector3 hit,Vector3 normal)
{
float radius = TreePainter.Instance.brushSize;
float randomness = TreePainter.Instance.scaleRandom;
float density = TreePainter.Instance.density;
GameObject tree = TreePainter.Instance.Plants[TreePainter.Instance.PlantSelect];
if (tree == null) return;
int collidercount = Physics.OverlapSphereNonAlloc(hit, radius, colliders);
int treeCount = 0;
for (int i = 0; i< collidercount; i++){
if (colliders[i].CompareTag("PLANTS"))
{
treeCount++;
}
}
int spawnCount = Mathf.Max(0, (int)density - treeCount);
//index = 0;
for (int i = 0; i< spawnCount; i++)
{
Vector3 pos = new Vector3(hit.x + Random.Range(-radius, radius)*randomness
, hit.y,
hit.z + Random.Range(-radius, radius) * randomness
);
Vector3 bp = new Vector3(pos.x,pos.y+100,pos.z);
Ray ray = new Ray(bp, new Vector3(0, -1, 0));
RaycastHit hit2;
if (!Physics.Raycast(ray, out hit2, 100f, LayerMask.GetMask("TERRAIN")))
{
continue;
}
GameObject g= Instantiate(tree, hit2.point, Quaternion.identity);
tree.tag = "PLANTS";
g.transform.localScale = TreePainter.Instance.scaleBase * g.transform.localScale;
g.transform.LookAt(g.transform.position + hit2.normal);
//
//PLANTSinfo pl = new PLANTSinfo();
//pl.objectType = g;
//pl.position = pos;
//pl.rotation = Quaternion.Euler(new Vector3(-90, 0, 0));
//if (index < 1005) lastOperation[index++] = pl;
//operationType = 0;
//undo
}
}
void SpawningGrass(Vector3 hit, Vector3 normal)
{
float radius = TreePainter.Instance.brushSize;
float randomness = TreePainter.Instance.scaleRandom;
float density = TreePainter.Instance.density;
GameObject tree = TreePainter.Instance.Plants[TreePainter.Instance.PlantSelect];
if (tree == null) return;
int spawnCount = (int)density;
float dx = (radius / density);
float dz = dx;
int lft = (int)Mathf.Sqrt(spawnCount);
//index = 0;
for (int i = -lft;i<=lft ; i++)
{
for (int j = -lft; j <=lft; j++)
{
//Vector3 pos = CountVetexPosition(new Vector3((Mathf.Floor(hit.x / dx)) * dx
// , hit.y,
// (Mathf.Floor(hit.z / dz)) * dz
// ), i * dx, j * dz, normal);
Vector3 bp = new Vector3(hit.x+i*dx, hit.y + 10, hit.z + j * dz);
Ray ray = new Ray(bp, new Vector3(0, -1, 0));
RaycastHit hit2;
if(!Physics.Raycast(ray,out hit2, 100f, LayerMask.GetMask("TERRAIN"))){
continue;
}
int collidercount = Physics.OverlapSphereNonAlloc(hit2.point, dx, colliders);
bool flg = false;
for(int k=0; k < collidercount; k++)
{
if (colliders[k].gameObject.tag == "PLANTS")
{
flg = true;
break;
}
}
Debug.Log(flg);
if (flg == true) continue;
GameObject g = Instantiate(tree, hit2.point, Quaternion.identity);
tree.tag = "PLANTS";
g.transform.LookAt(g.transform.position + hit2.normal);
grasses.Add(g);
if(g.transform.GetChild(0)!=null)
grasses.Add(g.transform.GetChild(0).gameObject);
}
}
}
void ErasingPlants(Vector3 hit)
{
float radius = TreePainter.Instance.brushSize;
float randomness = TreePainter.Instance.scaleRandom;
float density = TreePainter.Instance.density;
int collidercount = Physics.OverlapSphereNonAlloc(hit, radius, colliders);
GameObject tree = TreePainter.Instance.Plants[TreePainter.Instance.PlantSelect];
//index = 0;
GameObject prefab= PrefabUtility.GetCorrespondingObjectFromSource(tree);
for (int i = 0; i < collidercount; i++)
{
if (colliders[i].CompareTag("PLANTS"))
{
// if (PrefabUtility.GetCorrespondingObjectFromSource(colliders[i].gameObject) == prefab)
// {
//PLANTSinfo pl = new PLANTSinfo();
//pl.objectType = tree;
//pl.position = colliders[i].gameObject.transform.position;
//pl.rotation = colliders[i].gameObject.transform.rotation;
//if (index < 1005) lastOperation[index++] = pl;
//operationType = 1;
DestroyImmediate(colliders[i].gameObject);
// }
}
}
}
void MergeMesh()
{
Debug.Log("Merging Mesh");
List<Material> materials = new List<Material>();
List<CombineInstance> combineInstances = new List<CombineInstance>();
for (int i = 0; i < grasses.Count; i++)
{
GameObject g = grasses[i];
if (g == null) continue;
MeshFilter filter = g.GetComponent<MeshFilter>();
if (filter == null || filter.sharedMesh == null) continue;
EditorUtility.DisplayProgressBar("Mesh Combine", "Combine " + filter.sharedMesh.name, (float)i + 1 / Selection.objects.Length);
combineInstances.Add(new CombineInstance()
{
mesh = filter.sharedMesh,
transform = filter.transform.localToWorldMatrix
});
MeshRenderer renderer = g.GetComponent<MeshRenderer>();
materials.AddRange(renderer.sharedMaterials);
}
EditorUtility.ClearProgressBar();
GameObject combine = new GameObject("Combined Mesh");
//if (grasses[0]!=null)
// combine.transform.position = grasses[0].transform.position;
MeshFilter meshFilter = combine.AddComponent<MeshFilter>();
meshFilter.mesh = new Mesh { name = $"Combined Mesh" };
meshFilter.sharedMesh.CombineMeshes(combineInstances.ToArray(), true);
MeshRenderer meshRenderer = combine.AddComponent<MeshRenderer>();
meshRenderer.materials = materials.ToArray();
}
void MergingPlants()
{
MergeMesh();
foreach(GameObject g in grasses)
{
DestroyImmediate(g);
}
grasses.Clear();
}
//void Undo()
//{
// if (TreePainter.Instance.discarding == false) return;
// TreePainter.Instance.discarding = false;
// if (operationType == 0)
// {
// for(int i = 0; i < index; i++)
// {
// DestroyImmediate(lastOperation[i].objectType);
// }
// }
// else
// {
// for(int i = 0; i < index; i++)
// {
// PLANTSinfo pl = lastOperation[i];
// Instantiate(pl.objectType,pl.position,pl.rotation);
// }
// }
//}
//void SpawningGrassObject(Vector3 point)
//{
// grassObject = new GameObject("GrassObject");
// grassFilter = grassObject.AddComponent<MeshFilter>();
// grassFilter.mesh = new Mesh{name = $"GrassMesh"};
// grassFilter.sharedMesh = grassFilter.mesh;
// havVectors = new Dictionary<Vector3, int>();
//}
Vector3 CountVetexPosition(Vector3 center, float dx, float dz, Vector3 normal)
{
float d = -(dz * normal.z + dx * normal.x) / (normal.y + 0.0001f);
return new Vector3(center.x + dx, center.y + d, center.z + dz);

}
//void DrawingGrass(Vector3 point,Vector3 normal,float grassDensity)
//{
// Vector2 xz = new Vector2(Mathf.Floor(point.x / grassDensity), Mathf.Floor(point.z/grassDensity));
// if (havVectors[new Vector3(xz.x, 0, xz.y)] == 1)
// {
// return;
// }
// havVectors[new Vector3(xz.x, 0, xz.y)] = 1;

//}
void DiscardingGrassObject()
{
grassObject=null;
grassFilter = null;
havVectors.Clear();
}
void Planting()
{
// Undo();
Event e = Event.current;
RaycastHit raycastHit = new RaycastHit();
Ray terrain=HandleUtility.GUIPointToWorldRay(e.mousePosition);
if (Physics.Raycast(terrain, out raycastHit, Mathf.Infinity,LayerMask.GetMask("TERRAIN")))
{
//if (e.type == EventType.MouseDown)
//{
// if (TreePainter.Instance.type == 3) SpawningGrassObject(raycastHit.point);
//}
// Gizmos.DrawWireSphere(raycastHit.point, TreePainter.Instance.brushSize);
if (e.type == EventType.MouseUp)
{
if (TreePainter.Instance.type == 3) MergingPlants();
}
if (e.type==EventType.MouseDrag&&e.button==0)
{
if(TreePainter.Instance.type==1)SpawningPlants(raycastHit.point,raycastHit.normal);
if (TreePainter.Instance.type == 2) ErasingPlants(raycastHit.point);
if (TreePainter.Instance.type == 3) SpawningGrass(raycastHit.point, raycastHit.normal);
}
}
}
}