Svelte-Cubed: Loading Your glTF Models

alexwarnes

Alex Warnes

Posted on May 26, 2022

Svelte-Cubed: Loading Your glTF Models

This article is the fourth in a beginner series on creating 3D scenes with svelte-cubed and three.js. If you want to learn how we got here you can start from the beginning:

Part One: Svelte-Cubed: An Introduction to 3D in the Browser
Part Two: Svelte-Cubed: Adding Motion to 3D Scenes
Part Three: Svelte-Cubed: Creating Accessible and Consistent Experience Across Devices

Octahedrons are great, but in many cases you’ll want to use a more complex model created in a modeling tool like Blender. These models can come in a variety of file types (e.g. glTF, fbx, etc). We’ll be working with gltf files, but this approach will work for others as well.

Simply put, we’re using the plain ol’ threejs GLTFLoader and then passing the model to a svelte cubed component. Simple. But if you’re new to 3D like me, a step by step walkthrough is a helpful reference and we’ll break it into several small steps to show a model, and then some optional patterns you can implement for reusability and loading.

If you know what you’re doing and just want to skip to the code, jump into the final scene: https://svelte.dev/repl/8ea0488302bb434991cc5b82f653cdb5?version=3.48.0

What We’ll Cover:

  1. Find cool models
  2. Create a Svelte component for the model
  3. Import the GLTFLoader from threejs
  4. Load the model onMount
  5. Conditionally pass it into a svelte cubed <Primative /> component

Optional:

  • Create a reusable GLTF Component
  • Handle loading states

Step 1: Find Cool Models

Threejs Example Models

You can find a bunch of example glTF/glb models from threejs itself in the manual or in the examples directory

Khronos Group

The Khronos group is responsible for the glTF spec. They also have an examples directory

SketchFab

There’s a huge selection of models on SketchFab, many of which you can download for free. And of course please give credit to model creators.

Step 2: Create a Svelte Component for the Model

Use this REPL to start with a basic svelte cubed scene: https://svelte.dev/repl/c0c34349f1f6405bb25700599a841083?version=3.48.0

Add a new component called LittleCity.svelte with a script tag add two variables:

  1. A URL to the glTF file
  2. An empty variable that will hold the model once we get it
<script>
/*
Model taken from the threejs examples. Created by antonmoek:
https://sketchfab.com/antonmoek
*/
  const modelURL = 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf'
  const model = null;
</script>
Enter fullscreen mode Exit fullscreen mode

Step 3: Import the GLTFLoader from threejs

Whatever filetype you have, threejs probably has a loader for that. In our case (or if using a glb) we’ll use the GLTFLoader. Add that import to the top of your script tag and BE SURE TO INCLUDE THE FILE EXTENSION.

  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
Enter fullscreen mode Exit fullscreen mode

Step 4: Load the Model onMount

When our component mounts, we want to load the model asynchronously and assign it to the model variable.

We’ll make a load function first, which will instantiate the loader and return a promise with our model, and then invoke that onMount and handle the response (which is the model).

import { onMount } from 'svelte';

// …

function loadGLTF() {
  const loader = new GLTFLoader();
  return loader.loadAsync(modelURL)
}

onMount(() => {
  loadGLTF().then(_model => model = _model);
})
Enter fullscreen mode Exit fullscreen mode

That’s it! We’ll add a catch block later when we make this a reusable component. If you’ve never worked with models before, definitely console log the model to see what that data structure looks like! There are a lot of interesting and (potentially) useful artifacts there.

Step 5: Conditionally Render the Model

Svelte Cubed provides a component called Primitive to handle almost anything-thats-not-a-mesh (e.g. models, axes, grid, etc). Primitive can accept props like position, rotation, scale, and other things you would expect. The Primitive’s object prop will take the scene of our model data. Naturally, we only want to render this if we have our model loaded, so we’ll wrap it in a conditional statement.

<script>
import * as SC from 'svelte-cubed';
// …
</script>

