(转)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.htmlhttp://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/

时间: 2024-08-02 09:50:59

(转)Unity 3D中的无限大地形的生成和调度的相关文章

在Unity 3D中加入Image图片

在Unity 3D中加入Image图片,我刚开是加不进去,为什么呢?因为没有图片,图如下: 原因就是我们没有把图片设置为Script,图片的格式还是默认的那个,这只能作为贴图使用.我们将图片进行如下设置就Ok了.

Unity 3D中的阴影设置

在Unity 3D中,经常需要用到光照阴影,即Directional Light的Shadow,Shadow分为Hard Shadow和Soft Shadow.区别是Soft Shadow的阴影边缘比较平滑,接近真实,但是性能消耗大于Hard Shadow. Lightmapping有3种选择:实时光照阴影(RealTimeOnly).场景烘焙阴影(BakedOnly).以及上面两者结合的阴影(Auto). RealTimeOnly:所有场景物体的光照都实时计算,实时光照对性能消耗比较大: Ba

Unity 3D中的Transform.Rotate 与Transform.RotateAround 的区别

Transform.Rotate 旋转 应用一个欧拉角的旋转角度,eulerAngles.z度围绕z轴,eulerAngles.x度围绕x轴,eulerAngles.y度围绕y轴(这样的顺序). 如果相对于留空或者设置为Space.Self 旋转角度被应用围绕变换的自身轴.(当在场景视图选择物体时,x.y和z轴显示)如果相对于 Space.World 旋转角度被应用围绕世界的x.y.z轴. 1 gameObject.transform.Rotate(new Vector3(0,1,0),Inpu

Unity 3D中不得不说的yield协程与消息传递

1. 协程 在Unity 3D中,我们刚开始写脚本的时候肯定会遇到类似下面这样的需求:每隔3秒发射一个烟花.怪物死亡后20秒再复活之类的.刚开始的时候喜欢把这些东西都塞到Update里面去,就像下面这样写. 1 float nowTime = 3.0f; 2 bool isDead = true; 3 float deadTime = 20.0f; 4 5 void startFireworks() 6 { 7 // 放烟花 8 } 9 10 void revival() 11 { 12 //

如何在Unity 3D中掷骰子

1.介绍 2.滚一个骰子 3.导入模型 4.添加脚本 5.方法 6.识别骰子上的随机面值 客观的 这篇文章的主要目的是给你一个关于如何在Unity 3D中掷骰子的想法. 第一步介绍 1.构建一个棋盘游戏,但对骰子有问题;这里是一个示例代码,演示如何像真正的骰子一样掷骰子,以及如何在游戏控2.制台上识别骰子的表面值. 3.这个问题分为两个主要部分: 4.如何掷骰子? 5.确定在1和6之间的随机整数(6个标准骰子)的面值. 第二步掷骰子 2.1导入模型 将一个标准骰子模型导入到unity3D中.调整

Unity 3d中Shader是什么,可以吃吗?

众所周知,Unity3d是一款跨平台非常广的游戏引擎,上手容易,界面友好,集成功能众多,是目前开发手游的主流引擎.本人有幸使用Unity 3d进行开发已一年多时间,已领略了这歀引擎的强大之处. 编写shader也是我工作内容的一部分,先来说说shader是什么吧,我以自己的理解说明一下: 首先 shader是一种语言,一种在GPU,也就是显卡上执行的高级语言.shader的本意是着色器,可以自定义GPU的渲染管线中的两个环节(即顶点和片段).由此,我们可以控制对象在屏幕上的渲染效果,甚至实现一些

Unity 3D中的内存管理

本文欢迎转载,但烦请保留此行出处信息:http://www.onevcat.com/2012/11/memory-in-unity3d/ Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极差的体验.类似这种情况并不少见,但是绝大部分都是可以避免的.虽然理论上Unity的内存管理系统应当为开发者分忧解难,让大家投身到更有意义的事情中去,但是对于Unity对内存的管理方式,官方文档中并没有太多的说明,基本需要依靠

如何在Unity 3D中设置Google AdMod

在Unity中启用Google广告游戏,你需要做到如下所示:要求– Unity 4或者更高(链接:https://github.com/)– 谷歌移动广告SDK(链接:https://github.com/) 安装1.通过访问以下网址转到谷歌的游戏开发者页面:https://github.com/. 2.导航到页面上的“Unity”部分. 3.在本节将有两个按钮(如“下载插件”和“查看源代码”).点击“下载插件”按钮.这将带给你一个GitHub的页面,你可以下载“谷歌移动广告'Unity包.查找

Unity 3D 中新建游戏物体过程中 Intantiate Transform 空物体和本体之间的关系

我们通过Unity构建场景的过程中,经常发现一个现象,就是物体在拖进场景中后,我们会发现物体是反的,通过改变物体的rotation属性后,得到了正确的方向,可物体的坐标系又变得和默认坐标系(右上角系统自带的坐标系)不一样了,这样就给后续的脚本工作(通常是控制(Transform)脚本和生成(Intantiate)脚本)带来了困扰,因为脚本写作的过程中是按照正常的坐标系来的,物体的脚本和默认的不一样了就会出现诸如按下键物体向上的现象. 其实,在游戏场景的创建过程中,不仅仅是物体的方向,一个游戏物体