Introduction To 3D With Svelte
Published Oct 31, 2022
Table of Contents
Introduction
By the end of this post you’re going to learn how to spice up your boring site using 3D with Svelte in the browser.
⚠️ The video is outdated but the post has been updated to the newest Threlte 6 version.
🧪 The project files are available on GitHub and you can try it on StackBlitz.
What Is Threlte?
Three.js is a 3D framework for JavaScript that abstracts having to write low-level graphics code yourself and lets you create anything from immersive 3D experiences to games in your browser.
🐿️ You might have heard of Svelte Cubed, or Svelthree but Threlte seems to be the winner.
Threlte is a 3D framework for Svelte for using Three.js in a more declarative way.
What does declarative even mean?
Using imperative code you have to specify each step to get to a desired outcome.
const titleEl = document.createElement('h1')
titleEl.innerText = 'Hello'
document.body.append(titleEl)
Using declarative code you just state the desired outcome.
<h1>Hello</h1>
Creating Your First 3D Scene
If you want to learn more about Three.js and 3D in general read Three.js fundamentals and you can use the Three.js editor to play around.
The way you learn Three.js is from the Three.js documentation and you’re going to see how intuitive it’s to translate a Three.js example to Threlte.
The easiest way to get started with Threlte is using their CLI.
npm create threlte
┌ Welcome to Threlte!
│
◇ Where should we create your project?
│ (hit Enter to use current directory)
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ Select additional options (use arrow
keys/space bar)
│ none
│
◇ Select Threlte packages (use arrow
keys/space bar)
│ @threlte/extras
│
◇ Initialize a git repository?
│ No
│
◇ Install dependencies using pnpm?
│ No
│
└ Your project is ready!
The only additional package I’m going to use is @threlte/extras
for some Threlte utils.
If you haven’t, install the dependencies with npm i
and start the development server with npm run dev
.
You can play around with the default example but I’m going to delete the lib
folder and start from nothing.
Inside routes
create a +layout.svelte
file.
<div class="scene">
<slot />
</div>
<svelte:head>
<title>Threlte</title>
</svelte:head>
<style>
.scene {
position: absolute;
inset: 0;
background: radial-gradient(hsl(220 14% 20%), hsl(220 20% 10%));
}
</style>
Using a wrapper element .scene
is important because the dimensions of <canvas>
are based on the parent.
Inside +page.svelte
I’m going to set up the scene.
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './scene.svelte'
</script>
<Canvas>
<Scene />
</Canvas>
Create a scene.svelte
component where the magic is going to happen and add a camera, lights and a mesh to your scene.
<script lang="ts">
import { T } from '@threlte/core'
import { Grid, OrbitControls, TransformControls } from '@threlte/extras'
import * as Three from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils'
</script>
<!-- Grid -->
<Grid cellColor="#808080" sectionSize={0} />
<!-- Camera -->
<T.PerspectiveCamera position={[20, 20, 20]} fov={50} makeDefault>
<!-- Controls -->
<OrbitControls enableDamping />
</T.PerspectiveCamera>
<!-- Lights the scene equally -->
<T.AmbientLight color="#ffffff" intensity={0.2} />
<!-- Light that casts a shadow -->
<T.DirectionalLight
color="#ffffff"
intensity={2}
position={[10, 10, 0]}
shadow.camera.top={8}
castShadow
/>
<!-- Sphere -->
<T.Mesh position={[0, 4, 0]} let:ref castShadow>
<T.SphereGeometry args={[4, 64, 64]} />
<T.MeshStandardMaterial color="#ffffff" />
<TransformControls object={ref} />
</T.Mesh>
<!-- Floor -->
<T.Mesh rotation.x={DEG2RAD * 90} receiveShadow>
<T.PlaneGeometry args={[20, 20]} />
<T.MeshStandardMaterial color="#ffffff" side={Three.DoubleSide} />
</T.Mesh>
<T>
is where everything comes from and it extends the base Three.js class- I’m using a
<PerspectiveCamera>
that’s slightly above the mesh with a set field of view - The
<AmbientLight>
is going to equally light your scene, think of it as cheap global illumination - The
<DirectionalLight>
is more like a sun in your scene that also casts a shadow - The sphere and floor use
<T.Mesh>
where you can set the geometry and material for the mesh - The floor uses
DoubleSide
from Three.js, so it’s visible from both sides and uses theDEG2RAD
helper to rotate it by 90 degrees because it uses radians
There’s a bunch of helpers from @threlte/extras
. You can render a grid with the <Grid>
component and use <OrbitControls>
to use your mouse to orbit around your scene. If you want to move an object in the scene you can attach <TransformControls />
to meshes and lights.
Threlte has an intuitive API.
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh position.y={1}>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshBasicMaterial color="aqua" />
</T.Mesh>
The same example using Three.js.
const mesh = new THREE.Mesh()
const geometry = new THREE.BoxGeometry(1, 2, 1)
const material = new THREE.MeshBasicMaterial()
mesh.position.y = 1
material.color.set('aqua')
Translating a Three.js example to Threlte is intuitive.
You can pass the constructor arguments via args
and if you want to know other options look at Object3D from the Three.js docs which is the base class for most objects and has things like position
, rotation
, and scale
.
Threlte uses pierced props which you have already seen used to pass position.y
for a mesh, and shadow.camera.top
for the camera to increase how far the shadow is cast.
You only have to look at a Three.js example and you can intuitively translate it to Threlte.
Importing 3D Models
You can get free 3D models from Sketchfab, and make sure you check “downloadable” to filter the results.
There’s a lot of options for 3D file formats but you want GLB (GL Transmission Format Binary file) that’s more efficient for sharing 3D data on the web (GLTF is also fine but GLB keeps everything in one binary file).
Threlte makes it easy to import a 3D model using the useGltf
hook, and you can even control animations with the useGltfAnimations
hook.
<script lang="ts">
import { T, useFrame } from '@threlte/core'
import { OrbitControls, useGltf } from '@threlte/extras'
import Bloom from './bloom.svelte'
let y = 2
let rotation = 0
// Spooky floating ghost 👻
function levitate() {
const time = Date.now() / 1000
const speed = 1
const offset = 3
y = Math.sin(time * speed) + offset
requestAnimationFrame(levitate)
}
// Rotates model on `Y` axis
useFrame((_, delta) => {
rotation += delta * 0.4
})
levitate()
</script>
<!-- Bloom postprocessing effect -->
<Bloom />
<!-- Orthographic camera -->
<T.OrthographicCamera position={[10, 10, 10]} zoom={40} makeDefault>
<!-- Controls -->
<OrbitControls enableDamping />
</T.OrthographicCamera>
<!-- Ambient light for ambience -->
<T.AmbientLight color="#0000ff" intensity={10} />
<!-- Main light -->
<T.PointLight intensity={2} position={[4, 2, 4]} color="#76aac8" />
<!-- Ghost -->
{#await useGltf('/assets/ghost.glb') then ghost}
<T is={ghost.scene} position={[0, y, 0]} scale={0.4} />
{/await}
<!-- Garden -->
{#await useGltf('/assets/garden.glb') then garden}
<T is={garden.scene} rotation.y={rotation} />
{/await}
You can look at the <Bloom />
postprocessing effect in the example, but I mostly copied it from the Threlte docs.
That’s it! 🥳