📜 ⬆️ ⬇️

Unity3d We play with the mesh. Part 1 - Generating a mesh using an elevation map

A long time ago I worked with Unity3D. These times are gone, but warm and not so memorable memories remain.


The other day, I decided to clean up my cloud storage from old files and came across my old practices - the assetpackage PlanetWalker .


It was an old project in which it was planned to travel through space and visit various planets, the surface of which was to be generated from a randomly chosen height map.


Unfortunately, apart from the ship class and the unfinished mesh generation class, there was nothing in this project. But I decided to fix it and write a couple of editor extensions. Firstly, I solemnly swear that I am only plotting a prank ...



Content


  1. Unity3d We play with the mesh. Part 1 - Generating a mesh using an elevation map
  2. Unity3d We play with the mesh. Part 2 - Deformation of the mesh with a height map
  3. Unity3d We play with the mesh. Part 3 - Collision Based Mesh Warp

In the image above, you could see the mesh. By the way, this is the already generated mesh using the height map. However, before we start, I suggest you understand what a mesh is.


What is a mesh


We use a mesh to display visual geometry in our games. But what is he like?


In Unity mesh has:



That is, a mesh is a class with a set of attributes, with the help of which it is rendered, reflects light, displays texture, etc. True, if you look at it from this point of view, then nothing complicated? :)


What I would like to note right away is that in Unity the number of vertices should not exceed 65025 pieces. This can be solved by describing your mesh class. But why?


What is a height map?



You probably have already met the maps of heights. Roughly speaking, the elevation map is such a black and white drawing. And the darker the pixel, the lower the height at that point, and the brighter - the higher the height.


Theory


How can we build the mesh itself using a height map? It turns out that we have a picture, the color of the pixel which is responsible for the height of the vertex. In theory, we have to go through each pixel of the height map and find out what color it is, convert the pixel color to float from 0 to 1 and convert the resulting data into a mesh in accordance with the specified dimensions.


GetPixel and Color.grayscale will help us with this . Keep in mind that the texture of the height map must be checked Read&Write , otherwise it will not work to read the data.


Basic preparations


With what a mesh we met, it's time to get to work. In order not to clutter up the code, create the ParentEditor class. In it we will thrust all sorts of auxiliary methods and we will inherit our extension scripts from it. As planned, we will have editor windows, so the class itself will inherit from EditorWindow .


I think that explanations are not needed here.
 using UnityEditor; using UnityEngine; public class ParentEditor : EditorWindow { public void CreatePaths() { if (!AssetDatabase.IsValidFolder("Assets/MeshTools")) AssetDatabase.CreateFolder("Assets", "MeshTools"); if (!AssetDatabase.IsValidFolder("Assets/MeshTools/Meshes")) AssetDatabase.CreateFolder("Assets/MeshTools", "Meshes"); if (!AssetDatabase.IsValidFolder("Assets/MeshTools/Meshes/Generated")) AssetDatabase.CreateFolder("Assets/MeshTools/Meshes", "Generated"); if (!AssetDatabase.IsValidFolder("Assets/MeshTools/Meshes/Updated")) AssetDatabase.CreateFolder("Assets/MeshTools/Meshes", "Updated"); if (!AssetDatabase.IsValidFolder("Assets/MeshTools/Prefabs")) AssetDatabase.CreateFolder("Assets/MeshTools", "Prefabs"); if (!AssetDatabase.IsValidFolder("Assets/MeshTools/Prefabs/Generated")) AssetDatabase.CreateFolder("Assets/MeshTools/Prefabs", "Generated"); if (!AssetDatabase.IsValidFolder("Assets/MeshTools/Prefabs/Updated")) AssetDatabase.CreateFolder("Assets/MeshTools/Prefabs", "Updated"); } public int Cut(int value) { return Mathf.Min(value, 255); } public Mesh CopyMesh(Mesh mesh) { Mesh newmesh = new Mesh(); newmesh.vertices = mesh.vertices; newmesh.triangles = mesh.triangles; newmesh.uv = mesh.uv; newmesh.normals = mesh.normals; newmesh.tangents = mesh.tangents; return newmesh; } } 

Fine! Go to the interesting ...


Mesh generation


Create a new MeshGenerator class and inherit it from ParentEditor .


Let's designate in it several variables we need, make them displayed and specify the path in the toolbar:


