How to design a Minecraft-inspired world with Mapbox and Unity

David Rhodes
Wed, 11 Jan 2017 12:00:00 -0500

A few weeks ago I used the Mapbox Unity SDK to create a Minecraft-inspired world. I got a lot of interest after that initial blog post, so I put together a step-by-step guide of how to create your own Minecraft-inspired world with the Mapbox Unity SDK.

See what my Minecraft-inspired world looks like during gameplay.

Step 0: Pick your location

Before you get started, look up the latitude and longitude of the real world location you want to style with Minecraft-inspired voxels.

I chose to style Mont Blanc, the highest mountain in the Alps, located at 45.8326° N, 6.8652° E.

Mont Blanc is the highest mountain in the Alps.

Step 1: Create your style

Open up Mapbox Studio to create your game world’s style. The colors you choose here will be used later on to determine which voxels to generate. Make sure you choose colors that are highly contrasting. If you’re new to Mapbox Studio, check out our guide for getting started.

For my Mont Blanc example, I started with an empty style and added layers. Giving distinct colors to grass, wood, water, and more. Similar layers, such as glacier and snow, share the same style color (cyan, in my case). Don’t forget to set filters for sources such as Vector Terrain landcover.

The style for my Minecraft-inspired Mont Blanc world uses contrasting colors to highlight different landcover across the map.

Step 2: Fetch your style data in Unity

Publish your style within Mapbox Studio and pass the style URL to the MapId property of the RasterTile.

if (!string.IsNullOrEmpty(_styleUrl))
{
    _raster.MapId = _styleUrl;
}

Then fetch your style as raster data.

void Start()
{
    _raster = new Map<RasterTile>(_fileSource);
    _raster.Subscribe(this);

    // Mont Blanc Latitude/Longitude. You can use our Geocoding API to obtain this.
    RequestWorldData(new GeoCoordinate(45.967, 6.902));
}

void RequestWorldData(GeoCoordinate coordinates)
{
    var bounds = new GeoCoordinateBounds();
    bounds.Center = coordinates;
    _raster.SetGeoCoordinateBoundsZoom(bounds, _zoom);
    _elevation.SetGeoCoordinateBoundsZoom(bounds, _zoom);
}

public void OnNext(RasterTile tile)
{
    if (tile.CurrentState == Tile.State.Loaded && tile.Error == null)
    {
        _rasterTexture = new Texture2D(2, 2);
        _rasterTexture.LoadImage(tile.Data);
    }
}

Step 3: Downsample the bitmap

Each pixel of the bitmap will generate one voxel, so downsample in order to generate fewer voxels for the tile that we’ve requested. Using real-world scale would create a dull voxel environment, so for more exaggeration, I recommend shrinking the texture by half.

TextureScale.Point(_rasterTexture, _tileWidthInVoxels, _tileWidthInVoxels);

Step 4: Map pixel colors to voxel types

The next step is to map each pixel’s color to a specific voxel type or unique prefab.

For Mont Blanc, I used various voxel types for each landuse defined in my style. You can see how I’ve mapped bright green to a Grass voxel.

This "Grass" voxel and the other voxels we used for this world are from Pixel Cube World Atlantis, available in the Unity asset store.

To map the voxels, I used a simple “nearest color” formula, which is very similar to the Pythagorean Theorem.

namespace Mapbox.Examples.Voxels
{
    public class VoxelFetcher : MonoBehaviour
    {
        [SerializeField]
        VoxelColorMapper[] _voxels;

        public GameObject GetVoxelFromColor(Color color)
        {
            GameObject matchingVoxel = _voxels[0].Voxel;
            var minDistance = Mathf.Infinity;
            foreach (var voxel in _voxels)
            {
                var distance = GetDistanceBetweenColors(voxel.Color, color);
                if (distance < minDistance)
                {
                    matchingVoxel = voxel.Voxel;
                    minDistance = distance;
                }
            }
            return matchingVoxel;
        }

        public static float GetDistanceBetweenColors(Color color1, Color color2)
        {
            return Mathf.Sqrt(Mathf.Pow(color1.r - color2.r, 2f)
			                  + Mathf.Pow(color1.g - color2.g, 2f)
			                  + Mathf.Pow(color1.b - color2.b, 2f));
        }
    }

    [Serializable]
    public class VoxelColorMapper
    {
        public Color Color;
        public GameObject Voxel;
    }
}

Step 5: Place voxels based on pixel location

Now you’ll need to place your voxels in Unity space based on the location of each pixel in the texture.

for (int x = 0; x < _rasterTexture.width; x++)
{
    for (int z = 0; z < _rasterTexture.height; z++)
    {
        _voxels.Add(new VoxelData() { Position = new Vector3(x, 0, z),
                                      Prefab = _voxelFetcher.GetVoxelFromColor(color) });
    }
}

Step 6: Add in global terrain data

To determine where to vertically position each voxel, you can use our global terrain layer, Terrain-RGB.

Use this code to extract absolute height from the color of a pixel.

public static float GetAbsoluteHeightFromColor(Color color)
{
    return (float)(-10000 + ((color.r * 256 * 256 * 256 + color.g * 256 * 256 + color.b * 256) * 0.1));
}

You’ll also need to adjust the height based on meters per pixel for the specified zoom level.

void Awake()
{
    _tileScale = _tileWidthInVoxels / GetTileScaleInMeters((float)coordinates.Latitude, _zoom);
}

public static float GetTileScaleInMeters(float latitude, int zoom)
{
    return 40075000 * Mathf.Cos(Mathf.Deg2Rad * latitude) / (Mathf.Pow(2f, zoom + 8)) * 256;
}

void Extrude()
{
    var height = (int)GetAbsoluteHeightFromColor(_elevationTexture.GetPixel(x, z));
    height = (int)(height * _tileScale);
}

See how the stylized Mont Blanc world came together: from downsampling in Step 3 shown in the image on the left, to initial voxel placement in the middle, to extruded voxels on the right.

Step 7: Experiment

Play with zoom level, texture downsampling, and height multipliers to stylize your environment.

For the image on the left, I tried out a high zoom (14) with real world heights. On the right, I used a lower zoom (9) with 3x heights.

There’s a ton you can do with styling and the Mapbox Unity SDK. You could use landuse data in our vector tiles to inform organic biome generation, rather than mapping voxels to raster data. Waterways could be used to inform physics-based flow systems rather than spawning voxels. Additionally, you could use our building footprint data to create voxel buildings and villages. You could add gameplay elements by allowing players to create and destroy features.

We can’t wait to see what you come up with. Sign up to receive the Mapbox Unity SDK Beta and download my Mont Blanc voxel example for the full code.