Loading GLB images through a loop: only renders final object

Hi there,

I am attempting to load 3 identical glb objects onto a single page. I’m running each object through a while loop, however only the final loop outputs the object as expected.

Specifically, to my case, I’m running a loop via WordPress (via ACF).

The canvas loads for all three objects, however only the third model generates as expected. While the first two canvases load at the correct size - however they appear to be empty.

The loop I’m running is as follows:

 <?php
if( have_rows('development_process') ):
while ( have_rows('development_process') ) : the_row();
    $model = get_sub_field('step_3d_model');

?> 
    <script>
        if ( WEBGL.isWebGLAvailable() ) {

            console.log("webgl is available: <?php echo $model['caption'] ?>");

            container = document.getElementById( '<?php echo $model['caption'] ?>' );
            var camera, controls, scene, renderer;

            init();
            animate();

            function init(){

                // Create the camera
                camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 50);
                // camera.position dictates how far away it will be from the rotational axis. 1 is close and 1< is further away
                // Ultimatley, this will dictate the size of the object - by varying the distance between the object and the camera
                camera.position.z = 7.25;

                // Set camera controls (REMEMBER: It's the camera that's rotating, not the object)
                // controls = new THREE.TrackballControls( camera );
                controls = new THREE.TrackballControls(camera, document.getElementById('<?php echo $model['caption'] ?>'));
                controls.addEventListener('change', render);

                // Removes camera pan on right click
                controls.noPan = true;
                // Removes camera zoom on scroll
                controls.noZoom = true;

                // Create the scene where the object will be located
                scene = new THREE.Scene();

                // Add light to the scene so that objects can be seen
                ambientLight = new THREE.AmbientLight(0x404040, 5, 0.5);
                scene.add(ambientLight);

                // Add a spotlight to the scene for some direct lighting
                light = new THREE.PointLight(0xf5f5f5, 5, 5)
                // variable.position.set(x, y, z) references where the light will be angled
                light.position.set(3, 4, 3);
                light.castShadow = true;
                light.shadow.camera.near = 0.001;
                light.shadow.camera.far = 25;
                scene.add(light);

                // for instances where you need more than one direct light source, you may add multiple spot lights
                light = new THREE.PointLight(0x404040, 5, 5)
                light.position.set(3, -2, 3);
                light.castShadow = true;
                light.shadow.camera.near = 0.001;
                light.shadow.camera.far = 25;
                scene.add(light);

                renderer = new THREE.WebGLRenderer({
                    alpha: true 
                });

                renderer.setSize(window.innerWidth, window.innerHeight);
                container.appendChild( renderer.domElement );

                var loader = new THREE.GLTFLoader();
                renderer.gammaOutput = true;
                renderer.gammaFactor = 2.2;

                loader.load( '<?php echo $model['url'];?>', function ( glb ) {
                    object = glb.scene.children[0];
                    // variable.scale.set(x, y, z) references the size of the object
                    object.scale.set(1.4,1.4,1.4);
                    // .position refers to the location of the object on the canvas
                    object.position.set(-0.02,-0.75,2);
                    // .rotation refers to the angle of the object
                    object.rotation.set(90, 0.25, -0.5);
                    
                    scene.add( object );

                    // This code will center the cameras roatational axis around the loaded object.
                    var boundingBox = new THREE.Box3();
                    boundingBox.setFromObject( object );
                    var center = boundingBox.getCenter();
                    // set camera to rotate around center of object
                    controls.target = center;

                }, undefined, function ( error ) {
                    console.error( error );

                } );

            }

            function animate(){

                requestAnimationFrame( animate );
                controls.update();
            }

            function render(){
                renderer.render(scene, camera );
            }

        } else {

            var warning = WEBGL.getWebGLErrorMessage();
            document.getElementById( '<?php echo $model['caption'] ?>' ).appendChild( warning );
            console.log("webgl is NOT available");
        }
    </script>

<?php
    endwhile;
else :
?>
    <script>
        console.log("error");     
    </script><?php
endif;
?>

Am I loading these canvases in an incorrect manner? Or is there a specific function that I’m missing - which allows you to load multiple objects?

Any light on the subject would be greatly appreciated! Thanks

My guess is that your while loop is creating as many script tags as iterations, creating several renderers and scenes. So in the end, you only see the last created scene.
Could you try and move the while loop so it only encompasses the loader?

1 Like

Yeah, it looks like the PHP loop is declaring global variables that get over-written on each iteration.

var camera, controls, scene, renderer;

So by the time the final loop is complete, you’re only going to have control over the reference to the last camera, last scene, and last renderer. All the previous ones got written over.

Maybe you could issue unique variable names for each iteration?

var camera1, scene1, renderer1;
var camera2, scene2, renderer2;
///... etc
1 Like

Thanks Marquizzo!

I think that this is the right direction, as pointed out by @Gui too.

However, I’m receiving unexpected errors when running a loop that concatenates a unique number to the end of each loop of variables.

  • When I run a loop with the variable camera0 or camera1, really anything other than just “camera”, I receive the following console error: Uncaught ReferenceError: near is not defined at init.

    the line being refferenced is:

    light.shadow.<?php echo $camera ?>.near = 0.001;

  • When I run the updated variables for var renderer and/or var scene I receive a console error that reads: TypeError: Cannot read property 'children' of undefined

    the line being refferenced is:

    object = glb.<?php echo $scene ?>.children[0];

Do either of you have any idea as to what might be causing this bug? If it’s not ovbious, I’ve added my code below.

Here is my foreach loop, with a counter, that is used to concatenate a variable to the end of the camera, controls, scene, and renderer variable names:

<?php
    if( have_rows('development_process') ):
      while ( have_rows('development_process') ) : the_row();
           $model = get_sub_field('step_3d_model');
           foreach (range(0, 2) as $number) {

                $rows = get_field('development_process' ); // get all the ACF repeater rows
                $first_row = $rows[$number]; // call a specific row
                $first_row_image = $first_row['step_3d_model' ]; // get the sub field value

                // If the caption of the $first_row_image matches the $model caption...
                // ... concatonate a unique number to the end of each variable 
                if($model['caption'] == $first_row_image['caption']){

                   $loopNum    = $number;
                   // These will become the unique JS variable for each loop
                   $camera     = "camera".$number;
                   $controls   = "controls".$number;
                   $scene      = "scene".$number;
                   $renderer   = "renderer".$number;
                }
           }
?> 

container = document.getElementById( '<?php echo $model['caption'] ?>' );
var <?php echo $camera; ?> , <?php echo $controls; ?>, <?php echo $scene; ?>, <?php echo $renderer; ?>;

Aside from the bugs listed above, this loop is working as expected.
If this is inadequate, I will post a response with the while loop in it’s entierty.

Thanks guys, I appreciate the insight

For the convenience of anyone who may be able to provide insight, the loop (in its entierty) is as follows:

<script>

