Opposite of OnWillRenderObejct?

So I thought I solved my previous problem of figuring out if an object is visible. I used isVisible and turned off both casting and receiving of shadows for that object, so it wouldn’t be rendered along with Occlusion Culling.

What I didn’t realize, is that I forgot to actually test for it being fully obstructed. And when I did it today, it still triggers the relevant code.

I found out about “OnWillRenderObejct” which if I’m reading the documentation right, is exactly what I need. But I don’t know how to activate code if the opposite is true (if the object will not render).

Anyone any ideas?

Try this …
http://www.dras.biz/download/Occluder/Occluder.unitypackage
from here: Check wheter something is visible for the main camera? - Unity Engine - Unity Discussions

I didn’t read it all or really test it myself (I ran the scene for 2 mins and it appeared to be doing something right). Beyond that, and what you can use to make it work in your own game, I didn’t get that far.

Other than that, I tried for a while to setup OnWillRenderObject and despite what the docs say about culling, it seemed in my code I could not get it to work, and it always said it was visible. It’s possible my code was bad, but I saw this:

that had the same problem I found in my tests.

lol all in all wish ya good luck. That sample project I linked above seemed to decently remove the little spheres as I moved around, so I reckon there’s a way for you to get what you want out of it. :slight_smile: Cheers.

I’ve noticed that you been make several posts recently about if an object is visible and whatnot…

have you ever considered using the CullingGroup API?

it allows you a direct line into the rendering pipeline with occlusion and frustum culling.

I realize that the CullingGroup doesn’t give you anything you can immeadiately use so heres some code I wrote from a previous project that should help get you started.

API stuff

public interface ICullingGroupListener
{
    BoundingSphere BoundingSphere{get;}
    void WhenStateChanges(CullingGroupEvent sphere);
}
public ActionEvent: UnityEvent{}
public IntEvent: UnityEvent<int>{}

Culling Group Listener

    [AddComponentMenu("Culling Group")]
    public class CullingGroupListener: MonoBehaviour, ICullingGroupListener
    {
        public CullingGroupData CullingGroup;

        public float radius;
        protected int currentBand = -1;

        public IntEvent    OnDistanceBandChange = new IntEvent();
        public ActionEvent OnVisible            = new ActionEvent();
        public ActionEvent OnInvisible          = new ActionEvent();

        #region ICullingGroupListener implementation

        public virtual BoundingSphere BoundingSphere
        {
            get
            {
                BoundingSphere sphere;
                sphere.position = transform.position;
                sphere.radius = radius;
                return sphere;
            }
        }

        public virtual void WhenStateChanges(CullingGroupEvent sphere)
        {
            if(sphere.hasBecomeVisible)    OnVisible.Invoke();
            if(sphere.hasBecomeInvisible) OnInvisible.Invoke();

            if(sphere.currentDistance != currentBand) OnDistanceBandChange.Invoke(sphere.currentDistance);

            currentBand = sphere.currentDistance;
        }

        #endregion

        protected virtual void OnEnable()
        {
            if(!CullingGroup) return;

            CullingGroup.AddSphere(this);
        }
        protected virtual void OnDisable()
        {
            if(!CullingGroup) return;

            CullingGroup.RemoveSphere(this);
        }

        protected virtual void OnTransformParentChanged()
        {
            if(!CullingGroup) return;
            CullingGroup.UpdateSphere(this);
        }
        protected virtual void OnTransformChildrenChanged()
        {
            if(!CullingGroup) return;
            CullingGroup.UpdateSphere(this);
        }

        protected virtual void Update()
        {
            if(!transform.hasChanged) return;
            if(!CullingGroup) return;

            CullingGroup.UpdateSphere(this);
        }

    }

