Codementor Events

Naive City Generation

Published Jan 02, 2021Last updated Jun 30, 2021
Naive City Generation

Last week I played with an idea for city generation, I didn’t want to do my regular old 2D view, I actually wanted to challenge myself a little, so I looked towards 3D. I’ve known of a JavaScript library for a while called THREE.js, and I’ve tried to use it before but I never really had an end goal in mind, so I only went through the examples, but this time, there was an end goal, and I feel like the result wasn’t all that bad.

It’s fairly commonly known that to generate a city there’s a lot of ideas and algorithms that can be used, L-Systems and Space Colonisation for roads, Binary Space Partitions for building interiors, every element that you build needs to be generated somehow, and that’s what these algorithms are for. For this little project, I just wanted to get a quick prototype up and running, a proof of concept, if you like.

I decided that to keep it basic, I’d have only three types of ’tiles’, a building tile, a road tile and a grass tile. I also decided that I was only going to render a general city outline, you’d see the roads and the shape/size of the buildings, but you wouldn’t be able to go inside any of the buildings (although this could potentially come later).

Once I had decided on this, it was time to decide how to lay them all out. I essentially generated a 2D array of values, which could be ‘road’, ‘building’ or ‘grass’.

Here’s the code for the grid generation:

var grid = [];
for(var x = 0; x <= 12; x++) {
  grid[x] = [];
  for(var y = 0; y <= 12; y++) {
    if(x % 4 == 0 || y % 4 == 0) {
      grid[x][y] = new Road(x, y);
    } else {
      if(Math.random() > 0.5) {
        grid[x][y] = new Building(x, y, Math.ceil(Math.random() * 3))
      } else { 
        grid[x][y] = new Grass(x, y);
      }
    }
  }
}
return grid;

So, we’re generating 12 arrays with 12 values, this gives us a grid that is 12 blocks by 12 blocks. The logic follows that if x is divisible by 4 or y is divisible by 4, then that block will be a road block. This gives us a nice grid pattern for the roads, with a border around the edge of our ‘city’, and leaves 3 x 3 plots of land for buildings or grass to be placed in between. Whilst this doesn’t necessarily match up to how cities look in real life, it emulates the grid pattern that we see in major cities to a good enough extent for our prototype. The buildings/grass part of the logic is essentially a coin flip, there’s a (roughly) 50/50 chance of the remaining blocks in our city being buildings/grass.

The next step is to actually draw this to the screen. As you can see in the code snippet above, I’ve actually called constructors when assigning elements of the array. I had written a data structure for each type of block. Each block had coordinates in its constructor parameters to decide where the block should be placed, the only difference was the building block. I gave the building a height as well, this would ensure that we would have some different sized buildings, and make it look more realistic. The buildings generated would all be between one and four ‘stories’ high. Having identified the dimensions/position of each block, it was time to create the mesh.

A mesh is something that THREE.js can take and render to the screen. To create a mesh, you need a couple of things, a material and a ‘geometry’. The geometry dictates the shape of the object and the material dictates the way the object looks, what colour it is, or if it has a texture. For all of the blocks, creating these was very straightforward.

Building:

this.geometry = new THREE.BoxGeometry(1, height, 1);
//Coloured a mid-grey
this.material = new THREE.MeshLambertMaterial({ color: 0x888888 });

Road:

this.geometry = new THREE.BoxGeometry(1, 1, 1);
//Coloured a dark-grey
this.material = new THREE.MeshLambertMaterial({ color: 0x333333 });

Grass:

this.geometry = new THREE.BoxGeometry(1, 1, 1);
//Coloured a bright green
this.material = new THREE.MeshLambertMaterial({ color: 0x22FF32 });

Then for each block all that was left to do was create the mesh:

this.mesh = new THREE.Mesh(this.geometry, this.material);

Finally, on the mesh object, I just had to set the position:

this.mesh.position.x = x;
this.mesh.position.y = 0;
this.mesh.position.z = y;

This is somewhat confusing to you probably, and that’s because it’s just straight up bad code. What I’ve done is transferred from a 2D array into a 3D space, meaning that our grid has now been flattened, z becomes y, and now we have to fill y with something. If you imagine that we’re looking at a board that’s standing up straight, with our y axis running up/down the board, and our x axis running horizontally along the board. Imagine now, the board falling over. When the board hits the floor, the y axis no longer runs up/down the board, it’s now the z-axis doing so, the y-axis will now be extending out from the board.

Now that we’ve established all of this, it’s time to actually draw it to the screen. I set up THREE.js by creating a scene and a camera:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
var renderer = new THREE.WebGLRenderer();

Having created these, I needed to set up a renderer. Finally, we just need to set up the position/rotation of the camera, and the renderer size and background colour, and attach it to our document, so that we’ll actually see it.

Camera:

camera.position.x = 4.5;
camera.position.y = 10;
camera.position.z = 20;
camera.rotation.x = -0.5;

Renderer:

renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor( 0xeaeaff, 1 );
document.body.appendChild(renderer.domElement);

I also set up a light:

var light = new THREE.HemisphereLight(0xafafaf, 1);
scene.add(light);

Now all that was left to do was to add the meshes to the scene:

for(var x = 0; x < grid.length; x++) {
  for(var y = 0; y < grid[x].length; y++) {
    var mesh = grid[x][y].mesh;
    scene.add(mesh);
  }
}

And finally, animate it:

function animate() {
  controls.update();
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

animate();

Which gives us a nice little result:

city-2

If you’re following along with the code in this article, you won’t be able to rotate/zoom on the result. To add this functionality, take a look at this orbit controls example.

You can find an interactive example and the full code over here, this should give you another example of the orbit controls in the context of the application.

This is a very naive and quick way of generating something that looks a bit like a city. It’s not very advanced at all. I’ll be expanding upon this topic in my book, being released next year using p5.js, where you’ll be able to find better ways of generating the roads and the buildings. Hopefully, you’ve been able to take something away from this article, at the very least, you’ve got a cool new toy to play with, somewhere to go and play with random cities.

There’s plenty of things that this could be expanded into, maybe you want to generate bigger cities, or potentially you might want to play with the density of the buildings, making more/larger buildings spawn in the middle, and less/smaller buildings spawn on the edges. Alternatively, you could make it so that the city grows, buildings get bigger, or the city gets wider over time, I’d be interested to see what people come up with 🙂

Discover and read more posts from Michael Curry
get started
post commentsBe the first to share your opinion
Show more replies