March 21 2022
Learn how to make your expensive scripts cost nothing while they are offscreen.
First off: what is an offscreen object?
An object is offscreen when you cannot see it from your camera’s perspective. This may happen because your object is “behind” your camera (therefore culled by frustrum) or behind walls/other occluders (occlusion culling).
So if an object is offscreen and we cannot see it, we don’t have to dedicate expensive computational resources to it right?
Wrong. That’s what I thought when I was a kid. Not anymore.
Here are some of the hardware costs you are paying for offscreen objects:
So what can we do about this? Let’s talk about the CullingGroup API.
You’re playing a RPG and you’ve got a friend behind you. You just don’t know it yet.
Since that rogue character is offscreen, you surely don’t have to execute its behavior?
Ok wait, if we completely froze it, it wouldn’t stab you with a dagger. So apparently we still have to run some code. Let’s think about some aspects:
In general, you want to avoid cosmetic operations on offscreen objects, such as tweaking materials’ properties, minor rotations and such.
But… how can you do this?
Let’s assume you have a script called AngerRenderer. It only does one thing: to make the character redder the angrier it is. And it does so by simply adjusting a material property.
Something like this:
class AngerRenderer : MonoBehavior
{
void Update()
{
var skinColor = Color.Lerp(Color.white, Color.red, 1f - _health / 100);
_material.SetProperty("_MainColor", skinColor);
// More costly operations here
// Play sweat particles
// Play steam particles
}
}
Of course it doesn’t compile. It shouldn’t, as I want to make the point instead.
What can we do now to disable this “costly” script whenever that character isn’t in view?
So we use the CullingGroup API that Unity offers us.
This API informs us of visibility updates. And guess what, when you know whether your object becomes visible or invisible, then you can just disable the components you want or stop executing its behavior.
The code could then look more like this:
class AngerRenderer : MonoBehavior
{
CullingGroup _cullingGroup;
BoundingSphere[] _boundingSpheres;
float _playerRadius = 1f;
void Start()
{
_cullingGroup = new CullingGroup();
_cullingGroup.targetCamera = Camera.main;
_boundingSpheres = new BoundingSphere[1];
}
void OnDestroy()
{
_cullingGroup.Dispose();
_cullingGroup = null;
}
void Update()
{
if (_cullingGroup.IsVisible(0))
{
var skinColor = Color.Lerp(Color.white, Color.red, 1f - _health / 100);
_material.SetProperty("_MainColor", skinColor);
// More costly operations here
// Play sweat particles
// Play steam particles
}
_boundingSpheres[0] = new BoundingSphere(transform.position, _playerRadius);
_cullingGroup.SetBoundingSpheres(spheres);
_cullingGroup.SetBoundingSphereCount(1);
}
}
Now, you must select a radius that works for your player, take the right position at its center and so on. You get the idea.
And you can do many more things there, like receiving visibility updates instead of polling. Or you can also pack more bounding spheres into the array to reduce the overhead of running your bounding spheres.
The key takeaway is: you can often skip certain behaviors while your object isn’t being rendered. Figure out what expensive scripts you don’t have to run on invisible objects and your users’ hardware will thank you for that.
Discover step-by-step how the CullingGroup API works in practice (with examples) in PerformanceTaskforce’s lesson “2021.10”:
→ https://www.PerformanceTaskforce.com ←
All in all, a solid performance tool to have in your toolbox.
Till next time,
Ruben (The Gamedev Guru)