Thread: [Tutorial] Total Map Optimisation

Results 1 to 6 of 6
  1. #1
    Super moderator Hezus's Avatar
    Join Date
    Aug 2001
    The Netherlands

    [Tutorial] Total Map Optimisation

    This tutorial helps you understand how the engine renders your map. Many tutorials have been written about this in the past, but some of that information is outdated or incomplete. Also, this guide aims to help you through the process of improving the performance by optimising an example map, rather than just give you generic information. Really good for newbies, but also for veteran mappers as it might give you some new insights.

    For this guide we're going to use vluzacn's v34 compile tools and his version of zhlt.fgd. It's got many extra features to optimise the map. Get it here or use the compile tools from the SvenCo-op SDK (based on vhlt). Also make sure you have zhlt.wad loaded.

    Optimising your map
    For this tutorial I've created a very basic and very badly optimised example map. We're going through some basic steps to fix it.

    1. Checking the performance
    In the main menu, open the console and enter these commands:
    developer 1
    maxplayers 1
    sv_cheats 1
    r_speeds 1
    map yournamename

    In the top left corner, you'll notice some values:
    59 FPS: Current amount of frames/sec.
    0 ms: The current latency (lag).
    363 wpoly (World Polygons): the amount of brushes (walls and such) being rendered.
    53338 epoly (Entity Polygons): the amount of polygons in models being rendered.
    Let's start with the optimisation of the epolies.

    2. Epolies
    There are quite a lot of epolies at work here. The pistol takes up 2.359 of those epolies. You could use a lower detailed pistol and save some epolies but I don't want to sacrifice detail for that. Instead, I'm going to look at my map and see what I can do.

    Behind the door are 12 monster_human_grunts. Those grunts are currently doing nothing but are being rendered by the engine for 50.979 polies. I only want them to pop up once it's necessary. So I'm going to change them all to squadmakers and name them. Then I'll place a trigger_once in front of the door and target the squadmakers. I'm also going to reduce the amount of grunts to 6 and let others spawn in a few seconds later from behind the crates. This way there will only be 6 grunts rendered at the same time at the most and I only use 25.489 polies max.

    Next to the eploy reduction, it will also increase performance since your computers CPU doesn't have to calculate effects and AI, and it will also improve the server load in multiplayer.

    3. Wpolies
    The wpolies is where you, as a mapper, can make a difference. There are several techniques to improve them but lets first try to understand how the rendering works.

    Go into the console and type:
    gl_wireframe 1
    gl_wireframecolor 255 0 0
    (gl_wireframecolor only available in SC 5.17)

    You can see how the map is being cut up into squares and sometimes triangles. This is called face subdivision and it's the way the engine works. Each square/triangle is a face and the more faces, the more polies. This is how the engine sees your map, but that's not the full story. Go into the console and type:

    gl_wireframe 2

    A lot of extra faces appeared in the room behind the door. Now this is how the engine actualy renders your map. You might not see that room, but the engine does and adds the wpolies from that room to the total amount. We're going to try to bring that number down by using some tricks. We'll start with the easy tricks and move down the list to the more advanced measures to optimise our map as much as possible.

    3.1 func_detail
    Whenever a brush touches another brush it will generate more faces, as you can see from the following screenshot.

    The crates touch the floor and generate extra faces there and the pipes cut really bad faces into the ceiling. To stop this from happening, we're going to turn them into func_details.

    As you can see, the extra faces are gone and we've nearly saves 100 wpolies here! I'm also going to use this for all the other things in the map, except for the outer walls, since that would cause a leak. Everything in purple is now a func_detail.

    Everything fine, except for this part:

    The panel and the pipe are both func_detail, yet they still cut extra faces. What's going on? If you look into the properties of the func_detail, you'll notice an entry called 'Detail level' or 'zhlt_detaillevel'. This controls the way the compiler treats your brushes and the face subdivision. Brushes with the same detail level number will cut into eachother, so if one func_detail brush touches another func_detail brush then make sure they have different level numbers. Note: even if you tie 2 brushes into 1 func_detail, it's still going to cut into eachother, so always make a seperate func_detail for each individual brush.

    I'm going to use level 1 on the panel and level 2 on the pipe and the extra cuts are gone! Another 8 wpolies saved easily.

    A lot of guides might tell you to use func_walls. Func_walls also stop faces from forming but they also count towards the model limit, which func_details don't. Also, func_details will cast shadows by default. This all makes it the superior entity for this purpose.

    3.2 NULL
    As wireframe mode shows us, the engine renders far more than we can see. Also some faces that players are never going to see. Luckily you can manually remove these by putting the NULL texture (from zhlt.wad) onto that face. Like the backsides of these crates: players will not be going there so I'm going to put the NULL texture on there, so those faces will not be drawn. This way I'm saving some more wpolies.

    But you don't have to NULL all and every backside. The compiler already takes care of that for you, but not always. Go into the console and type:
    cl_noclip 1
    When I fly around my map I see faces already been removed by the compiler, even on func_details touching eachother.

    But not on the func_door I'm using.

    Many moving brush entities (like func_doors, func_rotating, func_trains, etc) will keep all their faces. Since they can move and turn, the compiler doesn't automaticly remove faces. You're going to have to manually use the NULL texture on these. More wpolies saved!

    3.3 Texture scaling
    Another way to improve the wpoly is to scale the textures up. The textures will become a bit blurry because of the scaling, so I'm going to use it on a texture you won't see up close: the ceiling. I'm using a value of 2.0 for the scale.

    Let's compare the results. The top half is the ceiling on scale 1.0 and the lower one 2.0. On scale 1.0 there are 4 horizontal faces and on 2.0 only 2, thus there is a reduction of 30 wpolies. It's good to know this trick, but just scaling up isn't always going to help or even necessary. It's better to know exactly why this happens, so you'll have to know a bit more about face subdivision.

    3.4 Face subdivision
    The wireframe mode shows you quite clearly where the faces are being made but how does the engine determine those? The engine creates a new face every 240 pixels when a texture is on scale 1.0. Scaling the texture up will also increase that pixel limit. So on 2.0 the faces are being cut at 480 pixels, and on 0.5 on 120 pixels.

    Knowing this, you can experiment to find the optimal scale rate. If you look closely at the 2.0 scaled ceiling you can see that the faces on the left are smaller than the faces on the right. That means that this scale is not fully efficient. So I'm going to scale it to 1.75 and compile again:

    As you can see, the faces fit the ceiling perfectly at this scale and we didn't lose a single wpoly. Yet, the texture looks a little bit better! The only way to improve the wpoly now is to scale it up to 3.5 (creating 1 large face across) but that would make it look horrible. So, some guides might tell you to scale up your textures to redicilous amounts but now you see that it's not useful to do that.

    3.5 256x textures
    Now you know that faces are being made every 240 pixels, which means that 256 units high walls with 256x (or larger) textures will always make an additional cut, as you can see here:

    So, I'm going to grab the texture (C3A2A_W3) from the hl.wad with Wally and export it to a TGA.

    Now in Photoshop (or any other editor) I'm going to reduce the size of the texture from 256x96 to 240x96. Make sure you always use a power of 16 or Wally won't accept it when you upload it into your own WAD file. I'm going to rename the texture to C3A2A_NEW, so it doesn't conflict with the original. Then I'm swapping the new texture with the old and setting the scale to 1.067. This will make the 240x texture fit perfectly on the 256 units high wall and will also be the perfect scale for the face subdivision. Let's compile and see!

    The faces now fit perfectly and I've already won 20 wpolies in this corner! There is a tiny loss in quality (it has 16 pixels less) but it's hardly noticable on this texture and there is quite some gain. Since the face subdivision is 240, 240x240 textures are the perfect size. However you scale them up or down, it will always give you the most ideal quality/wpoly balance.

    Some guides might frown upon 240x textures, because older version of the engine scaled the textures down to 128x first and then back up to 240x resulting in a very blurry texture. As of SvenCo-op 5.17 this problem has been removed, so 240x240 textures are as sharp as they are meant to be! More info on that topic here.

    3.6 Texture alignment
    Faces are also affected by the alignment of a texture. Like this example: sometimes grass or sand can look better if you rotate the texture, but that also means the faces are being rotated.

    Using no rotation means less faces are being generated in this area. So if you really want a better effect you're better off editing the texture itself.

    The alignment also matters, especially on sloped surfaces. This wedge uses the FACE alignment which makes it shrink a bit horizontally and thus generating another face cut.

    Setting the texture to WORLD alignment fixes this problem and gives us back 1 wpoly.

    Texture alignment is also very important whereever 2 brushes meet, like in this corner:

    If you look closely, you can see that these textures are not properly aligned to eachother.

    And above you can see the result of it ingame, it created an extra face. To fix this, align the textures by selecting one of them, and then RIGHT CLICK on the other (Lift+Select mode should be on by default). As you can see, the problem was solved:

    It's easy to miss those faces, so a general good practise is to cut corners with a diagional line. This way there won't be a potentional extra face and I was also able to create a way better looking effect on the top of the brush. Some guides claim that this practise will always improve wpoly, but as you can see it actually comes down to the alignment of the textures in adjacent brushes.

    3.7 Texture merge
    Very often, you see mappers use multiple textures on walls to make maps look interesting. Many texture packs even come with these 'border'-textures. They are smaller (32x or 64x) textures with the same visual style, which can accompany a larger texture (128x or 256x) to create a nice effect. Here is an example from the hl.wad:

    The lower border texture is 64px high and the higher main texture is 128px high. And since they're seperate textures, additional faces will be created. It looks like I'm going to use this combination a lot, so a little math tells me that merging these two textures will result in a single 192px high texture. This is well within the 240px face subdivision cut-off, so definatly worth the effort. I'm exporting both textures from the hl.wad, combining them into a new texture and load it into game.

    And there you have it. Looks exactly the same and we've saved half the wpoly! Even if you're combining two 128px textures into one 256px texture then simply rescale that texture to 240px and you're back within the face limits.

    3.8 -subdivide
    So, we've read about the face subdivision limit, which is 240px. However, there is a compile command that can override this limit and create larger faces and thus less wpoly. I'm going to add '-subdivide 256' to the BSP option and here is the result.

    Only 182 wpolies, which is quite a gain in performance! So why haven't we used this before? Well, there is a very likely chance this option will cause your compile process to crash once it reached RAD. Overscaling the face subdivision can interfere with the way the engine handles the lightmap. If your map is simple and small enough (like mine), you might try to give this a shot. But as said, it's very likely this won't work for your map, so I'm not going to include this technique in my final optimisation results.

    3.9 Results
    So, back to our starting area. With these simple tricks we've managed to bring down the wpoly from 363 to 205 from this point of view.

    This is a great improvement, especially considering we haven't touched the main design of the map. Yet, there are more ways to make this map perform even better. Let's dig deeper into the workings of VIS.
    Last edited by Hezus; 12-08-2018 at 12:47 PM.

  2. #2
    Super moderator Hezus's Avatar
    Join Date
    Aug 2001
    The Netherlands

    Re: [Tutorial] Total Map Optimisation

    4.0 VIS
    VIS is a part of the compiler which handles the visibility, or in other words what the engine can 'see'. We've already determined by use of 'gl_wireframe 2`, that the engine sees more than the player does. I was able to see the room being rendered behind the door, since brush entities (like func_door, func_detail, etc) do not block engine visibily. Only solid world geometry can function as a visibility block, often refered to as a 'visblock'.

    To handle the visibility, the compiler cuts your parts into sectors. Here's a topview of a small example map.

    Since this is a very simple map, VIS did a good job and created the sectors exactly where the rooms and hallways are. The more complicated your brushwork, the harder it's going to be to determine where the sectors are. You can't highlight the sectors in-game, but you can see them pop in and out of existence with 'gl_wireframe 2', when you move around.

    These shots show pretty well how VIS works. The player is standing in sector #1. The engine can draw direct lines from sector #2 and #3 to sector #1. If that is the case, then these sectors will be rendered. There is no straight connecting possible from sector #1 to #4 or #5, so these won't be rendered.

    The player walks into sector #2 and the system updates. There are now straight connections to #1, #3 and #4, which are being rendered. Sector #5 still can't be 'seen' by the engine from sector #2 and remains unrendered.

    4.1 VISblock
    Back to our map. Since the door doesn't block VIS, the engine can look straight into the second room and it will always be rendered. To remedy this, one can make a 'visblock'. This is a world brush, strategicly placed to break the straight visibles lines we discussed above. I'm going to put it straight in front of the door.

    As you can see, this clearly worked! The visblock brush caused VIS to make extra sectors (#3, #4, #5) and there no longer is a straight line from sector #1 to #2 or even from the newly created sector #3! In result, the wpoly has gone down to 100. Quite an improvement from the original 363!

    4.2 HINT brushes
    Next to VISblocks, you can use another trick to manipulate the sectors created by VIS: HINT brushes!

    Before we go there, we need to get some more insights on where VIS cuts the map into sectors. You can't see it in game, but there is an option for the compiler. Use the `-viewportal` command on BSP and compile the map. Now in the editor, go to Map > Load Pointfile and find 'mapname_portal.pts'.

    I've created an example map for this purpose and you can clearly see where the cuts are made:

    Let's see how the engine approaches this situation:

    When the player is in sector #2, you can draw straight lines to all sectors, even #4! Let's try to visualise what sector #4 can see by imagining it's a flashlight and it casts light into the other sectors. This way we can see how far it's influence goes.

    As you can see it only touches sector #2 a little, but it still affects the entire sector. Time to put our thinking caps on! We can't affect the range of sector #4, but we can manualy create another sector in between #2 and #3! This is where we use HINT brushes!

    Create a new block and make sure all 6 sides have the SKIP texture on it. On the face where you want the cut to be, place the HINT texture. The cuts are always two-dimensional, so that's why you only need 1 straight face to use for the cut. The compiler is going to ignore the faces with SKIP on it. Now position the brush so that it cuts diagonally across the corner, like so:

    Let's see what it did. First a visual representation:

    As you can see, a new sector was created (#5) and this causes the straight connection between sector #4 (and even #3!) to break. This is what that looks like in-game:

    Everything around the corner is now unrendered. A huge improvement! HINT is a very good technique to use but as you can see, it takes a bunch of logical thinking and imagination to get it right. Using the portal file and the flashlight visualisation helps a lot but it can still be very tricky to get it right. And in some cases it might not even help or make the wpoly even worse.

    Let's see how this would affect our map when using the flashlight representation. I've removed the visblock wall we created earlier.

    Sector #1, affects #3 (the door) and nearly all of #2, except for a very tiny corner. If you created another sector there with the help of HINTs you'd be able to hide a really tiny crate there, but that doesn't seem all that effective. That means HINT isn't going to help us in this case and the visblock wall is our best option.

    When putting back the VISblock wall, I've noticed that VIS didn't pick the most optimal place to cut the sector.

    As you can see, the line between sector #2 and #4 is cut in a way that it creates a direct connection into sector #1. Therefore, when the player is in sector #1, everything in sector #4 will be rendered. Below, at sector #3, the cut has been make in the right way. There is no connection between #4 and #1 there. We're going to manually change the cut by placing a HINT brush:

    As you can see, the HINT brush helped the compiler to put the cut in the right spot. Now sector #4 will no longer be rendered. So, we think we did good, right? Let's put the player into the far corner and see what happens:

    The entire map is rendered! When we look back at our representation, you can clearly see why:

    There is a straight connection between sector #6 and #3, which also renders #4 and #5. We need to break those connections by creating additional sectors like so:

    Which then creates the following representation, in which you can clearly see that the connection to sector #6 has been cut.

    And many wpolies were saved in the process, from 215 to 104.

    I can even take it a little bit further by dividing the other room into 3 sectors:

    When standing in sector #5, there is no connection to #1, so that'll stay unrendered, saving a few more polies. And the same happens on the opposite side: when standing in #6, #3 isn't rendered.

    4.3 -maxnodesize
    The BSP compiler has an option called 'maxnodesize' which is on 1024 by default. This is the maximum size of a generated VIS sector. As you can see above, sometimes it helps to create smaller sectors, so you can let the compiler do this job for you. At its best, it can improve wpoly if it places extra sectors to stop direct connections between other sectors (like we did when we used HINTs). At its worst, it can create extra wpoly if the sectors arn't cut in an optimal way and it will generate a lot of extra sectors. This increases compile times and there's a limit of how many sectors the compiler can handle.

    A good way to work with this, is to set the -maxnodesize to a lower value, like 256 (64 is the lowest possible) and see what effect it has. If it improves wpoly in certain areas it means you've found an optimisation opportunity! Load up the portal file and see where this extra cut was made and how it affects the map. If it's beneficial, then recreate the cut with a HINT brush. Go around your map and repeat this step. Once you've found them all, you can set the -maxnodesize back to it's default. This way you've optimised the map fully and you're not creating unnecesarry sectors.

    4.4 Results
    So, by understanding the workings of VIS, implentation of a VISblock and the use of HINT brushes, we've greatly optimised our map. With all techniques learned from the guide so far, we've brought down the wpoly by a factor of 3, without changing too much about the layout or the quality of the map. This is the maximum of optimisation I could squeeze out of my small test map.

    In the next chapters I'm going to show you some misc. optimisation tricks and will also delve deeper into the other aspects and limits of the engine and how to optimise them!
    Last edited by Hezus; 09-08-2018 at 08:13 AM.

  3. #3
    Super moderator Hezus's Avatar
    Join Date
    Aug 2001
    The Netherlands

    Re: [Tutorial] Total Map Optimisation

    5.0 More tricks & Engine limits
    Coming soon!
    Last edited by Hezus; 09-08-2018 at 07:00 AM.

  4. #4
    trigger_delay & func_lazy TrEmPlEr's Avatar  
    Join Date
    Jul 2002
    Valve Hammer Editor 3.5 beta

    Re: [Tutorial] Total Map Optimisation

    Oh my gosh <3 nice guide so far, can´t wait for more !

    Modstatus: Unknown

  5. #5
    Advanced Leveldesigner SourceSkyBoxer's Avatar
    Join Date
    Apr 2011

    Re: [Tutorial] Total Map Optimisation

    Nice @Hezus thanks for explanation! I will fix my ragemap2018

    PS: Where is example of func_detail if it happens who mapper doesn't know like who made func_detail outside of hollow room like I show you example hallway with curvy surface or spaceship-like wall than it will avoid for leak right?

    Example it is wrong:

    It is correct:

    It is really important for func_detail brushes with closed hollowed brushes.

    Thanks for avoiding of leaks.

    // EDIT What does it happen with gl_wireframecolor command?

    For me nothing found:
    Last edited by SourceSkyBoxer; 09-08-2018 at 04:12 PM.
    Hello guys, Svencoop. I am sorry Please respect me! I am deaf. Thanks, Sven-Coop Forum!

    Please do not share to shit dropbox! Please share only!
    Muhahaha, Facebook crashed!

  6. #6
    Super moderator Hezus's Avatar
    Join Date
    Aug 2001
    The Netherlands

    Re: [Tutorial] Total Map Optimisation

    The wireframecolor command is only available in 5.17. Forgot to add that. It'll be released soon

    As for your example: The guide is for optimising the map, not for general mapping errors. If you want to optimise your map, I automaticly presume that you've managed to make a map without leaks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts