Hello and welcome back to the Blightmare dev blog! Today's post revisits the topic of culling - first discussed here. It turns out that there were some problems with the initial implementation, and I finally got around to fixing them. I'll start by discussing the problems that need to be addressed and then we'll move on to the solution.
The previous culling implementation made use of the built-in CullingGroupAPI which is based around bounding spheres. One of the problems that we had with this is related to shadows. Some of the levels in Blightmare have quite a bit of vertical space to them which is a great part of the game, and there's no reason that the design should change to accommodate a graphics optimization (at least in this case). The problematic scenario is when a shadow caster is high enough that it ends up being culled, but its shadow still intersects the visible portion of the screen. In the worst cases, this results in a shadow flicker every time Blissa jumps, which is definitely not what we want. Here's a picture of the problem (green is the culling bounds of the shadow caster, blue is the camera bounds):
Alright, so now that we know what the problem is, the next step is to figure out a plan for how to fix it. A simple solution would be to just increase the size of the bounds of the shadow casters so that they aren't culled, but that not only still has some upper limit of the height that a level can be, and we won't be culling many more things that we could. The more complicated solution is to implement our own culling that supports different culling shapes. In this case, we just want to cull based on the x coordinates instead of a sphere so that we never cull things above us. This does result in some things not being culled that may be way above us that don't cast shadows, but if that becomes a problem, we can solve that directly instead of losing correctness.
Another bonus of writing our own culling system is that we can provide exactly the API that is convenient for our code which saves a bit of bookkeeping. The only thing left to do is to just write the code. It turns out that the code is not very complicated, so here is the whole thing:
public class CullingManager { private enum Visibility { Unknown, Visible, NotVisible, } private struct CullingData { public int Handle; public Visibility Visibility; public Vector2 Extents; public Action<bool> OnVisibilityChanged; } private CullingData[] _data; private int _activeCount; private int _nextHandle; private Dictionary<int, int> _handleTable; public CullingManager(int size) { _data = new CullingData[size]; _activeCount = 0; _nextHandle = 1; _handleTable = new Dictionary<int, int>(); } public int RegisterCulling(BoundingSphere bounds, Action<bool> onVisibilityChanged) { int handle = _nextHandle++; int index = _activeCount++; _data[index].Handle = handle; _data[index].Visibility = Visibility.Unknown; _data[index].Extents = new Vector2(bounds.position.x - bounds.radius, bounds.position.x + bounds.radius); _data[index].OnVisibilityChanged = onVisibilityChanged; _handleTable[handle] = index; return handle; } public void UnregisterCulling(int handle) { int index = _handleTable[handle]; // Only shuffle if we're removing an entry other than the last one if (index < _activeCount - 1) { // Swap the hole to the end _data[index] = _data[_activeCount - 1]; _handleTable[_data[index].Handle] = index; } _activeCount--; _handleTable.Remove(handle); } public void Cull(Camera camera) { float cameraViewHalfWidth = camera.orthographicSize * camera.aspect; Vector3 cameraPosition = camera.transform.position; float minX = cameraPosition.x - cameraViewHalfWidth - 1; float maxX = cameraPosition.x + cameraViewHalfWidth + 1; for (int idx = 0; idx < _activeCount; ++idx) { bool visible = (_data[idx].Extents.y >= minX) && (maxX >= _data[idx].Extents.x); Visibility visibility = visible ? Visibility.Visible : Visibility.NotVisible; if (_data[idx].Visibility == Visibility.Unknown || visibility != _data[idx].Visibility) { _data[idx].OnVisibilityChanged(visible); } _data[idx].Visibility = visibility; } } }
And... that's it! There's a little bit of fudging that has to happen on the bounds of shadow casters to account for the angle of the shadow, but that's very manageable. Now there's no shadow artifacts, and the solution may actually be slightly more efficient because it's designed to directly fit into Blightmare in a good way.
Thanks for stopping by! Be sure to check out Blightmare on Steam and visit our Twitter for all the latest updates. Have a great week!