Unity generates mesh from grayscale height image and calculates UVs

I happened to see a website with a 3D terrain display column, which described that it was made with Unity+grayscale image. It looked very interesting, so I studied something related by the way.

design sketch

I don't have a corresponding realistic map, so I used the shader of particles for color gradient for demonstration.

Basic knowledge

1. Point, line, surface and mesh

Points form lines, lines form faces, and faces and their related vertices, additional information, etc. form meshes. In modern 3d rendering, triangular faces are generally used as faces in rendering.

The information contained in a mesh mainly includes points, their geometric relationships (faces), UVs, normals, etc.

2. Unity foundation

Traditional rendering pipeline can be roughly divided into application stage, geometric stage and rasterization stage.

In Unity, if it does not involve self writing Shaders, Unity handles the work in the geometric phase and rasterization phase for us, and also simplifies the work in the application phase.

In the application phase, a game object passes MeshFilter The component reads a mesh grid, and then passes the MeshRenderer Component and the corresponding Shader shader render it.

3. Gray scale height map

Height map is a kind of image that marks the height of the terrain. Generally, gray height map is common. In the grayscale height map, each pixel has 0 – 255 values in total, which represents the height of the point.

In gray-scale images, pure black (255) is generally used to represent the lowest point, while pure white (0) is used to represent the highest point. Of course, the opposite is also true.

I use this picture as the height map, and the original image resolution of this picture is 1081 * 1081.

Calculate vertices, triangles and normals

Taking a simple case as an example, define a 3 * 3 plane grid, and its contents are as follows:

1. Vertex

Unity Mesh uses a Vector3 array vertices Save vertices and use an int array at the same time triangles Save vertex relationships (faces). stay triangles , each value is vertices Index within.

In fact, the order in vertices is not particularly important, as long as the correct index is specified in triangles. But for convenience, set the vertex order as follows:

From the above, the index of each vertex is x + (y * n) Where n is the number of vertices in each line.

2. Triangular surface

Mesh.triangles The length of the array is a multiple of 3, where every three represents a triangle. triangles The value in is vertices Index in.

As an example of the simple mesh in the above article, each grid can be divided into at least two triangular faces, and the segmentation method is determined by yourself. I chose to slice diagonally, which formed triangles 1 and 2.

Declare triangle data in a clockwise manner, and the data are as follows:

  • Triangle 1: (A, C, D)
  • Triangle 2: (A, D, B)

Suppose the index of point A at the lower left corner of the grid is o, and there are n vertices in each row, then the index of the other three points is:

  • B:(o + 1)
  • C:(o + n)
  • D:(o + n + 1)

Each grid involves 6 points in total (including 2 common points).

3. Normal

Unity uses a left-handed coordinate system, so it uses a clockwise form to declare triangles.

According to the left-handed rule, the normal of the face is upward when it is clockwise. In single-sided rendering, you can see the image from top to bottom.

Calculate UVs

The UV information represents the mapping coordinates of each vertex. Between vertices, the mapping coordinates of each pixel are generally set by linear interpolation (depending on vertex shaders, slice shaders, etc.), and then the pixel color is set.

The situation discussed in this article is relatively simple, so it is enough to divide the UV evenly between horizontal and vertical vertices.

Code

