单块几何形状的描述方法:
*长度 - 这是Unity单位中的块边框的大小
*高度 - 这是块中地形的最大可用高度(也称为Unity距离单位)
*高度图和透明图的分辨率——它们可以表示出块的网格和纹理有多么的准确——分辨率越高,我们就能获得越复杂的网格。根据Unity文档,它的尺寸需要满足N的2次方+1(例如129,513)。
将它放在代码中——这个是地形块设置类:
[代码]:
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
}
[代码]:
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——将在下面讨论。
[代码]:
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噪音接收这个值(参考以上两个链接)——而这仅仅只是一个开始。
[代码]:
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自动完成。
[代码]:
01
public void CreateTerrain()
02
{
03
v<a href="http://www.52vr.com/armr/" style="font-weight: bold;color: ;" target="_blank">AR</a> 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>
[代码]:
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米。
[代码]:
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
}
它目前确实看起来还不太完善,因为还没有应用纹理,但是您已经可以看到一些山丘起伏,这已经是一个很好的开始了。
现在我们需要完善它,创建一些更多的块,使地形看起来更大:
[代码]:
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类来保存有关块的位置的信息。
我还添加了删除块的功能。删除块时,就把它添加到队列中。每个帧缓存块都会检查此队列,并尝试删除这些块。如果块正在生成则无法删除,这种情况下,它的删除将被延迟,直到完成生成块时才可被删除。它可能不是最有效的方式,但是处理速度很快,且操作方便。只需缓存块类中的删除整列块指令就可实现块的删除。
现在我们来编写在玩家周围生成大量的块的程序。我们需要在玩家周围创建的所有块的坐标列表,以确定玩家的位置,以及它与生成的新块的距离。下面来看看这段代码:
[代码]:
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>
这一技术的神奇之处就是——只是给玩家提供一个幻想中的无限的地形。为了达成这个效果,我们必须经常查看他的位置,当他面向不同的方向时为他添加新的大块地形。而那些视线之外的旧块则被删除。这样,玩家不但能移动很长的距离,而且仍然能看到他(或她)附近几英里的地形。
我们正通过新创建的游戏控制类监控玩家的运动轨迹。 它负责对高级应用程序(管理玩家)的控制,以及与地形发生器和UI交互的通信。 一旦检测到某个玩家已经移动到块的边界时(换句话说,他移动到需要创建新块的范围了),我们就在相应的地方创建新的块并删除视线范围外的块。
以下是相应的编码:
[代码]:
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)。所有这些信息都输入到我们已经知道的地形数据类中。纹理存储在地形块设置类中。
在本教程中,我用了两种纹理:一个是用于平坦地形的,一个是用于陡峭表面的。每个纹理效果的呈现都是基于地形的陡度(我们可以在应用高程数据后从地形数据类获得这些坡度数值)。
[代码]:
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
}