r_speeds and Optimizing Quake and GoldSrc Levels
(September 22, 2017)
While the thought of optimizing a Half-Life level for modern computers might seem laughable, if you're going to map for a game which will run without a graphics card, you might as well go all the way. Not to mention, games that old have a tendency to break if you push them hard enough, with disappearing walls, brush entities, and things generally going rather glitchy. Both Quake and GoldSrc games (which will work identically with the following instructions) use the same commands to test the optimization of your map, the main one being
r_speeds ("render speeds") command displays the number of polygons, both entity and world, being rendered. World polygons are anything brush-related, while entity polygons are enemies, NPCs, viewmodels, muzzleflashes, and so on. The Source engine does not use
r_speeds; it has a much more in-depth and complicated visual for how well your maps run, the Showbudget panel.
r_speeds 1 in the console to enable it. What you get will vary depending on if you're running the game with the software renderer or the OpenGL renderer. If the former, the console will begin to be spammed with the following:
27.3 ms 260/161/ 16 poly 283 surf 28.3 ms 265/181/ 24 poly 8 surf 28.5 ms 291/189/ 33 poly 10 surf 29.2 ms 290/193/ 45 poly 14 surf
The first number is the number of milliseconds a single frame takes to render. As discussed in the Showbudget guide, a steady 60fps would require each frame to render in under 16ms. For 30fps, which is about the playable minimum, each frame needs to take, at maximum, 33ms. Any longer, and you'll start to see some pretty big framerate drops.
The three numbers in the middle all refer to polycounts. Something to keep in mind is that the BSP program splits nice, clean walls and brushwork into tens of ugly polygons. We'll get into the details in a moment. For now, keep in mind that the first number (260) refers to all world polygons in the potentially visible set, the second number (161) refers to those in the PVS that are being drawn, and the third number (16) is the number of undrawn polygons in the PVS.
The final number (
283 surf) refers to the amount of those polygons which are being dynamically-lit—that is, any kind of non-static light is hitting them. This includes lights compiled to flicker or turn on with a targetname, as well as muzzle flashes. It's best to keep these as low as possible, or some of the lights will simply refuse to work altogether.
If you're testing your map in the OpenGL renderer, your console spam will look more like this:
0 ms 315 wpoly 1890 epoly 0 lmap 0 ms 315 wpoly 1890 epoly 0 lmap 0 ms 313 wpoly 1890 epoly 0 lmap 0 ms 309 wpoly 1890 epoly 0 lmap
Much simpler. The first number still refers to the amount of time a frame takes to render.
315 wpoly means 315 world polygons are currently being rendered, and
1890 epoly means 1,890 entity polygons are currently being rendered. The last number,
0 lmap still refers to the amount of dynamically-lit polygons currently being rendered.
r_speeds and speeding up rendering
r_speeds is a bunch of numbers, yes. Perhaps an overwhelming bunch of numbers.
r_speeds is remarkably simple when you get down to it, however; unless you have a good reason, keep the amount of world polygons being rendered under 500 or so. This is about the sweet spot for these engines, and while modern computers and modern source ports will play high-poly maps with no issue, it's still relatively poor practice. Plus, if you're working on a mod, people might choose to play it on an older machine for authenticity. If there's a chance map performance might be an issue, why risk it? Optimize, optimize, optimize.
Easier said than done though. Sure, it's easy to keep the detail level relatively consistent with what's already in the game. Likely, your bigger issue will be visibility control. Here's a scenario: you're hammering away at a map when you discover your
r_speeds through a certain area are especially high, even though nothing in view is especially detailed. To understand why, let's explain a bit about how VIS does its thing.
Visleaves and the potentially visible set
I mentioned something called a potentially visible set earlier. Simply put, walls block views. As a result, if you're somewhere along the wall, you won't see what's behind it. Potentially. There's no guarantee. You could be on the side of that wall and see all around it. Let's extend the wall with an invisible line. This line marks a plane where your view might be blocked. Now imagine every wall in your level doing this. This is the premise behind visleaves and the potentially visible set. So we don't get confused, here's a visual in the level editor (you can load this view by going to "Map > Load Portal File"):
Notice that the wall in the very back creates a bubble through the room. Each wall will split the space you can walk around in into these bubbles. VIS determines which bubbles you can see from any other bubble. After all, there's no point in the game rendering what you can't see, right? Now replace "bubble" with "visleaf" and you have the way VIS works. Problem is, so your VIS times aren't longer than they already are, VIS is conservative about it. Even if something really can't be seen from one point, VIS might say it can anyway. This avoids any kind of world holes or rendering artifacts, but isn't as efficient.
In short: walls split rooms into bubbles. Some bubbles can be seen from other bubbles. Some can't. VIS calculates all of that, but doesn't get it exactly right. As a result, even if you can't see into a room, VIS might be able to. Keep in mind that doors and windows don't block VIS from seeing into a room. To VIS, it's like the doors don't even exist.
Using the PVS to your advantage
The solution to all your woes is more walls. If VIS can see into another room, you can use more walls to make it so it can't. Here's a sample map of two rooms:
Even though there's a
func_door in the way, VIS will render both rooms at the same time. If you stuffed them both full of details without realizing it, you could have
r_speeds in the 750s or higher. The solution is to use a "visblocker" wall, as seen here:
As clumsy as that might look, it's actually quite common in official id maps, notably DM3:
The other solution is to use corners, specifically a corner hallway. If you can afford to warp the brushwork, put the rooms at 90-degree angles and connect them using an L-shaped hallway:
Both of these will block VIS and allow you to stuff both rooms full of detail. Be careful that the bend in the hallway doesn't become the new lynch point in being able to see both rooms.
There's two more commands that help when it comes to testing visibility,
r_drawflat replaces all polygons, or in the case of GoldSrc, world polygons, with a brightly colored solid texture. Remember what I said earlier about QBSP splitting up solid faces into polygon messes? This is what I mean:
This command is especially helpful if you have complex geometry splitting a wall in two. You can see exactly where the polygons are and why they're broken up the way they are.
The other command,
r_draworder, requires a software renderer, so either put your GoldSrc game into software mode in the settings or, for Quake, run your map with a software-rendered engine like WinQuake or Mark V WinQuake. Then, in the console, enable
r_draworder. This inverts the order in which rooms are rendered, causing faraway rooms to be rendered over close walls. Here it is, showing two rooms that really need a visblocker, with
r_speeds being rendered in the corner for maximum developeriness:
Get creative with the way you block visibility, and you can have good-looking maps that run well, even on old, dated machines.