Natural precipitation

Posted by Adam on 7 July 2018 at 11:12 pm

In our unannounced title, upon starting a new game, you'll first be met with one of the most important screens, the world generator. This is the world in which you're going to spend a lot of time, whether that's in the frigid tundras of the polar north, in the dusty savannahs of the equatorial regions, or in a temperate forest somewhere in between. Either way, connecting with the in game world is important in setting the tone for the game ahead.

Terrain generation

Procedural terrain generation is so much fun to work with. That's why we wanted to show off our natural precipitation model, which uses terrain features such as mountains and hills, as well as oceans, to generate natural rainfall and therefore forests and vegetative biomes where the land is more moist.

Natural precipitation

It's easy to get lost in procedural generation. We have more features to add: inland lakes and rivers, natural erosion, volcanoes for magma forges... right?

We look forward to sharing more images in the future, such as the biomes themselves, up close and personal.

And magma forges.

Psst, for more on game development & to see more posts about our unannounced title in the making, why not follow us on Twitter @plumphelmetcave? Thanks.

This post is from the First Era of Plump Helmet.

This was before the great break of 2019-2021 and the predominant project of the time was Feudalia. The project was placed into cold storage and has not been revived yet. The dream remains alive however. These posts have been kept for posterity.

Edge Smoothing

Posted by Adam on 28 June 2018 at 2:18 pm

To begin, let's first admit that these textures are of potato quality. Let's just get that out there. I know. But their quick fabrication was to test a concept, of spreading a large texture over a number of smaller quads in order to reduce the repetitive nature of a 2D tiled terrain. In the picture below, you can make out the size of a quad, and each texture covers 256 of these in a 16,16 grid.

Sans smoothing

It works well. The tiling is only really evident when you zoom right out, and in reality, that won't happen "in the game", and rarely do you have so much terrain of the same type. Soil may have some rich soil mixed in, perhaps some gravel and stone, and a bit of marshy bog as well, not to mention the trees, plants, and demonic obelisks. Maybe.

The overall terrain is built in layers. In this example, there are three layers: water, sand, and soil. These layers each have their own meshes, which are generated in that order and drawn in that order too. When viewed with an orthagraphic camera, they meld into a single, unified terrain.

Below you can see the sand layer selected in the Unity editor.

Sand layer proper

With a tiled approach, where each 64 pixel block is represents a terrain type and is represented by a quad, the visuals become very blocky, and it doesn't really look good. So we need to blend these layers into each other. I attempted a few different techniques to achieve this, but in the end, without sacrificing the layered approach, it wasn't really possible (for me.)

So to blend the terrain layers, each layer checks for tiles at it's edge, and, where it finds a tile that is dissimilar, it creates a new quad with vertex-level opacity. For example, if the current tile is water and the tile to the north is sand, then the water layer will create a new quad to the north (tile x,y+1) with four vertices, the "southern" two (x,y + x+1,y) with an opacity of 1 and the "northern" two (x,y+1 + x+1,y+1) with an opacity of 0. These quads are part of the same mesh as the rest of the tiles, and therefore benefit from the same UV mapping & tiling.

Doing this gives us a border of faded quads. As the terrain layers are rendered in order, the fading quads of the layers underneath are culled and the fading quads from the above layers are visible.

Sand layer + smoothing

But how did we get to this result?

The first step is generating the map layers, but the real magic happens in CalculateEdges.

// Iterate over every map tile, starting at bottom left,
// and going left to right, bottom to top.
for (var y = 0; y < mapHeight; y++)
{
    for (var x = 0; x < mapWidth; x++)
    {
        if (_mapTiles[x, y].TerrainType != _type) continue;

        _meshData.AddVertex(x, y, 0);
        _meshData.AddVertex(x, y + 1, 0);
        _meshData.AddVertex(x + 1, y + 1, 0);
        _meshData.AddVertex(x + 1, y, 0);
        _meshData.AddQuadTriangles();
        _meshData.AddQuadColors();
        _meshData.AddUV(CalculateUV(x, y));
        _meshData.AddUV(CalculateUV(x, y + 1));
        _meshData.AddUV(CalculateUV(x + 1, y + 1));
        _meshData.AddUV(CalculateUV(x + 1, y));

        CalculateEdges(x, y);
    }
}

In CalculateEdges we iterate over all directions, cardinal (N,E,S,W) as well as ordinal (NE,SE,SW,NE), checking whether the tile in these directions are a different type than the current tile, and when they are, we create the faded quad as per these directions.

// Iterate over all directions, check whether an edge tile should
// be generated for this direction, and if so, generate one.
List<Direction> list = new List<Direction>();
Array directions = Enum.GetValues(typeof(Direction));
foreach(Direction direction in (Direction[])directions)
{
    switch (direction)
    {
        case Direction.North:
            if (y + 1 < mapHeight && _mapTiles[x, y + 1].TerrainType != _type)
                list.Add(Direction.North);
            break;

        case Direction.NorthEast:
            if (y + 1 < mapHeight && x + 1 < mapWidth && _mapTiles[x + 1, y + 1].TerrainType != _type)
                list.Add(Direction.NorthEast);
            break;

        case Direction.East:
            if (x + 1 < mapWidth && _mapTiles[x + 1, y].TerrainType != _type)
                list.Add(Direction.East);
            break;

        case Direction.SouthEast:
            if (y - 1 >= 0 && x + 1 < mapWidth && _mapTiles[x + 1, y - 1].TerrainType != _type)
                list.Add(Direction.SouthEast);
            break;

        case Direction.South:
            if (y - 1 >= 0 && _mapTiles[x, y - 1].TerrainType != _type)
                list.Add(Direction.South);
            break;

        case Direction.SouthWest:
            if (y - 1 >= 0 && x - 1 >= 0 && _mapTiles[x - 1, y - 1].TerrainType != _type)
                list.Add(Direction.SouthWest);
            break;

        case Direction.West:
            if (x - 1 >= 0 && _mapTiles[x - 1, y].TerrainType != _type)
                list.Add(Direction.West);
            break;

        case Direction.NorthWest:
            if (y + 1 < mapHeight && x - 1 >= 0 && _mapTiles[x - 1, y + 1].TerrainType != _type)
                list.Add(Direction.NorthWest);
            break;
    }
}

Finally, once we have a list of directions where an edge tile is needed, we generate them.

list.ForEach(delegate(Direction direction) {
    GenerateEdge(x, y, direction);
});

The GenerateEdge method makes a copy of the tiles current x,y coordinates and for the direction it needs to generate an edge for, it either adds or subtracts a value from the copied x and/or y values, and also rather importantly, defines which vertices are opaque and which are not.

int dx = x;
int dy = y;

// Calculate relative position as well as the vertex alpha.
switch (direction)
{
    case Direction.North:
        dy += 1;
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 1);
        break;

    case Direction.NorthEast:
        dx += 1;
        dy += 1;
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        break;

    case Direction.East:
        dx += 1;
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        break;

    case Direction.SouthEast:
        dx += 1;
        dy -= 1;
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        break;

    case Direction.South:
        dy -= 1;
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 0);
        break;

    case Direction.SouthWest:
        dx -= 1;
        dy -= 1;
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 0);
        break;

    case Direction.West:
        dx -= 1;
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 1);
        _meshData.AddColor(1, 1, 1, 1);
        break;

    case Direction.NorthWest:
        dx -= 1;
        dy += 1;
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 0);
        _meshData.AddColor(1, 1, 1, 1);
        break;
}

Then, like before, it's just a case of defining the quad.

// Add vertex, triangle, and UV data
_meshData.AddVertex(dx, dy, 0);
_meshData.AddVertex(dx, dy + 1, 0);
_meshData.AddVertex(dx + 1, dy + 1, 0);
_meshData.AddVertex(dx + 1, dy, 0);
_meshData.AddQuadTriangles();
_meshData.AddUV(CalculateUV(dx, dy));
_meshData.AddUV(CalculateUV(dx, dy + 1));
_meshData.AddUV(CalculateUV(dx + 1, dy + 1));
_meshData.AddUV(CalculateUV(dx + 1, dy));

The last part, which is essential, is the shader. We define the color fixed4 color : COLOR parameter in appdata and v2f, and when it comes time to manipulate each pixel, we multiply the texel by the color return tex2D(_MainTex, i.uv) * i.color.

Shader "2D/VertexBlend"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                fixed4 color : COLOR;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv) * i.color;
            }
            ENDCG
        }
    }
}

It's not a complex operation. In essence, it's actually quite simple, but almost always the simplest solution is the best, and in this case, once you understand the simple solution, is hard to understand how it could ever have been so tricky.

Thanks for reading, and why not follow us on Twitter @plumphelmetcave for more on game development & our unannounced title?

This post is from the First Era of Plump Helmet.

This was before the great break of 2019-2021 and the predominant project of the time was Feudalia. The project was placed into cold storage and has not been revived yet. The dream remains alive however. These posts have been kept for posterity.

Coloured Cube

Posted by Adam on 23 April 2018 at 9:08 pm

Using a custom shader (assigned to a custom material), we are able to colour a cube with the vertex colour data provided to the mesh object.

VertexColour (Shader)

Shader "Custom/VertexColour"
{
  Properties
  {
    _Color ("Main Color", Color) = (1,1,1,1)
    _SpecColor ("Spec Color", Color) = (1,1,1,1)
    _Emission ("Emmisive Color", Color) = (0,0,0,0)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.7
    _MainTex ("Base (RGB)", 2D) = "white" {}
  }

  SubShader
  {
    Pass
    {
      Material
      {
        Shininess [_Shininess]
        Specular [_SpecColor]
        Emission [_Emission]
      }

      ColorMaterial AmbientAndDiffuse
      Lighting On
      SeparateSpecular On

      SetTexture [_MainTex]
      {
        Combine texture * primary, texture * primary
      }

      SetTexture [_MainTex]
      {
        constantColor [_Color]
        Combine previous * constant DOUBLE, previous * constant
      }
    }
  }

  Fallback "VertexLit", 1
}

ColouredCube

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class ColouredCube : MonoBehaviour
{
    Mesh mesh;
    Vector3[] vertices;
    int[] triangles;
    Color32[] colors32;

    void Awake()
    {
        mesh = GetComponent<MeshFilter>().mesh;
        vertices = new Vector3[] {
            new Vector3(0, 0, 0), // front bottom left
            new Vector3(1, 0, 0), // front bottom right
            new Vector3(1, 1, 0), // front top right
            new Vector3(0, 1, 0), // front top left
            new Vector3(0, 1, 1), // back top left
            new Vector3(1, 1, 1), // back top right
            new Vector3(1, 0, 1), // back bottom right
            new Vector3(0, 0, 1)  // back bottom left
        };
        triangles = new int[] {
            0, 2, 1, // front
            0, 3, 2,
            0, 7, 4, // left
            0, 4, 3,
            5, 4, 7, // back
            5, 7, 6,
            1, 2, 5, // right
            1, 5, 6,
            2, 3, 4, // top
            2, 4, 5,
            0, 6, 7, // bottom
            0, 1, 6
        };

        colors32 = new Color32[] {
            new Color32(0, 255, 0, 255), // front bottom left
            new Color32(0, 240, 0, 255), // front bottom right
            new Color32(0, 225, 0, 255), // front top right
            new Color32(0, 210, 0, 255), // front top left
            new Color32(0, 195, 0, 255), // back top left
            new Color32(0, 180, 0, 255), // back top right
            new Color32(0, 165, 0, 255), // back bottom right
            new Color32(0, 150, 0, 255)  // back bottom left
        };
    }

    void Start()
    {
        mesh.Clear();
        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.colors32 = colors32;
        mesh.RecalculateNormals();
    }
}

This post is from the First Era of Plump Helmet.

This was before the great break of 2019-2021 and the predominant project of the time was Feudalia. The project was placed into cold storage and has not been revived yet. The dream remains alive however. These posts have been kept for posterity.

Simple Chunk

Posted by Adam on 22 February 2018 at 9:08 pm