{#if model}
  <SC.Primitive
    object={model.scene}
    scale={[.05,.05,.05]}
  />
{/if}
Enter fullscreen mode Exit fullscreen mode

Over in App.svelte let’s import and drop in our component:

<script>
  // … other imports
  import LittleCity from './LittleCity.svelte';

  // …
</script>

<SC.Canvas>
<!-- … all the scene stuff is here -->

<LittleCity />

</SC.Canvas>
Enter fullscreen mode Exit fullscreen mode

WOW! Take a look around that city!

A low-poly 3D model of a city with several buildings and cars.

That’s the basic approach for loading an individual model, but that’s a lot of code to rewrite every time you want to load a single model. It might also be nice to indicate the loading status of models. So in the next section we’ll create a basic reusable GLTF component and handle loading.

To recap, here’s what we have so far in our LittleCity.svelte component:

<script>
  import * as SC from 'svelte-cubed';
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
  import { onMount } from 'svelte';
  import { modelURL } from './stores';

  const modelURL = 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf'
  let model = null;

  function loadGLTF() {
    const loader = new GLTFLoader();
    return loader.loadAsync(modelURL['littleCity']);
  }

  onMount(() => {
    loadGLTF().then(_model => model = _model);
  })

</script>

{#if model}
  <SC.Primitive
    object={model.scene}
    scale={[.05,.05,.05]}
  />
{/if}

Enter fullscreen mode Exit fullscreen mode

Optional: A Reusable GLTF Component

This is a basic implementation. Share your improvements in the comments!

First we’ll setup some global state in a new file stores.js to help us manage model URLs and handle loading states.

import { writable, readable, derived } from 'svelte/store';

// Contains the status of all models
export const statusOfModels = writable({}); // { uniqueName: 'LOADING' | 'ERROR' | 'SUCCESS' }

// Returns a boolean if any model has a status of loading
export const modelsLoading = derived(statusOfModels, statusObj => {
  return Object.values(statusObj).includes('LOADING');
})

// Updates a model's status based on its unique name
export const updateModelStatus = (name, status) => {
  statusOfModels.update(current => {
    return {  
      ...current,
      [name]: status,
    }
  })
}

// List of example model URLs
export const modelURL = {
  littleCity: 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
  mountains: 'https://threejs.org/manual/examples/resources/models/mountain_landscape/scene.gltf',
  llama: 'https://threejs.org/manual/examples/resources/models/animals/Llama.gltf',
  pug: 'https://threejs.org/manual/examples/resources/models/animals/Pug.gltf',
  sheep: 'https://threejs.org/manual/examples/resources/models/animals/Sheep.gltf',
}
/*
  Models taken from the threejs examples. 
  Little City model created by antonmoek:
  https://sketchfab.com/antonmoek
*/

Enter fullscreen mode Exit fullscreen mode

Next we’ll create a new svelte component called ReusableGLTF.svelte that will accept some props and emit a custom event to share its loading status with its parent component.


<script>
  import * as SC from 'svelte-cubed';
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
  import { createEventDispatcher } from 'svelte';
  import { onMount } from 'svelte';

  // Component Props
  export let modelURL;
  export let scale = [1, 1, 1];
  export let position = [0, 0, 0];
  export let rotation = [0, 0, 0];
  export let name = 'UniqueName_' + Math.random() + Date.now();

  let model = null;

  //   Custom Event to track loading status from parent
  const dispatch = createEventDispatcher();
  function emitStatus(status){
    dispatch('statusChange', {name, status});
  }

  function loadGLTF() {
    const loader = new GLTFLoader();
    return loader.loadAsync(modelURL)
  }

  onMount(() => {
    if (modelURL) {
      emitStatus('LOADING');
      loadGLTF()
        .then(_model => {
          model = _model;
          emitStatus('SUCCESS');
        })
        .catch(err => {
          console.error('Error loading model:', name, err)
          emitStatus('ERROR');
      })
    }
  })

</script>

{#if model}
  <SC.Primitive
      object={model.scene}
      {scale}
      {position}
      {rotation}
    />
{/if}

Enter fullscreen mode Exit fullscreen mode

Should look pretty similar to our previous LittleCity.svelte component! Now, I know what you’re thinking, “Math.random() is a red flag!” And you’re right, this implementation would be much stronger with a uuid instead of name to use for tracking loading states. Do it! You can use a uuid package for that and update the loading patterns to use id instead of name. Let’s live dangerously and carry on.

In our App.svelte component, let’s import ReusableGLTF.svelte, import modelURL and then remove our LittleCity.svelte component and use our new component instead!

<script>
  // … other imports
  import ReusableGLTF from './ReusableGLTF.svelte';
  import { modelURL } from './stores';
  // …
</script>

<SC.Canvas>
<!-- … all the scene stuff is here -->

<!-- Don't need this once we have a reusable component! -->
  <!-- <LittleCity /> -->

  <ReusableGLTF 
    modelURL={modelURL['littleCity']} 
    name="littleCity" 
    scale={[.05,.05,.05]} 
  />


</SC.Canvas>
Enter fullscreen mode Exit fullscreen mode

There it is! Let’s add some more…

  <ReusableGLTF 
    modelURL={modelURL['llama']} 
    name="llama" 
    position={[-6, 17, 0]} 
    rotation={[0, Math.PI * 1.25, 0]} 
  />
  <ReusableGLTF 
    modelURL={modelURL['pug']} 
    name="pug" 
    position={[0, 17, 0]} 
  />
  <ReusableGLTF 
    modelURL={modelURL['sheep']} 
    name="sheep" 
    position={[-6, 17, 4]} 
    rotation={[0, Math.PI * 1.25, 0]} 
  />
Enter fullscreen mode Exit fullscreen mode

The heroes our city needs!

A low-poly 3D model of a city with several buildings and cars. There is a llama, sheep, and pug standing on a rooftop overlooking the city.

Lastly, let’s handle loading and then we’re done (for now). Remember each ReusableGLTF component emits a custom event called statusChange. Learn more about custom events here. Let’s update our App.js to handle that event by importing from the store.js and creating a new handle function:

  import { statusOfModels, modelURL, modelsLoading, updateModelStatus } from './stores';

  function handleStatusChange(evt) {
    updateModelStatus(evt.detail.name, evt.detail.status);
  }

Enter fullscreen mode Exit fullscreen mode

Optional: Handle Loading States

Now add the event listener and handler to each ReusableGLTF component (or at least any component you want to track loading for) like so:

  <ReusableGLTF 
    modelURL={modelURL['littleCity']} 
    name="littleCity" 
    scale={[.05,.05,.05]} 
    on:statusChange={handleStatusChange} 
  />

Enter fullscreen mode Exit fullscreen mode

Now that’s some good data! Feel free to drop in console logs to see what’s going on, or just use $: console.log(“statuses:”, $statusOfModels) to get a log any time that updates.

Armed with this data, we can create a new Loading.svelte component that takes one prop:

<script>
  export let showLoading = false;
</script>

{#if showLoading}
  <div class="loading-container">
    <p>
      Loading...
    </p>  
  </div>
{/if}

<style>
  .loading-container {
    position: fixed;
    left: 1rem;
    bottom: 1rem;
    background: #00000088;
    color: #fafbfc;
    padding: .5rem .875rem;
  }

  p {
    margin: 0;
  }
</style>

Enter fullscreen mode Exit fullscreen mode

Let’s import it and drop it into our App.svelte component. We already set up a store that returns a boolean if any model status === loading, so we’ll pass that as a prop:

<script>
  // … other imports
  import Loading from './Loading.svelte';

  // …
</script>

<Loading showLoading={$modelsLoading} />

Enter fullscreen mode Exit fullscreen mode

Aaaaand we’re done! A lot of what’s going on here can be improved and customized for your needs, but now we have the building blocks of adding and combining glTF/glb models. You could build a whole museum or showcase your own models. We didn’t even talk about animations yet… Maybe next time.

Share your improvements and implementations in the comments! Seeing what other people build is a huge source of inspiration.

Resources

REPL: https://svelte.dev/repl/8ea0488302bb434991cc5b82f653cdb5?version=3.48.0

App.svelte

<script>
import * as THREE from 'three';
import * as SC from 'svelte-cubed';
import LittleCity from './LittleCity.svelte';
import ReusableGLTF from './ReusableGLTF.svelte';
import Loading from './Loading.svelte';
import { statusOfModels, modelURL, modelsLoading, updateModelStatus } from './stores';

function handleStatusChange(evt) {
  updateModelStatus(evt.detail.name, evt.detail.status);
  }

</script>

<SC.Canvas
  background={new THREE.Color("skyblue")}
  antialias
>

  <SC.PerspectiveCamera 
    position={[-10, 36, 20]}
    near={0.1}
    far={500}
    fov={40}
  />

  <SC.OrbitControls 
    enabled={true}
    enableZoom={true}
    autoRotate={false}
    autoRotateSpeed={2}
    enableDamping={true}
    dampingFactor={0.1}
    target={[-6, 17, 0]}
  />

  <SC.DirectionalLight
    color={new THREE.Color(0xffffff)}
    position={[0,10,10]}
    intensity={0.75}
    shadow={false}
  />
  <SC.AmbientLight
    color={new THREE.Color(0xffffff)}
    intensity={0.75}
  />

  <!-- Don't need this once we have a reusable component! -->
  <!--   <LittleCity /> -->

  <ReusableGLTF 
    modelURL={modelURL['littleCity']} 
    name="littleCity" 
    scale={[.05,.05,.05]} 
    on:statusChange={handleStatusChange} 
  />
  <ReusableGLTF 
    modelURL={modelURL['llama']} 
    name="llama" 
    position={[-6, 17, 0]} 
    rotation={[0, Math.PI * 1.25, 0]} 
    on:statusChange={handleStatusChange} 
  />
  <ReusableGLTF 
    modelURL={modelURL['pug']} 
    name="pug" 
    position={[0, 17, 0]} 
    on:statusChange={handleStatusChange} 
  />
  <ReusableGLTF 
    modelURL={modelURL['sheep']} 
    name="sheep" 
    position={[-6, 17, 4]} 
    rotation={[0, Math.PI * 1.25, 0]} 
    on:statusChange={handleStatusChange} 
  />

</SC.Canvas>
<Loading showLoading={$modelsLoading} />
Enter fullscreen mode Exit fullscreen mode

ReusableGLTF.svelte


<script>
  import * as SC from 'svelte-cubed';
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
  import { createEventDispatcher } from 'svelte';
  import { onMount } from 'svelte';

  // Component Props
  export let modelURL;
  export let scale = [1, 1, 1];
  export let position = [0, 0, 0];
  export let rotation = [0, 0, 0];
  export let name = 'UniqueName_' + Math.random() + Date.now();

  let model = null;

  //   Custom Event to track loading status from parent
  const dispatch = createEventDispatcher();
  function emitStatus(status){
    dispatch('statusChange', {name, status});
  }

  function loadGLTF() {
    const loader = new GLTFLoader();
    return loader.loadAsync(modelURL)
  }

  onMount(() => {
    if (modelURL) {
      emitStatus('LOADING');
      loadGLTF()
        .then(_model => {
          model = _model;
          emitStatus('SUCCESS');
        })
        .catch(err => {
          console.error('Error loading model:', name, err)
          emitStatus('ERROR');
      })
    }
  })

</script>

{#if model}
  <SC.Primitive
      object={model.scene}
      {scale}
      {position}
      {rotation}
    />
{/if}

Enter fullscreen mode Exit fullscreen mode

Loading.svelte

<script>
  export let showLoading = false;
</script>

{#if showLoading}
  <div class="loading-container">
    <p>
      Loading...
    </p>  
  </div>
{/if}

<style>
  .loading-container {
    position: fixed;
    left: 1rem;
    bottom: 1rem;
    background: #00000088;
    color: #fafbfc;
    padding: .5rem .875rem;
  }

  p {
    margin: 0;
  }
</style>

Enter fullscreen mode Exit fullscreen mode

stores.js

import { writable, readable, derived } from 'svelte/store';

// Contains the status of all models
export const statusOfModels = writable({}); // { uniqueName: 'LOADING' | 'ERROR' | 'SUCCESS' }

// Returns a boolean if any model has a status of loading
export const modelsLoading = derived(statusOfModels, statusObj => {
  return Object.values(statusObj).includes('LOADING');
})

// Updates a model's status based on its unique name
export const updateModelStatus = (name, status) => {
  statusOfModels.update(current => {
    return {  
      ...current,
      [name]: status,
    }
  })
}

// List of example model URLs
export const modelURL = {
  littleCity: 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
  mountains: 'https://threejs.org/manual/examples/resources/models/mountain_landscape/scene.gltf',
  llama: 'https://threejs.org/manual/examples/resources/models/animals/Llama.gltf',
  pug: 'https://threejs.org/manual/examples/resources/models/animals/Pug.gltf',
  sheep: 'https://threejs.org/manual/examples/resources/models/animals/Sheep.gltf',
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
alexwarnes
Alex Warnes

Posted on May 26, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related