Discover what happens when occlusion culling is enabled

With occlusion culling enabled, is there a way to discover if a drawn object was culled or not?

I am aiming to use the engine to display a very large environment which is designed to take advantage of occlusion culling. It is divided logically into cubic cells and the environment is primarily “underground” or enclosed in some form such as caves, caverns, rooms and corridors etc (the graphics are free form, not necessarily following the cubic data structure). The cells have pointers to adjacent cells which are set only if the content allows a view through to that cell.

The aim is to load cells into memory, either from local files or downloading on demand from a server. A cell which currently is not loaded would be drawn simply as a black cube (in a drawing order which takes advantage of occlusion culling). Where an empty cell like this is not culled then it knows it needs to obtain the data (accepting that temporarily a black cell will show on screen for several frames until it is populated, where these cells are far away this will be obscured in fog). Where a cell is culled then whether or not it contains data, it knows that the cell may be unloaded. (if the current drawing rate is sufficient then it may choose not to unload occluded cells). Where a cell is actually drawn, if it has “pointers” to so far unconsidered cells then if the drawing rate allows it will allocate the new cells. If the drawing rate is near slowing then it will deallocate the cells least recently drawn to make room for cells that are likely to be drawn soon.

So, this functionality depends on knowing the result of occlusion culling.

Many thanks!

It is not available yet, but it could be easily added. Are you interested in information whether the whole TCastleScene was culled or not, or a particular shape within that scene?

The occlusion query works per-shape, so we can expose this information for each shape (this would be within Scene.Shapes tree) or for whole scene (whether it was at least partially visible – so whether any shape was at least partially visible). Or both :slight_smile:

Hi Michalis,

The “cells” I am talking of each use an IndexedFaceSet mesh for the static geometry, so I suppose per-shape data would work. It wouldn’t matter if I received multiple notifications per cell, I could deduplicate them before retrieving cell data.

If implementing both wouldn’t be a problem, I suppose that would be the most flexibility for your engine. I imagine it could be done by assigning an event for whatever is needed. If the “user” is interested in TCastleScenes then they assign an event that listens at that level. And a corresponding event for TShapes.

But as long as the containing TCastleScene can be found from a returned TShape, then all of the available data is exposed anyway, and the receiver just has to decide how they deal with duplicate hits per TCastleScene.

I would be grateful for whichever way you implement it!

Could you do that please?

@DSK Done, with a demo! :slight_smile:

Committed to GitHub in Implement TCastleScene.WasVisible, TAbstractShapeNode.WasVisible, with · castle-engine/castle-engine@8adafca · GitHub . You can either wait a few hours until the binary build of CGE is updated, or get CGE source code and recompile it now following Compiling from source | Castle Game Engine .

New methods:

  • TCastleScene.WasVisible, docs:
    { Whether the scene was (potentially, at least partially) visible in the last rendering event.

      The "was visible" means that "some shape was visible", that is:
      some shape passed frustum culling and occlusion culling (see https://castle-engine.io/occlusion_query )
      tests.

      For this method it doesn't matter if the scene contains some lights
      that only make some other scenes brighter. Or if the scene contains some background
      that affects TCastleViewport skybox. Only the visibility of shapes matters.

      If this scene instance is used multiple times within some viewport,
      or when multiple viewports render the same scene,
      then it is enough that at least one shape in one of the scene instances
      was visible last frame.  }
    function WasVisible: Boolean;
  • TAbstractShapeNode.WasVisible, docs:
    { Whether the shape was (potentially, at least partially) visible in the last rendering event.

      Note that the same shape reference may be present multiple times
      within one scene.
      Moreover, the same scene may be present multiple times within one viewport.
      Moreover, multiple viewports may render the same scene.
      The shape is marked as "was visible" if any instance of it was visible
      in any viewport in any scene. }
    function WasVisible: Boolean;



Note some caveats:

  • There are no events, like OnShow / OnHide. Instead I exposed methods WasVisible, and you have to query them as often as you need to detect changes.

    This made it simpler to implement, as should be good enough for many use-cases. Engine has optimizations that allow to completely avoid rendering things that are not visible (e.g. whole scene rendering will be ignored if frustum culling is used and scene bounding box is not inside frustum, in which case we don’t even iterate over shapes). So if we wanted to have events, we’d need to add additional loops that iterate over “watched” scenes/shapes (with some OnShow/OnHide registered) and generate OnShow / OnHide when their WasVisible changed.

    It seems simpler and more flexible to avoid this loop on the engine side, and delegate to user code the job of querying WasVisible (as often as you need, for the scenes/shapes you are interested in).

  • In the end I didn’t use TShape to store / query this information, contrary to what I said earlier, as TShape and Scene.Shapes tree is harder to access, and it may be rebuild in case of some changes to scene graph.

    Instead I placed WasVisible in TAbstractShapeNode (ancestor of TShapeNode), this is more stable, as this node is guaranteed to remain persistent across scene modifications.

    I am also considering to move the TShape and Scene.Shapes to internal engine parts. Most code should just use nodes, TShapeNode and TAbstractShapeNode, not TShape.

1 Like

This is the perfect response!

I agree with what you say, it makes absolute sense to delegate the iteration to the user code as this will depend on the “use case”, rather than the engine needlessly iterating on everything.

Thank you, Michalis!

1 Like