THREE.js: minecraft in a weekend

I have been enjoying exploring the ideas behind procedural worlds and in a previous post even showed a simple voxel renderer based in THREE.js which would load chunks when the camera got close enough to the edge.

That was based on the great tutorial here which explains how to create a voxel-based mesh in THREE.js such that it doesn’t use all of the machine’s resources for the smallest of worlds. I found that to be an invaluable source of information and continually referenced back to that throughout the weekend.

Looking back at my git logs for this project to be able to type this up, I have no intention of this post being a tutorial in itself, my progress was as follows:

  • Walking around rather than observer view
  • Sideways collision detection - not walking through the map
  • Changing to a system where there was a sensible grass - sand - water height system
  • Adding trees
  • Another optimisation for terrain generation in addition to the aforementioned blog
  • Block removal (From the origin chunk)
  • Block removal (all chunks)
  • 3D chunking

Walking around rather than observer view

In the post which I follow the camera is in an observer view. That is to say that the camera rotates around some point, which holding down other keys you can drag the world around.

What I wanted was a more Minecraft mouse-capture wasd walk around system.

It turns out that this exists as a demo in the THREE.js archives!

You may be able to see from the screenshot that the ground plane from that demo still exists under where terrain has not yet generated.

For some reason I had to start with the demo and put the code I had working elsewhere into it rather than put code from the demo into my existing code.

I never understand why this works but I assume it just gets rid of a load of possible things that could have gone wrong.

Sideways collision detection

One thing that wasn’t included in the demo was sideways collision detection. This is something that should have been easy!

Unfortunately as was a bit of a theme of the weekend coordinate transformations are not as simple as they might seem. Particularly when I’m being caught out with byref/byval in javascript.

The coordinates from the demo I used are with respect to the orientation of the camera, along with the velocity elements implemented in that demo, it made finding if there was a positive velocity in the direction of an adjacent cube very challenging.

My solution was to rewrite the velocities to be with respect to the world orientation rather than the camera and things eventually fell into place.

Changing the system to a sensible grass/water

This wasn’t too challenging. No complex coordinate transformations to deal with at least.

Changing the generation rule such that bellow a value there was a water block, at a particular value there was a sand block, above that a grass block.

I ended up making this a separate module at this time, rather than the if statement it is in the original demo. That helped a lot when adding cactus and trees.

I had to update the rule for the faces from the original demo since the presence of a water block meant the land edge wasn’t rendered. Changing this such that adjacent water blocks don’t show a face but a change water-non-water does, worked well. There are some issues with the edges of chunks being more visible in water but it isn’t worth dealing with in an alpha.

Adding trees

Having already moved the terrain definition code to it’s own module. I wrote a function that was a little `random` (x*y)%10==7 to define where the trees should go. The generator then adds their blocks in the relevant positions and of the relevant types.

For the cactus, I had the rule that it had to be placed on the sand.

This did lead to some issues if a tree was generated with its trunk on one block but the leaves obtruding into another however this was later resolved when I moved the generation to a world level rather than a chunk level.

Another optimisation

To be able to have the world larger I needed another optimisation. Loading each new chunk as the camera moved about was causing the whole thing to hang for too long. A longer term solution for this would be to move the terrain generation into a second thread using a webworker however I could see a simple method.

In the implementation outlined by the base blog post the type of each block was set when the world was created. This meant that every block in the chunk had to be cycled through.

Now at this point in my code there was no adding or removing of the blocks and so setting ground blocks bellow the surface seemed wasteful.

We knew the height of each surface block so rather than looking through x,y and z we could just loop through x and z picking the y from the terrain height function.

This made it far quicker, particularly because I was setting the y (height) of the chunk to 256 rather than the 16 that I was using for x and z.

I was doing this because although I was chunking in x and z I wasn’t in y.

So while I wasn’t chunking in y this sped up the loading of a chunk about 200 times.

There were some issues though, rendering only the top block of the terrain means that there are holes in the side of cliffs, or wherever there is more than a single block drop in height.

This was easily solved by for each square of the surface, looking at the height for the 4 adjacent squares and loading in blocks enough so that there would be no holes in the sides of cliffs.

Block removal (From the origin block)

It is at this point (After much ecstatic running about in the infinite world I had generated) that I felt it was time to add block removal.

I thought that this was something else that could simply be passed through from the original tutorial.

The first issue I ran into was in trying to gauge the direction of the camera. This is something I had originally thought would be simple. This is a classic case of the time taken to develop something is often completely devoid of the estimate. `Solving`, it was more of a workaround, this issue took much of my Saturday and caused a ridiculous amount of frustration.

The issue comes from the unproject function from the camera element of THREE.js. The coordinate frame of the camera the vector 0,0,-1 should be a single unit in front of the camera. Therefore applying an unproject call to this simple vector should yield the position, in the coordinate frame of the world, of a point one unit in front of the camera.

This never worked for me.

Instead, I had to project a ray whose direction was set by the camera object and then retrieve the direction vector from that ray and pass that to the block ray algorithm.

Not the best solution but it works.

Block removal (All chunks)

One of the issues I had after getting the direction correct was getting the block ray traversal to progress through voxels. This I solved by having the algorithm run at the world level rather than the chunk level. This seemed to still have the issue that when the ray went above y=16 it would fail.

That led me to move to a 3D chunking model, something I had wanted eventually anyway.

3D chunking

Adapting the code so as to add chunking in the y direction was mainly just changes extending the code that I had used in 2D and adding another component to the vectors. There was one element that was different however, that was if the terrain should exist in the chunk at all.

For that, I needed to do some adjustments to the terrain generation algorithm to take into account the y component adjustment. This does, however, mean that any y chunk will look to see if it is the surface. I’m not sure what the effect of this will be when other features are added but if this is a constant addition, that is to say, O(1) then I’m happy to keep it in.

I’m not sure if it was at this point or earlier that I realised that in javascript -1%2 === -1

In javascript like many other languages, the percent symbol is representative of the modulus operator. However, the gotcha that I fell foul of was that in javascript the modulus operator preserves sign. 


One of the things that I don't like about code demos written in languages other than javascript is that you can never play with them in the post in which they are presented. To that end here is the current link to a demo.


Popular posts from this blog

An exploration in number systems

Structural engineering with cardboard

The twelve fold way