1
\$\begingroup\$

I am working on an infinite procedural terrain generator and at the moment I am trying to generate a proper heightmap from a biomes map.

I'm not sure what is the best way in creating a proper biomes map, but I decided to use a cellular noise.

enter image description here

I have setup a simple biomes by providing a threshold and a noise generator for each of the biome.

When I generate a heightmap, I sample a biomes map and I get a biomes index by using a sampled value. Once I retrieve an index, I use a biome sample function to get a noise value at the position.

Currently this is the result I am getting

enter image description here

Which is expected, however I am currently struggling in blending these biomes seamlessly together. Also at the moment the biomes map defines these sharp regions, but instead I would like to make it more organic. I tried to blend the sampled cellular noise with a perlin noise, however it resulted in the same map.

I use this library in my code to generate noise maps: https://github.com/Auburn/FastNoiseLite/tree/master/CSharp

This is the whole code:

using System;
using UnityEngine;
using UnityEngine.UI;

public class BiomesMap : MonoBehaviour
{
    [SerializeField] RawImage output;

    const int size = 512;

    [SerializeField] int seed = 5;

    [SerializeField] Biome[] biomes;

    void Start()
    {
        for (int i = 0; i < biomes.Length; i++)
        {
            biomes[i].InitNoise(seed);
        }
        GenerateMap();
    }

    float[,] GenerateBiomesMap()
    {
        var noise = new FastNoiseLite(seed);

        noise.SetFractalType(FastNoiseLite.FractalType.None);
        noise.SetDomainWarpType(FastNoiseLite.DomainWarpType.BasicGrid);
        noise.SetFrequency(0.005f);
        noise.SetCellularJitter(1f);

        noise.SetNoiseType(FastNoiseLite.NoiseType.Cellular);
        noise.SetCellularDistanceFunction(FastNoiseLite.CellularDistanceFunction.EuclideanSq);
        noise.SetCellularReturnType(FastNoiseLite.CellularReturnType.CellValue);

        float[,] map = new float[size, size];

        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                map[x, y] = (noise.GetNoise(x, y) + 1) * .5f;
            }
        }
        return map;
    }

    void GenerateMap()
    {
        float[,] biomesMap  = GenerateBiomesMap();

        float[,] heightmap = new float[size, size];
        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                var value = biomesMap[x, y];
                int biomesIndex = GetBiomeIndex(value);

                if (biomesIndex == -1) { Debug.LogErrorFormat("Biome not found. {0}", value); return; }

                heightmap[x, y] = biomes[biomesIndex].Sample(x, y);
            }
        }
        Output(heightmap);
    }

    int GetBiomeIndex(float value)
    {
        for (int i = 0; i < biomes.Length; i++)
        {
            if (value <= biomes[i].threshold)
            {
                return i;
            }
        }
        return -1;
    }

    void Output(float[,] heightmap)
    {
        Color[] pixels = new Color[size * size];

        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                float value = heightmap[x, y];

                pixels[x + (y * size)] = new Color(value, value, value, 1);
            }
        }
        Texture2D result = new Texture2D(size, size);
        result.SetPixels(pixels);
        result.Apply();

        output.texture = result;
    }
}
[Serializable()]
public class Biome
{
    public string name;

    [Range(0, 1)]
    public float threshold;
    public NoiseGenerator noiseParameters = new NoiseGenerator();

    FastNoiseLite noise;

    public void InitNoise(int seed)
    {
        noise = new FastNoiseLite();

        noise.SetSeed(seed);
        noise.SetNoiseType(noiseParameters.noiseType);
        noise.SetFrequency(noiseParameters.frequency);

        noise.SetFractalType(noiseParameters.fractalType);
        if (noiseParameters.fractalType != FastNoiseLite.FractalType.None)
        {
            noise.SetFractalOctaves(noiseParameters.octaves);
            noise.SetFractalLacunarity(noiseParameters.lacunarity);
            noise.SetFractalGain(noiseParameters.gain);
            noise.SetFractalWeightedStrength(noiseParameters.weightedStrength);
            noise.SetFractalPingPongStrength(noiseParameters.pingPongStrength);
        }
        if (noiseParameters.noiseType == FastNoiseLite.NoiseType.Cellular)
        {
            noise.SetCellularDistanceFunction(noiseParameters.distanceFunction);
            noise.SetCellularReturnType(noiseParameters.cellularReturn);
            noise.SetCellularJitter(noiseParameters.cellularJitter);
        }
        noise.SetDomainWarpType(noiseParameters.domainWarpType);
        noise.SetDomainWarpAmp(noiseParameters.amplitude);
    }
    public float Sample(float x, float y)
    {
        return (noise.GetNoise(x, y) + 1) * .5f;
    }
}
[Serializable()]
public class NoiseGenerator
{
    [Header("General")]
    public FastNoiseLite.NoiseType noiseType;
    public float frequency;

