How browser 3D games work: WebGL and three.js, explained simply
Years ago a 3D game meant installing from a disc or a large download. Today you can open a link and a few seconds later fly a camera over a whole city with cars, traffic lights and a day–night cycle. Here is what happens behind the screen — without formulas, but without oversimplifying either.
The graphics card does the heavy lifting
Every 3D object on screen is made of small triangles. A building may have hundreds, a detailed car thousands. For each frame, something has to work out where each triangle lands, which ones are visible and what colour every pixel should be. The processor (CPU) is good at complex logic but does relatively few things at once. The graphics card (GPU) is built for the opposite: thousands of simple calculations in parallel.
So 3D games leave the rules of the game — money, traffic, which building is going up — to the processor and hand the drawing over to the graphics card. The question is how a web page can talk to the graphics card without being given dangerous access to the computer.
WebGL and WebGPU: the bridge to the graphics card
The answer is WebGL — a standard that lets JavaScript on a page draw with the help of the graphics card inside a <canvas> element. WebGL 1.0 was published in 2011 and WebGL 2 in 2017, and today it is supported by every mainstream browser, on desktop and on mobile.
The next generation is WebGPU. It first shipped in a stable release in Chrome 113 in 2023 and is gradually arriving in other browsers. It is closer to how modern graphics cards work and also allows calculations that are not drawing. For most games, however, WebGL is still the safest choice because it runs almost everywhere.
The browser sits between the page and the graphics card: it checks the requests and does not let the site read other data. That is why 3D in the browser is safe in the same way as the rest of the page’s content.
three.js: not writing everything from scratch
Working with WebGL directly takes a lot of code even for one spinning cube. That is why most projects use a library. The most popular is three.js, started by Ricardo Cabello (known as mrdoob) in 2010 and maintained by a large community.
three.js introduces concepts that are easy to grasp:
- Scene — the world in which all the objects live.
- Camera — where we look from and in which direction. In a city builder this is usually a camera above, at an angle.
- Mesh — geometry (the triangles) plus a material (what the surface looks like).
- Materials and lights — whether a surface is matte or shiny, how strongly the sun or a street lamp lights it.
- Shaders — small programs that run on the graphics card and calculate the position and colour of every point. The materials in three.js are in fact ready-made shaders.
- Renderer — the part that turns the scene and the camera into a picture on screen.
The frame loop and the 16.7 ms budget
A game does not draw one picture but dozens per second. The browser offers the function requestAnimationFrame: “call me before the next screen refresh”. Each time it is called, the game updates its state — cars move on, the sun sinks lower — and draws a new frame.
At 60 frames per second, one frame has about 16.7 milliseconds. In that time the logic must be calculated, the objects prepared and the graphics card must draw everything. If the work takes longer, the frame is late and the picture starts to stutter. So optimising 3D games is largely a fight for milliseconds.
A good practice is to make the logic depend on elapsed time, not on the number of frames. That way a car travelling at 50 km/h moves equally fast on a powerful computer and on an older phone — the phone simply shows the movement in fewer frames.
How thousands of objects get drawn
Each separate request to the graphics card (“draw this object with this material”) is called a draw call. It has its own cost, however small the object. Thousands of separate cars and trees mean thousands of requests and a slow game. There are a few basic techniques:
- Instancing — the same model, such as a tree or a street lamp, is drawn in many places with one request.
- Level of detail (LOD) — distant buildings are shown with simpler models, because the difference cannot be seen.
- Culling — objects outside the camera’s view are not sent for drawing at all.
- Merging geometry — static objects such as streets and pavements are combined into fewer, larger meshes.
Textures — the pictures on surfaces — take up the graphics card’s memory. A 4096 × 4096 texture needs four times as much memory as a 2048 × 2048 one. On a phone memory runs out quickly, so smaller textures and compressed formats that the graphics card can read directly are used.
Models, loading and caching
Models of buildings, cars and machines are usually made in programs such as Blender and saved in the glTF format (or its binary variant, .glb). It is often called “the JPEG of 3D”: it is compact, contains geometry, materials and animations, and loads easily into three.js.
Because nothing is installed, everything is downloaded over the network. So that the player does not have to wait, models load gradually: the essentials first, the rest later while the game is already running. The browser caches downloaded files, so the second time they come from the disk. A game can also be a PWA (progressive web app) — added to the home screen and opened like an app, without a store.
Where progress is kept
The simplest place to save is localStorage — a small store in the browser on that particular device. It is convenient, but if you clear the browser’s data or switch devices, the save stays behind. So more serious games also keep progress on a server (“in the cloud”), tied to an account. Then you can start on the computer and carry on on your phone.
An example: the city in Construction Empire
Our game Construction Empire (Строителна империя) is built with three.js and runs in the browser on desktop and phone, including as a PWA. In it you can see all the ideas above in practice: cities with traffic and pedestrians, traffic lights, buildings that grow stage by stage, and a day–night cycle in which windows and street lamps light up. Each of these has to fit into the frame budget — which is why much of the work on a game like this is not just adding something new, but making it run smoothly on a weaker phone as well.
If you are curious how the economy in such a game is designed, see the article on economy design in a city builder.