CullingGroupData

    [CreateAssetMenu(menuName = "ScriptableObject Asset/Culling Group")]
    public class CullingGroupData : ScriptableObject
    {
 
        private CullingGroup m_group;
        public int MaximumBoundingSpheres=100;
        public float[] BoundingDistances={1f,10f,50f};
        private BoundingSphere[] spheres;
        private ICullingGroupListener[] listeners;
        private int sphereCount = 0;
        private Dictionary<ICullingGroupListener,int> sphereMap = new  Dictionary<ICullingGroupListener,int>();
        private List<int> freeIndexes = new List<int>();
 
        private void OnEnable()
        {
            spheres = new BoundingSphere[MaximumBoundingSpheres];
            listeners = new ICullingGroupListener[MaximumBoundingSpheres];
     
            m_group = new CullingGroup();
            m_group.onStateChanged += WhenStateChanges;
            m_group.SetBoundingSphereCount(sphereCount);
            m_group.SetBoundingDistances(BoundingDistances);
            m_group.SetBoundingSpheres(spheres);
            m_group.SetDistanceReferencePoint(Vector3.zero);
        }
 
        virtual protected void OnDisable()
        {

            m_group.onStateChanged -= WhenStateChanges;
            sphereCount = 0;
            sphereMap.Clear();
            freeIndexes.Clear();
            m_group.Dispose();
            m_group = null;
        }
 
 
        public void Pause()             { m_group.enabled = !m_group.enabled; }
        public void Pause(bool enabled) { m_group.enabled = enabled; }
 
 
        public void AddSphere(ICullingGroupListener listener)
        {
            if(sphereMap.ContainsKey(listener))//if listener already in group then update the bounding sphere data
            {
                spheres[sphereMap[listener]] = listener.BoundingSphere;
                return;
            }
     
            if(sphereCount++ == spheres.Length)
            {
                MaximumBoundingSpheres += Mathf.Max(100, Mathf.FloorToInt(spheres.Length * 0.5f));
                var newSpheres  = new BoundingSphere[MaximumBoundingSpheres];
                var newListeners = new ICullingGroupListener[MaximumBoundingSpheres];
         
                spheres.CopyTo(newSpheres,0);
                listeners.CopyTo(newListeners,0);
                spheres = newSpheres;
                listeners = newListeners;
                m_group.SetBoundingSpheres(spheres);
            }
     
            int targetIndex = sphereCount-1;
            if(freeIndexes.Count>0)
            {
                targetIndex = freeIndexes[freeIndexes.Count-1];
                freeIndexes.RemoveAt(freeIndexes.Count-1);
            }
            else
            {
                m_group.SetBoundingSphereCount(sphereCount);
            }
     
            spheres[targetIndex] = listener.BoundingSphere;
            listeners[targetIndex] = listener;
            sphereMap.Add(listener,targetIndex);
        }

        public void UpdateSphere(ICullingGroupListener listener)
        {
            int index;
            if(!sphereMap.TryGetValue(listener, out index))
            {
                AddSphere(listener);
                index = sphereMap[listener];
            }

            spheres[index] = listener.BoundingSphere;
        }
 
        public void RemoveSphere(ICullingGroupListener listener)
        {
            int index;
            if(!sphereMap.TryGetValue(listener,out index))
            {
                // if listener not in group nothing needs to be done
                return;
            }
            sphereMap.Remove(listener);
            listeners[index] = null;
            sphereCount--;
     
     
            if(sphereCount>index)
            {
                freeIndexes.Add(index);
            }
            else
            {
                m_group.SetBoundingSphereCount(sphereCount);
            }
     
        }
        public void SetDispatcher(Transform dispatcher)
        {
            m_group.SetDistanceReferencePoint(dispatcher);
        }
        public void SetCamera(Camera targetCamera) { m_group.targetCamera = targetCamera; }
        public void SetCamera(GameObject targetCamera)
        {
            if(!targetCamera)
            {
                Debug.LogWarning("Target Camera Gameobject is null. Setting Target Camera for Culling Group to null");
                m_group.targetCamera = null;
                return;
            }

            Camera cam = targetCamera.GetComponent<Camera>();
            if(!cam)
            {
                Debug.LogWarning("Target Gameobject does not have a Camera Component. Setting Target Camera for Culling Group to null");
                m_group.targetCamera = null;
                return;
            }
            m_group.targetCamera = cam;
        }
        public void SetToCurrentCamera()
        {
            m_group.targetCamera = Camera.main;
        }
 
        private void WhenStateChanges(CullingGroupEvent sphere)
        {
            ICullingGroupListener listener = listeners[sphere.index];
     
            if(listener == null) return;
     
            listener.WhenStateChanges(sphere);
        }
 
    }

one limitation of the CullingGroup is that it doesn’t support Dynamic Occluders. just against objects you flag as static Occluder (in the static dropdown next to the Layers dropdown for an object).

If you need Dynamic Occlusion you should look into Occlusion Culling and mess with the Occlusion Area component

1 Like

Great answer. I saw (even kinda tried both of those), but not nearly well enough, it would seem.
Hopefully that helps him out. :slight_smile:

I’m getting a lot of “could not be found. Are you missing an assembly reference” errors.

Do I need to implement the “API stuff” part into those two scripts? If so, how (where to put them)?

It’s honestly crazy how something as simple as “check if this is visible” can be so complex and hard to implement. Specifically while taking occlusion into consideration.

Those could go in any script – just not inside a class. (re: the API stuff). They’re declaring 1 interface & 2 classes.

