大概就是根据一个灰度图,生成一个地形。

分两步来实现吧;首先,用随机数生成地形;然后,根据灰度图生成地形。

小白,没啥基础,所以只能慢慢来。

首先,得有一些基本概念的:

00.一些基本概念

演示

我是个小白,所以,刚开始,来点直观的吧

新建了一个空物体gameobject,手动添加了3样东西给它:

  1. MeshFilter组件
  2. MeshRender组件
  3. 考虑到没有材质球会成粉色,所以新建了个默认的材质球给它。

前两个组件是主要的,下面的动图就简单的演示了这两个组件的作用。




BlenderGIS 灰 blendergis灰地图_数据


顺便提一下,这里有个线框模式显示的开关。



BlenderGIS 灰 blendergis灰地图_数据_02


大概有了个朦胧的认识:

  • MeshFilter里的Mesh可以控制物体的形状
  • MeshRender负责物体的显示

现在,开始文档里的正式介绍。

Mesh

Unity - Scripting API: Mesh (unity3d.com)

《inherits from Object》

里面按一定规则存着模型的数据,比如顶点什么的。【就是顶点着色器里的那个顶点】



BlenderGIS 灰 blendergis灰地图_数据_03


看定义可能比较朦胧,看这个示例代码,就很清楚它是什么了:



BlenderGIS 灰 blendergis灰地图_List_04


MeshFilter

Unity - Manual: Mesh Filter component (unity3d.com)

这个组件,也很简单呐;里面就一个Mesh。。



BlenderGIS 灰 blendergis灰地图_数据_05


具体可以这么用:

Unity - Scripting API: MeshFilter.mesh (unity3d.com)

从代码里可以看到,这个组件里的Mesh,就是上面的那个Mesh类



BlenderGIS 灰 blendergis灰地图_List_06


MeshRender

Unity - Manual: Mesh Renderer component (unity3d.com)

材质球,UnityShader,就是拖给这个组件的。再结合它的名字猜一下——它负责把网格画出来



BlenderGIS 灰 blendergis灰地图_数据_07


小结

  • MeshFilter和MeshRender是一对
  • MeshFilter组件和Mesh类是一对
  • 负责提供数据
  • 如顶点在模型坐标系下的坐标
  • MeshRender组件和材质球,shader是一对
  • 定义了如何使用数据
  • 比如在片元着色器里把quad给discard成ball,point sprite就是这么来的

后面就是按着视频来了。

01.大小为1的平面

这个是视频里的代码。

结合上面介绍的基本概念,大概知道它在干什么吧。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Terrian : MonoBehaviour
{
    public float width = 0.1f;

    MeshRenderer meshRenderer;
    MeshFilter meshFilter;

    // 用来存放顶点数据
    List<Vector3> verts;
    List<int> indices;

    private void Awake()
    {
    }

    private void Start()
    {
        verts = new List<Vector3>();
        indices = new List<int>();

        meshRenderer = GetComponent<MeshRenderer>();
        meshFilter = GetComponent<MeshFilter>();

        Generate();
    }

    public void Generate()
    {
        ClearMeshData();

        // 把数据填写好
        AddMeshData();

        // 把数据传递给Mesh,生成真正的网格
        Mesh mesh = new Mesh();
        mesh.vertices = verts.ToArray();
        //mesh.uv = uvs.ToArray();
        mesh.triangles = indices.ToArray();

        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        meshFilter.mesh = mesh;
    }

    void ClearMeshData()
    {
        verts.Clear();
        indices.Clear();
    }

    void AddMeshData()
    {
        verts.Add(new Vector3(0, 0, 0));
        verts.Add(new Vector3(0, 0, 1));
        verts.Add(new Vector3(1, 0, 1));
        verts.Add(new Vector3(1, 0, 0));

        indices.Add(0);  indices.Add(1);  indices.Add(2);
        indices.Add(0);  indices.Add(2);  indices.Add(3);
    }
    
}


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Terrian : MonoBehaviour
{
    public float width = 0.1f;

    MeshRenderer meshRenderer;
    MeshFilter meshFilter;

    // 用来存放顶点数据
    List<Vector3> verts;
    List<int> indices;

    private void Awake()
    {
    }

    private void Start()
    {
        verts = new List<Vector3>();
        indices = new List<int>();

        meshRenderer = GetComponent<MeshRenderer>();
        meshFilter = GetComponent<MeshFilter>();

        Generate();
    }

    public void Generate()
    {
        ClearMeshData();

        // 把数据填写好
        AddMeshData();

        // 把数据传递给Mesh,生成真正的网格
        Mesh mesh = new Mesh();
        mesh.vertices = verts.ToArray();
        //mesh.uv = uvs.ToArray();
        mesh.triangles = indices.ToArray();

        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        meshFilter.mesh = mesh;
    }

    void ClearMeshData()
    {
        verts.Clear();
        indices.Clear();
    }

    void AddMeshData()
    {
        verts.Add(new Vector3(0, 0, 0));
        verts.Add(new Vector3(0, 0, 1));
        verts.Add(new Vector3(1, 0, 1));
        verts.Add(new Vector3(1, 0, 0));

        indices.Add(0);  indices.Add(1);  indices.Add(2);
        indices.Add(0);  indices.Add(2);  indices.Add(3);
    }
    
}

