Track Optimization

Note

This page covers some of the core principles of 3D rendering to ensure you have solid grasp of what’s happening under the hood. The concepts are being explained at a high level and the goal is that understanding these concepts will help make the process of optimizing your tracks clearer.

The quick rundown

Modern graphics cards can render a staggering amount of polygons very fast, and because of BallisticNGs low shader complexity, it’s even easier for your graphics card to churn out pixels.

Unity has many of it’s own rendering optimization systems in place, however internal BallisticNG tracks don’t need to lean into them as much as other games due to the atlased textures, low material counts and aggressive merging of meshes in the modeling software. This means Unity has less work to do when processing objects for rendering.

A little bit about how rendering works

Rendering a game is a process that involves both the CPU and GPU. First the CPU must figure out what we want to draw, then it sends all of that information off to the GPU to finally draw what we tell it to.

On a standard desktop PC with a dedicated GPU the slowest part of this rendering process is almost always the initial gathering of objects. If you’ve played a game that isn’t visually impressive but runs bad, this will usally be the culprit. A modern graphics card can handle millions of polygons being thrown at it, but the CPU will struggle to keep up if those millions of polygons are split over many, many objects.

So then this brings us to the core point of discussion for this page: Bandwidth

Bandwidth

The CPU is normally going to be the bottleneck in rendering because while it can push a lot of data in a single stream pretty fast, it can’t push a lot of data in multiple streams anywhere near as fast.

This then means we need to be clever about how we’re getting our scene from the CPU to the GPU, and luckily for us BallisticNG scenes are static and relatively small. This is perfect because since we know information about every object ahead of time, and where the player will be with certainty, we can apply some aggressive optimization techniques to our tracks.

Draw Calls

This process of sending rendering information from the CPU to the GPU is called a draw call, which we perform for each object in the scene. If the object has multiple materials, that object will generate multiple draw calls since the material splits the mesh into sub-meshes which require individual drawing.

It’s best practice to use as little materials as possible. Because BallisticNG tracks are made with texture atlases, this means the entire scene can usually be assigned just a handful of materials depending on the effects desired.

Overdraw

When there’s a transparent surface, any pixels behind the surface are redrawn so the transparent pixels can be composited on top of them. This is called overdraw and will quickly drain performance if you have lots of transparent surfaces taking up large portions of the screen. This is not necessarily a problem you will run into when making track scenery, but can an issue if working with features such as particle systems.

Opaque and Cutout shaders do not present this issue. Transparent, additive and multiplicative shaders however do.

Fill Rate

FIll rate determines how fast your graphics card can draw triangles (filling them with pixels). There are a few factors that play into how fast a triangle can be fully rendered:

  • The speed of your graphics card

  • How many pixels on your screen the triangle is taking up

  • The complexity of the shader drawing the triangles pixels

This means that lots of geometry in the distance is actually faster to render then the same geometry taking up the entire screen, assuming that the same number of triangles is visible at both distances. Keep in mind that this does not mean you should make high detailed objects in the background, as this is still a waste of processing time which could be spent on objects closer to the camera, or not at all.

BallisticNGs shaders are not very complex, so lots of triangles can be drawn quickly on appropriate hardware.


Frustum Culling

This is automatically handled behind the scenes and you don’t need to do anything for this, but it’s included here just so you know it’s happening

Frustum Culling figures out what the camera can see so we’re only working with objects that are visible to the player. Let’s say we have a really dense city and we’re at the edge of it. If we’re facing away from the city then we don’t care about all of those buildings because we can’t see them. This means we can skip render pre-processing on these objects and save ourselves a lot of time preparing them. Note that this can’t determine when an object is behind another object entirely and will render anything that is inside of the cameras frustrum, regardless of if the object is actually visible or not.

Batching

Batching is the process of taking multiple objects which share a material and bundling them into a single draw call. This can very dramatically increase performance if lots of objects are involved, but does still have an overhead in and of itself. Unity has two types of batching: static and dynamic.

Static batching performs the batching ahead of time so the game doesn’t have to figure it out at runtime. This however has a major caveat, which is it only works with very low poly objects.

Dynamic batching performs the batching for each rendered frame. It’s slower then static batching, but still faster then no batching at all. This works with much larger meshes, which makes it the preferable method of batching.

However do keep in mind that as previously said, batching does itself have an overhead which we can actually eliminate ourselves by merging objects that should be rendered at the same time in our modeling software.

Baked Occlusion Culling

TODO

Render Zones (BallistiNG specific feature)

Not available yet