偶然看到一个网站有三维地形展示栏目,描述说用 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。
梓喵出没博客(azimiao.com)版权所有,转载请注明链接:https://www.azimiao.com/7073.html
欢迎加入梓喵出没博客交流群:313732000

我来吐槽

*

*