使用Perlin Noise生成等轴测图

Tulenber 15 May, 2020 ⸱ Intermediate ⸱ 7 min ⸱ 2019.3.13f1 ⸱

我们继续关于Unity中程序生成的 系列

上一次,我们使用Perlin Noise作为生成曲面的基础。这种方法可以非常轻松快捷地获得接近实际的高度图。根据高度添加颜色可以进一步增强印象。下一步是使用高度图来构建实际的体积空间。

上一篇文章基于塞巴斯蒂安·拉格(Sebastian Lague)的一系列视频,这些视频应用了该算法来生成三维曲面,但这并不是唯一可用的可视化选项。横向滚动,自上而下和等轴测视图在游戏创作者中也很流行。他们并没有失去与三维图形开发的联系,并允许您为该项目赋予独特的风格和氛围。Unity一直在致力于扩展开发人员可用的工具,而tilemaps是此类相对较新的工具之一,可促进创建上述类型的表示形式。

Tilemaps

Unity在2017年增加了使用2D磁贴的功能,简化了自上而下和侧滚动的创建。一年后,还添加了六角形瓷砖和等距图,由于六角形长期以来是“文明”系列的基础,因此六角形瓷砖和等距图的使用引起了极大的兴趣。等轴测视图对于RPG类型的大多数代表来说都是至关重要的 ^_^ 因此,让我们尝试迈向角色扮演游戏的第一步,并将使用“ Perlin Noise”生成的地图转移到等距空间。

目标

使用Unity等轴测图贴图来显示由“ Perlin Noise”生成的高度图。

制备

我们将使用上一篇文章中的Perlin Noise生成器作为此实现的基础。

来自Devil’s Work.shop的带瓷砖的资源包被用作表面表示。 我们从中选择九个图块。

设置等轴测图是一个非常棘手的过程,Alice Hinton-Jones在“使用Tilemap的等轴测2D环境”一文中对此进行了很好的描述,因此重写它是没有意义的。

让我们列出配置此项目的tilemap部分所采取的步骤:

  1. Edit > Project Settings > Graphics > Camera Settings > Transparency Sort Mode - Custom Axis
  2. Edit > Project Settings > Graphics > Camera Settings > Transparency Sort Axis - X=0 Y=1 Z=-0.289 - Z取决于图块的大小,您可以在Alice的文章中找到计算它的算法。 首先,取网格的Y尺寸,将此值乘以负0.5,然后再减去0.01
  3. 导入瓷砖资源作为纹理
  4. Pixels Per Unit863 真实的瓷砖大小
  5. Pivot0.4921, 0.7452 - 瓷砖顶部的中心,如Alice的文章所述
  6. 允许Read/Write用于测试目的的瓷砖纹理
  7. 创建Isometric Z as Y tilemap
  8. 设置在Grid宾语Y Cell Size0.577 取决于图块的大小,计算规则也在Alice文章中。 要确定图块的正确Y网格值,请获取单个图块的底部(或顶盖)的高度,然后将其除以宽度
  9. 创建等轴测图palette一样Y Cell SizeGrid组件在0.577
  10. 改变Tilemap Renderer > Mode组件设置为Individual. 如果图块的纹理位于不同的地图集中,则可以使用此设置来解决计算彼此重叠的图块的错误。 当将瓷砖纹理烘焙到一个图集中时,可以使用Chunk选项,您可以在生产构建阶段的准备工作中进行此操作

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;
    }
}

预习

出于测试目的,部分噪声渲染到颜色纹理也从上一篇文章中转移了。为避免手动填充高度比例,我们从瓷砖阵列中添加了自动填充。

NoiseMapEditorGenerate-在负责创建图块贴图的对象的编辑器中添加一个用于生成测试纹理的按钮(有关Unity编辑器扩展的更多信息,您可以在另一篇文章中找到):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TileMapHandler))]
public class NoiseMapEditorGenerate : Editor
{
    public override void OnInspectorGUI()
    {
        // 获取组件
        TileMapHandler noiseMap = (TileMapHandler)target;

        // 绘制标准视图并听取更改以生成新地图
        if (DrawDefaultInspector())
        {
            noiseMap.GenerateMap();
        }

        // 添加按钮以重新生成地图
        if (GUILayout.Button("Generate"))
        {
            noiseMap.GenerateMap();
        }
    }
}

NoiseMapRenderer-负责从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
using System;
using System.Collections.Generic;
using UnityEngine;

public class NoiseMapRenderer : MonoBehaviour
{
    [SerializeField] public SpriteRenderer spriteRenderer = null;

    // 根据高度确定地图的颜色
    [Serializable]
    public struct TerrainLevel
    {
        public float height;
        public Color color;
    }
    [SerializeField] public List<TerrainLevel> terrainLevel = new List<TerrainLevel>();

    // 创建要显示的纹理和精灵
    public void RenderMap(int width, int height, float[] noiseMap)
    {
        Texture2D texture = new Texture2D(width, height);
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.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);
    }

    // 根据高度将包含噪声数据的数组转换为颜色数组,以传输到纹理
    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;
    }
}

等轴测图生成

有两种方法可以在等轴测图贴图中添加高度差异,可以在Alice的文章中找到这两种方法的概述。我们根据Z分量的使用情况选择方法。这样可以避免为每个海拔级别创建额外的tilemap对象。

与前一篇文章相比,高度标尺分布均匀,从视觉上看也有不错的结果,但是减少了填充表面水准仪的时间。

  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
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class TileMapHandler : MonoBehaviour
{
    // 地图对象
    public Tilemap tilemap = null;
    // 瓷砖清单
    public List<Tile> tileList = new List<Tile>();
    
    // 我们的噪声发生器的输入数据
    [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;

    // 在第一帧更新之前调用开始
    void Start()
    {
        // 隐藏具有测试纹理的对象
        NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
        mapRenderer.gameObject.SetActive(false);
        
        // 生成高度图
        float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(width, height, seed, scale, octaves, persistence, lacunarity, offset);

        // 创建瓷砖
        for (int y = 0; y < width; y++)
        {
            for (int x = 0; x < height; x++)
            {
                // 当前图块的噪声水平
                float noiseHeight = noiseMap[width*y + x];

                // 生成的表面的水平在噪声等级上均匀分布
                // 将噪声比例“拉伸”到瓷砖阵列的大小
                float colorHeight = noiseHeight * tileList.Count;
                // 选择一个低于结果值的图块
                int colorIndex = Mathf.FloorToInt(colorHeight);
                // 考虑在阵列中寻址以获得最大噪声值
                if (colorIndex == tileList.Count)
                {
                    colorIndex = tileList.Count-1;
                }

                // 磁贴资产允许您使用2z的高度
                // 因此,我们用色彩“拉伸”了超过2倍的噪点
                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 - width / 2, y - height / 2, tileHeightIndex);
                tilemap.SetTile(p, tile);
            }
        }
    }

    // 使用为噪声发生器指定的参数生成测试纹理的函数
    // 在编辑器扩展NoiseMapEditorGenerate中使用
    public void GenerateMap()
    {
        // 生成高度图
        float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(width, height, seed, scale, octaves, persistence, lacunarity, offset);

        // 取决于使用瓦片素材填充阵列,我们根据噪声高度生成均匀分布的颜色依赖性
        List<NoiseMapRenderer.TerrainLevel> tl = new List<NoiseMapRenderer.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);
            // 创建一个新的色噪等级
            NoiseMapRenderer.TerrainLevel lev = new NoiseMapRenderer.TerrainLevel();
            lev.color = color;
            // 将索引转换为范围为[0,1]的刻度位置并向上移动
            lev.height = Mathf.InverseLerp(0, tileList.Count, i) + heightOffset;
            // 保存新的色噪等级
            tl.Add(lev);
        }

        // 应用新的色噪比例并根据指定的参数基于该比例生成纹理
        NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
        mapRenderer.terrainLevel = tl;
        mapRenderer.RenderMap(width, height, noiseMap);
    }
}

结果

生成的地图的测试纹理如下所示:
Open Test Runner

从相同坐标生成的等轴测图:
Open Test Runner

Open Test Runner

Open Test Runner

所有水位和第一砂层都对齐以提供进一步的信誉。

结论

tilemap的基本用法并不难。但是请不要忘记,将磁贴提高到视觉上可接受的水平需要大量的资产工作。而且,在我们的示例中使用tilemap进行的工作还远远没有完成。有必要增加增加地图高度的可能性,这将需要用大于2z的高度差来填充自由空间。为了在地图上自由移动,必须实现块加载系统。添加动态对象将需要创建一个考虑高程的运动系统,可能使用标准对撞机。并且视觉改善还需要一种用于接合具有不同类型表面的瓷砖的机制。 在本过程的下一篇文章中,我们将处理所有这些主题。下次见!^_^


如果您喜欢这篇文章,可以为它提供支持



Privacy policyCookie policyTerms of service
Tulenber 2020