Svelte-Cubed: Loading Your glTF Models
Tags: svelte, threejs, beginners, tutorial
Published: 2022-05-26 15:45:13

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](https://dev.to/alexwarnes/svelte-cubed-an-introduction-to-3d-in-the-browser-1ea3)
**Part Two:** [Svelte-Cubed: Adding Motion to 3D Scenes](https://dev.to/alexwarnes/svelte-cubed-adding-motion-to-3d-scenes-51lo)
**Part Three:** [Svelte-Cubed: Creating Accessible and Consistent Experience Across Devices](https://dev.to/alexwarnes/svelte-cubed-creating-an-accessible-and-consistent-experience-across-devices-42ae)

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](https://github.com/mrdoob/three.js/tree/master/manual/examples/resources/models) or in the [examples directory](https://github.com/mrdoob/three.js/tree/master/examples/models/gltf)

### Khronos Group
The Khronos group is responsible for the   glTF spec. They also have an [examples directory](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0)

### SketchFab
There’s a huge selection of models on [SketchFab](https://sketchfab.com/search?features=downloadable&type=models), 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

```html
<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>
```

## 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.**

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

## 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).

```javascript
import { onMount } from 'svelte';

// …

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

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

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.

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

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

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

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

  // …
</script>

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

<LittleCity />

</SC.Canvas>
```

WOW! Take a look around that city!

![A low-poly 3D model of a city with several buildings and cars.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9xwbw9ye2rcnb6s6uaj.png)

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:
```html
<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}

```

## 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.

```javascript
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
*/

```

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.

```html

<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}

```

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!

```html
<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>
```

There it is! Let’s add some more…

```html
  <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]} 
  />
```

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ma9fno7m3b9scvsmut6g.png)

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](https://svelte.dev/tutorial/component-events). Let’s update our `App.js` to handle that event by importing from the `store.js` and creating a new handle function:

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

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

```

## 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:

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

``` 

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:

```html
<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>

```

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:

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

  // …
</script>

<Loading showLoading={$modelsLoading} />

```

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**
```html
<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} />
```

**ReusableGLTF.svelte**
```html

<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}

```

**Loading.svelte**

```html
<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>

```

**stores.js**
```javascript
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',
}
```