Meshhenerator
 using UnityEditor; using UnityEngine; public class MeshGenerator : ParentEditor { public Texture2D heightMap; public Material mat; public Vector3 size = new Vector3(2048, 300, 2048); public string mname = "Enter name"; GameObject generated; [MenuItem("Tools/Mesh Generator")] static void Init() { MeshGenerator mg = (MeshGenerator)GetWindow(typeof(MeshGenerator)); mg.Show(); } void OnGUI() { heightMap = (Texture2D)EditorGUILayout.ObjectField("Height map", heightMap, typeof(Texture2D), false); mat = (Material)EditorGUILayout.ObjectField("Material", mat, typeof(Material), false); size = EditorGUILayout.Vector3Field("Size", size); mname = EditorGUILayout.TextField("Name", mname); } 

Let's now write a method that will generate a mesh for us and return a full GO


Generate
  GameObject Generate() { //  GO   ,   GameObject go = new GameObject(mname); go.transform.position = Vector3.zero; go.AddComponent<MeshFilter>(); go.AddComponent<MeshRenderer>(); // ,    .    -   if (mat != null) go.GetComponent<Renderer>().material = mat; else { Debug.LogError("No material attached! Aborting"); return null; } //    Unity   //        255 // 255*255 = 65025 int width = Cut(heightMap.width); int height = Cut(heightMap.height); //   Mesh mesh = new Mesh(); Vector3[] vertices = new Vector3[height * width]; //    Vector2[] UVs = new Vector2[height * width]; // uv  Vector4[] tangs = new Vector4[height * width]; //   //      Vector2 uvScale = new Vector2(1 / (width - 1), 1 / (height - 1)); Vector3 sizeScale = new Vector3(size.x / (width - 1), size.y, size.z / (height - 1)); //  ,     Vector2 (x, y) //     Vector3(x, y, z).   //     z  int index; float pixelHeight; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { index = y * width + x; //   pixelHeight = heightMap.GetPixel(x, y).grayscale; //       Vector3 vertex = new Vector3(x, pixelHeight, y); //   vertices[index] = Vector3.Scale(sizeScale, vertex); //         Vector2 cur_uv = new Vector2(x, y); //  uv  UVs[index] = Vector2.Scale(cur_uv, uvScale); //  uv        /* Vector*.Scale(Vector* a, Vector* b)     .  , , Vector3.Scale(a=(1, 2, 1), b=(2, 3, 1))    (2, 6, 1) */ /*  :           X.       ,        . W        -1,  1,         W   */ Vector3 leftV = new Vector3(x - 1, heightMap.GetPixel(x - 1, y).grayscale, y); Vector3 rightV = new Vector3(x + 1, heightMap.GetPixel(x + 1, y).grayscale, y); Vector3 tang = Vector3.Scale(sizeScale, rightV - leftV).normalized; tangs[index] = new Vector4(tang.x, tang.y, tang.z, 1); } } //      mesh.vertices = vertices; //   mesh.uv = UVs; //  uv  //     index = 0; int[] triangles = new int[(height - 1) * (width - 1) * 6]; for (int y = 0; y < height - 1; y++) { for (int x = 0; x < width - 1; x++) { //   triangles[index++] = (y * width) + x; triangles[index++] = ((y + 1) * width) + x; triangles[index++] = (y * width) + x + 1; triangles[index++] = ((y + 1) * width) + x; triangles[index++] = ((y + 1) * width) + x + 1; triangles[index++] = (y * width) + x + 1; } } //        mesh.triangles = triangles; //  ,    mesh.RecalculateNormals(); //          mesh.tangents = tangs; //     go.GetComponent<MeshFilter>().sharedMesh = mesh; return go; } 

Super! Generation sorted out. It remains to embed it all in our extension. Let's change our OnGUI method, at the same time adding a save function to it.


New OnGUI
  void OnGUI() { heightMap = (Texture2D)EditorGUILayout.ObjectField("Height map", heightMap, typeof(Texture2D), false); mat = (Material)EditorGUILayout.ObjectField("Material", mat, typeof(Material), false); size = EditorGUILayout.Vector3Field("Size", size); mname = EditorGUILayout.TextField("Name", mname); if (GUILayout.Button("Generate mesh", GUILayout.Height(20))) generated = Generate(); if (GUILayout.Button("Save", GUILayout.Height(20))) { Mesh generated_mesh = generated.GetComponent<MeshFilter>().sharedMesh; CreatePaths(); AssetDatabase.CreateAsset(generated_mesh, "Assets/MeshTools/Meshes/Generated/" + mname + ".asset"); //   PrefabUtility.CreatePrefab("Assets/MeshTools/Prefabs/Generated/" + mname + ".prefab", generated); //    GO } } 

If you did everything correctly, then you have a new drop-down menu item Tools->Mesh Generator and you can already test your samopisny extension of the editor by inserting height maps and materials there! :)


If something ParentEditor with you, check that you have created the ParentEditor class, and your class itself looks like this:


MeshGenerator.cs
 using UnityEditor; using UnityEngine; public class MeshGenerator : ParentEditor { public Texture2D heightMap; public Material mat; public Vector3 size = new Vector3(2048, 300, 2048); public string mname = "Enter name"; GameObject generated; [MenuItem("Tools/Mesh Generator")] static void Init() { MeshGenerator mg = (MeshGenerator)GetWindow(typeof(MeshGenerator)); mg.Show(); } void OnGUI() { heightMap = (Texture2D)EditorGUILayout.ObjectField("Height map", heightMap, typeof(Texture2D), false); mat = (Material)EditorGUILayout.ObjectField("Material", mat, typeof(Material), false); size = EditorGUILayout.Vector3Field("Size", size); mname = EditorGUILayout.TextField("Name", mname); if (GUILayout.Button("Generate mesh", GUILayout.Height(20))) generated = Generate(); if (GUILayout.Button("Save", GUILayout.Height(20))) { Mesh generated_mesh = generated.GetComponent<MeshFilter>().sharedMesh; CreatePaths(); AssetDatabase.CreateAsset(generated_mesh, "Assets/MeshTools/Meshes/Generated/" + mname + ".asset"); PrefabUtility.CreatePrefab("Assets/MeshTools/Prefabs/Generated/" + mname + ".prefab", generated); } } GameObject Generate() { GameObject go = new GameObject(mname); go.transform.position = Vector3.zero; go.AddComponent<MeshFilter>(); go.AddComponent<MeshRenderer>(); if (mat != null) go.GetComponent<Renderer>().material = mat; else { Debug.LogError("No material attached! Aborting"); return null; } int width = Cut(heightMap.width); int height = Cut(heightMap.height); Mesh mesh = new Mesh(); Vector3[] vertices = new Vector3[height * width]; Vector2[] UVs = new Vector2[height * width]; Vector4[] tangs = new Vector4[height * width]; Vector2 uvScale = new Vector2(1 / (width - 1), 1 / (height - 1)); Vector3 sizeScale = new Vector3(size.x / (width - 1), size.y, size.z / (height - 1)); int index; float pixelHeight; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { index = y * width + x; pixelHeight = heightMap.GetPixel(x, y).grayscale; Vector3 vertex = new Vector3(x, pixelHeight, y); vertices[index] = Vector3.Scale(sizeScale, vertex); Vector2 cur_uv = new Vector2(x, y); UVs[index] = Vector2.Scale(cur_uv, uvScale); Vector3 leftV = new Vector3(x - 1, heightMap.GetPixel(x - 1, y).grayscale, y); Vector3 rightV = new Vector3(x + 1, heightMap.GetPixel(x + 1, y).grayscale, y); Vector3 tang = Vector3.Scale(sizeScale, rightV - leftV).normalized; tangs[index] = new Vector4(tang.x, tang.y, tang.z, 1); } } mesh.vertices = vertices; mesh.uv = UVs; index = 0; int[] triangles = new int[(height - 1) * (width - 1) * 6]; for (int y = 0; y < height - 1; y++) { for (int x = 0; x < width - 1; x++) { triangles[index++] = (y * width) + x; triangles[index++] = ((y + 1) * width) + x; triangles[index++] = (y * width) + x + 1; triangles[index++] = ((y + 1) * width) + x; triangles[index++] = ((y + 1) * width) + x + 1; triangles[index++] = (y * width) + x + 1; } } mesh.triangles = triangles; mesh.RecalculateNormals(); mesh.tangents = tangs; go.GetComponent<MeshFilter>().sharedMesh = mesh; return go; } } 

We are testing our extension

Take the height map and specify MaxSize in 256, tick Read&Write and click on Apply .


Open our extension and set the parameters. Choose Tools->Mesh Generator
And we see the next window (in my case the window is already with the settings):

Click on Generate Mesh and ...



Shaded and Wireframe mapping



Elevation map used


By clicking on the Save button we save the generated mesh and object prefab in the folders "Assets / MeshTools / Meshes / Generated" and "Assets / MeshTools / Prefabs / Generated", respectively.


Mischief managed! :)


Continue to indulge ...


')

Source: https://habr.com/ru/post/328498/


All Articles