Visualize frustrum culling in Editor

Hi,

Working on a simple 2D game, i’m thinking of using Unity’s existing culling for enabling/disabling some additional features on my objects, when they are off-screen (using Renderer’s OnBecameVisible and OnBecameInvisible).

However it is hard to visualize, as whatever leaves the camera’s frustrum during playmode is automatically culled, but i can not see the effect (since its off screen - out of camera’s frustrum). This is in the “Game” window.

Now lets hop into the “Scene” window during playmode. We have 2 cameras here, the “main” scene camera (or the “active” camera), that we added into an empty scene. This is the camera that shows what we see in Game window. And then there is the camera that we are “looking through” into the scene. Lets call this “Editor camera”.

My question is:
Is there a way to visualize frustrum culling in Editor?
Example: Editor camera looks at scene main camera that culls objects (it can see it’s cone of vision, lets call this “view rect” [we’re in 2D]). Editor camera can see the view rect of the active scene camera (Unity supports this already), and can see how objects which leave the frustrum of the active camera - how their Renderers - are disabled - how they “disappear” - so that at that point the Editor camera can’t see them! (i dont see Unity being able to do this atm? What am i missing?)

Anything like this? Any techniques? Im sure this is possible, or there is some kind of BEST PRACTICE to test frustrum culling in Unity already, as i’ve seen some pictures that demonstrate this…

UPDATE: after some more reasearch, i’ve discovered, that what i want is similiar to visualization of “occlusion culling” that Unity does for 3D/MeshRenderers (involves baking the information, more here: Unity - Manual: Occlusion culling). However we can not use the occlusion culling visualization for SpriteRenderers (so not for Sprites). I’ve also found 4 more unanswered questions in spirit of “how to visualize occlusion culling for SpriteRenderer”. So it seems that - atm in editor - for simple 2D sprites - we can not “visualize” frustrum culling, neither disable it to implement our own (+visualization).

However i’ve also found that by increasing camera’s FOV (assume .size in case of 2D/orthographic) in Camera.onPreCull and setting it back to default in Camera.PreRender, i might be able to “fake” what i need (play mode only). Ill leave this question open, until i test the solutions provided, including the one i just outlined, and close it once i’ve found one that suits best the use-case above.

UPDATE2: SOLUTION1:
Apart from solution posted below by @Bunny82 (SOLUTION2) (which is awesome), i’ve decided to post my quick hack, using the FOV switch mentioned above. This allows to visualize frustrum culling for a specific camera inside editor’s “Game” window (using the [ExecuteInEditMode] directive).

Here is a screen:

And here is the code:

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

[ExecuteInEditMode]
public class VisualizeCulling : MonoBehaviour
{
    /// <summary>
    /// camera to use for frustrum culling visualization
    /// </summary>
    public Camera targetCamera;

    /// <summary>
    /// Original target camera size (if target camera is ortographic/2D), or FOV (if target camera is perspective/2D)
    /// NOTE: target camera size will be enlarged from originalCameraSize to cullingCameraSize during culling visualization
    /// </summary>
    private float originalCameraSize;

    /// <summary>
    /// Camera size to use during frustrum culling visualization
    /// This should be higher value than the target camera's size/FOV
    /// NOTE: target camera size will be enlarged from originalCameraSize to cullingCameraSize during culling visualization
    /// </summary>
    public float cullingCameraSize;

    /// <summary>
    /// During frustrum culling visualization,
    /// the original target camera size is visualized using semi-transparent debug rectangle
    /// - only for 2D cameras
    /// </summary>
    public Color originalCameraSizeDebugColor = new Color(1, 0, 0, 0.5F);
    
    /// <summary>
    /// Disable this script by default;
    /// Let user set the inspector values first and enable manually after;
    /// </summary>
    public void Awake()
    {
        enabled = false;
    }

    /// <summary>
    /// Enable script to re-initialize with inspector-set values
    /// </summary>
    public void OnEnable()
    {
        if (CheckCamera()) {
            CheckCullingSize();

            // register delegates for culling visualization
            Camera.onPreCull += PreCullAdjustCamSize; // shrink
            Camera.onPreRender += PreRenderAdjustCamSize; // back to default
        }
        
            EditorApplication.playmodeStateChanged += StateChange;
    }