This simple chunk comprises of five classes: Chunk, Block, BlockSolid, BlockEmpty, and MeshData. The MeshData class holds the vertex/index data, which is instantiated in the Chunk class and passed to each Block class on render.

Chunk

This class holds a three-dimensional array of Block-derived classes, which it populates in the Awake() method. When Update() runs, it checks to see if the public boolean update is set to true, and if so, it updates & renders the blocks.

It creates a new MeshData class to hold render data for later on, which it passes to each block it renders.

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Chunk : MonoBehaviour
{
    public static int chunkSize = 16;
    public bool update = true;

    Block[,,] blocks;
    MeshFilter meshFilter;

    void Awake()
    {
        // initialise a simple 50/50 solid/empty block array
        blocks = new Block[chunkSize, chunkSize, chunkSize];
        for (int x = 0; x < chunkSize; x++)
        {
            for (int y = 0; y < chunkSize; y++)
            {
                for (int z = 0; z < chunkSize; z++)
                {
                    Block block = (y < chunkSize / 2)
                        ? (Block)new BlockSolid()
                        : (Block)new BlockEmpty();
                    SetBlock(x, y, z, block);
                }
            }
        }
    }

    void Start()
    {
        meshFilter = GetComponent<MeshFilter>();
    }

    void Update()
    {
        if (update)
        {
            update = false;
            UpdateChunk();
        }
    }

    void UpdateChunk()
    {
        MeshData meshData = new MeshData();
        for (int x = 0; x < chunkSize; x++)
        {
            for (int y = 0; y < chunkSize; y++)
            {
                for (int z = 0; z < chunkSize; z++)
                {
                    blocks[x, y, z].Render(this, meshData, x, y, z);
                }
            }
        }
        RenderMesh(meshData);
    }

    void RenderMesh(MeshData meshData)
    {
        meshFilter.mesh.Clear();
        meshFilter.mesh.vertices = meshData.vertices.ToArray();
        meshFilter.mesh.triangles = meshData.triangles.ToArray();
        meshFilter.mesh.RecalculateNormals();
    }

    public Block GetBlock(int x, int y, int z)
    {
        if (x >= 0 && x < chunkSize &&
            y >= 0 && y < chunkSize &&
            z >= 0 && z < chunkSize)
        {
            return blocks[x, y, z];
        }
        else
        {
            return null;
        }
    }

    public void SetBlock(int x, int y, int z, Block block)
    {
        blocks[x, y, z] = block;
    }
}

Block

This class holds base functionality for a block, including the directional checks to ensure that only visible faces are rendered.

using System.Collections.Generic;
using UnityEngine;

public class Block
{
    public enum Direction { North, East, South, West, Up, Down };
    public Dictionary<Direction, Vector3> directionVectors;

    public Block()
    {
        directionVectors = new Dictionary<Direction, Vector3>() {
            { Direction.Up, new Vector3(0, 1, 0) },
            { Direction.Down, new Vector3(0, -1, 0) },
            { Direction.North, new Vector3(0, 0, 1) },
            { Direction.South, new Vector3(0, 0, -1) },
            { Direction.East, new Vector3(1, 0, 0) },
            { Direction.West, new Vector3(-1, 0, 0) }
        };
    }

    public virtual bool IsSolid(Direction direction)
    {
        return true;
    }

    public virtual void Render(Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        foreach (var directionVector in directionVectors)
        {
            Vector3 vector = directionVector.Value;
            Block block = chunk.GetBlock(x + (int)vector.x, y + (int)vector.y, z + (int)vector.z);

            if (block == null || !block.IsSolid(directionVector.Key))
            {
                RenderFace(directionVector.Key, chunk, meshData, x, y, z);
            }
        }
    }

