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:
- Reducing the number of props
- Using less detailed props
- Replacing props with sprites or models
- Reusing bsp models
- 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 anORIGIN
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 anORIGIN
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 |
---|---|
use_model | {useglobal('shrub_bsp_template')} |
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:
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:
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 |
---|---|
_mess_merge_entity_id | merged_shrub_{parentid()} |
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:
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: