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

Jon Proietti
7 min readJun 18, 2021

Before digging in, what is Obsidian Server?

Obsidian is an open source software implementation of a Minecraft Server in C# .NET. You can connect to an Obsidian Server using any Minecraft Client (at least, that’s the objective). At the time of writing, only the Java client is supported.

I’m one of the project’s developers and my focus is mostly on world generation. Check out the project on GitHub if you’re interested: https://github.com/ObsidianMC/Obsidian, or come hang out with us on Discord.

DISCLAIMER: None of Mojang’s source code and algorithms are used in the Obsidian project. Obsidian is a good-faith recreation of (frankly, a love letter to) the server-client protocol and gameplay mechanics as published here: https://wiki.vg/Main_Page. The contents of this article do not describe Mojang’s implementation of world generation.

I’ll be talking a lot about noise functions but not in-depth. There are several good articles about noise functions and practical applications in video games. This article assumes a foundational understanding of noise functions.

For Obsidian, I use the wonderfully written SharpNoise library for C#. Check it out on GitHub: https://github.com/rthome/SharpNoise. The complex planet example (which itself is based on a C++ library implementation called libnoise) was paramount in helping me understand how to use the various SharpNoise modules and honestly is how I learned about noise.

Lastly, I’ll link to source code in some instances, but Obsidian is under heavy active development and those links could break over time. I’ll try to keep the article up-to-date with any major refactoring.

Okay, intros/disclaimers/attributions out of the way. Time to dive in.

Where to even start?

When you think about Minecraft’s worlds, their intricacy, rivers, biomes, mountains… it can be very overwhelming. Frankly, it is overwhelming and complex. So I’m going to start with the foundational: Land or Ocean. When I need to make a decision in a noise generator, I write a Selector.

The Selector

A Selector is strategy wherein one control noise map will be referenced to determine whether to output values from one of two other input noise maps. So, feed it two input noise functions, and a control noise function, and it will return results from one of the two inputs based on values returned by the control function.

It’s even more powerful than that, though. When a selector transitions from one input function to the other, it can blend the results into each other.

Let’s see this in action. First, the control noise map. In my implementation I call this the LandOceanSelector and it’s just basic Perlin noise.

new Perlin
{
Seed = settings.Seed,
Frequency = 0.125,
Persistence = 0.5,
Lacunarity = 1.508984375,
OctaveCount = 3,
Quality = NoiseQuality.Best,
}

“Basic Perlin Noise he says… What the heck is Lacunarity?”

I hear you. Stay with me. It’s a fancy name for a multiplier that’s applied to the frequency of successive octaves. So this perlin actually runs 3 times (3 octaves). Each time it runs, the frequency is multiplied by the lacunarity value.

So what’s Persistence, then? Persistence is a multiplier applied to each octave’s output. So the first octave is full strength, the second is half, third is quarter, and so on… As the frequency goes up, the magnitude of the output goes down.

The practical result of all of this is that we get “noisier” noise.

Perlin Noise function with 3 octaves
Land/Ocean Perlin Noise Selector Results

A perlin noise function will typically return results between -1 and 1. This would make my Selector 0-based, which is to say Oceans would be negative, land would be positive. For my purposes, I want values between 0 and 1, making it 0.5-based.

Aside: I tend to write all of my selectors as 0.5-based. I get weird results when selectors go negative. However, Terrain noise values are always between -1 and 1, and are 0-based where 0 is sea level.

To do this, I just feed the perlin noise to a ScaleBias, like so:

new ScaleBias
{
Bias = 0.5 + settings.OceanLandRatio,
Scale = 0.5,
Source0 = new Perlin
{
Seed = settings.Seed,
Frequency = 0.5 * 0.25,
Persistence = 0.5,
Lacunarity = 2.508984375,
OctaveCount = 4,
Quality = NoiseQuality.Best,
}
}

The ScaleBias function is pretty simple. The input’s result is multiplied by the Scale, and Bias is added to the result.

You can see that I allow customization here with setttings.OceanLandRatio. By modifying this value upwards, the results will be skewed more positive, resulting in more land mass.

