zoukankan      html  css  js  c++  java
  • (转)Unity 3D中的无限大地形的生成和调度

    随着硬件性能的不断提高,游戏的地形变得越来越大也更加细节化了(增加了更有特点的地形,大片的草地,还添加了树木,水等物体。在过去几年时间里,地形已经逐渐增加到长达数百平方英里,特别是在RPG游戏中。

    在本教程中,我将向您展示如何生成需要超级长的时间才能浏览完的3D地形。我们将使用Unity3D引擎和C#语言编写代码。需要一些基本的编程知识——尽管完整的源代码可以免费下载(见下文),但在本文中,我只会解释最重要的部分并说明示例的代码。

       

    教程开始

    最流行的观看3D地形的方式就是应用某种形式的高度图。高度图是由一组海拔数据构成的图像,图像的尺寸与地形的宽度和高度相匹配。颜色越暗,地面越高。下面您将看到这样的数据是如何转换成可见的网格物体的:

       

       

       

       

    因为我们想要的地形是无限大的(或者说是非常广阔的),我们不能直接使用高度图——因为存储所有数据所需的内存空间太巨大了,并且高度图的分辨率会增长到数千个像素。

    我们可以将它分成一个个的被称为"块"的片段,而不是完整的做一个特别大的地形。每个块都将有自己的网格,并且几个相邻的块将无缝地融合到一个更大的地形中。您可以将它们视为具有2D(X / Z)坐标的方形图块。关键是要在玩家周围创建多个地形块(在视线范围内),并随着玩家位置的移动并不断调整地形块的分布。离相机太远的旧块将被删除以释放内存。见下图:

       

       

       

    单块几何形状的描述方法:

    *长度 - 这是Unity单位中的块边框的大小

    *高度 - 这是块中地形的最大可用高度(也称为Unity距离单位)

    *高度图和透明图的分辨率——它们可以表示出块的网格和纹理有多么的准确——分辨率越高,我们就能获得越复杂的网格。根据Unity文档,它的尺寸需要满足N的2次方+1(例如129,513)。

       

    将它放在代码中——这个是地形块设置类:

    [代码]:

    view source

    print?

    1

    public class TerrainChunkSettings

    2

    {

     

    3

        public int HeightmapResolution { get; private set; }

    4

        public int AlphamapResolution { get; private set; }

    5

    6

        public int Length { get; private set; }

    7

        public int Height { get; private set; }

     

    8

    }

       

    这个是地形块类(现在我们将跳过这个方法):

    [代码]:

    view source

    print?

    01

    public class TerrainChunk

    02

    {

     

    03

        public int X { get; private set; }

    04

    05

        public int Z { get; private set; }

    06

    07

        private Terrain Terrain { get; set; }

    08

    09

        private TerrainChunkSettings Settings { get; set; }

    10

    11

        private NoiseProvider NoiseProvider { get; set; }

    12

    }

       

    每个块由其X / Z位置(见上图),设置和Unity 地形对象(通过网格表现的实际游戏物体以及渲染场景所需的所有东西)等因素来进行定义。最后一个字段-——NoiseProvider——将在下面讨论。

    [代码]:

    view source

    print?

    01

    public class TerrainChunk

    02

    {

     

    03

        public int X { get; private set; }

    04

    05

        public int Z { get; private set; }

    06

    07

        private Terrain Terrain { get; set; }

    08

    09

        private TerrainChunkSettings Settings { get; set; }

    10

    11

        private NoiseProvider NoiseProvider { get; set; }

    12

    }

       

    那么如何创建一个拥有很多起伏和纹理的山谷或山丘的高度图呢?

    有很多方法可以达到这一目的——您可以在程序生成维基上找到大量的关于它的信息。我们将通过使用LibNoise库,来用相关的噪声值填充我们的高度图。关于噪声值的细节以及使用方法可以参考以下两个网站(http://libnoise.sourceforge.net/tutorials/tutorial3.html http://libnoise.sourceforge.net/tutorials/tutorial3.html)——我强烈推荐这两篇文章。

    暂且忽视那些细节问题,3D空间(x,y,z)中的所有位置,都可以代表示一个特定的噪声值,这些噪音值转化为纹理后,就会形成一个类似于真实的地形的图像。

    目前我们只介绍X 和z两个部分,因为我们只在平面上创建地形,所以跳过Y方向上的,。LibNoise将噪声值从-1返回到1,而我们则需要将其缩放为0到1的范围(该比例更方便)。

    我创建了INoiseProvider接口,强制在Unity世界空间中为给定的X / Z坐标返回一个值(这是一个重要的信息)。 NoiseProvider类会通过Perlin噪音接收这个值(参考以上两个链接)——而这仅仅只是一个开始。

    [代码]:

    view source

    print?

    01

    public class NoiseProvider : INoiseProvider

    02

    {

     

    03

        private Perlin PerlinNoiseGenerator;

    04

    05

        public NoiseProvider()

    06

        {

     

    07

            PerlinNoiseGenerator = new Perlin();

    08

        }

    09

    10

        public float GetValue(float x, float z)

    11

        {

     

    12

            return (float)(PerlinNoiseGenerator.GetValue(x, 0, z) / 2f) + 0.5f;

    13

        }

     

    14

    }

       

    好了——我们已经有了简单的噪音发生器。现在就来着手解决关于Unity地形的技术问题。

    通常您可以从GameObject / 3D Object / Terrain菜单创建一个地形。 但是,如果要通过代码创建地形,我们需要地形数据对象(其中包含生成地形网格所需的大部分信息),然后就可以设置高度图值,分辨率和地图的大小了。之后,我们使用Unity创造地形游戏物体的指令来创造地形图里的游戏物体,设置物体的变换位置,运用所有的数据确定新生成物体的最合适的位置——其余的由Unity自动完成。

    以下是简单介绍:

    [代码]:

    view source

    print?

    01

    public void CreateTerrain()

    02

    {

     

    03

        var terrainData = new TerrainData();

    04

        terrainData.heightmapResolution = Settings.HeightmapResolution;

     

    05

        terrainData.alphamapResolution = Settings.AlphamapResolution;

    06

    07

        var heightmap = GetHeightmap();

    08

        terrainData.SetHeights(0, 0, heightmap);

     

    09

        terrainData.size = new Vector3(Settings.Length, Settings.Height, Settings.Length);

    10

    11

        var newTerrainGameObject = Terrain.CreateTerrainGameObject(terrainData);

    12

        newTerrainGameObject.transform.position = new Vector3(X * Settings.Length, 0, Z * Settings.Length);

     

    13

        Terrain = newTerrainGameObject.GetComponent<terrain>();

    14

        Terrain.Flush();

     

    15

    }</terrain>

       

    您将看到一个GetHeightmap指令,这个指令就是通过使用前面提到的噪声值来填充我们的高程值:

    [代码]:

    view source

    print?

    01

    private float[,] GetHeightmap()

    02

    {

     

    03

        var heightmap = new float[Settings.HeightmapResolution, Settings.HeightmapResolution];

    04

    05

        for (var zRes = 0; zRes < Settings.HeightmapResolution; zRes++)

    06

        {

     

    07

            for (var xRes = 0; xRes < Settings.HeightmapResolution; xRes++)

    08

            {

     

    09

                var xCoordinate = X + (float)xRes / (Settings.HeightmapResolution - 1);

    10

                var zCoordinate = Z + (float)zRes / (Settings.HeightmapResolution - 1);

    11

    12

                heightmap[zRes, xRes] = NoiseProvider.GetValue(xCoordinate, zCoordinate);

    13

            }

     

    14

        }

    15

    16

        return heightmap;

    17

    }

       

    它是如何工作的?

    为了填充整个海拔数组值(其尺寸等于地形分辨率),首先需要叠加这些噪音值数据以获得每个位置(X / Z)的值。NoiseProvider的最终坐标=块位置+叠加后的数据 /(分辨率-1)。这样我们可以将X / Z方向缩放为0..1(第一块),1..2(第二块),2..3(第三块)等。而且我们新增加的数据不会破坏之前创建的NoiseProvider,只是在以前的基础上完善地图的细节。

    好的,现在核心的应用程序已经设置好,是时候进行一些测试了。

       

    创建一个129分辨率的单块,尺寸为100米,高20米。

    [代码]:

    view source

    print?

    1

    void Test()

    2

    {

     

    3

        var settings = new TerrainChunkSettings(129, 129, 100, 20);

    4

        var noiseProvider = new NoiseProvider();

     

    5

        var terrain = new TerrainChunk(settings, noiseProvider, 0, 0);

    6

        terrain.CreateTerrain();

     

    7

    }

       

    使用上述程序设置好后就能得到如下地形图啦!

       

       

       

    它目前确实看起来还不太完善,因为还没有应用纹理,但是您已经可以看到一些山丘起伏,这已经是一个很好的开始了。

       

    现在我们需要完善它,创建一些更多的块,使地形看起来更大:

    [代码]:

    view source

    print?

    1

    void Test()

    2

    {

     

    3

        Settings = new TerrainChunkSettings(129, 129, 100, 20);

    4

        NoiseProvider = new NoiseProvider();

     

    5

        for (var i = 0; i < 4; i ++)

    6

            for (var j = 0; j < 4; j++)

     

    7

                new TerrainChunk(Settings, NoiseProvider, i, j).CreateTerrain();

    8

    }

       

       

    从上图可以看出,地形正在增长,说明我们的目的正逐步实现。目前我们有16块地形,每个块都有各自独立并拥有自己的网格特点。我们可以添加更多的块,来扩大地图,但让我们先停下来思考一下...

    您可能已经注意到了,创建更大的地形需要很多时间。在我的PC上创建16个块需要约1500毫秒,而这期间整个应用程序都会被冻结,玩家体验时很容易发现这一点,这会给他们带来不顺畅的游戏体验。

    大部分的延迟是由于需要大量的计算每个地形部分的噪声值。这种性能问题在单线程应用程序中很常见。要解决这一问题,我们需要将高度生成函数放在独立于主线程的单独线程中。我们可以通过在创建的线程上创建块来提高计算的效率。主应用程序的线程就不会冻结,生成时间也会加快。

       

    这种改进会使代码发生很多变化,主要包括:

    *添加了地形块生成类——它可以添加和删除块,使地形一直保持最新状态,它可以用作地形和其他应用程序之间的主要接口。如果某些地形需要修改,那么应该通过使用此类中相应的指令来进行修改。

    *添加了缓存块类——它用于保存所有正在请求和已经创建的块的信息。它还追踪块的状态。

    *块由X / Z位置来标识,这是唯一的区别不同块的方式。我创建了Vector2i类来保存有关块的位置的信息。

    我还添加了删除块的功能。删除块时,就把它添加到队列中。每个帧缓存块都会检查此队列,并尝试删除这些块。如果块正在生成则无法删除,这种情况下,它的删除将被延迟,直到完成生成块时才可被删除。它可能不是最有效的方式,但是处理速度很快,且操作方便。只需缓存块类中的删除整列块指令就可实现块的删除。

    现在我们来编写在玩家周围生成大量的块的程序。我们需要在玩家周围创建的所有块的坐标列表,以确定玩家的位置,以及它与生成的新块的距离。下面来看看这段代码:

    [代码]:

    view source

    print?

    01

    private List<vector2i> GetChunkPositionsInRadius(Vector2i chunkPosition, int radius)

    02

    {

     

    03

        var result = new List<vector2i>();

    04

    05

        for (var zCircle = -radius; zCircle <= radius; zCircle++)

    06

        {

     

    07

            for (var xCircle = -radius; xCircle <= radius; xCircle++)

    08

            {

     

    09

                if (xCircle * xCircle + zCircle * zCircle < radius * radius)

    10

                    result.Add(new Vector2i(chunkPosition.X + xCircle, chunkPosition.Z + zCircle));

     

    11

            }

    12

        }

    13

    14

        return result;

    15

    }</vector2i></vector2i>

    该个程序需要输入初始块的位置和半径数值,并给出与圆方程匹配的所有坐标。比如: 输入位置(0,0),组块半径为7(玩家位置在中间):

       

       

       

       

    这一技术的神奇之处就是——只是给玩家提供一个幻想中的无限的地形。为了达成这个效果,我们必须经常查看他的位置,当他面向不同的方向时为他添加新的大块地形。而那些视线之外的旧块则被删除。这样,玩家不但能移动很长的距离,而且仍然能看到他(或她)附近几英里的地形。

    我们正通过新创建的游戏控制类监控玩家的运动轨迹。 它负责对高级应用程序(管理玩家)的控制,以及与地形发生器和UI交互的通信。 一旦检测到某个玩家已经移动到块的边界时(换句话说,他移动到需要创建新块的范围了),我们就在相应的地方创建新的块并删除视线范围外的块。

       

    以下是相应的编码:

    [代码]:

    view source

    print?

    01

    public void UpdateTerrain(Vector3 worldPosition, int radius)

    02

    {

     

    03

        var chunkPosition = GetChunkPosition(worldPosition);

    04

        var newPositions = GetChunkPositionsInRadius(chunkPosition, radius);

    05

    06

        var loadedChunks = Cache.GetGeneratedChunks();

    07

        var chunksToRemove = loadedChunks.Except(newPositions).ToList();

    08

    09

        var positionsToGenerate = newPositions.Except(chunksToRemove).ToList();

    10

        foreach (var position in positionsToGenerate)

     

    11

            GenerateChunk(position.X, position.Z);

    12

    13

        foreach (var position in chunksToRemove)

    14

            RemoveChunk(position.X, position.Z);

     

    15

    }

    首先,设置好玩家所在的块,然后计算玩家周围的新块,确认好要删除哪些块和添加哪些块。分别做好生成和删除地形的操作。

    每次玩家从一个块移动到另一个块时都重复这一操作。

    为了测试上面创建的所有内容,我添加了一个标准的Unity FPS控制器,并在游戏控制类中创建了玩家管理代码(添加UI以生成新的地形)。现在玩家就可以体验在数百英里的无限地形里走动的感觉了!

    但是,仅仅这样还是有点无聊。

    最后我们需要做的就是添加一些纹理,使地形看起来更真实。

    操作方法很简单:首先,定义一些可应用于地形上的纹理(SplatPrototypes),然后为地形上的每个点指定需要添加的各个纹理的数量(数量取决于AlphamapResolution)。所有这些信息都输入到我们已经知道的地形数据类中。纹理存储在地形块设置类中。

    在本教程中,我用了两种纹理:一个是用于平坦地形的,一个是用于陡峭表面的。每个纹理效果的呈现都是基于地形的陡度(我们可以在应用高程数据后从地形数据类获得这些坡度数值)。

    以下是部分编码:

    [代码]:

    view source

    print?

    01

    private void ApplyTextures(TerrainData terrainData)

    02

    {

     

    03

        var flatSplat = new SplatPrototype();

    04

        var steepSplat = new SplatPrototype();

    05

    06

        flatSplat.texture = Settings.FlatTexture;

    07

        steepSplat.texture = Settings.SteepTexture;

    08

    09

        terrainData.splatPrototypes = new SplatPrototype[]

    10

        {

     

    11

            flatSplat,

    12

            steepSplat

     

    13

        };

    14

    15

        terrainData.RefreshPrototypes();

    16

    17

        var splatMap = new float[terrainData.alphamapResolution, terrainData.alphamapResolution, 2];

    18

    19

        for (var zRes = 0; zRes < terrainData.alphamapHeight; zRes++)

    20

        {

     

    21

            for (var xRes = 0; xRes < terrainData.alphamapWidth; xRes++)

    22

            {

     

    23

                var normalizedX = (float)xRes / (terrainData.alphamapWidth - 1);

    24

                var normalizedZ = (float)zRes / (terrainData.alphamapHeight - 1);

    25

    26

                var steepness = terrainData.GetSteepness(normalizedX, normalizedZ);

    27

                var steepnessNormalized = Mathf.Clamp(steepness / 1.5f, 0, 1f);

    28

    29

                splatMap[zRes, xRes, 0] = 1f - steepnessNormalized;

    30

                splatMap[zRes, xRes, 1] = steepnessNormalized;

     

    31

            }

    32

        }

    33

    34

        terrainData.SetAlphamaps(0, 0, splatMap);

    35

    }

    这样设置以后效果是不是好多了:

       

       

       

    现在,一个功能完备的地形就做好啦,您可以体验在无限宽广的地形里步行的感觉。虽然这并不是真正的无限,但至少我们提供的无限大地形能完美的欺骗您的感官。

       

    以上就是所有的基础教程。 在地形方面还有很多细节可以改进(主要是性能和视觉方面的改进),我们将在另一篇文章里介绍相关流程。

       

    本文中的源码工程提供下载:

    链接:http://pan.baidu.com/s/1miC1oow 密码:491q

       

    希望能帮到您!

    原文出处:http://code-phi.com/infinite-terrain-generation-in-unity-3d/

  • 相关阅读:
    gThumb 3.1.2 发布,支持 WebP 图像
    航空例行天气预报解析 metaf2xml
    Baruwa 1.1.2 发布,邮件监控系统
    Bisect 1.3 发布,Caml 代码覆盖测试
    MoonScript 0.2.2 发布,基于 Lua 的脚本语言
    Varnish 入门
    快速增量备份程序 DeltaCopy
    恢复模糊的图像 SmartDeblur
    Cairo 1.12.8 发布,向量图形会图库
    iText 5.3.4 发布,Java 的 PDF 开发包
  • 原文地址:https://www.cnblogs.com/workhai/p/7581948.html
Copyright © 2011-2022 走看看