april 30, 2014

Voxel engine and LibGDX – Randomly created landscapes

Voxel engine and LibGDX – Randomly created landscapes

In the last post we added code so new chunks gets created automatically when we move around but the chunks themselves looks quite boring don’t they?

So how about generating some landscapes?

Perlin noise

Most people that have played Minecraft have probably noticed that every time a new game is created the world looks different and when you move around the generated landscape is totally new.
So how is that possible? Did someone at Mojang sit down and designed all those landscape by hand? Off course not. They are all random generated.

I don’t know if they still use this algorithm but in the early days it used something called Perlin noise which is an advanced algorithm that will take a few parameters as input and returns one single value. Luckily there are open source and public domain implementations of this algorithm and I have used one by a fellow swede named Stefan Gustavson which is an improved version of the original Perlin noise algorithm called Simplex noise.

So if we input something we get random values back? No.. Perlin noise is not random generated. If we input the same values we always get the same value back. So how can that be used to randomly generate a landscape?

When the first chunk is created we create a offsetValue by using Javas builtin Random class and sets a seed on it. The seed can either be randomly generated (for example by using the DateTime of the computer) or setting a predefined seed. And here is the secret to how Minecraft can share terrain seeds. If one user puts in a seed manually he will get the exact same landscape as the user who shared the seed and thats because the random generator in java will always return the same value for every calls to this instance.

if (random == null) {
            random = new Random();
            random.setSeed(Terrain.SEED);
            landscapeRandomOffset = new Vector3((float) random.nextDouble() * 10000, (float) random.nextDouble() * 10000, (float) random.nextDouble() * 10000);
        }

So now we have a Vector3 with some random numbers that will be used as offset when we input the parameters to the noise calculation. I have talked about offsetting.. what are we offsetting? The position of the chunk we are creating. Those will be the same we just add an offset value to them. That means that there is a chance that we will enter the same kind of landscape somewhere if we run around enough. I also have a scale value that I can tweak to modify the input value a bit.

double getNoiseValue(Vector3 pos, Vector3 offset, double scale) {
        double noiseX = Math.abs((double) (pos.x + offset.x) * scale);
        double noiseY = Math.abs((double) (pos.y + offset.y) * scale);
        double noiseZ = Math.abs((double) (pos.z + offset.z) * scale);
 
        return Math.max(0, SimplexNoise.noise(noiseX, noiseY, noiseZ));
    }

So doing like this we will always get the same landscape back for the same seed. Every single time. Atleast as long as we keep the same landscape generation code. Because we can process the values we get back diffrently. And we could also ranom generate a few more of those offsets and use different ones depending on what we are generating. So we can have one offset for mountains, one for lakes and one for caves.

This is how I use Perlin noise generation with our randomly created offset to create the mountains in the screenshot

void calculateChunk(Vector3 baseWorldPosition) {
        Vector3 worldPosOfXYZ = new Vector3();
        for (int x = 0; x < Terrain.WIDTH; x++) {
            for (int y = 0; y < Terrain.HEIGHT; y++) {
                for (int z = 0; z < Terrain.WIDTH; z++) {
                        worldPosOfXYZ.set(x, y, z).add(baseWorldPosition);
                        setBlock(x,y,z,getByteAtWorldPosition(x,y,z,worldPosOfXYZ));
                }
            }
        }
    }
 
public byte getByteAtWorldPosition(int x, int y, int z, Vector3 worldPositionOfXYZ){
        if (y == 0) {
            return 1;
        }
        float maxHeight = Terrain.HEIGHT - 10;
 
        double noise = getNoiseValue(worldPositionOfXYZ, landscapeRandomOffset, 0.009f);
 
        noise *= maxHeight;
 
        if (noise >= y) {
            return 33;
        }
        return 0;
    }

So now we have some landscapes generated. When we fly around we can see how mountains pops up chunk by chunk. If we restart with the same seed we get the same landscape.

Optimzations

I have done one optimization of how many meshes that are created. In our check if there exists a block or not in each direction (and if true we don’t create that side since it will be invisible) we did only check the blocks in the current chunk, we didn’t check over the border into the next one. So if a big mountain is created the inside of that mountain should be completly empty but between the chunk borders the sides of the mountains are still created vasting expensive verticies. And here comes another good thing with that Perlin noise aren’t randomly generated. When we create the blocks on the border we can actually look into the next chunk without actually creating it. All we have to do is ask for the noise value of the position and if the chunk doesn’t exist it doesn’t matter since it will be the same value when it’s finally created.

private byte getByte(int x, int y, int z) {
        if (outsideHeightBounds(y)){
            return 0;
        }
        if (outsideThisChunkBounds(x, z)) {
            Vector3 positionToFind = position.cpy().add(x,y,z);
            Chunk chunk = Terrain.findChunk(positionToFind);
            if (chunk != null){
                return chunk.getByte(x,y,z);
            }else{
                Vector3 worldPosition = position.cpy().add(x, y, z);
                return getByteAtWorldPosition(x,y,z,worldPosition);
            }
        }
        return map[y][x][z];
    }

I have also added a sky color plus the classic OpenGL fogging to make meshes slowly dissapear into the distance.

Git-repo with the source code

Edit 2024: Unfortunately I stopped writing this series after this post 10 years ago and I had three more articles prepared. At least I finished the source code (I think). It's available at:

Part 4 - Lights

Part 5 - Block and optimisations

Part 6 - Sunlight and shadows

and the whole code at LibGDXVoxelEngine