if ( WEBGL.isWebGLAvailable() ) {

    console.log("webgl is available");

    <?php
        if( have_rows('development_process') ):
            while ( have_rows('development_process') ) : the_row();
                $model = get_sub_field('step_3d_model');
                foreach (range(0, 2) as $number) {
                    $rows = get_field('development_process' ); // get all the ACF repeater rows
                    $first_row = $rows[$number]; // call a specific row
                    $first_row_image = $first_row['step_3d_model' ]; // get the sub field value
                    if($model['caption'] == $first_row_image['caption']){
                        $loopNum    = $number;
                        // These will become the unique JS variable for each loop
                        $camera     = "camera".$number;
                        $controls   = "controls".$number;
                        $scene      = "scene".$number;
                        $renderer   = "renderer".$number;
                    }
                }
    ?> 
    container = document.getElementById( '<?php echo $model['caption'] ?>' );

    var <?php echo $camera ?> , <?php echo $controls ?>, <?php echo $scene ?>, <?php echo $renderer ?>;

    init();
    animate();

    function init(){
           // Create the camera
            <?php echo $camera ?> = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 50);

            // camera.position dictates how far away it will be from the rotational axis. 1 is close and 1< is further away
            // Ultimatley, this will dictate the size of the object - by varying the distance between the object and the camera
            <?php echo $camera ?>.position.z = 7.25;

            // Set camera controls (REMEMBER: It's the camera that's rotating, not the object)
            // controls = new THREE.TrackballControls( camera );
            <?php echo $controls ?> = new THREE.TrackballControls(<?php echo $camera ?>, document.getElementById('<?php echo $model['caption'] ?>'));
            <?php echo $controls ?>.addEventListener('change', render);

            // Removes camera pan on right click
            <?php echo $controls ?>.noPan = true;

            // Removes camera zoom on scroll
            <?php echo $controls ?>.noZoom = true;

            // Create the scene where the object will be located
            <?php echo $scene ?> = new THREE.Scene();

            // Add light to the scene so that objects can be seen
            ambientLight = new THREE.AmbientLight(0x404040, 5, 0.5);
            <?php echo $scene ?>.add(ambientLight);

            // Add a spotlight to the scene for some direct lighting
            light = new THREE.PointLight(0xf5f5f5, 5, 5)

            // variable.position.set(x, y, z) references where the light will be angled
            light.position.set(3, 4, 3);
            light.castShadow = true;
            light.shadow.<?php echo $camera ?>.near = 0.001;
            light.shadow.<?php echo $camera ?>.far = 25;
            <?php echo $scene ?>.add(light);

            // for instances where you need more than one direct light source, you may add multiple spot lights
            light = new THREE.PointLight(0x404040, 5, 5)
            light.position.set(3, -2, 3);
            light.castShadow = true;
            light.shadow.<?php echo $camera ?>.near = 0.001;
            light.shadow.<?php echo $camera ?>.far = 25;
            <?php echo $scene ?>.add(light);

            <?php echo $renderer ?> = new THREE.WebGLRenderer({
                alpha: true 
            });

            <?php echo $renderer ?>.setSize(window.innerWidth, window.innerHeight);
            container.appendChild( <?php echo $renderer ?>.domElement );

            var loader = new THREE.GLTFLoader();
            <?php echo $renderer ?>.gammaOutput = true;
            <?php echo $renderer ?>.gammaFactor = 2.2;

            loader.load( '<?php echo $model['url'];?>', function ( glb ) {
                
                object = glb.<?php echo $scene ?>.children[0];
                // variable.scale.set(x, y, z) references the size of the object
                object.scale.set(1.4,1.4,1.4);
                // .position refers to the location of the object on the canvas
                object.position.set(-0.02,-0.75,2);
                // .rotation refers to the angle of the object
                object.rotation.set(90, 0.25, -0.5);
                
                <?php echo $scene ?>.add( object );

                // This code will center the cameras roatational axis around the loaded object.
                var boundingBox = new THREE.Box3();
                boundingBox.setFromObject( object );
                var center = boundingBox.getCenter();
                // set camera to rotate around center of object
                <?php echo $controls ?>.target = center;

            }, undefined, function ( error ) {
                console.error( error );

            } );

        }

        function animate(){

            requestAnimationFrame( animate );
            <?php echo $controls ?>.update();
        }

        function render(){
            <?php echo $renderer ?>.render(<?php echo $scene ?>, <?php echo $camera ?> );
        }

    <?php endwhile; else : ?>
        console.log("error");     
    <?php endif; ?>

} else {

    var warning = WEBGL.getWebGLErrorMessage();
    document.getElementById( '<?php echo $model['caption'] ?>' ).appendChild( warning );

    console.log("webgl is NOT available");

}

</script>

If I remove the line of code that instigates the ReferenceError: x is not defined at init, x just changes to another variable in the script. The result is that many of the default properties, from within the three.js library, trigger console errors.

Further, I’d also like to clarify that this is a unique error that has only occurred since the intorduction of the php foreach loop.

Thanks!

Have you looked at the output javascript? I’d rather you share that to figure out what’s wrong.

Looking at php heavily mixed with js like this is very hard to reason about. I really think you should avoid having so many php insert on so many lines.
I assume you do this because the display is going to depend on what your database contains? And if I understand correctly you need multiple scenes and renders because you have several canvas elements?

I’d really recommend you only generate a single json object with php, so you have this in a couple lines, and then have the rest of your code look it up, only in javascript, no using php anymore. And use arrays instead of scene1, scene2, scene3.

To me this seems error prone in so many ways:

  • object = glb.<?php echo $scene ?>.children[0]; should probably just be object = glb.scene.children[0]; as the scene object returned by the loader will not use scene1 or scene2
  • you still only have a single animate or render functions
2 Likes

Thanks man!

& Yeah, your understanding is correct. To clarify, I am outputting these models onto a variable number of separate canvas elements. Right now its set to 3, but that may change in the future.

Here is one rendition of the output javascript:

container = document.getElementById( 'model-0' );

var camera0 , controls0, scene0, renderer0;

init();
animate();

