There are tons of tricks developers use to get the maximum out of their art assets.
The first is to optimize for polycount. Here's a great example:
When you look at the texture model, it looks amazing detailed with probably like a million triangles. But the truth is, it only has the polygons shown on the right! In this case, an artist used the following process:
- Build an amazingly detailed demon model in Maya, 3DS Max or Blender. We're talking a million or more polygons.
- She reduces the amount of polygons in the model to a more reasonable size, like 400 or so, and saves it to a new file.
- Finally, she uses a package to generate a normal map from the million-polygon model and puts it on the reduced detail model.
- Alternatively, she uses ZBrush to "sculpt" the normals for the model.
There is also tons of detail being added using just the diffuse texture.
Batching and instancing
Others have touched upon this, but it's very true. If you're going to render the same tree model hundreds of times, it makes no sense to load it into memory a hundred times. It's better to load it into memory once and render it at different positions.
Here's a great article on that: http://blog.wolfire.com/2009/11/Fast-object-instancing
For our space RTS, we actually had multiple representations for our ships:
Very far away - icons. No model or anything, just a small icon to show the position of a ship on the map.
Far away - small ship. These ships had about 100 triangles each. But even at that reduced scale, it doesn't matter. At that range, all you really care about is that the outline of the model stays the same.
Near - big ship. These are the base models. Each had about 2000 triangles, with a diffuse map, normal map, specular map and glow map. Shaders became more expensive (alpha pass, glow pass) and particles were turned on.
This is a topic of research that is increasingly becoming more important. The basic idea is: why render something that is drawn behind something else? However, the intricacies can become quite complicated.
The most common approach is to label certain objects as "occluders". The rendering then happens in stages:
First, the occluders are rendered into a depth buffer without textures or shaders.
Objects compare themselves to this buffer, if they're behind it they don't get drawn.
Objects that are still in view are rendered with textures and shaders.
A very simple approach to occlusion culling could be: "don't render anything that isn't in the camera's view frustum". Objects check if they're inside the camera's view frustum (the box that defines the camera's transformation of the world to the screen) and if they are, they're added to a list of objects to render.
Finally, the mother of all memory savings: chunking. If you're playing in a large open world, it makes no sense to load everything into memory at once. Especially on platforms like the Xbox 360 and the PS3, where memory is worth its weight in gold. So a solution could be to divide your level into chunks.
When you turn a corner in an FPS game, the next section is loaded while the old one is thrown away. However, this affects the level design greatly and basically ensures corridors everywhere. There's also no silver bullet: what works for a game like Just Cause 2 doesn't work at all for a game like Fable 3.
For any game, you should look at what's important to you. If you're on PC, you don't have to care about memory that much. Most gaming rigs these days will have at least 2 GB of memory and 1 GB of video memory. That's a lot of models and textures! Keep in mind that any optimization you make for memory will usually have a price for processing. If you compress your textures to PNG for example, your files will be smaller than BMP images, but they'll take longer to load.