- Home /
OnBecameInvisible in the Editor?
Does anyone happen to know of a way to deal with on became invisible in the editor? It's a massive pain to relocate my scene camera anytime I want to play the game to make sure I get the correct camera calls. Is there a generally accepted way to handle this particular problem? Is there another "OnWontRender" callback that is called anytime a camera stops rendering an object and not just when no cameras will render the object?
Answer by Bunny83 · Jul 02, 2017 at 04:56 AM
No, there is no real replacement for that callback that does not take the sceneview camera into account. The sceneview camera is just a normal camera, just that it is hidden in the hierarchy.
Depending on what you want to achieve you may be able to use OnWillRenderObject which is called for every camera that is about to render the object. Of course you don't get a call if an object is not rendered by a camera.
However you could manually test if an object is inside the camera frustum. You could use Unity's CalculateFrustumPlanes as well as TestPlanesAABB. However CalculateFrustumPlanes allocates memory for the Plane array.
Instead you could use this extension i wrote:
// VisibilityTools.cs
using UnityEngine;
public enum EFrustumIntersection
{
Outside,
Inside,
Intersecting
}
public struct Frustum
{
public Vector4 l;
public Vector4 r;
public Vector4 b;
public Vector4 t;
public Vector4 n;
public Vector4 f;
public Plane left { get { return new Plane { normal = l, distance = l.w }; } }
public Plane right { get { return new Plane { normal = r, distance = r.w }; } }
public Plane bottom { get { return new Plane { normal = b, distance = b.w }; } }
public Plane top { get { return new Plane { normal = t, distance = t.w }; } }
public Plane near { get { return new Plane { normal = n, distance = n.w }; } }
public Plane far { get { return new Plane { normal = f, distance = f.w }; } }
public Vector4 this[int aIndex]
{
get
{
switch(aIndex)
{
case 0: return l;
case 1: return r;
case 2: return b;
case 3: return t;
case 4: return n;
case 5: return f;
default: throw new System.ArgumentOutOfRangeException("aIndex", "value must be between 0 and 5");
}
}
}
public EFrustumIntersection TestBounds(Bounds aBounds)
{
var min = aBounds.min;
var max = aBounds.max;
var x = new Vector2(min.x, max.x);
var y = new Vector2(min.y, max.y);
var z = new Vector2(min.z, max.z);
var res = EFrustumIntersection.Inside;
for (int i = 0; i < 6; i++)
{
var plane = this[i];
int xi = plane.x > 0 ? 1 : 0;
int yi = plane.y > 0 ? 1 : 0;
int zi = plane.z > 0 ? 1 : 0;
if (plane.x * x[xi] + plane.y * y[yi] + plane.z * z[zi] + plane.w < 0)
return EFrustumIntersection.Outside;
if (plane.x * x[1-xi] + plane.y * y[1-yi] + plane.z * z[1-zi] + plane.w < 0)
res = EFrustumIntersection.Intersecting;
}
return res;
}
public EFrustumIntersection TestPoint(Vector3 aPoint)
{
for (int i = 0; i < 6; i++)
{
var plane = this[i];
float d = plane.x * aPoint.x + plane.y * aPoint.y + plane.z * aPoint.z + plane.w;
if (d < 0)
return EFrustumIntersection.Outside;
}
return EFrustumIntersection.Inside;
}
}
public static class VisibilityTools
{
public static Frustum GetFrustum(this Camera aCam)
{
var m = aCam.projectionMatrix * aCam.worldToCameraMatrix;
Frustum res;
var x = m.GetRow(0);
var y = m.GetRow(1);
var z = m.GetRow(2);
var w = m.GetRow(3);
res.l = GetNormalizedPlane(w + x);
res.r = GetNormalizedPlane(w - x);
res.b = GetNormalizedPlane(w + y);
res.t = GetNormalizedPlane(w - y);
res.n = GetNormalizedPlane(w + z);
res.f = GetNormalizedPlane(w - z);
return res;
}
private static Vector4 GetNormalizedPlane(Vector4 aVec)
{
return aVec*(1f / Mathf.Sqrt(aVec.x * aVec.x + aVec.y * aVec.y + aVec.z * aVec.z));
}
}
It basically provides the same functionality as Unity's methods but uses a struct with 6 plane definitions instead of an array. It also differentiates between outside, inside and intersecting which might also be useful in some cases. Note that the check is based on the axis aligned bounding box you provide (which you usually get from "Renderer.bounds"). That AABB usually is larger than the actual object. Though Unity uses the same for it's visibility check.
To use this extension, just place this "VisibilityTools" script into your project. This allows you to test the visibility of an object against a specific camera. If you need to test multiple objects, make sure you "cache" and reuse the frustum definition you get from GetFrustum. Though keep in mind whenever the camera moves, rotates or is changed in any way you need to refresh the frustum. There's no problem refreshing the frustum once every frame. Though you might want to avoid calculating it several times within a single frame.
Renderer someObject;
Frustum f = Camera.main.GetFrustum();
if (f.TestBounds(someObject.bounds) == EFrustumIntersection.Outside)
{
// "someObject" is not rendered by the main camera
}
As additional note: the normals of the frustum planes point "inwards". That means a given point is inside the frustum when the point is on the "positive side" of each plane. Likewise if a point is behind any of the 6 planes(on the negative side) the point is outside.
As alternative when you only worry about the sceneview camera to disturb your testing inside the editor you can use an editor script like this which allows you to easily disable / enable all sceneview cameras:
// EditorCameraControls.cs
using UnityEngine;
using UnityEditor;
public class EditorCameraControls : EditorWindow
{
[$$anonymous$$enuItem("Tools/EditorCameraControls")]
private static void Init()
{
GetWindow<EditorCameraControls>();
}
int state = -1;
bool autoDisable = false;
private void OnEnable()
{
bool allOff = true;
bool allOn = true;
foreach (var cam in SceneView.GetAllSceneCameras())
{
allOff &= !cam.gameObject.activeSelf;
allOn &= cam.gameObject.activeSelf;
}
if (allOn && !allOff)
state = 1;
else if (!allOn && allOff)
state = 0;
else
state = -1;
EditorApplication.playmodeStateChanged += OnPlaymodeChange;
}
private void OnDisable()
{
EditorApplication.playmodeStateChanged -= OnPlaymodeChange;
}
void SetCamState(bool aState)
{
foreach (var cam in SceneView.GetAllSceneCameras())
{
cam.gameObject.SetActive(aState);
}
state = aState?1:0;
Repaint();
}
private void OnGUI()
{
if (state == 1)
GUI.color = Color.green;
else if(state == 0)
GUI.color = Color.red;
else
GUI.color = Color.yellow;
GUILayout.BeginHorizontal();
if (GUILayout.Button("Enable"))
{
SetCamState(true);
SceneView.RepaintAll();
}
if (GUILayout.Button("Disable"))
{
SetCamState(false);
}
GUILayout.EndHorizontal();
GUI.color = Color.white;
autoDisable = GUILayout.Toggle(autoDisable,"AutoDisable","Button");
}
void OnPlaymodeChange()
{
if (autoDisable)
{
SetCamState(!EditorApplication.isPlaying);
}
}
}
Just place the script in an "editor" folder in your project and open the window from the "Tools" menu. When you enable the "autodisable" toggle the window will automatically disable the sceneview cameras when you enter playmode and re-enable them when you stop playing.
Though keep in $$anonymous$$d while the sceneview camera is disabled the sceneview can't be redrawn properly. That means it can't be used while the cameras are disabled. Though you can press the "Enable" button in the window at any time to re-enable the cameras. Of course this would have an effect on the OnBecameVisible / Invisible callbacks.
Thanks for this awesome extension! FYI I think theres a typo here:
if (plane.x * x[xi] + plane.y * y[xi] + plane.z * z[zi] + plane.w < 0)
return EFrustumIntersection.Outside;
if (plane.x * x[1-xi] + plane.y * y[1-xi] + plane.z * z[1-zi] + plane.w < 0)
res = EFrustumIntersection.Intersecting;
I think you should be using y[yi] and y[1 - yi] ins$$anonymous$$d of the xi indices. Thanks again!
Yes, you are totally right ^^. It was a copy&paste error. I've fixed it. Thanks.
ps: there was another error in line 74. Originally i returned "Outside" which didn't make much sense.
Answer by FlaSh-G · Jul 01, 2017 at 09:58 AM
Put this at the beginning of your OnBecameInvisible:
#if UNITY_EDITOR
if(Camera.current && Camera.current.name == "SceneCamera") return;
#endif
The preprocessor directives around the actual line prevent this then-unneccessary test to be compiled into your build.
A more precise solution (not relying on the camera's name) would be to check if Camera.current
is referenced in the array returned by SceneView.GetAllSceneCameras()
.
This is not the issue. The issue is that the OnBecameInvisible callback is never fired if the scene camera can see the object. Functionally I guess it's fine because this behavior doesn't manifest in builds. However it makes testing in the editor extremely strange. I basically need to unfocus the scene view or else my camera logic code will break.
Is this just not the correct way of monitoring drawn objects for game logic? Are you really mean to check the cameras AABB or monitor isVisible on individual objects every frame?
Answer by Hexkonst · Oct 26, 2019 at 01:53 PM
Vector3 viewPos = Camera.main.WorldToViewportPoint(this.transform.position);
if (viewPos.x >= 0 && viewPos.x <= 1 && viewPos.y >= 0 && viewPos.y <= 1)
{
hasBeenVisible = true;
}
else
{
if(hasBeenVisible) Destroy(gameObject);
}