1

I am using the GLTF loader to load a custom model in my scene.

I have a class Spaceship.js responsible for loading the model.

// Spaceship.js

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

export default class Spaceship {
  constructor() {
    this.GLTFLoader = new GLTFLoader();

    this.loadModel(this.GLTFLoader, './spaceship_model.gltf').then(result => {
      this.model = result.scene;
    });
  }

  loadModel(loader, url) {
    return new Promise((resolve, reject) => {
      loader.load(
        url,

        gltf => {
          resolve(gltf);
        },

        undefined,

        error => {
          console.error('An error happened.', error);
          reject(error);
        }
      );
    });
  }
}

and a class ThreeShell.js acting as a shell for the whole three scene

import * as THREE from 'three';
import Spaceship from './Spaceship.js';

export default class ThreeShell {
  constructor(container = document.body) {
    this.container = container;
    this.setup();
  }

  setup() {
    ...

    this.spaceship = new Spaceship();
    console.log(this.spaceship);
    console.log(this.spaceship.model);

    ...
  }
}

Somehow, when logging this.spaceship I get an object with the model property. But when logging this.spaceship.model, I get undefined.

enter image description here

I guess this might have to do with promises, which I am not comfortable with at the moment. That's why I am asking for your help.

ronnow
  • 163
  • 3
  • 3
  • 14

2 Answers2

3

The GLTFLoader loads assets asynchronously.

this.spaceship = new Spaceship(); // Loading begins...
console.log(this.spaceship);

// Doesn't yet exist because it gets executed immediately, before loading has completed
console.log(this.spaceship.model);

If you want to gain access to this.spaceship.model, you'll need to use the Promise from outside your Spaceship class:

this.spaceship = new Spaceship(); // Don't load in constructor...
console.log(this.spaceship);

// Perform load call here
this.spaceship.loadModel().then((result) => {
    // Now GLTF will exist here because you're waiting
    // for the asynchronous callback
    console.log(result.scene);
});

It looks like you already have a good grasp on how Promises work, but here's a bit of further clarification.

M -
  • 26,908
  • 11
  • 49
  • 81
  • Yeah, I thought about that as a workaround. But I wanted to abstract away the loader, so that it doesn't obstruct with my main file. Thank you! – ronnow May 22 '20 at 07:34
1

As Marquizzo said the model loads asynchronously so these lines

    this.spaceship = new Spaceship();
    console.log(this.spaceship.model);

won't work. There are many ways to fix this.

Another would be to add a wait function that returns the loading promise and to use an async function to wait for it

// Spaceship.js

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

export default class Spaceship {
  constructor() {
    this.GLTFLoader = new GLTFLoader();

    this._loadingPromise = this.loadModel(this.GLTFLoader, './spaceship_model.gltf').then(result => {
      this.model = result.scene;
    });
  }

  waitForLoad() {
    return this._loadingPromise;
  }

  loadModel(loader, url) {
    return new Promise((resolve, reject) => {
      loader.load(
        url,

        gltf => {
          resolve(gltf);
        },

        undefined,

        error => {
          console.error('An error happened.', error);
          reject(error);
        }
      );
    });
  }
}

Then in setup

import * as THREE from 'three';
import Spaceship from './Spaceship.js';

export default class ThreeShell {
  constructor(container = document.body) {
    this.container = container;
    this.setup();
  }

  async setup() {
    ...

    this.spaceship = new Spaceship();
    console.log(this.spaceship);
    await this.spaceship.waitForLoad();
    console.log(this.spaceship.model);

    ...
  }
}

I'm not suggesting this is better or worse, just pointing out there are more ways and you don't have to move the loading out of the constructor.

You can also do this

  setup() {
    ...

    this.spaceship = new Spaceship();
    console.log(this.spaceship);
    this.spaceship.waitForLoad().then(() => {
      console.log(this.spaceship.model);
    });

    ...
  }
gman
  • 100,619
  • 31
  • 269
  • 393
  • Works like a charm! Now, I know this is only a question of taste, but how would you go about abstracting it away even more? Is there no way of initializing the class and get the loaded model directly? Or does `GLTFLoader` prevent us from doing that? – ronnow May 22 '20 at 09:05
  • I'm not sure what you're asking. I'd certainly move the loader out of Spaceship as well as `loadModel`. Neither of those are referencing anything in Spaceship. One way or another though you need to wait for the model to load. Either you do that with a callback, a promise, async/await. It's up to you. – gman May 22 '20 at 09:13
  • Sorry if I was unclear, I was asking if there was a way of loading the model inside the class and not having to call a method `waitForLoad` outside of the class. I was thinking that it would make more sense and make the code more clear. So you would suggest making a new class responsible for loading a model? And then use it inside Spaceship? – ronnow May 22 '20 at 09:27
  • If spaceship controls everything about model then there is no reason to access model outside spaceship. Spaceship can load the model, add it to the scene and deal with it entirely. I guess that assumes you're calling `spaceshipInstance.update` in your render loop or [the equivalent](https://threejsfundamentals.org/threejs/lessons/threejs-game.html) – gman May 22 '20 at 10:38