Alex Warnes
Posted on May 26, 2022
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:
- Find cool models
- Create a Svelte component for the model
- Import the GLTFLoader from threejs
- Load the model
onMount
- 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:
- A URL to the glTF file
- 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>
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';
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);
})
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}
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>
WOW! Take a look around that city!
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}
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
*/
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}
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>
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]}
/>
The heroes our city needs!
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);
}
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}
/>
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>
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} />
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} />
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}
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>
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',
}
Posted on May 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.