Reference Source

src/plugins/lib/culling/ObjectCullStates.js

/**
 * For each Entity in its Scene, efficiently combines updates from multiple culling systems into a single "culled" state.
 *
 * Two culling systems are supported:
 *
 * * View culling - culls Entities when they fall outside the current view frustum, and
 * * Detail culling - momentarily culls less visually-significant Entities while we are moving the camera.
 *
 * @private
 */
class ObjectCullStates {

    /**
     * @private
     * @param scene
     */
    constructor(scene) {

        this._scene = scene;

        this._objects = []; // Array of all Entity instances that represent objects
        this._objectsViewCulled = []; // A flag for each object to indicate its view-cull status
        this._objectsDetailCulled = []; // A flag for each object to indicate its detail-cull status
        this._objectsChanged = []; // A flag for each object, set whenever its cull status has changed since last _applyChanges()
        this._objectsChangedList = []; // A list of objects whose cull status has changed, applied and cleared by _applyChanges()

        this._modelInfos = {};

        this._numObjects = 0;
        this._lenObjectsChangedList = 0;

        this._dirty = true;

        this._onModelLoaded = scene.on("modelLoaded", (modelId) => {
            const model = scene.models[modelId];
            if (model) {
                this._addModel(model);
            }
        });

        this._onTick = scene.on("tick", () => {
            if (this._dirty) {
                this._build();
            }
            this._applyChanges();
        });
    }

    _addModel(model) {
        const modelInfo = {
            model: model,
            onDestroyed: model.on("destroyed", () => {
                this._removeModel(model);
            })
        };
        this._modelInfos[model.id] = modelInfo;
        this._dirty = true;
    }

    _removeModel(model) {
        const modelInfo = this._modelInfos[model.id];
        if (modelInfo) {
            modelInfo.model.off(modelInfo.onDestroyed);
            delete this._modelInfos[model.id];
            this._dirty = true;
        }
    }

    _build() {
        if (!this._dirty) {
            return;
        }
        this._applyChanges();
        const objects = this._scene.objects;
        for (let i = 0; i < this._numObjects; i++) {
            this._objects[i] = null;
        }
        this._numObjects = 0;
        for (let objectId in objects) {
            const entity = objects[objectId];
            this._objects[this._numObjects++] = entity;
        }
        this._lenObjectsChangedList = 0;
        this._dirty = false;
    }

    _applyChanges() {
        if (this._lenObjectsChangedList > 0) {
            for (let i = 0; i < this._lenObjectsChangedList; i++) {
                const objectIdx = this._objectsChangedList[i];
                const object = this._objects[objectIdx];
                const viewCulled = this._objectsViewCulled[objectIdx];
                const detailCulled = this._objectsDetailCulled[objectIdx];
                const culled = (viewCulled || detailCulled);
                object.culled = culled;
                this._objectsChanged[objectIdx] = false;
            }
            this._lenObjectsChangedList = 0;
        }
    }

    /**
     * Array of {@link Entity} instances that represent objects in the {@link Scene}.
     *
     * ObjectCullStates rebuilds this from {@link Scene#objects} whenever ````Scene```` fires a ````modelLoaded```` event.
     *
     * @returns {Entity[]}
     */
    get objects() {
        if (this._dirty) {
            this._build();
        }
        return this._objects;
    }

    /**
     * Number of objects in {@link ObjectCullStates#objects},
     *
     * Updated whenever ````Scene```` fires a ````modelLoaded```` event.
     *
     * @returns {Number}
     */
    get numObjects() {
        if (this._dirty) {
            this._build();
        }
        return this._numObjects;
    }

    /**
     * Updates an object's view-cull status.
     *
     * @param {Number} objectIdx Index of the object in {@link ObjectCullStates#objects}
     * @param {boolean} culled Whether to view-cull or not.
     */
    setObjectViewCulled(objectIdx, culled) {
        if (this._dirty) {
            this._build();
        }
        if (this._objectsViewCulled[objectIdx] === culled) {
            return;
        }
        this._objectsViewCulled[objectIdx] = culled;
        if (!this._objectsChanged[objectIdx]) {
            this._objectsChanged[objectIdx] = true;
            this._objectsChangedList[this._lenObjectsChangedList++] = objectIdx;
        }
    }

    /**
     * Updates an object's detail-cull status.
     *
     * @param {Number} objectIdx Index of the object in {@link ObjectCullStates#objects}
     * @param {boolean} culled Whether to detail-cull or not.
     */
    setObjectDetailCulled(objectIdx, culled) {
        if (this._dirty) {
            this._build();
        }
        if (this._objectsDetailCulled[objectIdx] === culled) {
            return;
        }
        this._objectsDetailCulled[objectIdx] = culled;
        if (!this._objectsChanged[objectIdx]) {
            this._objectsChanged[objectIdx] = true;
            this._objectsChangedList[this._lenObjectsChangedList++] = objectIdx;
        }
    }

    /**
     * Destroys this ObjectCullStAtes.
     */
    _destroy() {
        this._clear();
        this._scene.off(this._onModelLoaded);
        this._scene.off(this._onTick);
    }

    _clear() {
        for (let modelId in this._modelInfos) {
            const modelInfo = this._modelInfos[modelId];
            modelInfo.model.off(modelInfo.onDestroyed);
        }
        this._modelInfos = {};
        this._dirty = true;
    }
}

const sceneObjectCullStates = {};

/**
 * @private
 */
function getObjectCullStates(scene) {
    const sceneId = scene.id;
    let objectCullStates = sceneObjectCullStates[sceneId];
    if (!objectCullStates) {
        objectCullStates = new ObjectCullStates(scene);
        sceneObjectCullStates[sceneId] = objectCullStates;
        scene.on("destroyed", () => {
            delete sceneObjectCullStates[sceneId];
            objectCullStates._destroy();
        });
    }
    return objectCullStates;
}

export {getObjectCullStates};