Covering terrain: optimization

Covering terrain: optimization
Covering terrain: optimization

Table of contents:

1. GoldSource limitations

The GoldSource engine is old and comes with many limitations:

  • It can only render a couple thousand world polygons per frame before performance takes a hit.
  • No more than 256 entities can be rendered at the same time.
  • Map files can only contain up to 512 unique brush entities (bsp models).
  • Map files can't contain more than 32 thousand planes and faces, and 65 thousand vertices.

This means that optimization is especially important - it should be taken into account right from the start. Levels are usually divided into separate areas, with limited visibility between them, which allows more detail to be added to each individual area. However, open terrain usually spans a large area, so we'll need more tools in our optimization toolkit.

2. Optimization techniques

There are several things that we can do in our map to stay below these limits:

  1. Reducing the number of props
  2. Using less detailed props
  3. Replacing props with sprites or models
  4. Reusing bsp models
  5. Merging props

2.1. Reducing the number of props

This is the easiest solution that helps with all of the above limits, but the downside is that our map will look more barren. All we need to do is to lower the coverage factor or instance count in our macro_cover terrain, or increase the instance radius.

2.2. Using less detailed props

This too is relatively straightforward: we just replace the contents of our shrub, rock and cactus templates with less detailed versions. This will improve performance and reduce plane, face and vertex usage, but our map will look less detailed.

2.3. Replacing props with sprites or models

Similar to using less detailed props, we can replace the contents of our templates with env_sprite or mtl_env_model entities. These are much easier for the engine to render, and they don't use up bsp model slots and planes, faces and vertices. The downsides are less detailed lighting (models) or even a lack of lighting (sprites), and lack of collision.

2.4. Reusing bsp models

Normally, every brush entity is turned into a unique bsp model, and a map can only contain 512 of those. A map that contains multiple large macro_cover entities could easily hit that limit.

The ZHLT compile tools introduced a feature that allows entities to use the bsp model of another entity:

  • The brush entity whose bsp model will be used must be given a name (targetname) and an ORIGIN brush.
  • The brush entities that will use that bsp model must be given the custom zhlt_usemodel property. The value of that property must be the targetname of the above entity. These entities must also be given an ORIGIN brush.

The downsides are that all instances will get the same scale, orientation and lighting, and that decals applied to one instance will show up on all of them. This technique is therefore mostly suitable for small func_illusionary props.

Applying this to one of our templates requires some scripting: we want the first instance to serve as a bsp model template, so it must have a targetname, and we want all other instances to reference the first with a zhlt_usemodel property.

Select the shrub macro_template and add the following custom property:

key value

The useglobal function returns false (none) the first time it is called with a specific string, and it returns true (1) on subsequent calls. This means that for the first instance of this template, use_model will be false, and for all subsequent instances it will be true.

Next, select the shrub func_illusionary and add the folllowing custom property:

key value
{use_model ? 'zhlt_usemodel' : 'targetname'}shrub_bsp_model

If use_model is true, then the key will be named 'zhlt_usemodel', else it will be named 'targetname'. This means that the first instance will have a targetname key, and all subsequent instances will have a zhlt_usemodel key, all with the same value.


Don't worry if you don't understand what's going on here! This is an advanced optimization technique after all. Just copy the above keys and values into your templates, and change the shrub_bsp_template and shrub_bsp_model names to something unique in each template, and you should be good to go.

Finally, add an ORIGIN brush to the shrub. It doesn't really matter where we put it, so we'll just place it at the center of the shrub:

Shrub template with zhlt_usemodel
Shrub template with zhlt_usemodel

When we recompile the map, we can see in the hlbsp log that the models count is now lower, and many other counts are now lower as well. If we open the in-game console and use the entities command, we can see that many entities now use the same bsp model. However, all the shrubs now have the same size and orientation:

Same bsp model, same size, same orientation, same lightmap, same decals...
Same bsp model, same size, same orientation, same lightmap, same decals...

2.5. Merging props

Merging multiple props into a single entity reduces the number of bsp models, and the number of entities that are visible at the same time.

One problem with this is that if an entity becomes too large, the game may no longer be able to determine when the entity is out of sight. In that case it will always be rendered, which reduces performance all across the map. To avoid that, we can split our terrain into multiple macro_cover entities, and only merge props that were generated by the same macro_cover.

To merge our shrubs, add the following custom property to the shrub func_illusionary in the shrub template:

key value

The {parentid()} part will be replaced by the unique ID of the macro entity that is creating the current instance. What that means is that all shrubs created by the same macro_cover will be merged into a single func_illusionary. We can divide our terrain into multiple macro_cover entities to control which props get merged:

4 separate macro_cover terrain segments
4 separate macro_cover terrain segments

When we recompile our map, the MESS log will show that there are now fewer entities in the map. We may be getting some Ambiguous leafnode content ( EMPTY and SOLID ) warnings about our func_wall entities, but that's generally harmless.

We can also apply this to our other props, but keep in mind that we should only merge entities of the same type and with the same properties. We don't want cactuses to become passable, or shrubs to block player movement and bullets! So we should use a different merge ID for our rocks, such as merged_rock_{parentid()}. With different IDs for each template, we'll end up with at most 12 entities, each consisting of multiple shrubs, rocks or cactuses:

multiple shrubs merged into a single entity
multiple shrubs merged into a single entity

Example map: