Making model visible interferes with render loop?

[NOTE: This question was cross-posted to Stack Overflow:]

In my ThreeJS app (r124) I have a GLB animation model that I attach a Spotlight to, to make it look like a camera drone with a light that turns on and off. I have a function to “turn on the light” and the only thing it does is make the light visible by setting its visible property to true:

    this.turnOnLight = function(bUseLaserScan=false) {
        if (self.CAC.modelInstance.visible === false)
            self.CAC.modelInstance.visible = true;
    }

Unfortunately, there is something about doing this that interferes with the render/animate loop. Every time I turn on the light (or off) there is a long pause in the 200-300 millisecond range, freezing everything in the scene for that length of time. In other words, the render loop is not being called for 200-300 milliseconds. If I comment out the statements that change the light’s visibility property, the pause/delay goes away.

Why would simply turning a model’s visibility property on and off freeze the scene, and how can I fix this problem?

If I was loading a large model or something every time I turn on the light sub-object, I could see this happening. But how is this happening when all I am doing is setting the visible property to TRUE?

Here is how I construct the Spotlight and attach it to the camera drone main animation object. Note, self.CAC.modelInstance is simply an object I use to aggregate ThreeJS objects and properties common to all my animation models.


function AnimatedLight(
    modelName,
    modelDeclJsonObj,
    threeJsLightObjectName='PointLight',
    topLevelAnimationModelObj=null,
    bAddLensSphere=false,
    bAddLaserScan=false) {
    const self = this;

    this._buildTheLight = function(
        threeJsLightObjectName,
        modelDeclJsonObj) {
        const methodName = self.constructor.name + '::' + `_buildTheLight`;
        const errPrefix = '(' + methodName + ') ';

        let retLightObj = null;

        if (misc_shared_lib.isEmptySafeString(threeJsLightObjectName))
            throw new Error(`${errPrefix}The threeJsLightObjectName parameter is empty.`);

        if (!misc_shared_lib.isNonNullObjectAndNotArray(modelDeclJsonObj))
            throw new Error(`${errPrefix}The modelDeclJsonObj is not a valid object.`);

        //  Provide default values for the properties the modelDeclJsonObj 
        //   object doesn't have.
        //
        // Default color is RED.
        let color = typeof modelDeclJsonObj.color !== 'undefined' ? modelDeclJsonObj.color : 0xf41321; // 0xffffff;
        let intensity = typeof modelDeclJsonObj.intensity !== 'undefined' ? modelDeclJsonObj.intensity : 8.6; //  1.0;
        let distance = typeof modelDeclJsonObj.distance !== 'undefined' ? modelDeclJsonObj.distance : 0.0;
        let decay = typeof modelDeclJsonObj.decay !== 'undefined' ? modelDeclJsonObj.decay : 1.0;

        // These properties are only for spot-lights, which have an inner angle.
        // NOTE: We store angles in the JSON model initialization declaration
        //  object because they are easier to specify then angles expressed
        //  in radians.

        let angle = (0.8 * MAX_ANGLE_FOR_SPOTLIGHT); // Math.PI / 3;  // Default value.

        if (typeof modelDeclJsonObj.inner_angle_in_degrees !== 'undefined') {
            if (typeof modelDeclJsonObj.inner_angle_in_degrees !== 'number')
                throw new Error(`${errPrefix} The "inner_angle_in_degrees" property in the model JSON declaration object is not a number.`);
            angle = THREE.MathUtils.degToRad(modelDeclJsonObj.inner_angle_in_degrees);
        }

        let penumbra = 0;

        if (typeof modelDeclJsonObj.penumbra_angle_in_degress !== 'undefined') {
            if (typeof modelDeclJsonObj.penumbra_angle_in_degress !== 'number')
                throw new Error(`${errPrefix} The "penumbra_angle_in_degress" property in the model JSON declaration object is not a number.`);

            // ThreeJS uses a range of 0 to 1.0 for the penumbra angle, so
            //  we divide by 180 to rescale the provided value.
            penumbra = Math.min(THREE.MathUtils.degToRad(modelDeclJsonObj.penumbra_angle_in_degress / 180.0), 1);
        }

        // Build the correct ThreeJS light object given the specified
        //  light object type.
        if (threeJsLightObjectName === 'PointLight') {
            retLightObj = new THREE.PointLight(color, intensity, distance, decay);
        }
        else if (threeJsLightObjectName === 'DirectionalLight') {
            retLightObj = new THREE.DirectionalLight(color, intensity);
        }
        else if (threeJsLightObjectName === 'SpotLight') {
            // Create a mini-menu for this type of light.
            retLightObj = new THREE.SpotLight(color, intensity, distance, angle, penumbra, decay);

            // Is a lens sphere desired?
            if (bAddLensSphere) {
                // Yes.  Create it.

                // .................... BEGIN: SUB-OBJECT - Lens Sphere ............

                const radius = 3;
                self.lensSphereObj =
                    new THREE.Mesh(
                        new THREE.SphereBufferGeometry(radius, 20, 20),
                        new THREE.MeshPhongMaterial({color: 0xFF0000}));

                // Add it to the top level animation model.
                // self.CAC.modelInstance.add(cameraLensObj);

                // Add it to the spotlight object.
                retLightObj.add(self.lensSphereObj);

                // .................... END  : SUB-OBJECT - Lens Sphere ............
            }
        }
        else
            throw new Error(`${errPrefix}Invalid threeJsLightObjectName value: ${threeJsLightObjectName}`);

        return retLightObj;
    }

    /**
     * Makes the light visible.  
     */
    this.turnOnLight = function(bUseLaserScan=false) {
        if (self.CAC.modelInstance.visible === false)
            self.CAC.modelInstance.visible = true;
    }

    /**
     * Makes the light invisible.
     */
    this.turnOffLight = function() {
        if (self.CAC.modelInstance.visible === true)
            self.CAC.modelInstance.visible = false;
    }

    this.setTargetForSpotLight = function(targetObj) {
        self.CAC.modelInstance.target = targetObj;
    }

    /**
     * This method must be called to initialize this
     *  animated light for animation.  
     */
    this.initializeModel = function (
        parentAnimManagerObj,
        initModelArgs= null,
        funcCallWhenInitialized = null,
    ) {
        const methodName = self.constructor.name + '::' + `initializeModel`;
        const errPrefix = '(' + methodName + ') ';

        // Store the reference to the AnimationManager() object
        //  that owns us in the CommonAnimationObject we
        //  aggregate.
        self.CAC.parentAnimManagerObj = parentAnimManagerObj;

        // Create a ThreeJS light object from the model (light)
        //  declaration in the JSON declarations block.
        self.CAC.modelInstance = self._buildTheLight(threeJsLightObjectName, initModelArgs);

        self.lensSphereObj.position.set(
            0,
            0,
            -20);

        // Animated lights don't use a model loader.
        self.CAC.modelLoader = null;

        // Set the initial position.
        self.CAC.modelInstance.position.x = initModelArgs.initialPos_X;
        self.CAC.modelInstance.position.y = initModelArgs.initialPos_Y;
        self.CAC.modelInstance.position.z = initModelArgs.initialPos_Z;

        // Add the model to the scene.
        g_ThreeJsScene.add(self.CAC.modelInstance);

        // Finish the initialization process..
        self.CAC.initializeModelCompletely(null);

        // Execute the desired callback function, if any.
        if (funcCallWhenInitialized)
            funcCallWhenInitialized(self);
    }

i think you should try intensity 0/1 instead of visible. i am not sure, but i believe that visible on/off with lights will re-compile shaders.

edit:

hmm, according to The Big List of three.js Tips and Tricks! | Discover three.js visible is ok, but try it anyway.

1 Like

I tried that but that turns the light into a “black light”, where what ever it shines on is turned completely dark (see screenshot). The black sphere with the red dot is the sentry drone and it is pointing a SpotLight at an object on the ground. As you can see, the object and ground have been turned completely black. Cool effect, but not helpful:

Preface: I still would like know if the actual problem was due to changing visible property, perhaps because it causes the shaders to be recompiled as mentioned above. I have Radeon RX580 on a Windows 10 box so I would think that would be good enough for recompiling the shaders for a simple SpotLight in real-time?

Either way, as I stated above, trying to use the intensity property set to 0 resulted in the entire light path “painting everything black”. Instead, I have changed my SpotLight to use physically correct mode and I am setting the power property to 20 (i.e. - 20 lumens or 4W) and 0 to turn it on and off respectively. Anything far above “20” for the on setting resulted in a complete “whiteout” of the textures in the light’s path. Hopefully this information helps others who have the problem I had.

In the past I have used various alternatives for hiding and showing objects – out of curiosity. Have you tried:

  1. Three.js Layers
  2. Moving the object off-screen or behind the camera (generally anywhere outside the frustum)
  3. Setting an important parameter to NaN (e.g. a position coordinate, a scale factor, etc)
  4. Removing the object from the parent

TBH, I didn’t expect that visible should impose such a delay. My expectation was that it will only set a flag and the object will be skipped during the next renderings.

It would be nice if there is a working example, so that other people can testify whether they experience such delay. It might be ANGLE-related issue?!?

UPDATE

@PavelBoytchev - The reason may be due to the recompilation of shaders as someone replied to me on Stack Overflow. I have a Radeon Rx580, certainly an old card but not the weakest, but now that I’ve changed to altering the power or intensity properties (physically correct vs. not physically correct mode), everything works fine so I can’t see what else would be the root cause.

@drcmda - The “paint it black” problem was due to my setting the intensity property to an undefined value. I fixed the code and your answer is correct.

1 Like

I faced similar issue of object turning black.
Solved by setting render order of the object to 1 (can be any integer).

Pl ignore if already solved.

it’s not just that one light, it recompiles all applicable shaders (imo). mounting lights runtime is a real expense that should be avoided at all cost. i was always under the impression that visible wouldn’t case a re-compile but perhaps that has changed.

Thanks. Ok, then I have also learned the other general lesson (at least applicable to r124), to avoid manipulating the visible property as much as possible.