    public void OnDisable()
    {
        // set camera size back to default size
        if (targetCamera != null) {
            if (targetCamera.orthographic) {
                targetCamera.orthographicSize = originalCameraSize;
            }
            else {
                targetCamera.fieldOfView = originalCameraSize;
            }
        }
        
        // unregister delegates
        Camera.onPreCull -= PreCullAdjustCamSize;
        Camera.onPreRender -= PreRenderAdjustCamSize;
    }
    
    private void StateChange()
    {
        // app will change playMode
        if (EditorApplication.isPlayingOrWillChangePlaymode) {
            if(!EditorApplication.isPlaying) {
                // will play;
                // disable during play
                enabled = false;
            }
        }
    }
    
    /// <summary>
    /// Before Camera culling, set camera size to default/original size
    /// </summary>
    /// <param name="cam"></param>
    public void PreCullAdjustCamSize(Camera cam)
    {
        if (cam == targetCamera)
        {
            if (cam.orthographic) {
                cam.orthographicSize = originalCameraSize;
            }
            else {
                cam.fieldOfView = originalCameraSize;
            }
        }
    }
    
    /// <summary>
    /// Before Camera rendering, set camera size to culling size (bigger than original)
    /// </summary>
    public void PreRenderAdjustCamSize(Camera cam)
    {
        if (cam == targetCamera)
        {
            if (cam.orthographic) {
                cam.orthographicSize = cullingCameraSize;
            }
            else {
                cam.fieldOfView = cullingCameraSize;
            }
        }
    }

    /// <summary>
    /// Use defaults, if user doesnt specify inspector values
    /// </summary>
    private bool CheckCamera()
    {
        // if targetCamera was not defined via inspector
        if (targetCamera == null) {
            // Then use main camera
            if (Camera.main != null) {
                targetCamera = Camera.main;
            }
            // If no camera is tagged as MainCamera
            else {
                Debug.LogError("VisualizeCulling: OnEnable->CheckCamera: ERROR: Please set target camera via inspector to enable culling visualization!");
                return false;
            }
        }

        // if main camera is orthographic/2D
        if (targetCamera.orthographic) {
            originalCameraSize = targetCamera.orthographicSize;
        }
        // else its perspective
        else {
            originalCameraSize = targetCamera.fieldOfView;
        }

        return true;
    }

    /// <summary>
    /// If user doesnt supply camera size for culling, or sets wrong culling size,
    /// use mult of original camera size as default culling size
    /// </summary>
    private void CheckCullingSize()
    {
        if (cullingCameraSize <= originalCameraSize) {
            cullingCameraSize = originalCameraSize * 1.5f;
        }
    }
    
    /// <summary>
    /// Draw debug viewport rect of targetCamera's original size, while culling is enabled
    /// - Actual Camera size is enlarged during culling
    /// - Only for 2D cameras
    /// </summary>
    void OnDrawGizmos()
    {
        if (targetCamera != null && targetCamera.orthographic) {
            Gizmos.color = originalCameraSizeDebugColor;
            Gizmos.DrawCube(transform.position, new Vector2(originalCameraSize * 2 * targetCamera.aspect, originalCameraSize * 2));
        }
    }
}
#endif

Usage:
Simply attach the class to any GameObject in your scene. Then set the culling camera size - this value should be greater than your target camera FOV (if your camera is 3D camera) or ortographic size (if your camera is 2D camera). To illustrate what this value means, just imagine another camera looking at your target camera, with same attributes, only “greater”. The bigger that number, the more you will be able to see “outside” of your target camera. One more variable to set via inspector - targetCamera - just drag and drop your target camera here, for which you want to visualize culling. The script will try to auto-correct the settings, in case you mess something up, so no worries there. Once you are done setting the variables, enable the script (it should be disabled by default).

Cheers!

You may want to use this custom frustum intersection test i’ve just implemented. The built-in frustum culling in Unity always uses all active cameras which includes the SceneView camera. So there is no way to visually “see” something being culled as it’s only culled when it isn’t seen by any camera.

With my solution you can manually perform a frustum test between a camera / object pair.