Release: Sections Occlusion System 1.00

A simple free plug-and-play sections/portals system to define volumes of visibility. It helps to occlude entities and increase performance when they aren’t visible on screen, but still inside the camera frustrum.

How it works?
The system on initialization creates a bounding box for each occluder. On runtime it checks if the position of the camera is contained in the bounding boxes. If it is contained then all entities for that section are enabled.

How to setup?

  1. You attach the section-system.js script to a section entity and set up the tags that are used for this section.
  2. You have to create one or more occluder entities that have a box collision component defining the visibility areas. These entities have to be tagged accordinigly and be children of the section entity.
  3. You tag your occludees which are basically all the entities you would like to trigger their state for this section. These entities have to be children of the section entity.
  4. You tag your cameras which can be global for all sections. If they are active (enabled === true) they will be tested one after the other against the occluders. If a camera is inside an occluder all entities will be enabled for that section.

37

Where to use it in my project?

  • You can setup sections and gain a nice performance boost from the reduced draw calls and polycount. Combined with the new batching system it can help a lot, especially on mobile.
  • You can enable the broadcast property on the script and you will get app wide enabled/disabled events for a section. Which you can subscribe to from your scripts, like this:
this.app.on("Sections:Section Name:enabled", function(){
   // switch on the lights
}, this);
  • You can switch off the staticOccluders property for a section and be able to move it during runtime. The visibility checks will adapt accordingly.
  • You can configure the interval property of a section which sets how often it will trigger visibility checks.

Here is a public project example:
https://playcanvas.com/project/548975/overview

Enjoy!

section-system.js:

var SectionSystem = pc.createScript('sectionSystem');

SectionSystem.attributes.add('occluderTag', {
    type: 'string',
    default: 'section-occluder',
    description: 'Specify a tag used to identify the occluders in this section. These are the volumes that defines the visible parts of the section. Only entities with a box collision component are supported.'
});

SectionSystem.attributes.add('occludeeTag', {
    type: 'string',
    default: 'section-occludee',
    description: 'Specify a tag used to identify objects that will be occluded. These are the entities that their enabled state will be disabled as soon as a camera steps out of this section.'
});

SectionSystem.attributes.add('cameras', {
    type: 'string',
    default: 'section-camera',
    description: 'Specify a tag used to identify the cameras this section will work with. You can specify an arbitary list of cameras and only the enabled ones will be checked on each interval.'
});

SectionSystem.attributes.add('interval', {
    type: 'number',
    default: 0.3,
    description: 'Specify the interval in seconds on which this section will update. A smaller interval means more section checks per seconds, which might have a performance impact.'
});

SectionSystem.attributes.add('staticOccluders', {
    type: 'boolean',
    default: true,
    description: 'Specify if occluders are static. Otherwise their bounding box will be updated on each interval. This has a performance impact.'
});


SectionSystem.attributes.add('broadcast', {
    type: 'boolean',
    default: true,
    description: 'Fire an app-wide event on section state change.'
});


// initialize code called once per entity
SectionSystem.prototype.initialize = function() {
  
    
    // --- variables
    this.nullVec = new pc.Vec3();
    
    this.occludersList = undefined;
    this.boundingBoxes = undefined;
    this.occludeesList = undefined;
    this.camerasList = undefined;
    
    this.currentState = undefined;
    
    this.accumulator = 0.0;
    
    
    // populate lists once in start up
    this.populateLists();
    
    // set occludees state to disabled initially
    this.setOccludeState(false, false);
    

};


// update code called every frame
SectionSystem.prototype.update = function(dt) {


    this.accumulator += dt;
   

    // update only if the specified interval has passed
    if( this.accumulator >= this.interval ){

        this.accumulator = 0.0;
        
        
        if( this.staticOccluders === false ){
            this.updateOccluders();
        }

        this.checkAndOcclude();
    }

};


SectionSystem.prototype.populateLists = function(){
  
    
    // get entities by tag
    this.occludersList = this.entity.findByTag(this.occluderTag);
    this.boundingBoxes = [];
    this.occludeesList = this.entity.findByTag(this.occludeeTag);
    this.camerasList = this.app.root.findByTag(this.cameras);
    
    
    // check if occluders have a collision component, if they have create a bounding box
    var i = this.occludersList.length;
    while (i--) {
        
        var occluder = this.occludersList[i];
        
        if( occluder.collision && occluder.collision.type === 'box' ){

            var bounding = this.createBoundingBox(occluder.getPosition(), occluder.collision.halfExtents);

            this.boundingBoxes.push( bounding );
        }
    }
};



SectionSystem.prototype.createBoundingBox = function(center, halfExtents){

    return new pc.BoundingBox(center, halfExtents);
};



SectionSystem.prototype.setOccludeState = function(state, events){
    
    if( this.occludeesList === null ){ return false; }
    if( this.currentState === state ){ return false; }
    
    
    // loop through occludees and set state
    var i = this.occludeesList.length;
    while (i--) {
        
        if( this.occludeesList[i] ){
            this.occludeesList[i].enabled = state;   
        }
    }
    
    this.currentState = state;
    
    
    if( this.broadcast === true && events === true ){
        
        if( state === true ){
            this.app.fire('Sections:'+this.entity.name+':enabled');  
        }else{
            this.app.fire('Sections:'+this.entity.name+':disabled');
        }

    }
};



SectionSystem.prototype.checkAndOcclude = function(){
  
    
    // check if everything is in place to check with
    if( this.boundingBoxes === null ){ return false; }
    if( this.occludeesList === null ){ return false; }
    if( this.camerasList === null ){ return false; }
    
    
    // loop through all enabled cameras and check against the occluders
    var i = this.camerasList.length;
    var inside = false;
    
    while (i--) {
        
        var camera = this.camerasList[i];
        
        if( camera.enabled === false ){
            continue;
        }
        
        
        // loop through all occluders and check active camera if it is inside of a bounding box
        var j = this.boundingBoxes.length;
        
        var cameraPos = camera.getPosition();
        
        while (j--) {

            var occluder = this.boundingBoxes[j];

            // if an occluder contains the camera, break from this loop, no need for additional testing
            if( occluder.containsPoint( cameraPos ) === true ){
                inside = true;
                break;
            }
        }
        
        if( inside === true ){            
            break;
        }
    }
    
                
    this.setOccludeState(inside, true);
};



SectionSystem.prototype.updateOccluders = function(){
    
    
    // loop through all occluders and check active camera if it is inside of a bounding box
    var i = this.occludersList.length;

    while (i--) {

        var occluder = this.occludersList[i];
        var bounding = this.boundingBoxes[i];

        // update bounding box
        bounding.center = occluder.getPosition();
        bounding.halfExtents = occluder.collision.halfExtents;
        
        bounding._min.copy(this.nullVec);
        bounding._max.copy(this.nullVec);
    }
};
4 Likes

Very cool! Do you have a GitHub to host this work?

Here you go, PRs are more than welcome:

1 Like