Unity中的程序生成和Perlin Noise
Tulenber ⸱
17 April, 2020 ⸱
Intermediate ⸱
6 min ⸱
2019.3.9f1 ⸱
这篇文章延续了一个无止境的世界创造的循环 ,在这个循环中我们将熟悉程序生成的基础。
似乎很大一部分人是出于创造性原因而进入游戏行业的。创造力的方向之一可以是创造世界。此外,如果您在互联网上遇到有关此主题的问题,您会发现相对大量的文章,这些文章可以证明人们渴望成为创作者。这篇文章将跟随许多前辈的脚步,并讨论一种最简单,最基本的方法来创建一个小但仍属于您的宇宙,称为“ Perlin Noise”。
程序生成
从程序员的角度来看,有两种创建任何主题的方法:手动创建主题或编写代码生成主题。尽管最有可能,但只有第二个。第一个,由于某种原因,被其他人使用。首先,让我们定义基本概念:
- 程序生成(PCG)-使用算法自动创建内容的机制的通用名称。 这个概念包括从音乐到游戏规则的一切创作。
- 伪随机数生成器(PRNG)-一种生成数字序列的算法,其元素几乎彼此独立并服从给定的分布。
- 种子(Seed)-确定PRNG发出的编号顺序。
当然,如果您只需要少量内容,那么最简单的方法就是用手创建内容(在地图上排列树或绘制纹理)。但是,如果我们谈论的是大量材料,那么程序生成可能是一种更有利可图的方式。原理很简单。当您需要某些内容时,您将获得PRNG,种子,其他输入数据以及一些用于创建必要内容并生成唯一内容单元的算法。如果生成后发生任何更改,请分别保存。随后,如果需要重复对象,知道种子,输入数据和已执行的更改,则可以重现其生成并获取给定的对象。
这是该方案的一般说明,该方案使用各种算法,几乎适合任何任务,从音乐生成开始,以火的纹理继续,最后以“ No Man’s Sky”中某个星球的动物生成结束。在本文中,我们将考虑生成程序到地球表面的目的。
创造我们的土地
描述地球表面的一种通用方法是使用高度图。它是一个二维数组,其中包含地面上点的高度的数据。如果从这样的地图中获取垂直切片,则会得到以下视图:
因此,如果我们设法生成这样的高度图,则可以从中创建一个景观。假设我们通过常规Random生成点,并得到如下图:
如果以二维形式查看,则生成的地图将如下所示:
从图片中应该清楚,通常的PRNG不太适合创建地图,因为白噪声并不意味着依赖于附近的点。因此,我们得到了与实际表面不太相似的明显的表面高度差异。
佩林噪声(Perlin Noise)
为了生成相关数据,我们将使用称为Perlin Noise的PRNG,它将创建如下图像:
我们不会深入研究算法本身的原理。 有关更多信息,请参阅此文章。
我们需要进一步工作的概念:
- Amplitude-变量相对于平均值的偏移或变化的最大值。
- Frequency-周期性过程的特征等于每单位时间重复或事件(操作)发生的次数。
- Lacunarity-控制频率的变化。
- Persistence-控制振幅的变化。
- Octave-具有不同频率的样本。
我们将演示三倍频程算法的操作原理:
Lacunarity = 2;
Persistence = 0.5;
第一个八度设置主要海拔(海,山)
Frequency = Lacunarity ^ 0 = 1
Amplitude = Persistence ^ 0 = 1
第二个八度设置平均表面下降(巨石)
Frequency = Lacunarity ^ 1 = 2
Amplitude = Persistence ^ 1 = 0.5
第三个八度音阶定义了小的表面下落(小石头)
Frequency = Lacunarity ^ 2 = 4
Amplitude = Persistence ^ 2 = 0.25
我们将八度音阶叠加在一起,结果,我们在本节中得到了类似于真实高度图的东西:
实作
该实现的基础是塞巴斯蒂安·拉格(Sebastian Lague)的视频系列“程序地形生成”(抱歉,这是youtube链接)。 您可以在bilibili上搜索他的另一个视频,它们非常有趣。
制作三个脚本
- NoiseMapGenerator-负责使用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
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
|
using UnityEngine;
public static class NoiseMapGenerator
{
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;
}
}
|
- 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
|
using UnityEngine;
public enum MapType
{
Noise,
Color
}
public class NoiseMap : MonoBehaviour
{
// 输入噪声发生器的数据
[SerializeField] public int width;
[SerializeField] public int height;
[SerializeField] public float scale;
[SerializeField] public int octaves;
[SerializeField] public float persistence;
[SerializeField] public float lacunarity;
[SerializeField] public int seed;
[SerializeField] public Vector2 offset;
[SerializeField] public MapType type = MapType.Noise;
private void Start()
{
GenerateMap();
}
public void GenerateMap()
{
// 生成地图
float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(width, height, seed, scale, octaves, persistence, lacunarity, offset);
// 将地图传递到渲染器
NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
mapRenderer.RenderMap(width, height, noiseMap, type);
}
}
|
- NoiseMapRenderer-为地图显示创建纹理
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
|
using System;
using System.Collections.Generic;
using UnityEngine;
public class NoiseMapRenderer : MonoBehaviour
{
[SerializeField] public SpriteRenderer spriteRenderer = null;
// 根据高度确定地图的颜色
[Serializable]
public struct TerrainLevel
{
public string name;
public float height;
public Color color;
}
[SerializeField] public List<TerrainLevel> terrainLevel = new List<TerrainLevel>();
// 根据类型,我们会绘制黑白或彩色噪声
public void RenderMap(int width, int height, float[] noiseMap, MapType type)
{
if (type == MapType.Noise)
{
ApplyColorMap(width, height, GenerateNoiseMap(noiseMap));
}
else if (type == MapType.Color)
{
ApplyColorMap(width, height, GenerateColorMap(noiseMap));
}
}
// 创建要显示的纹理和精灵
private void ApplyColorMap(int width, int height, Color[] colors)
{
Texture2D texture = new Texture2D(width, height);
texture.wrapMode = TextureWrapMode.Clamp;
texture.filterMode = FilterMode.Point;
texture.SetPixels(colors);
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);;
}
// 将具有噪声数据的数组转换为黑白数组,以传输到纹理
private Color[] GenerateNoiseMap(float[] noiseMap)
{
Color[] colorMap = new Color[noiseMap.Length];
for (int i = 0; i < noiseMap.Length; i++)
{
colorMap[i] = Color.Lerp(Color.black, Color.white, noiseMap[i]);
}
return colorMap;
}
// 根据高度将包含噪声数据的数组转换为颜色数组,以传输到纹理
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;
}
}
|
- 使用新脚本和Sprite Renderer组件创建一个对象
- 设置高度图调色板
结果
结果,我们得到了一个看起来像景观的单独部分的高度图。
结论
我们熟悉了过程生成和Perlin Noise的基本概念,并且还生成了一个高度图,可以像真实的风景一样。这是一种简单的算法,可让您在短时间内获得可接受的结果。最明显的应用是将其值覆盖在类似Plane的对象上,从而创建3D风景,就像Sebastian在他的视频系列中所做的那样。但是,这只是一种基本算法,例如,在创建景观,气候时不计算许多参数。同样,计算的独立性允许您创建无限的空间,但产生的结果非常相似。最后,它不允许您在没有其他机制的情况下创建分为大陆或岛屿的地图。但是,在本系列的以下文章中,我们将尝试将这种基本算法应用于更有趣的事物,因为在大多数情况下,主要限制仅在于我们的想象力。毕竟,即使Minecraft最初也基于Perlin Noise。下次见!^_^
奖金
我最喜欢的文章之一同一主题。通常,此博客包含许多有关 游戏机制。
如果您喜欢这篇文章,可以为它提供支持