When I put it outside of a class, “ActionEvent” underlines and I’m getting the “Unexpected symbol” error.

Pretty sure it should be:

// if you didn't have this already..
using UnityEngine.Events;
// minor correction:
public class ActionEvent: UnityEvent{}
public class IntEvent: UnityEvent<int>{}

That did it.

Should I be putting this in both of the scripts?
Because I did and now I’m getting three new errors.

Also, once the errors are fixed, what exactly am I supposed to do with those scripts? Like what do I attach them to? Do I reference them in stuff? etc.

I feel like I’m just being given bags of cement, and told “make a skyscraper”, while not being given any blueprints. I don’t mean to be rude, but I have no idea what to do with these two scripts to get what I need out of the API. Also, I have absolutely no experience in using APIs (other than the standard one). So giving me something of this scale, expecting me to be able to make something out of it, is like giving a pilgrim an iPhone and expecting them to not call you a witch and accuse you of sorcery.

no. you define them once I didn’t write them twice…

Okay, removed it from one of the scripts, and now all the errors are gone… Now what? As I said, I have absolutely no clue what I’m supposed to do with these scripts now.

I found this old thread, and a Unity employee is basically stating that “isVisible” only returns true if the object is used during rendering. I have been wrestling with “isVisible” for a while, and I already thought that was the case, even before finding the thread.

The reason I wasn’t 100% sure about it, is because even though I have receiving and casting shadows off for my object, and it is being culled, it is still causing “isVisible” to return true. Any idea what might be wrong?

Because if I get this working, then it might just be what I need. Instead of having to dive into APIs.

EDIT: Just realized my object isn’t even being culled properly. It’s only culled when I look away, but when I look at the giant wall (cube object) covering it, it appears.

Yeah, you’re looking for Occlusion Culling. You get Frustum Culling out of the box cause the math for that is simple (by comparison). Occlusion Culling on the other hand is computationally expensive to do so it needs to be manually setup and pre-calculated and cached (the term for this is “Bake”) in the editor. This way its quick and smooth at run-time.

You’re not going to get Occlusion Culling (with renderers, Culling Groups, whatever) straight out of the box without first doing some baking. Please understand its a ton of math it has to do. its inconceivable for it to attempt such calculations at run-time.

You will need to dive into the API. You will need to define Static Occluders, and Static Occludees. You will need to set up Occlusion Areas (so unity knows where to focus its math).

But I can’t define static occludees, since the object is going to move.

read closely from the Occlusion Culling page

Moving objects can be culled. Static Occludees are just cheaper to cull so you should flag them when possible.

Moving Objects cannot be occluders (they can’t cull other objects) at least as far as I’m aware.

1 Like

So what would be the advantage of using the API over Occlusion Culling in that case?

When I read your previous reply, I understood it as “it’s not possible to cull moving objects, as it would be increadibly performance heavy”.

Welp, I got it working finally. Had to remove every cache file with Command Prompt, and just re-baked it, and made sure to mark the object only as “Occludee Static”.

I’d still love to hear about the API. I usually try to avoid getting into things way out of my experience level, but I’m just curious as to the advantages and such, of using the API.

The Culling Groups don’t require a renderer, nor a Transform nor a GameObject, just a point in space and a radius. plus they implement Distance Bands which is typically used for LOD, but you can hook into it for other means.

With the exception of occlusion culling the other features work immediately without the need of baking. You can subdivide portions of a GameObject into multiple bounding spheres and share those spheres over multiple cameras and set up a complex visibility result. You can end up with some unique effects.

one example is the Zelda “Lens of Truth” effect, where an object is only interactive when seen with a special item and within a certain distance band. CullingGroups provide much more control with behavior than OnBecameVisible/OnBecameInvisible, however those two functions have the benefit of working without the need of extra setup.

Also Culling Groups can be generated at run-time.

and no my previous reply said its expensive, so it needs to be baked and cached.

2 Likes

Hi, sorry for bumping this old thread, but it seems that you know what you are talking about.

I’m trying to implement custom LODSystem for Unity Renderers based on distance using culling groups. Only way I have found is to hook to Camera.OnPreCull and Camera.OnPostRender and update visibility there.

In Camera.OnPreCull step I use computed visibility to disable/enable renderers for defined LODs
In Camera.PostRender step restore renderers previous state for next camera that may or may not implement LODSystem

Problem is that disable/enable renderer generate a lot off subsequent garbage (mainly from AnimatorControllers). I have also tried to modify render.gameObject.layer to use benefits of layerCullingMask, but this approach didn’t work at all.Do you know about any faster approach how to modify culling result for specified camera?

Thanks