在上面的那个演示的基础上,把它拖给空物体,就可以了。



BlenderGIS 灰 blendergis灰地图_游戏引擎_08


02.更大规模的平面

灌数据到Mesh的原理

主要就俩数组,一个是顶点,一个是索引。

视频里这个图挺好的。

顶点数据



BlenderGIS 灰 blendergis灰地图_BlenderGIS 灰_09


索引数据

稍微复杂一点,因为顶点是单独的,这个是相互关联的。



BlenderGIS 灰 blendergis灰地图_List_10


很形象的图,涉及到二维逻辑地址和一维物理地址的换算。



BlenderGIS 灰 blendergis灰地图_BlenderGIS 灰_11


从特殊到一般:



BlenderGIS 灰 blendergis灰地图_数据_12


最终应用【顺序是比较重要的,因为单面剔除,cull on,cull off之类的】



BlenderGIS 灰 blendergis灰地图_BlenderGIS 灰_13


试一试

修改前:

void AddMeshData()
{
    verts.Add(new Vector3(0, 0, 0));
    verts.Add(new Vector3(0, 0, 1));
    verts.Add(new Vector3(1, 0, 1));
    verts.Add(new Vector3(1, 0, 0));

    indices.Add(0);  indices.Add(1);  indices.Add(2);
    indices.Add(0);  indices.Add(2);  indices.Add(3);
}
void AddMeshData()
{
    verts.Add(new Vector3(0, 0, 0));
    verts.Add(new Vector3(0, 0, 1));
    verts.Add(new Vector3(1, 0, 1));
    verts.Add(new Vector3(1, 0, 0));

    indices.Add(0);  indices.Add(1);  indices.Add(2);
    indices.Add(0);  indices.Add(2);  indices.Add(3);
}

修改后

void AddMeshData()
{
    int N = 10;
    //01填充顶点数据
    for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以最外层的循环是z不是x
    {
        for(int x = 0; x < N; ++x)
        {
            Vector3 temp = new Vector3(x, 0, z);
            verts.Add(temp);
        }
    }
    //02填充索引数据
    for(int z = 0; z < N - 1; ++z)
    {
        for(int x = 0; x < N - 1; ++x)
        {
            int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
            int index_lt = (z + 1) * N + x;
            int index_rt = (z + 1) * N + x + 1;
            int index_rb = z * N + x + 1;

            indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
            indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
        }
    }
}
void AddMeshData()
{
    int N = 10;
    //01填充顶点数据
    for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以最外层的循环是z不是x
    {
        for(int x = 0; x < N; ++x)
        {
            Vector3 temp = new Vector3(x, 0, z);
            verts.Add(temp);
        }
    }
    //02填充索引数据
    for(int z = 0; z < N - 1; ++z)
    {
        for(int x = 0; x < N - 1; ++x)
        {
            int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
            int index_lt = (z + 1) * N + x;
            int index_rt = (z + 1) * N + x + 1;
            int index_rb = z * N + x + 1;

            indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
            indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
        }
    }
}

结果,符合预期;在原点那里放了个cube,作参照。



BlenderGIS 灰 blendergis灰地图_List_14


03.从平面到地形

这个不难,加一行

void AddMeshData()
{
int N = 10;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
    for(int x = 0; x < N; ++x)
    {
        float height = Random.Range(0.1f, 1.0f);//随机加个高度
        Vector3 temp = new Vector3(x, height, z);
        verts.Add(temp);
    }
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
    for(int x = 0; x < N - 1; ++x)
    {
        int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
        int index_lt = (z + 1) * N + x;
        int index_rt = (z + 1) * N + x + 1;
        int index_rb = z * N + x + 1;

        indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
        indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
    }
}


}
void AddMeshData()
{
int N = 10;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
    for(int x = 0; x < N; ++x)
    {
        float height = Random.Range(0.1f, 1.0f);//随机加个高度
        Vector3 temp = new Vector3(x, height, z);
        verts.Add(temp);
    }
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
    for(int x = 0; x < N - 1; ++x)
    {
        int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
        int index_lt = (z + 1) * N + x;
        int index_rt = (z + 1) * N + x + 1;
        int index_rb = z * N + x + 1;

        indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
        indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
    }
}


}

结果,有高度起伏了。



BlenderGIS 灰 blendergis灰地图_List_15


这个是10*10规模的,更大的规模也是一样的。计算机擅长重复。

04.从随机数到灰度图

准备

首先,得有个地形灰度图;这里用的是这个:



BlenderGIS 灰 blendergis灰地图_数据_16


其次,得能从C#脚本里读到纹理的值。

Unity - Scripting API: Texture2D (unity3d.com)

GetPixel函数

Unity - Scripting API: Texture2D.GetPixel (unity3d.com)

解释的很详细了:



BlenderGIS 灰 blendergis灰地图_数据_17


从下图可以看出,这个xy不是归一化后的uv:



BlenderGIS 灰 blendergis灰地图_List_18


这个返回值color,倒是归一化的:Unity - Scripting API: Color (unity3d.com)



BlenderGIS 灰 blendergis灰地图_List_19


视频里用的是更快的这个函数:

Unity - Scripting API: Texture2D.GetPixels32 (unity3d.com)

但是,我是小白,能搞出来就已经是极限了,哪里还顾得上什么性能问题。

试一试

代码

接着改那个函数就行

void AddMeshData()
{
    int N = 100;
    //01填充顶点数据
    for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
    {
        for(int x = 0; x < N; ++x)
        {
            int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);//没归一化的uv
            int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
            float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
    
            float height = grayValue*heightRatio;//灰度值范围是[0,1],所以得缩放一下
            Vector3 temp = new Vector3(x, height, z);
            verts.Add(temp);
        }
    }
    //02填充索引数据
    for(int z = 0; z < N - 1; ++z)
    {
        for(int x = 0; x < N - 1; ++x)
        {
            ……//这部分是没改,省略
        }
    }
}
void AddMeshData()
{
    int N = 100;
    //01填充顶点数据
    for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
    {
        for(int x = 0; x < N; ++x)
        {
            int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);//没归一化的uv
            int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
            float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
    
            float height = grayValue*heightRatio;//灰度值范围是[0,1],所以得缩放一下
            Vector3 temp = new Vector3(x, height, z);
            verts.Add(temp);
        }
    }
    //02填充索引数据
    for(int z = 0; z < N - 1; ++z)
    {
        for(int x = 0; x < N - 1; ++x)
        {
            ……//这部分是没改,省略
        }
    }
}

符合预期吧



BlenderGIS 灰 blendergis灰地图_游戏引擎_20


完整的代码

