🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

More Procedural Voxel World Generation

Published January 11, 2011
Advertisement

EDIT2: Fixed. Thanks, Michael Tanczos.
EDIT: Forgive my dumbness, but anyone know how I can get the image attachments to actually appear in the body of the post?

Been awhile. I wanted to follow up on the previous post a little bit, but it's been busy lately; I've been to Mexico for a cruise, and have been spending time back home in Wyoming looking for a job. Time to get out of Arizona, methinks; don't think I can take another summer.

In the last TL;DR post, I talked about using a series of noise functions to generate Minecraftian levels. I got as far as creating the landscape and adding caves. I want to flesh out the system a little bit more, tweak the terrain turbulence function a bit, and I want to demonstrate how the underlying gradient function can be used to generate terrain layers and add features.

I have changed somewhat the method I am using for visualization of the output. Initially, I was using PolyVox to build a mesh from a voxel field, and exporting it as an .OBJ for import into Blender to apply lighting and render. For this post, though, I wrote a script that dumps a voxel field to a mesh file ready to be included in a POVRay scene file instead. The script indexes each voxelt and assigns materials to each block face accordingly. It's a slow process, running as it does in Lua and with no optimizations, but on the whole it has sped up the generation of renders, allowing me to re-render just with the click of a button, rather than the tedious process of importing to Blender as an intermediate step. In order to work with the "new" system, I modified the system to ensure that the output of the final function is in the appropriate range. Specifically, a voxel field maps a module by truncating the output of the module to unsigned integer and casting to 8-bit unsigned char, so the output needs to be in the range of [0,255]. This holds with most Minecraft-type clones, as most will use an unsigned char to denote block types.

For the purposes of this discussion, I am interested only in terrain formation. Placement of vegetation (trees, grass, etc) or mobs is not an aspect of this. Particularly, adding vegetation tends to be an explicit process, performed by analyzing terrain, exposure to the sky, proximity of water, etc... and not an implicit process, able to be expressed as a simple function.

Building the Ground Layer Functions

The types of ground I am going to model are: dirt, standard stone, semi-rare stone, rare stone, and bedrock. The ground strata are arranged such that the very bottom layer is bedrock which can not be dug, the very top layer is dirt, and the strata of stone is a conglomerate of stone, semi-rare and rare such that the deeper one goes the higher the likelihood of encountering rare stone. Semi-rare appears at all elevation strata, and can sometimes even be found at the surface, but is more rare than standard stone; rare stone is encountered only at depths just above the bedrock. Bedrock acts as a basement layer, unaffected by turbulence in order to protect it and ensure that no openings into the void are allowed.

I am going to build each piece up modularly, combining the results into a whole. The end goal is a function that will tell me the type of block in a gradient ranging from open, to dirt, to stone, to bedrock, with a distribution of rare and semi-rare stone in between. The trick, when faced with something like this, is to break it down into smaller tasks. In particular, the tasks we need to model are:

1) The distribution of rare stone vs. other types of stone
2) The distribution of semi-rare stone vs. normal stone
3) The distribution of dirt vs. stone
4) The distribution of ground vs. open
5) The distribution of bedrock vs. everything else
6) Re-integration of ground shape and caves as outlined in the previous journal entry

Before we begin knocking out these tasks, let's lay down some ground work. First, we'll set up some constants representing the different block types we can use:


minecraftlevel2=
{
{name="Open", type="constant", constant=1},
{name="Dirt", type="constant", constant=2},
{name="Stone", type="constant", constant=3},
{name="SemiRare", type="constant", constant=4},
{name="Rare", type="constant", constant=5},
{name="Bedrock", type="constant", constant=6},

{name="Constant1", type="constant", constant=1},
{name="Constant0", type="constant", constant=0},

Let's also set up a gradient. We're going to set it up just slightly differently than we did before, taking into account the new system. By default, a voxel field is mapped from a (1x1x1) sized area of the function, starting at a specified point. So we'll set up the gradient to range from 0.5 to 0, and we'll remap the output range of this function to [0,1]:


{name="MainGradient", type="gradient", y1=0, y2=0.5},
{name="MainGradientRemap", type="scaleoffset", source="MainGradient", scale=0.5, offset=0.5},

The result of this is a gradient function that outputs 0 for any location where Y>0.5, 1 for any location where Y<0, and a gradient scale in between those points. When mapping to a voxel field this means that the top half of the voxel cube will be open, and the bottom half will be ground, and establishes the ground plane for us to work with.

And now, let's first tackle the distribution of semi-rare vs. normal stone, since this is pretty simple.


{name="SemiRareFBM", type="fractal", fractal_type="FBM", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=4, frequency=2},
{name="SemiRareFBMRemap", type="scaleoffset", source="SemiRareFBM", scale=0.5, offset=0.5},
{name="SemiRareSelect", type="select", main_source="SemiRareFBMRemap", low_source="Stone", high_source="SemiRare", threshold=SEMIRARE_DENSITY, falloff=0},

This little bit just sets up an FBM fractal and uses it to select between either Stone or SemiRare. Due to the remapping function, the output of the fractal is (roughly) in the range [0,1] and the amount of SemiRare stone distributed is adjustable via the SEMIRARE_DENSITY parameter.

stonesemirare.jpg

Next, let's model the distribution of rare stone. Scattering rare stone is similar to scattering semi-rare, except since rare only occurs at depth we need to modify the distribution FBM fractal by multiplying it with the main gradient. We can throw in a few parameters to help make this process adjustable. Note, of course, that as whenever you are working with fractals, you have to expect some experimentation in the selection of parameter values. Here is the code to set up a distribution for rare stone:


{name="RareFBM", type="fractal", fractal_type="FBM", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=3, frequency=3},
{name="RareFBMRemap", type="scaleoffset", source="RareFBM", scale=0.5, offset=0.5},
{name="RareFBMScale", type="scaleoffset", source="RareFBMRemap", scale=RARE_GRADIENT_SCALE, offset=0},
{name="RareMult", type="combiner", combiner_type="MULTIPLY", source_0="RareFBMScale", source_1="MainGradientRemap"},
{name="RareMultScale", type="scaleoffset", source="RareMult", scale=RARE_DENSITY, offset=0},
{name="RareSelect", type="select", main_source="RareMultScale", low_source="SemiRareSelect", high_source="Rare", threshold=0.5, falloff=0},

We set up a fractal FBM as with semi-rare, do some finagling of it to remap the output, then multiply it by our main gradient. The output of this we use as a selector to choose between either the stone/semi-rare hybrid function above, or Rare stone. The result looks somewhat like this (image on left is with stone/semi-rare omitted):
rare.jpg

allstones.jpg

As you can see, the deposits of Rare cluster near the bottom of the cube.

The next step is to differentiate stone from dirt and ground from open. We accomplish this by using a chain of selectors:

 


{name="DirtStoneSelect", type="select", main_source="MainGradientRemap", low_source="Dirt", high_source="RareSelect", threshold=DIRT_THRESHOLD, falloff=0},
{name="GroundSelect", type="select", main_source="MainGradientRemap", low_source="Open", high_source="DirtStoneSelect", threshold=0.000001, falloff=0},

The final output of this module chain shows us our layers of ground:
groundlayers.jpg

Applying Ground Shape

Now that we have a function to define ground layers, we can re-integrate ground deformation using a Y-axis turbulence function and a noise source. As described in the previous journal post, the ground shape function is a 3D function that acts as a Y-axis turbulence source, "pushing" each point in the voxel volume either toward or away from the threshold line, having the effect of sort of whipping the surface of the ground into a froth. At low frequencies and low turbulence strength values, this method results in fairly interesting rolling terrain, but as turbulence power is increased (to create taller mountains, for instance), or as the frequency or number of octaves is changed, the terrain can grow increasingly unrealistic and convoluted. This might not always be desirable, so in order to bring this under control I am going to add another module to scale the Y-coordinate component of the input to the GroundShape function by an arbitrary amount. As this amount approaches 0, the function acts more and more like a "conventional" heightmap function, perturbing the ground up and down as an entire column, rather than perturbing each voxel by its own amount.
 


{name="GroundShape", type="fractal", fractal_type="FBM", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=4, frequency=2},
{name="GroundYScale", type="scaledomain", source="GroundShape", scaley=GROUND_YSCALE},
{name="GroundTurb", type="turbulence", main_source="GroundSelect", y_axis_source="GroundYScale", y_power=0.5},

This uses just a simple FBM source for turbulence as before, with an adjustable Y-scaling parameter to modify how frothy the ground surface can be. Here is a sequence of images that show how adjusting Y-scale alters the behavior of the turbulence so that it approaches that of a "standard" heightmap.


groundturb10.jpg

groundturb7.jpg

groundturb4.jpg

groundturb0.jpg

As you can see, by tweaking the Y-scale you can bring unruly functions somewhat under control. It is even possible, using additional functions and multiple ground shape sources combined via Select or Blend functions, to generate non-homogenous terrain functions that can vary from smoothly rolling plains to wildly convoluted "badlands" terrain with a high turbulence and high Y-scale.

Bringing Back the Caves

And, of course, we can bring back the caves.


{name="CaveShape1", type="fractal", fractal_type="RIDGEDMULTI", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=1, frequency=2},
{name="CaveBase1", type="select", main_source="CaveShape1", low_source="Constant0", high_source="Constant1", threshold=1-CAVE_SIZE, falloff=0},

{name="CaveShape2", type="fractal", fractal_type="RIDGEDMULTI", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=1, frequency=2, seed=1323},
{name="CaveBase2", type="select", main_source="CaveShape2", low_source="Constant0", high_source="Constant1", threshold=1-CAVE_SIZE, falloff=0},
{name="CaveMult", type="combiner", combiner_type="MULTIPLY", source_0="CaveBase1", source_1="CaveBase2"},

{name="CaveTurbX", type="fractal", fractal_type="FBM", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=3, frequency=3, seed=1001},
{name="CaveTurbY", type="fractal", fractal_type="FBM", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=3, frequency=3, seed=1201},
{name="CaveTurbZ", type="fractal", fractal_type="FBM", basis_type="GRADIENT", interp_type="QUINTIC", num_octaves=3, frequency=3, seed=1301},
{name="CaveTurb", type="turbulence", main_source="CaveMult", x_axis_source="CaveTurbX", y_axis_source="CaveTurbY", z_axis_source="CaveTurbZ", x_power=0.25, y_power=0.25, z_power=0.25},

{name="CaveInvert", type="scaleoffset", source="CaveTurb", scale=-1, offset=1},
{name="CaveSelect", type="select", main_source="CaveInvert", low_source="Open", high_source="GroundTurb", threshold=0.5, falloff=0},


The final stage is to perform a select using the main gradient in order to add a thin layer of bedrock at the very bottom of the world:

 


{name="BedrockSelect", type="select", main_source="MainGradientRemap", low_source="CaveSelect", high_source="Bedrock", threshold=BEDROCK_THRESHOLD, falloff=0},

This ensures that nobody can dig down to the abyss.

Here are a few samples for your perusal:
final1.jpg

final2.jpg

final3.jpg

 
4 likes 2 comments

Comments

Michael Tanczos
So yeah, my bad.. I screwed up in configuring the software right so attachments weren't working. Hopefully they should work fine for you from here on out. mea culpa
January 11, 2011 05:41 AM
JTippetts
Oh, heh heh. Cool, thanks. And ignore my response to your journal entry. :D
January 11, 2011 05:45 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement