从灰度图到地形图

  • Post author:
  • Post category:其他



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

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

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

参考:


【萌新图形学】地形网格生成入门 含动画说明哦_哔哩哔哩_bilibili


【萌新图形学】地形生成下篇——随机大地形与真实地形_哔哩哔哩_bilibili

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

00.一些基本概念

演示

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

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

  1. MeshFilter组件

  1. MeshRender组件

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

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

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

大概有了个朦胧的认识:

  • MeshFilter里的Mesh可以控制物体的形状

  • MeshRender负责物体的显示

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

Mesh


Unity – Scripting API: Mesh (unity3d.com)

《inherits from Object》

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

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

MeshFilter


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

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

具体可以这么用:


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

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

MeshRender


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

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

小结


  • 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);
    }
    
}

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

02.更大规模的平面

灌数据到Mesh的原理

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

视频里这个图挺好的。

顶点数据

索引数据

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

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

从特殊到一般:

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

试一试

修改前:

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);
        }
    }
}

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

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);
    }
}


}

结果,有高度起伏了。

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

04.从随机数到灰度图

准备

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

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


Unity – Scripting API: Texture2D (unity3d.com)


GetPixel函数


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

解释的很详细了:

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

这个返回值color,倒是归一化的:

Unity – Scripting API: Color (unity3d.com)

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


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)
        {
            ……//这部分是没改,省略
        }
    }
}

符合预期吧

完整的代码

新建一个空物体,上面添加上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);
            }
        }



    }
    
}

后记

还想搞搞颜色,低的用绿色,高的用红色,中间用渐变色;更好的可视化一下;

但是,那个可能得写着色器。那很明显不是我这种小白干的来的。所以,忽略。



版权声明:本文为averagePerson原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。