A Noisy World — Obsidian Server’s Minecraft-Like World Generation Pt. 3

Actually, scratch that

The Problem

Overlaying noise maps to create biomes has a fatal flaw. The curviness of the individual noise maps means that some biomes will come to a point or otherwise end in weird, unnatural shapes, like so:

Noise map biomes :(
Voronoi Noise (credit www.ronja-tutorials.com)
  • Beaches should be on the border of an ocean
  • Deep oceans should be surrounded on all sides by normal oceans
  • Mooshroom islands should be isolated in a ocean
  • Some variants are rare
  • If a swamp borders a jungle, a jungle edge will generate.
  • If a swamp borders a desert, snowy taiga, or snowy tundra, a plains biome generates.

Extending SharpNoise

It’s clear at this point that I need to write a custom cell noise algorithm, one in which I can apply all of these rules. Fortunately SharpNoise was very easy to extend, and creating custom cell noise is trivial.

internal struct VoronoiCell
{
public (int x, int z) Index { get; set; }
public (double x, double z) Point { get; set; }
public double DistanceToPoint { get; set; }
public BaseBiome BaseBiome { get; set; }
public int Variant { get; set; }
public Biomes Biome { get; set; }
}
Span<VoronoiCell> cells = stackalloc VoronoiCell[25];
int index = 0;

for (var zCur = zSample - 2; zCur <= zSample + 2; zSample++)
{
for (var xCur = xSample - 2; xCur <= xSample + 2; xSample++)
{
var xPos = NoiseGenerator.ValueNoise3D(xCur, 0, zCur, Seed);
xPos += xCur;
var zPos = NoiseGenerator.ValueNoise3D(xCur, 0, zCur, Seed);
zPos += zCur;
var xDist = xPos - x;
var zDist = zPos - z;
double dist = xDist * xDist + zDist * zDist; var cell = new VoronoiCell
{
Index = (xint, zint),
Point = (xPos, zPos),
DistanceToPoint = dist
};

cells[index++] = cell;
}
}
MemoryExtensions.Sort(cells, (a, b) => 
{
return a.DistanceToPoint > b.DistanceToPoint ? 1 : -1;
});
var meVal = NoiseGenerator.ValueNoise3D(
(int)Math.Floor(cells[0].Point.x),
0,
(int)Math.Floor(cells[0].Point.z));

Cell Noise to Biomes

Time to talk Biomes. First order of business is to get the overall land/to sea ratio correct. The noise generator gives us values between -1 and 1, and an easy way to break land and sea up logically is to think of any negative value as sea, and positive value as land. For Obsidian, I decided that I want far more land than sea. By shifting the entire noisemap up by 0.5, then scaling appropriately, I get this affect.

private (BaseBiome, int) GetBaseBiome(double noise)
{
// Shift the whole map up by 1/2 for more land than sea.
noise += 0.5;
// Scale land (now 0 => 1.5) back down to 0 => 1
// Scale sea (now -0.5 => 0) back to -1 => 0
noise = noise > 0 ? noise * 0.667 : noise * 2.0;
}
if (noise < 0)
{
noise *= 4.0; // 4 ocean types
return ((BaseBiome)Math.Floor(noise), 0);
}
// Basically, the first 2 decimals becomes the variant.
// So variant will be b/w 0 => 99
int variant = (int)(noise * 1000.0) % 100;
// 4 base overworld types but we want the ratio to be
// 2 parts medium, 2 parts cold, 1 part frozen, 1 part dry
noise *= 6.0;
int val = (int)noise;
return val switch
{
// 18% chance for a rare medium biome.
0 or 1 => variant <= 18 ? (BaseBiome.MediumRare, variant) : (BaseBiome.Medium, variant),
// 15% chance for a rare cold biome.
2 or 3 => variant <= 15 ? (BaseBiome.ColdRare, variant) : (BaseBiome.Cold, variant),
// There are no frozen rare biomes.
4 => (BaseBiome.Frozen, variant),
// 10% chance for a rare dry biome.
5 => variant <= 10 ? (BaseBiome.DryRare, variant) : (BaseBiome.Dry, variant),
_ => (BaseBiome.Medium, variant),
};

This was a lot…

and this article is already pretty long. In the next one, I’ll talk about implementing rules for biomes.

--

--

Software Engineer and Nerd

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store