The code structure refers to Zhihu's article: https://zhuanlan.zhihu.com/p/53355843

 using JetBrains.Annotations; using System.Collections; using System.Collections.Generic; using UnityEngine; public class MapGenerator : MonoBehaviour { ///Blog( https://www.azimiao.com  ///Refer to Zhihu article for code structure: https://zhuanlan.zhihu.com/p/53355843 /// <summary> ///The number of sampling times is actually the number of cells divided horizontally and vertically. The more points, the more samples, but not more than the width of the image, and limited by the maximum number of vertices in a single mesh /// </summary> [Header ("Sampling Times")] public int gridW = 250; public int gridH = 250; /// <summary> ///Overall size /// </summary> [Header ("Size")] public int width = 1080; /// <summary> ///We have scaled the length and width, but not the height. This value is used to scale the height /// </summary> [Header ("Y Scale")] public float yScale = 120; public Texture2D heightMapTex; public Texture2D textureMap; public Gradient renderColor; private int hVertCount = 0, wVertCount = 0; private Vector3[] _vertices; private Vector2[] _uvs; private int[] _triangles; private Mesh _mesh; private Color[] _color; public void GeneratorTerrain() { float wStep = (heightMapTex.width - 1) / (float)gridW; float hStep = (heightMapTex.height - 1) / (float)gridH; wVertCount = gridW + 1; hVertCount = gridH + 1; _vertices = new Vector3[wVertCount * hVertCount]; _color = new Color[_vertices.Length]; for (int x = 0;  x < wVertCount; x++) { for (int y = 0;  y < hVertCount; y++) { //Set Each Vertex int nowIndex = x + y * wVertCount; //Set vertex position _vertices[nowIndex].x = x * width / gridW; _vertices[nowIndex].z = y * width / gridH; //Set vertex position Y Color cc = heightMapTex.GetPixel(Mathf. FloorToInt(x * wStep), Mathf.FloorToInt(y * hStep)); float height =  cc.grayscale * yScale; Color t = renderColor.Evaluate(cc.grayscale); _color[nowIndex] = t; _vertices[nowIndex].y = height; } } this.SetTrianglesData(); this.SetUVData(); this.DrawMesh(); this.DrawTexture(); } /// <summary> ///Set triangle data by each small square /// </summary> public void SetTrianglesData() { int triangleNum = gridH * gridW; int triangleVertNum = triangleNum * 6; _triangles = new int[triangleVertNum]; int index = 0; for (int x = 0;  x < gridW; x++) { for (int y = 0;  y < gridH; y++) { int nowIndex = x + y * wVertCount; //Debug. Log("x:" + x + "|y:" + y + "|index:" + nowIndex); //Triangle 1 _triangles[index] = nowIndex; _triangles[index + 1] = nowIndex +  wVertCount; _triangles[index + 2] = _triangles[index + 1] + 1; //Triangle 2 _triangles[index + 3] = nowIndex; _triangles[index + 4] = _triangles[index + 2]; _triangles[index + 5] = nowIndex + 1; //Six vertices per grid index += 6; } } } /// <summary> ///Set vertex UV data /// </summary> public void SetUVData() { _uvs = new Vector2[hVertCount * wVertCount]; float w = 1.0f / gridW; float h = 1.0f / gridH; for (int x = 0;  x < gridW; x++) { for (int y = 0;  y < gridH; y++) { int nowIndex = x + y * wVertCount; _uvs[nowIndex] = new Vector2(x * w, y * h); } } } /// <summary> ///Set Mesh /// </summary> public void DrawMesh() { if (gameObject. GetComponent<MeshFilter>() == null) { _mesh = new Mesh(); _mesh.name = "ssss"; gameObject.AddComponent<MeshFilter>().sharedMesh = _mesh; } else { _mesh = gameObject. GetComponent<MeshFilter>().sharedMesh; } _mesh. Clear(); _mesh.vertices = _vertices; _mesh.uv = _uvs; _mesh.colors = _color; _mesh.triangles = _triangles; _mesh. RecalculateNormals(); _mesh. RecalculateBounds(); _mesh. RecalculateTangents(); } /// <summary> ///Set Map /// </summary> public void DrawTexture() { if (gameObject. GetComponent<MeshRenderer>() == null) { gameObject.AddComponent<MeshRenderer>(); } //Material diffuseMap = new Material(Shader.Find("Particles/Standard Unlit")); Material diffuseMap = new Material(Shader. Find("Particles/Standard Surface")); diffuseMap.SetTexture("_MainTex", this.textureMap); gameObject.GetComponent<Renderer>().material = diffuseMap; } #if UNITY_EDITOR [UnityEditor.CustomEditor(typeof(MapGenerator))] public class ViveInputAdapterManagerEditor : UnityEditor.Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); if (! Application.isPlaying) { var targetNode = target as MapGenerator; if (targetNode == null) { return; } GUILayout.BeginHorizontal(); if (GUILayout. Button("Generator_Mesh")) { targetNode.GeneratorTerrain(); } GUILayout.EndHorizontal(); } } } #endif }

other

  • Note that a single mesh has a limit of less than 65000 vertices.
  • Because there is no corresponding terrain map, the particle shader supporting mesh. color is used.
Zimiao haunting blog (azimiao. com) All rights reserved. Please note the link when reprinting: https://www.azimiao.com/7073.html
Welcome to the Zimiao haunting blog exchange group: three hundred and thirteen million seven hundred and thirty-two thousand

Comment

*

*