function init(){
            
    // Create the camera
    camera0 = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 50);

    // camera.position dictates how far away it will be from the rotational axis. 1 is close and 1< is further away
    // Ultimatley, this will dictate the size of the object - by varying the distance between the object and the camera
    camera0.position.z = 7.25;

    // Set camera controls (REMEMBER: It's the camera that's rotating, not the object)
    // controls = new THREE.TrackballControls( camera );
    controls0 = new THREE.TrackballControls(camera0, document.getElementById('model-0'));
    controls0.addEventListener('change', render);

    // Removes camera pan on right click
    controls0.noPan = true;

    // Removes camera zoom on scroll
    controls0.noZoom = true;

    // Create the scene where the object will be located
    scene0 = new THREE.Scene();

    // Add light to the scene so that objects can be seen
    ambientLight = new THREE.AmbientLight(0x404040, 5, 0.5);
    scene0.add(ambientLight);

    // Add a spotlight to the scene for some direct lighting
    light = new THREE.PointLight(0xf5f5f5, 5, 5)

    // variable.position.set(x, y, z) references where the light will be angled
    light.position.set(3, 4, 3);
    light.castShadow = true;
    light.shadow.camera0.near = 0.001;
    light.shadow.camera0.far = 25;
    scene0.add(light);

    // for instances where you need more than one direct light source, you may add multiple spot lights
    light = new THREE.PointLight(0x404040, 5, 5)
    light.position.set(3, -2, 3);
    light.castShadow = true;
    light.shadow.camera0.near = 0.001;
    light.shadow.camera0.far = 25;
    scene0.add(light);

    renderer0 = new THREE.WebGLRenderer({
        alpha: true 
    });

    renderer0.setSize(window.innerWidth, window.innerHeight);
    container.appendChild( renderer0.domElement );

    var loader = new THREE.GLTFLoader();
    renderer0.gammaOutput = true;
    renderer0.gammaFactor = 2.2;
    loader.load( '.../wp-content/uploads/2019/09/Apple_Macintosh_1985_pbr_2-15.glb', function ( glb ) {
    object = glb.scene0.children[0];
    // variable.scale.set(x, y, z) references the size of the object
    object.scale.set(1.4,1.4,1.4);
    // .position refers to the location of the object on the canvas
    object.position.set(-0.02,-0.75,2);
    // .rotation refers to the angle of the object
    object.rotation.set(90, 0.25, -0.5);
                
    scene0.add( object );
    // This code will center the cameras roatational axis around the loaded object.
    var boundingBox = new THREE.Box3();
    boundingBox.setFromObject( object );
    var center = boundingBox.getCenter();
    // set camera to rotate around center of object
    controls0.target = center;

    }, undefined, function ( error ) {
        console.error( error );
    } );

}

function animate(){
    requestAnimationFrame( animate );
    controls0.update();
}

function render(){
    renderer0.render(scene0, camera0 );
}

After each loop the variable number changes from 0 to 1, and then to 2.

But if I understand the second bullet point in your previous comment correctly, the errors that I have been experiencing are a result of the repeating animate(); and render(); functions that are utilized during each loop?

Looking at your js, it doesn’t seem to be the output from 3 iterations, only from a single one.

I think if you do share what several iterations produce, the problem will become much clearer.

Why don’t you bundle all the unique variables inside a JavaScript class? A class would be cleaner to manage because it compartmentalizes all its variables in its own scope, instead of having to worry about managing unique global variables. Here’s a quick example:

class View {
	// Init all variables in its isolated scope
	constructor(objURL) {
		this.camera = new THREE.PerspectiveCamera();
		this.scene = new THREE.Scene();
		this.renderer = new THREE.WebGLRenderer();

		// Load custom object
		this.loader = new THREE.GLTFLoader();
		this.loader.load(objURL, function(glb){
			const obj = glb.scene0.children[0];
			this.scene.add(obj);
		});

		this.update();
	}

	update() {
		this.renderer.render(this.scene, this.camera);
		requestAnimationFrame(this.update.bind(this));
	}
}

You only need to define this class once to build your boilerplate scene setup. Then you can write a PHP loop that creates multiple instances of the same class, with a different URL to load:

// Write PHP loop that outputs the following:
var view1 = new View("folder/obj1.gltf");
var view2 = new View("folder/obj2.gltf");
var view3 = new View("folder/obj3.gltf");
var view4 = new View("folder/obj4.gltf");
var view5 = new View("folder/obj5.gltf");

Now all you have to manage is each view# variable, instead of its individual scenes, cameras, renderers, etc. If this is your first time writing a class, here’s a quick overview from MDN.

I apologize if my first answer suggested creating so many unique variables for each object, I didn’t expect the PHP loop to get so out of control! This approach should be easier to manage.

PS: The only caveat with class is that it’s not supported in IE. If you need to support IE, I recommend you use the similar (but more verbose) function + prototype approach explained here.

1 Like