Unity由灰度高度图生成Mesh并计算UV

偶然看到一个网站有三维地形展示栏目,描述说用 Unity + 灰度图做的,看着挺有意思,就顺便研究下相关的东西。
效果图
我没有对应的实景贴图,因此使用了粒子的 Shader 做颜色渐变用于演示。
基础知识
1.点线面及Mesh
点构成线,线构成面,面及其相关顶点、额外信息等构成 Mesh。在现代 3d 渲染中,一般使用三角面作为渲染中的面。
一个 Mesh 包含的信息主要有点、点的几何关系(面)、UV、法线等内容。
2.Unity基础
传统的渲染流水线可以大致分为应用阶段、几何阶段、光栅化阶段。
在 Unity 中,如果不涉及自编写 Shader,则 Unity 为我们处理了几何阶段及光栅化阶段的工作,同时也简化了应用阶段的工作。
在应用阶段,一个游戏物体通过MeshFilter
组件读取一个 Mesh 网格,之后通过MeshRenderer
组件及相应的 Shader 着色器将其渲染出来。
3.灰度高度图
高度图是一种标识了地形高度的图片,一般常见的是灰度高度图。在灰度高度图中,每个像素有 0 – 255 共 256 个值,该值代表该点的高度。
在灰度图中,一般使用纯黑(255)代表最低点,而用纯白(0)代表最高点,当然也有反过来的做法。
我使用这张图片作为高度图,该图片原图分辨率(1081 * 1081)。
计算顶点、三角面及法线
以简单的情况为例,定义一个 3 * 3 平面网格,其内容如下:
1.顶点
Unity Mesh 使用一个 Vector3 数组vertices
保存顶点,同时使用一个 int 数组triangles
保存顶点关系(面)。在triangles
中,每个值为vertices
内的索引。
实际上,vertices 中的顺序不是特别重要,只要 triangles 中指定了正确的索引即可。但为了方便,以如下方式设置顶点顺序:
由上可得,每个顶点的索引为x + (y * n)
,其中 n 为每行的顶点个数。
2.三角面
Mesh.triangles
数组的长度是 3 的倍数,其中每三个代表一个三角形。triangles
中的值为vertices
中的索引。
以上文中的简单网格为例,每个方格至少可以分成 2 个三角面,分割方法自定。我选择以斜上的方式切分,这样形成了三角形 1 和 2。
以顺时针的方式声明三角形数据,数据如下:
- 三角形1:(A,C,D)
- 三角形2:(A,D,B)
假设该方格左下角 A 点索引为 o,每行共有 n 个顶点,则其他三点的索引为:
- B:(o + 1)
- C:(o + n)
- D:(o + n + 1)
每个方格共涉及 6 个点(包含 2 个公用点)。
3.法线
Unity 采用的是左手坐标系,因此使用顺时针形式声明三角形。
根据左手定则,顺时针情况下,该面法线朝上。在单面渲染中,从上往下就可以看到图像。
计算 UV
UV 信息代表了每个顶点的贴图坐标。在顶点之间,一般会按照线性插值(取决于顶点着色器和片元着色器等)的方式设置每个像素的贴图坐标,进而设置像素颜色。
本文讨论的情况比较简单,所以横竖顶点均分 UV 即可。
Code
代码结构参考了知乎的文章:https://zhuanlan.zhihu.com/p/53355843
using JetBrains.Annotations;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapGenerator : MonoBehaviour
{
/// 梓miao出没博客(https://www.azimiao.com)
/// 代码结构参考了知乎文章:https://zhuanlan.zhihu.com/p/53355843
/// <summary>
/// 采样次数,实际上是横向纵向分成多少个小格子。分的越多,采样越多,但不能超过图像的宽度,且受限于单 Mesh 最大顶点数量限制
/// </summary>
[Header("采样次数")]
public int gridW = 250;
public int gridH = 250;
/// <summary>
/// 整体大小
/// </summary>
[Header("尺寸")]
public int width = 1080;
/// <summary>
/// 我们缩放了长宽,但没有缩放高度,这个值用来缩放高度
/// </summary>
[Header("Y轴缩放")]
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++)
{
//设置每个顶点
int nowIndex = x + y * wVertCount;
//设置顶点位置
_vertices[nowIndex].x = x * width / gridW;
_vertices[nowIndex].z = y * width / gridH;
//设置顶点位置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>
/// 按每个小方格设置三角形数据
/// </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);
//三角形1
_triangles[index] = nowIndex;
_triangles[index + 1] = nowIndex + wVertCount;
_triangles[index + 2] = _triangles[index + 1] + 1;
//三角形2
_triangles[index + 3] = nowIndex;
_triangles[index + 4] = _triangles[index + 2];
_triangles[index + 5] = nowIndex + 1;
//每个方格六个顶点
index += 6;
}
}
}
/// <summary>
/// 设置顶点 UV 数据
/// </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>
/// 设置 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>
/// 设置 贴图
/// </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
}
其他
- 注意单个 Mesh 有顶点数量小于 65000 的限制。
- 因为没有对应的地形贴图,因此使用支持 mesh.color 的粒子 shader。