使用Perlin Noise生成等轴测图。第2部分
Tulenber ⸱
22 May, 2020 ⸱
Intermediate ⸱
9 min ⸱
2019.3.14f1 ⸱
这是我们第二篇有关
程序生成周期中基于Perlin Noise的等距贴图的文章。
空间的过程生成意味着使用相互独立创建其元素的机制。将来,为了描述这些元素,我们将使用术语-扇区。上次,我们使用先前准备的Perlin噪声生成算法来模拟地球表面并将其转换为等轴测图。下一步将是将地图划分为多个扇区,分别生成各个扇区,然后合并为一个统一的对象。
目标
使用Noise Perlin为该扇区生成数据,并将几个扇区合并为一个覆盖相机视图的统一地图。
等轴测图
以前,我们将标准Unity机制用于等轴测图。使用坐标进行工作并不会超出平铺瓷砖的通常位置,也不需要深入了解该过程。但是,我们的进一步动作将需要等距空间的基础知识,因此让我们看一下该理论。与往常一样,我将尽量不深入讨论仅考虑当前文章元素的重要性。有关更多信息,您可以随时访问互联网。
等距投影是一种用于在视觉上表示沿坐标轴具有不同变形系数的二维三维物体的方法。上次,我们选择了等轴测图,其中使用X
和Y
轴进行定位,并使用Z轴
。因此,我们要注意我们向三维空间的过渡,并定义用于确定方向的术语。在X轴
上,我们将计算长度;在Y轴
-宽度和沿Z轴
-高度。高度目前对我们来说并不是一个重要因素,因此我们将忽略它的存在,而将重点放在X
和Y
轴上。
表示空间的最简单方法是将其显示在平面上,例如,二维空间中图块的坐标:
默认情况下,Unity等轴测图中的坐标轴分别朝上(相对于屏幕上的显示),这是我们空间的等轴测图:
扇区
扇区-是一组具有相同长度和宽度的瓷砖(高度,如我们之前所同意的,省略了)。我立即注意到扇区在空间中的行为与单个图块相同,因此我们可以忽略当前正在处理的对象。通常,我们将与扇区。
二维空间中的扇区:
等距空间中的扇区:
在屏幕上显示等距空间
我认为很明显,改变坐标轴的方向会改变长度和宽度的应用,从而确定填充整个屏幕所需的行数和列数。直接使用长度和宽度将使屏幕的角落不被填充,或者将许多扇区放置在可见区域的边界之外。
最有效的扇区配如下所示:
列数可以使用公式计算 int sectorCountX = Mathf.CeilToInt (cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1;
附加扇区关闭了极值。
因此,行数的公式为 int sectorCountY = 2 * (Mathf.CeilToInt (cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1);
其中一个额外的扇形也封闭了极值,并且由于线相对于彼此的偏移(仅高度的一半)而使尺寸加倍。
此公式不会为我们提供完美的位置,并且每一行中将包含相等数量的扇区。尽管如此,现在我们将放弃资源优化以减少代码复杂性。
屏幕填充
蛋糕上的樱桃将是这种“有效”部门分配的算法。
为了沿行偏移,我们使用X
和Y
坐标的顺序增加。为了与列一起偏移,根据等轴测坐标网格,有必要将X
增加,并将Y
减少一。
实作
本文的中心部分是添加扇区,这需要对现有代码库进行一些重构,并解决以下四个任务:
- 产生Perlin Noise的偏差的计算
- 计算填满屏幕所需的扇区数
- 计算空间中扇区的位置
- 屏幕填充的扇区位置
噪声发生器重构
噪声发生器的设置移至单独的结构; 这将简化它们在不同对象之间的传输。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
using System;
using UnityEngine;
[Serializable]
public struct GeneratorSettings
{
public float scale;
public int octaves;
public float persistence;
public float lacunarity;
public int seed;
public Vector2 offset;
}
|
因此,我们添加了一个界面来处理从这种结构到噪声发生器的设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
using UnityEngine;
public static class NoiseMapGenerator
{
public static float[] GenerateNoiseMap(GeneratorSettings settings, Vector2 offset)
{
// 偏离设置
Vector2 generationOffset = offset + settings.offset;
// 默认情况下,生成的区域的大小等于扇区的大小
return GenerateNoiseMap(TileMapSector.SectorSizeInTiles, TileMapSector.SectorSizeInTiles, settings.seed, settings.scale, settings.octaves, settings.persistence, settings.lacunarity, generationOffset);
}
public static float[] GenerateNoiseMap(int width, int height, int seed, float scale, int octaves, float persistence, float lacunarity, Vector2 offset)
{
// 一维顶点数据数组,一维视图将有助于以后消除不必要的循环
float[] noiseMap = new float[width*height];
// 种子元素
System.Random rand = new System.Random(seed);
// 八度移位以获得重叠的更有趣的图片
Vector2[] octavesOffset = new Vector2[octaves];
for (int i = 0; i < octaves; i++)
{
// 也使用外部位置偏移
float xOffset = rand.Next(-100000, 100000) + offset.x;
float yOffset = rand.Next(-100000, 100000) + offset.y;
octavesOffset[i] = new Vector2(xOffset / width, yOffset / height);
}
if (scale < 0)
{
scale = 0.0001f;
}
// 为了使视觉更舒适,将视图移至中央
float halfWidth = width / 2f;
float halfHeight = height / 2f;
// 生成高度图的点
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// 设置第一个八度的值
float amplitude = 1;
float frequency = 1;
float noiseHeight = 0;
float superpositionCompensation = 0;
// 倍频程叠加处理
for (int i = 0; i < octaves; i++)
{
// 计算要从Noise Perlin获得的坐标
float xResult = (x - halfWidth) / scale * frequency + octavesOffset[i].x * frequency;
float yResult = (y - halfHeight) / scale * frequency + octavesOffset[i].y * frequency;
// 从PRNG获取高度
float generatedValue = Mathf.PerlinNoise(xResult, yResult);
// 倍频程叠加
noiseHeight += generatedValue * amplitude;
// 补偿八度音阶叠加以保持在[0,1]范围内
noiseHeight -= superpositionCompensation;
// 下一个八度的幅度,频率和叠加补偿的计算
amplitude *= persistence;
frequency *= lacunarity;
superpositionCompensation = amplitude / 2;
}
// 保存高度图点
// 由于八度音阶的叠加,因此有可能超出[0,1]范围
noiseMap[y * width + x] = Mathf.Clamp01(noiseHeight);
}
}
return noiseMap;
}
}
|
重构生成空间的测试显示
测试渲染与负责生成等轴测图的主类分开,并重复了用于生成等轴测结构的算法,但是是二维纹理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class NoiseMapRenderer : MonoBehaviour
{
// 像素颜色与地图高度有关的结构
[Serializable]
public struct TerrainLevel
{
public float height;
public Color color;
}
[SerializeField] public List<TerrainLevel> terrainLevel = new List<TerrainLevel>();
// 使用为噪声发生器指定的参数生成测试纹理的函数
// 从编辑器扩展中使用
public void GenerateMap()
{
// 将图块设置转换为像素颜色与高度相关的结构
TileMapHandler handler = FindObjectOfType<TileMapHandler>();
List<Tile> tileList = handler.tileList;
terrainLevel = new List<TerrainLevel>();
float heightOffset = 1.0f / tileList.Count;
for (int i = 0; i < tileList.Count; i++)
{
Color color = tileList[i].sprite.texture.GetPixel(tileList[i].sprite.texture.width / 2, tileList[i].sprite.texture.height / 2);
TerrainLevel lev = new TerrainLevel();
lev.color = color;
lev.height = Mathf.InverseLerp(0, tileList.Count, i) + heightOffset;
terrainLevel.Add(lev);
}
// 渲染相机的测试空间
Vector2Int sectorSize = handler.GetScreenSizeInSectors();
RenderMap(sectorSize.x, sectorSize.y, handler.generatorSettings);
}
// 创建并绘制基于噪点的纹理
public void RenderMap(int sectorLength, int sectorWidth, GeneratorSettings settings)
{
// 删除先前生成的纹理用于编辑器模式
for (int i = transform.childCount; i > 0; --i)
DestroyImmediate(transform.GetChild(0).gameObject);
// 扇区遍历的初始参数
int startX = -sectorLength;
int startY = 0;
int currentSectorX = startX;
int currentSectorY = startY;
bool increaseX = true;
// 我们将索引移位一个以优化除法的其余部分对循环的使用
int lastSectorIndex = sectorLength * sectorWidth + 1;
for (int sectorIndex = 1; sectorIndex < lastSectorIndex; sectorIndex++)
{
// 产生当前扇区
GenerateSector(new Vector2Int(currentSectorX, currentSectorY), settings);
// 移至下一栏
currentSectorX++;
currentSectorY--;
// 转到下一行
if (sectorIndex % sectorLength == 0)
{
// 转到下一行,在增加X和Y之间交替
if (increaseX)
{
currentSectorX = ++startX;
currentSectorY = startY;
increaseX = false;
}
else
{
currentSectorX = startX;
currentSectorY = ++startY;
increaseX = true;
}
}
}
}
private void GenerateSector(Vector2Int sectorCoordinates, GeneratorSettings settings)
{
// 计算噪声发生器的扇区偏移
int sectorSize = TileMapSector.SectorSizeInTiles;
float sectorOffsetX = sectorSize * sectorCoordinates.x * sectorSize / settings.scale;
float sectorOffsetY = sectorSize * sectorCoordinates.y * sectorSize / settings.scale;
Vector2 sectorOffset = new Vector2(sectorOffsetX, sectorOffsetY);
// 产生噪音
float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(settings, sectorOffset);
// 生成并定位扇区纹理
GameObject sectorSprite = new GameObject();
SpriteRenderer spriteRenderer = sectorSprite.AddComponent<SpriteRenderer>();
Texture2D texture = new Texture2D(sectorSize, sectorSize)
{
wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Point
};
texture.SetPixels(GenerateColorMap(noiseMap));
texture.Apply();
spriteRenderer.sprite = Sprite.Create(texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100.0f);
sectorSprite.transform.SetParent(transform, false);
sectorSprite.name = "" + sectorCoordinates.x + "_" + sectorCoordinates.y;
float positionOffsetMultiplier = sectorSize * transform.localScale.x / spriteRenderer.sprite.pixelsPerUnit;
sectorSprite.transform.Translate(new Vector3(sectorCoordinates.x * positionOffsetMultiplier, sectorCoordinates.y * positionOffsetMultiplier), Space.Self);
}
// 将噪声转换为颜色以渲染纹理
private Color[] GenerateColorMap(float[] noiseMap)
{
Color[] colorMap = new Color[noiseMap.Length];
for (int i = 0; i < noiseMap.Length; i++)
{
// 基本是最高级别的颜色
colorMap[i] = terrainLevel[terrainLevel.Count-1].color;
foreach (var level in terrainLevel)
{
// 根据噪音水平选择颜色
if (noiseMap[i] < level.height)
{
colorMap[i] = level.color;
break;
}
}
}
return colorMap;
}
}
|
扇区
负责图块在地图上的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public static class TileMapSector
{
public static int SectorSizeInTiles = 10;
public static void GenerateSector(float[] noiseMap, List<Tile> tileList, Tilemap tileMap)
{
// 扇区尺码
int length = SectorSizeInTiles;
int width = SectorSizeInTiles;
// 清除以前的值
tileMap.ClearAllTiles();
// 绕过所有扇区
for (int y = 0; y < length; y++)
{
for (int x = 0; x < width; x++)
{
// 当前图块的噪声级别
float noiseHeight = noiseMap[length*y + x];
// 生成的表面的水平在噪声等级上均匀分布
// 将噪声比例“拉伸”到瓷砖阵列的大小
float colorHeight = noiseHeight * tileList.Count;
// 选择一个低于结果值的图块
int colorIndex = Mathf.FloorToInt(colorHeight);
// 我们考虑在数组中寻址以获取最大噪声值
if (colorIndex == tileList.Count)
{
colorIndex = tileList.Count-1;
}
// 磁贴资产允许您使用2z的高度
// 因此,我们用色彩“拉伸”了两倍以上的噪声等级
float tileHeight = noiseHeight * tileList.Count * 2;
int tileHeightIndex = Mathf.FloorToInt(tileHeight);
// 我们将获得的高度移动以使瓷砖与水和第一层沙子对齐
tileHeightIndex -= 4;
if (tileHeightIndex < 0)
{
tileHeightIndex = 0;
}
// 根据转换后的噪声水平获取资产图块
Tile tile = tileList[colorIndex];
// 根据转换后的噪音水平设置瓷砖高度
Vector3Int p = new Vector3Int(x - length / 2, y - width / 2, tileHeightIndex);
tileMap.SetTile(p, tile);
}
}
}
}
|
产生Perlin Noise的偏差的计算
在生成器的实现中,考虑坐标偏移以获得噪声看起来像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// 八度移位以获得重叠的更有趣的图片
Vector2[] octavesOffset = new Vector2[octaves];
for (int i = 0; i < octaves; i++)
{
// 也使用外部位置偏移
float xOffset = rand.Next(-100000, 100000) + offset.x;
float yOffset = rand.Next(-100000, 100000) + offset.y;
octavesOffset[i] = new Vector2(xOffset / width, yOffset / height);
}
// 为了使视觉更舒适,将视图移至中央
float halfWidth = width / 2f;
float halfHeight = height / 2f;
// 生成高度图的点
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// 倍频程叠加处理
for (int i = 0; i < octaves; i++)
{
// 计算要从Noise Perlin获得的坐标
float xResult = (x - halfWidth) / scale * frequency + octavesOffset[i].x * frequency;
float yResult = (y - halfHeight) / scale * frequency + octavesOffset[i].y * frequency;
}
}
}
|
因此,为了正确计算扇区的位移,使用以下公式:
1
2
3
|
int sectorSize = TileMapSector.SectorSizeInTiles;
float sectorOffsetX = sectorSize * sector.x * sectorSize / generatorSettings.scale;
float sectorOffsetY = sectorSize * sector.y * sectorSize / generatorSettings.scale;
|
计算平铺屏幕所需的扇区数
正如刚才提到的:
1
2
|
int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1;
int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1);
|
计算空间中扇区的位置
它考虑了瓦片相对于彼此的位移尺寸的一半以及坐标轴的投影。
1
2
3
4
5
|
var tileSize = grid.cellSize;
float positionOffsetMultiplierX = TileMapSector.SectorSizeInTiles * tileSize.x * 0.5f;
float positionOffsetMultiplierY = TileMapSector.SectorSizeInTiles * tileSize.y * 0.5f;
float positionX = currentSectorX * positionOffsetMultiplierX - currentSectorY * positionOffsetMultiplierX;
float positionY = currentSectorX * positionOffsetMultiplierY + currentSectorY * positionOffsetMultiplierY;
|
地图生成类的完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class TileMapHandler : MonoBehaviour
{
// 瓷砖的预制
public GameObject tileMapPrefab = null;
// 用于显示生成的图块图的网格
public Grid grid = null;
// 瓷砖清单
public List<Tile> tileList = new List<Tile>();
// 发电机设定
[SerializeField] public GeneratorSettings generatorSettings;
// 生成地图列表
private readonly List<Transform> _tileMaps = new List<Transform>();
// Start is called before the first frame update
void Start()
{
// 隐藏测试纹理
NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
mapRenderer.gameObject.SetActive(false);
// 屏幕尺寸(以扇区为单位)
Vector2Int screenSizeInSectors = GetScreenSizeInSectors();
// 在空间中定位扇区的参数
var tileSize = grid.cellSize;
float positionOffsetMultiplierX = TileMapSector.SectorSizeInTiles * tileSize.x * 0.5f;
float positionOffsetMultiplierY = TileMapSector.SectorSizeInTiles * tileSize.y * 0.5f;
// 按行列遍历图块的参数
int startX = -screenSizeInSectors.x;
int startY = 0;
int currentSectorX = startX;
int currentSectorY = startY;
bool increaseX = true;
// 我们将索引移位一个以优化除法的其余部分对循环的使用
int lastSectorIndex = screenSizeInSectors.x * screenSizeInSectors.y + 1;
for (int currentSectorIndex = 1; currentSectorIndex < lastSectorIndex; currentSectorIndex++)
{
// 扇区对象
Transform sector = GetTileMapSector(new Vector2Int(currentSectorX, currentSectorY));
sector.SetParent(grid.transform);
// 计算扇区在空间中的位置
float positionX = currentSectorX * positionOffsetMultiplierX - currentSectorY * positionOffsetMultiplierX;
float positionY = currentSectorX * positionOffsetMultiplierY + currentSectorY * positionOffsetMultiplierY;
sector.Translate(new Vector3(positionX, positionY, 0), Space.Self);
sector.name = "" + currentSectorX + "," + currentSectorY;
// 救扇区
_tileMaps.Add(sector);
// 移至下一栏
currentSectorX++;
currentSectorY--;
// 转到下一行
if (currentSectorIndex % screenSizeInSectors.x == 0)
{
if (increaseX)
{
currentSectorX = ++startX;
currentSectorY = startY;
increaseX = false;
}
else
{
currentSectorX = startX;
currentSectorY = ++startY;
increaseX = true;
}
}
}
}
// 屏幕尺寸(以扇区为单位)
public Vector2Int GetScreenSizeInSectors()
{
// 相机高度
float cameraHeight = Camera.main.orthographicSize * 2;
// 相机宽度等于相机高度*高宽比
float screenAspect = Camera.main.aspect;
float cameraWidth = cameraHeight * screenAspect;
// 计算平铺屏幕的磁贴数量
var cellSize = grid.cellSize;
int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1;
int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1);
return new Vector2Int(sectorCountX, sectorCountY);
}
// 扇区产生
private Transform GetTileMapSector(Vector2Int sector)
{
// 平铺地图对象
GameObject sectorGameObject = Instantiate(tileMapPrefab);
// 计算噪声发生器的扇区偏移
int sectorSize = TileMapSector.SectorSizeInTiles;
float sectorOffsetX = sectorSize * sector.x * sectorSize / generatorSettings.scale;
float sectorOffsetY = sectorSize * sector.y * sectorSize / generatorSettings.scale;
Vector2 sectorOffset = new Vector2(sectorOffsetX, sectorOffsetY);
Tilemap sectorTileMap = sectorGameObject.GetComponent<Tilemap>();
// 瓷砖地图扇区生成
TileMapSector.GenerateSector(NoiseMapGenerator.GenerateNoiseMap(generatorSettings, sectorOffset), tileList, sectorTileMap);
return sectorGameObject.transform;
}
}
|
结果
测试纹理:
生成等轴测图的结果分为多个扇区:
结论
对于我们的程序生成示例,添加扇区是一小步。尽管如此,它还是解决了诸如定位Perlin Noise的生成区域,用于等轴测坐标系统以及将空间划分为多个扇区的基本机制之类的关键任务。这三个任务是通过移动和实际创建无限空间为动态扇区加载做准备。但是我们将在本系列的下一篇文章中对此进行处理。下次见!^_^
如果您喜欢这篇文章,可以为它提供支持