Finally, I want to have the option to customize the size of the continents (and oceans). I can do this by applying a multiplier to the input of the noise function, which has an inverse relationship to the output. If I divide the input values by 10, the size of continents would be 10x larger.

To do this, I can leverage a ScalePoint function. Next, I want to ensure that the values returned are between 0 and 1, so I run it through a Clamp function. Finally, I run it through a Cache which, as the name implies, just stores the value. Should the function be called a second time, we won’t have to redo the math.

Here’s the result:

private Module LandOceanSelector()
{
return new Cache
{
Source0 = new Clamp
{
LowerBound = 0.0,
UpperBound = 1.0,
Source0 = new ScalePoint
{
// Scale horizontally to
// make Continents larger/smaller
XScale = 1 / (settings.ContinentSize * 1.01),
ZScale = 1 / (settings.ContinentSize * 1.01),
// Values are now Ocean < 0.5 < Land
Source0 = new ScaleBias
{
Bias = 0.5 + settings.OceanLandRatio,
Scale = 0.5,
// Actual noise for continents. Ocean < 0 < Land
Source0 = new Perlin
{
Seed = settings.Seed,
Frequency = 0.125,
Persistence = 0.5,
Lacunarity = 1.508984375,
OctaveCount = 3,
Quality = NoiseQuality.Best,
}
}
}
}
};
}

And here’s the resulting noise map, colorized so that it’s easier to see.

Selector Output

Ocean Terrain Noise

So of the 3 noise maps we need to feed the selector (two inputs and one control), we’ve finished with the control map. So, let’s generate some noise for when the value of the selector is less than 0.5, IE: Ocean Terrain.

I won’t go as in-depth into how the noise for the ocean floor is generated, but I will briefly cover the noise functions used.

The primary noise for the Ocean Terrain is the Billow function. Billow noise is very similar to Perlin noise, except that there’s an absolute value applied to the output.

Another function used in the Ocean Terrain is Turbulence. Turbulence modifies the input values randomly to create, well… more turbulent results.

Finally, since this is supposed to be under sea level, I ensure that the output values of this function are less than 0.

You can view the complete noise implementation here: https://github.com/ObsidianMC/Obsidian/blob/master/Obsidian/WorldData/Generators/Overworld/Terrain/OceanTerrain.cs

Ocean Terrain

Land Terrain Noise

I’m not going to lie, the terrain implementation is complicated and well outside the scope of Pt. 1. I will do a deep dive in Pt. 2, though. But for the purposes of this article, and b/c it’ll look cooler, I’m just going feed the selector mountainous terrain.

So let’s look at the mountain terrain implementation. The truth is, I borrowed directly from the SharpNoise Complex Planet example code, almost verbatim, because it’s an awesome implementation.

Check out the implementation here: https://github.com/ObsidianMC/Obsidian/blob/master/Obsidian/WorldData/Generators/Overworld/Terrain/MountainsTerrain.cs

Mountain Terrain

Bringing them together

I’ll just jump straight into the code

private Module MergedLandOcean()
{
return new Cache
{
Source0 = new Select
{
Source0 = ocean.Result,
Source1 = mountains.Result,
Control = LandOceanSelector(),
EdgeFalloff = 0.05,
LowerBound = 0.49,
UpperBound = 2.0
}
};
}

Here we’ve got our two inputs, ocean.Result and mountains.Result and the Selector will decide which one to return by referencing the LandOceanSelector() noise. Recall that I like to have my selectors 0.5-based, so I set the LowerBound to 0.5 (0.49 really…) and the upper bound to 2 (results should be b/w 0 and 1, but I like to play it safe).

Finally, I instruct the Selector to blend the two terrains into each other with an EdgeFalloff value of 0.05. Functionally, this means that the selector will blend the two when the control value is between 0.44 and 0.54:

0.49-0.05=0.44 and 0.49+0.05=0.54

And here’s the result:

Combined Ocean and Mountain Terrain using the LandOceanSelector

Stay tuned for Pt. 2. I’ll take the selector concept and apply it to multiple terrain types to create a more realistic world.

--

--