    protected virtual void RenderFace(Direction direction, Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        switch (direction)
        {
            case Direction.Up:
                meshData.AddVertex(new Vector3(x, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z));
                meshData.AddVertex(new Vector3(x, y + 1, z));
                break;
            case Direction.Down:
                meshData.AddVertex(new Vector3(x, y, z));
                meshData.AddVertex(new Vector3(x + 1, y, z));
                meshData.AddVertex(new Vector3(x + 1, y, z + 1));
                meshData.AddVertex(new Vector3(x, y, z + 1));
                break;
            case Direction.North:
                meshData.AddVertex(new Vector3(x + 1, y, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x, y, z + 1));
                break;
            case Direction.South:
                meshData.AddVertex(new Vector3(x, y, z));
                meshData.AddVertex(new Vector3(x, y + 1, z));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z));
                meshData.AddVertex(new Vector3(x + 1, y, z));
                break;
            case Direction.East:
                meshData.AddVertex(new Vector3(x + 1, y, z));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z));
                meshData.AddVertex(new Vector3(x + 1, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x + 1, y, z + 1));
                break;
            case Direction.West:
                meshData.AddVertex(new Vector3(x, y, z + 1));
                meshData.AddVertex(new Vector3(x, y + 1, z + 1));
                meshData.AddVertex(new Vector3(x, y + 1, z));
                meshData.AddVertex(new Vector3(x, y, z));
                break;
        }
        meshData.AddQuadTriangles();
    }
}

BlockSolid

This class wants to be seen.

public class BlockSolid : Block
{
    public BlockSolid() : base() { }

    public override void Render(Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        base.Render(chunk, meshData, x, y, z);
    }

    public override bool IsSolid(Block.Direction direction)
    {
        return true;
    }
}

BlockEmpty

This class does not wish to be seen.

public class BlockEmpty : Block
{
    public BlockEmpty() : base() { }

    public override void Render(Chunk chunk, MeshData meshData, int x, int y, int z)
    {
        // render nothing
    }

    public override bool IsSolid(Block.Direction direction)
    {
        return false;
    }
}

MeshData

This class holds triangle and vertex data, and offers a small helper method too.

using System.Collections.Generic;
using UnityEngine;

public class MeshData
{
    public List<Vector3> vertices = new List<Vector3>();
    public List<int> triangles = new List<int>();

    public MeshData() { }

    public void AddVertex(Vector3 vertex)
    {
        vertices.Add(vertex);
    }

    public void AddTriangle(int triangle)
    {
        triangles.Add(triangle);
    }

    public void AddQuadTriangles()
    {
        triangles.Add(vertices.Count - 4);
        triangles.Add(vertices.Count - 3);
        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 4);
        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 1);
    }
}

Summary

This basic prototype shows an easy way to render a large number of voxels with a single mesh, reducing the load on the GPU. This could be expanded with a further parent class which could allow chunk/block transversal for multiple chunks.

This post is from the First Era of Plump Helmet.

This was before the great break of 2019-2021 and the predominant project of the time was Feudalia. The project was placed into cold storage and has not been revived yet. The dream remains alive however. These posts have been kept for posterity.

Eight Triangle Quad

Posted by Adam on 18 December 2017 at 12:04 pm

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class SuperQuad : MonoBehaviour
{
    Mesh mesh;
    Vector3[] vertices;
    int[] triangles;

    void Awake()
    {
        mesh = GetComponent<MeshFilter>().mesh;
        vertices = new Vector3[] {
            new Vector3(0, 0, 0),
            new Vector3(1, 0, 0),
            new Vector3(2, 0, 0),
            new Vector3(2, 1, 0),
            new Vector3(2, 2, 0),
            new Vector3(1, 2, 0),
            new Vector3(0, 2, 0),
            new Vector3(0, 1, 0),
            new Vector3(1, 1, 0)
        };
        triangles = new int[] {
            0, 8, 1, // bottom left
            0, 7, 8,
            1, 8, 2, // bottom right
            2, 8, 3,
            3, 8, 4, // top right
            8, 5, 4,
            7, 6, 8, // top left
            8, 6, 5
        };
    }

    void Start()
    {
        mesh.Clear();
        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();
    }
}

This post is from the First Era of Plump Helmet.

This was before the great break of 2019-2021 and the predominant project of the time was Feudalia. The project was placed into cold storage and has not been revived yet. The dream remains alive however. These posts have been kept for posterity.

Newer Page 3 of 4 Older