This post explains how to code “city” , a demo recently released by @mrdoob. He built a fully procedural city in 100-lines of javascript. I found the algorithm very elegant, a simple and efficient solution. So I made a post explaining it.
A Few Remarks on the Algorithm
It always helps to get a big picture before going down to the details. The used algorithm is fully procedural. This means the whole city is built dynamically, so no download. It is quite elegant as well. The algorithm to generate the city in 3d is less than 100 lines long. What is this algo in a nutshell? Every building is a cube, they got random size and position. Simple enough ? It may seem far from realism but it is ok. The illusion is surprisingly convincing if you fly over at low altitude.
From a performance point of view, all buildings are merged into a single geometry, with a single material. As a cherry on the cake, we remove the bottom face as it is never seen. It is very efficient as there is no shader swap and a single draw call.
To improve realism, we simulate ambient occlusion thru a cheap trick
using vertexColor
.
In the city, at the street level you got shadow from the other buildings.
So the bottom of the buildings are darker than the top.
We can reproduce this effect with vertexColor
.
We take the bottom vertices of the building and make them darker than the top.
Let’s get started
To explain those 100 lines, we will explain it step by step: First, we “generate the base geometry for the building”. Then we use this geometry to know “where to place buildings in the city”. We use some clever trick “using vertexColor for ambient occlusion”. Then we “merge all buildings to make a city”, thus the whole city may be drawn in a single draw call. At the end we detail the “procedural generation of building’s texture”.
Ok so let’s get started!!
Generate the base Geometry for the building
We build a base geometry of our building. It will be reused several time while building the whole city. So we build a simple CubeGeometry
1
|
|
We change the pivot point to be at the bottom of the cube, instead of its center. So we translate the whole geometry.
1
|
|
Then we remove the bottom face. This is an optimisation. The bottom face of a building is never seen by the viewer as it is always on the ground. It is useless and we remove it.
1
|
|
Now we fix the UV mapping for the roof face.
We set them to the single coordinate (0,0)
.
So the roof will be the same color as a floor row.
As each face of the building is using a single texture, it can be drawn in a single draw call.
Sweet trick for optimisation.
1 2 3 4 |
|
Ok now that we got the geometry of a single building, let’s assemble buildings together to make a city!
Where to place buildings in the city
Well… to be honest we put them anywhere. All is random ;) Obviously, there are collisions but the illusion is nice if you fly at low altitude. So first, we put the building at random position.
1 2 |
|
Then we put a random rotation in Y.
1
|
|
Then we change the mesh.scale to change the building size. First how wide and deep a building can be.
1 2 |
|
Then how high it is.
1
|
|
What’s the deal with all those multiplication of Math.random()
?
Well it is a way to change the statistic distribution of the result
and center it closer to 0. Math.random()
is between 0 and 1
and got an average of 0.5. Math.random() * Math.random()
is
between 0 and 1 but got an average of 0.25. Math.random() * Math.random() * Math.random()
got an average of 0.125 and so on.
That’s it :)
We got the position/rotation/scale of our building all set.
Now let’s set its color, and how to use it to simulate shadows.
Using VertexColor for Ambient Occlusion
In a city with lots of buildings, the bottom of the building tends to be darker than the top. This is because the sun light hits the top harder than the bottom, at the bottom you have the shadow of another building. This is what we call ambient occlusion in graphic programming. This concept may be implemented in various ways: for example in screen space with screen space ambient occlusion or ssao or in this minecraft example from three.js
With three.js, it is is possible to assign a color to a vertice. It will alter the final color of the face. We gonna use that to simulate shadows at the bottom of building. First we define the base colors for the part which receives lights, and the ones which get shadows.
1 2 |
|
Those are constants for each building. Now we need to get a color for this particular building. We put some randomness for variety.
1 2 |
|
Now we need to assign the .vertexColor every vertex of every face.
If the face is a top face, we use baseColor
of the building.
If it is a side face, we use baseColor
multiplied by our light
for the top vertices and shaddow
for the bottom vertices,
as cheap ambient occlusion.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
We got a single building fully setup. Now let’s make a city with many buildings.
Merge all buildings to make a city
To make our city, we gonna merge 20000 buildings together. So we gonna loop and apply the above formulas for each building we add. We have already seen that reducing draw calls is good for performance. see “Performance: Merging Geometry” post. Here all buildings share the same material, so we gonna merge them all in a single geometry.
1 2 3 4 5 6 7 8 |
|
Now we got a single large geometry for the whole city, let’s build a mesh from it.
1 2 3 4 5 6 |
|
This mesh is a whole city. Rather cool! Now one last step, let’s explain how to make this texture.
Procedural Generation of Building’s Texture
Here we want to generate the texture for the side of each building. In a nutshell, it will show the floors for realism and variety. So it alternates between row of window and row of floor. Window rows are dark with a small noise to simulate light variations in each room. Then we upscale texture carefully avoiding filtering.
First you build a canvas. Make it small, 32x64.
1 2 3 4 |
|
Then you paint it in white
1 2 |
|
Now we need to draw on this white surface. We gonna draw floors on it. one windows row, then a floor row and we loop. In fact, as the face is already white, we just have to draw the window rows. To draw the window row, we add some random to simulate lights variations in each windows.
1 2 3 4 5 6 7 |
|
Now we got the texture… just it is super small, 32, 64
We need to increase its resolution. But lets be careful.
By default when you increase the resolution, you get a smoothed result, so it may easily appears blurry.
See on the right side, it doesn’t look good…
To avoid this artefact, we disable .imageSmoothedEnabled
on each plateform.
You can see the result on the left.
The blurry effect is no more.
It is as sharp as the original but with a better resolution.
Ok now lets code exactly that. First we create the large canvas of 1024 by 512.
1 2 3 4 |
|
We disable the smoothing
1 2 3 |
|
Now we just have to copy the small canvas into the big one.
1
|
|
Then all we need to do is to actually build the THREE.Texture
.
We set the anisotropie to a high number to get better result.
see tojiro on anisotropy for detail.
1 2 3 |
|
This was the last step. Now, you know how to do a procedural city in webgl with three.js. Rather cool! As a summary here is the whole code put together.
The Whole Code
Let’s put all that together. Here is the whole code commented.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
|
threex.proceduralcity extension
As usual, this code is gathered in easy-to-reuse threex package,
threex.proceduralcity.
It makes stuff super simple, just create an instance and it will return a THREE.Mesh
.
1 2 |
|
The demo live contains this city plus a ground, a first person control and a fog. This is rather cool result for such a small effort.
Conclusion
So now you know how to generate a whole city in 100 lines. No download. Rather clever algorithm. I hope you learned from it, it contains many tricks that you can reused in your own demos.
That’s all for today! Have fun :)