
Generating floating worlds is by contrast to 2D worlds incredibly different. We will still need to use heightmaps in some places, but the actual terrain body is made up of 3D perlin noise. If you’re familiar, usually you see perlin noise as a two dimensional image by which you create a heightmap. In this case, we have a 3D perlin noise as a density map. This is a little bit hard to explain but areas with a higher values at their XYZ coordinates become “solid” and those with lower values at their XYZ coordinates are air.
This can actually be applied as a negative mask to a 2D world to generate caves. This is in part how the amazing caves from the newer Minecraft updates work (1.18+). Classical style caves are made with Perlin worms cutting oblate spheroids into the world, as explained in the previous article about Perlin worms.
for(int y = 0; y < map->height; y++) {
for (int z = 0; z < map->width; z++) {
for (int x = 0; x < map->length; x++) {
uint32_t index = (y * map->length * map->width) + (z * map->width) + x;
densityMap[index] = (noise3d(x, y, z) + 1.0f) / 2.0f;
if(densityMap[index] > 0.67f) {
SetBlockInMap(map, x, y, z, 1);
}
}
}
}
This basic function iterates over every block and generates a density value. If this is above our threshold (0.67) then it is solid. Otherwise, it is empty. This is then reflected by placing stone into the map. Next, we’re going to generate TWO heightmaps. One is for the bottom, and another is for the top.
for (int z = 0; z < map->width; z++) {
for (int x = 0; x < map->length; x++) {
for(int y = map->height - 1; y >= 0; y--) {
uint8_t blk = GetBlockFromMap(map, x, y, z);
if(blk != 0){
heightMap[x + z * map->length] = y;
break;
}
}
}
}
for (int z = 0; z < map->width; z++) {
for (int x = 0; x < map->length; x++) {
for(int y = 0; y < map->height; y++) {
uint8_t blk = GetBlockFromMap(map, x, y, z);
if(blk != 0){
heightMap2[x + z * map->length] = y;
break;
}
}
}
}
The first heightmap marches from the top of the map down to the bottom to generate a “traditional” heightmap view. This will be used for generating stuff like plants, strata, and the surface. The second one is from the bottom up and finds the lowest values of the islands. This is used in strata generation in order to limit some generation artifacts that occur otherwise.
create_strata2(map, heightMap, heightMap2);
create_surface(map, heightMap);
create_ores(map);
create_plants(map, heightMap, 1);
So now, this code generates the strata in a slightly different way to the original strata function outlined. The create surface, ores, and plants all happily work in this code though!
void create_strata2(LevelMap* map, const int16_t* heightmap, const int16_t* heightmap2) {
for(uint16_t x = 0; x < map->length; x++){
for(uint16_t z = 0; z < map->width; z++){
float dirt_thickness = octave_noise(8, x, z, 0) / 24.0f - 4.0f;
int dirt_transition = heightmap[x + z * map->length];
if(dirt_transition >= 63 || dirt_transition <= 0)
continue;
int stone_transition = dirt_transition + dirt_thickness;
int start = heightmap2[x + z * map->length];
for (int y = dirt_transition; y >= start; y--) {
int block_type = 0;
if (y <= stone_transition) {
block_type = 1;
} else if (y <= dirt_transition) {
block_type = 3;
}
if(GetBlockFromMap(map, x, y, z) == 0)
break;
SetBlockInMap(map, x, y, z, block_type);
}
}
}
}
The create strata method is modified here to use the starting point and avoid iterating across empty space. The main highlight here is also the air check present in this method. The air check prevents us from continuing to the bottom of the void which can occur and generates UGLY islands.
Everything else is basically the same as the regular create_strata methods.
Remember to free your variables, and you’re set! You now have floating islands and actually a method to carve caves into the map too!