    [Header("Fractal Noise")]
    public FastNoiseLite.FractalType fractalType;
    [Range(1, 10)]
    public int octaves;
    public float lacunarity;
    public float gain;
    public float weightedStrength;
    public float pingPongStrength;

    [Header("Cellular Noise")]
    public FastNoiseLite.CellularDistanceFunction distanceFunction;
    public FastNoiseLite.CellularReturnType cellularReturn;
    public float cellularJitter;

    [Header("Domain Wrap")]
    public FastNoiseLite.DomainWarpType domainWarpType;
    public float amplitude;
}
\$\endgroup\$
6
  • \$\begingroup\$ You may be interested in How to randomly generate biome with Perlin noise and How to make many Perlin noise functions match \$\endgroup\$
    – DMGregory
    Commented Apr 10, 2021 at 0:04
  • \$\begingroup\$ Thank you! I will take a look at these! \$\endgroup\$ Commented Apr 10, 2021 at 0:09
  • \$\begingroup\$ So I need to generate two maps a moisture map and a temperature map? A moisture map determents the water areas and temperature map determents the biome? I have read your entire answer, which is really informative! However, I am struggling understanding how the biome selection should be done by "computing our distance in moisture-temperature space", what does that mean? Also, when I get the biome how do I apply its height properly? \$\endgroup\$ Commented Apr 10, 2021 at 0:34
  • \$\begingroup\$ I show two approaches, one "zoned" like yours, and the other "emergent" involving moisture/elevation. You don't need to use that one. The salient point is that you run your noise function for the biome you're inside, and for the biomes you're next to. Then you can use your distance from each biome cell to interpolate between the different noise output values. \$\endgroup\$
    – DMGregory
    Commented Apr 10, 2021 at 0:37
  • \$\begingroup\$ So I need to find two nearest neighbor points from the current biome point and interpolated the current biome height with each of the two biome noises? Also, I have generated a voronoi diagram before, although I'm not sure how to generate it seamless. I only managed to do it for a fixed size. Is there a way to apply offset and scale multiplier for each cell? So I could get bigger regions. \$\endgroup\$ Commented Apr 10, 2021 at 0:57

1 Answer 1

1
\$\begingroup\$

The problem is blending between borders right? It can be trickier than it sounds. I wrote an article on this exact problem recently. Basically, sample scattered points much like the Voronoi diagram itself uses, except at a higher resolution, and use the data points to generate blending data. Here's the article https://noiseposti.ng/posts/2021-03-13-Fast-Biome-Blending-Without-Squareness.html At the end, there is a link to Java code which should be straightforward to port to C#.

Blending is separate to biome map generation itself. You can go more involved, especially if you want to control which biomes border which. Then you can try temperature and humidity maps, or other methods of generating maps with more complex rulesets. But you don't have to worry about that immediately.

Sidenotes regarding your noise: FastNoiseLite is a great lib, but if you're going to use the Perlin option in it, I would do the following to avoid its characteristic square bias. Take it with a grain of salt when tutorials recommend Perlin noise with no caveats. Perlin is an older noise algo that, without the included 3D domain-rotation technique (SetRotationType3D), produces a lot of squareness that is an entirely unnecessary compromise for most applications.

noise.SetRotationType3D(FastNoiseLite.RotationType3D.ImproveXYPlanes);

then do

public float Sample(float x, float y)
{
    if (perlin -- or value, or value-cubic)
        return (noise.GetNoise(x, y, 0) + 1) * .5f;
    else
        return (noise.GetNoise(x, y) + 1) * .5f;
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .