Ad

Access Parts Of A GLTF Import In The Main Rendering Loop (sorry If Noobish)

- 1 answer

I’m a Unity developer, trying to learn Three.js. One the many problems I encounter may sound simple, but it’s a pain in the a** for me.

All I wanna do is to import and animate a 3D logo in my Three.js app. This logo is made out of 4 different meshes (elem1 to elem4) which don’t overlap. It was exported as a FBX, then converted to GLTF using an online converter. No problem when importing it, resizing it and even changing its material.

My problem is : how to refer to the whole object, and also to its 4 elements, in order to animate them in my « animate » function (I mean in my main rendering loop) ? The only thing I could do was to create a second « animate » function within the loader callback, which seems a bit weird to me. I can’t find a way to refer to them in the main scope of my app.

Dumping my GLTF import gives this hierarchy (these are called « nodes », am I right ?) :

AuxScene [Scene]
    *no-name* [Object3D]
        elem1 [Mesh]
        elem2 [Mesh]
        elem3 [Mesh]
        elem4 [Mesh]

Here’s my code, cleared from unnecessary stuff :

'use strict';

// CANVAS AND RENDERER
const canvas = document.querySelector('#myCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );

// SCENE AND BACKGROUND
var scene = new THREE.Scene();
const loader = new THREE.TextureLoader();
scene.background = loader.load( 'images/background.jpg');

// CAMERA
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.z = 100;

// MATERIALS
var material = new THREE.MeshNormalMaterial ();


// ---------------------------------- LOGO IMPORTATION
var gltfLoader = new THREE.GLTFLoader();
var root;
var elem1, elem2, elem3, elem4;

gltfLoader.load('gltf/logo.gltf', function(gltf)  {
  root = gltf.scene;
  root.rotation.x = Math.PI / 2;
  scene.add(root);

  root.traverse( function( child ) {
    if ( child instanceof THREE.Mesh ) { child.material = material; }
  } );

  elem1 = root.getObjectByName('element1');
  elem2 = root.getObjectByName('element2');
  elem3 = root.getObjectByName('element3');
  elem4 = root.getObjectByName('element4');

  console.log(dumpObject(root).join('\n'));

  // logo animations
  var speed = 0.0005;
  var turnsBeforeStop = 4;
  requestAnimationFrame( animate2 );

  function animate2( time ) {
    root.rotation.z = Math.sin (time * 0.0005) * 0.5;
    root.rotation.x = Math.PI/3 + Math.sin(time * 0.0003) * 0.5;

    if(elem1.rotation.y < Math.PI * turnsBeforeStop){
      elem1.rotation.y = time * speed*2;
      elem2.rotation.z = time * speed*2;
      elem3.rotation.y = time * -speed;
      elem4.rotation.z = time * -speed*2;
    }
    requestAnimationFrame( animate2 );
  }
});
// ------------------------------------------------------------ END LOGO


renderer.render( scene, camera );
requestAnimationFrame( animate );

// ANIMATION MAIN LOOP
function animate( time ) {
  /*
        This is where I would like to access my logo (as a whole, and also its separate parts).
        But root.rotation or elem1.rotation won't work here and give me this error :
        TypeError: undefined is not an object (evaluating 'elem1.rotation')
  */  
  renderer.render( scene, camera );
  requestAnimationFrame( animate );
}


// OBJECT DUMPING
function dumpObject(obj, lines = [], isLast = true, prefix = '              ') {
  const localPrefix = isLast ? '└─' : '├─';
  lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
  const newPrefix = prefix + (isLast ? '  ' : '│ ');
  const lastNdx = obj.children.length - 1;
  obj.children.forEach((child, ndx) => {
    const isLast = ndx === lastNdx;
    dumpObject(child, lines, isLast, newPrefix);
  });
  return lines;
}

Thanx for any help.

Ad

Answer

The problem is that you try to access root before a value (the glTF model) is assigned to it. Notice that GLTFLoader.load() is asynchronous. So the onLoad() callback which sets root is not immediately called but with some amount of delay.

There are several approaches to solve this issue. You can check in your animation loop if root is not undefined. It would look like so:

function animate( time ) {
    requestAnimationFrame( animate );

    if ( root ) {

        // do something with root

    }

    renderer.render( scene, camera );

}

Or you start animating after onLoad() has finished. In this case, the code would look like so:

'use strict';

// CANVAS AND RENDERER
const canvas = document.querySelector('#myCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize( window.innerWidth, window.innerHeight );

// SCENE AND BACKGROUND
var scene = new THREE.Scene();
const loader = new THREE.TextureLoader();
scene.background = loader.load( 'images/background.jpg');

// CAMERA
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.z = 100;

// MATERIALS
var material = new THREE.MeshNormalMaterial ();


// ---------------------------------- LOGO IMPORTATION
var gltfLoader = new THREE.GLTFLoader();
var root;
var elem1, elem2, elem3, elem4;

gltfLoader.load('gltf/logo.gltf', function(gltf)  {
  root = gltf.scene;
  root.rotation.x = Math.PI / 2;
  scene.add(root);

  root.traverse( function( child ) {
    if ( child instanceof THREE.Mesh ) { child.material = material; }
  } );

  animate(); // start animating

});
// ------------------------------------------------------------ END LOGO



// ANIMATION MAIN LOOP
function animate() {
    requestAnimationFrame( animate );
    // do something with root
    renderer.render( scene, camera );
}


// OBJECT DUMPING
function dumpObject(obj, lines = [], isLast = true, prefix = '              ') {
  const localPrefix = isLast ? '└─' : '├─';
  lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
  const newPrefix = prefix + (isLast ? '  ' : '│ ');
  const lastNdx = obj.children.length - 1;
  obj.children.forEach((child, ndx) => {
    const isLast = ndx === lastNdx;
    dumpObject(child, lines, isLast, newPrefix);
  });
  return lines;
}
Ad
source: stackoverflow.com
Ad