新建一个空物体,上面添加上MeshFilter,MeshRender两个组件,然后把这个脚本拖给这个空物体,再点击play,大概就行了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Terrian : MonoBehaviour
{

    public Texture2D texture2dHeightMap;
    [Range(1,100)]
    public float heightRatio = 30.0f;//一个系数,控制地形总体的高度的

    MeshRenderer meshRenderer;
    MeshFilter meshFilter;

    // 用来存放顶点数据
    List<Vector3> verts;
    List<int> indices;

    private void Awake()
    {
    }

    private void Start()
    {
        verts = new List<Vector3>();
        indices = new List<int>();

        meshRenderer = GetComponent<MeshRenderer>();
        meshFilter = GetComponent<MeshFilter>();

        
    }

    private void Update()
    {
        Generate();
    }

    public void Generate()
    {
        ClearMeshData();

        // 把数据填写好
        AddMeshData();

        // 把数据传递给Mesh,生成真正的网格
        Mesh mesh = new Mesh();
        mesh.vertices = verts.ToArray();
        mesh.triangles = indices.ToArray();

        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        meshFilter.mesh = mesh;
    }

    void ClearMeshData()
    {
        verts.Clear();
        indices.Clear();
    }

    void AddMeshData()
    {
        int N = 100;
        //01填充顶点数据
        for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
        {
            for(int x = 0; x < N; ++x)
            {
                int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);
                int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
                float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;

                float height = grayValue*heightRatio;
                Vector3 temp = new Vector3(x, height, z);
                verts.Add(temp);
            }
        }
        //02填充索引数据
        for(int z = 0; z < N - 1; ++z)
        {
            for(int x = 0; x < N - 1; ++x)
            {
                int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
                int index_lt = (z + 1) * N + x;
                int index_rt = (z + 1) * N + x + 1;
                int index_rb = z * N + x + 1;

                indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
                indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
            }
        }



    }
    
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Terrian : MonoBehaviour
{

    public Texture2D texture2dHeightMap;
    [Range(1,100)]
    public float heightRatio = 30.0f;//一个系数,控制地形总体的高度的

    MeshRenderer meshRenderer;
    MeshFilter meshFilter;

    // 用来存放顶点数据
    List<Vector3> verts;
    List<int> indices;

    private void Awake()
    {
    }

    private void Start()
    {
        verts = new List<Vector3>();
        indices = new List<int>();

        meshRenderer = GetComponent<MeshRenderer>();
        meshFilter = GetComponent<MeshFilter>();

        
    }

    private void Update()
    {
        Generate();
    }

    public void Generate()
    {
        ClearMeshData();

        // 把数据填写好
        AddMeshData();

        // 把数据传递给Mesh,生成真正的网格
        Mesh mesh = new Mesh();
        mesh.vertices = verts.ToArray();
        mesh.triangles = indices.ToArray();

        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        meshFilter.mesh = mesh;
    }

    void ClearMeshData()
    {
        verts.Clear();
        indices.Clear();
    }

    void AddMeshData()
    {
        int N = 100;
        //01填充顶点数据
        for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
        {
            for(int x = 0; x < N; ++x)
            {
                int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);
                int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
                float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;

                float height = grayValue*heightRatio;
                Vector3 temp = new Vector3(x, height, z);
                verts.Add(temp);
            }
        }
        //02填充索引数据
        for(int z = 0; z < N - 1; ++z)
        {
            for(int x = 0; x < N - 1; ++x)
            {
                int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
                int index_lt = (z + 1) * N + x;
                int index_rt = (z + 1) * N + x + 1;
                int index_rb = z * N + x + 1;

                indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
                indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
            }
        }



    }
    
}

bug的修复

上面的代码,存在内存泄漏的问题。

BlenderGIS 灰 blendergis灰地图_unity_21

尝试避免内存泄漏:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Terrain : MonoBehaviour
{

    public Texture2D texture2dHeightMap;
    [Range(1,100)]
    public float heightRatio = 30.0f;//一个系数,控制地形总体的高度的
 
    MeshFilter meshFilter; 

    Mesh mesh; // 之前是先获取meshfilter,再获取mesh,改成直接一步到位了.
 
    // 用来存放顶点数据
    List<Vector3> verts;
    List<int> indices;
 
    private void Awake()
    {
    }
 
    private void Start()
    {
        verts = new List<Vector3>();
        indices = new List<int>();
 
        meshFilter = GetComponent<MeshFilter>();

        mesh = GetComponent<MeshFilter>().mesh;
 
        
    }
 
    private void Update()
    {
        Generate();
    }
 
    public void Generate()
    {
        ClearMeshData();
 
        // 把数据填写好
        AddMeshData();
 
        // 把数据传递给Mesh,生成真正的网格
        // Mesh mesh = new Mesh(); // 每帧都在new,造成了内存泄漏.把这行绕过去,就能解决内存泄漏问题.

        mesh.vertices = verts.ToArray();
        mesh.triangles = indices.ToArray();
 
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();
 
        // meshFilter.mesh = mesh;
    }
 
    void ClearMeshData()
    {
        verts.Clear();
        indices.Clear();
    }
 
    void AddMeshData()
    {
        int N = 100;
        //01填充顶点数据
        for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
        {
            for(int x = 0; x < N; ++x)
            {
                int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);
                int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
                float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
 
                float height = grayValue*heightRatio;
                Vector3 temp = new Vector3(x, height, z);
                verts.Add(temp);
            }
        }
        //02填充索引数据
        for(int z = 0; z < N - 1; ++z)
        {
            for(int x = 0; x < N - 1; ++x)
            {
                int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
                int index_lt = (z + 1) * N + x;
                int index_rt = (z + 1) * N + x + 1;
                int index_rb = z * N + x + 1;
 
                indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
                indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
            }
        }
 
 
 
    }

}

结果:

BlenderGIS 灰 blendergis灰地图_游戏引擎_22