- Grischa Erbe 3D Developer and Creator of Threlte
Rapidly build interactive 3D apps for the web with Threlte 6. Let’s dive into the incredibly powerful new API design, the brand new Plugin API, a practical workflow for utilizing glTF files and additional enhancements that streamline the development of immersive 3D experiences in Svelte.
Hey everybody, my name is Grischa Erbe, i'm the creator of threlt and today i'd like
to give you a tour on the upcoming version of Threlt which is version 6 and
what it's like to work with it. Let's start out with the basics. What even is
Threlt? Threlt is a framework and an ecosystem that allows you to develop
3.js apps in a Svelte native way. Most of you will probably know 3.js, it's a
native vanilla whatever you want to call it 3.js have? For the sake of comparison
here's what we're used to when we work with the DOM and DOM elements. We benefit
from declarative markup style which is just very convenient to use and
therefore we mostly don't care about the underlying rendering or otherwise
functional nuances. It's also therefore easy to maintain for larger apps. We
don't need to clean up after ourselves, we just remove elements. And we get
resource caching out of the box with images, videos, and other elements like
audio and stuff like that. Now let's see how 3.js holds up to that. There is no
declarative markup style to it, but it's still convenient and it has a well
defined API. From my experience it's hard to maintain for larger apps and we
sometimes need to take care about how rendering is done actually, although most
of it is abstracted away. We do need to clean up after ourselves, so if we remove
something we need to dispose resources for example and there is no such thing
as caching out of the box because resources not only need to be loaded but
but also parsed and uploaded to the GPU.
Threlt aims to tackle most of these issues
with the help of Svelte.
With Threlt, we do get declarative markup
for everything that 3.js has to offer.
It's easy to maintain and reason about.
We still need to take care of rendering, sometimes
for advanced use cases, but there
are reasonable defaults in place.
And because Svelte components have something
called a lifecycle, we can use that lifecycle
to automatically dispose unused resources.
We also benefit from an app-wide resource caching,
which is a huge performance boost for larger Thread apps.
To give you an idea of what's possible using Thread,
let's build a quick example together.
OK, so let's assume there is a Thread conference happening
later this year.
To engage people to come to the conference,
we are issuing digital tickets.
But these tickets, they will have
like a physical appearance of some sorts,
kind of like the digital equivalent of a regular ticket.
And because it's about technology,
it's supposed to look fancy and futuristic.
So we go for a dark blueish purple palette
and make it shiny and reflective.
So what we are looking at here is a bare bones SvelteKit app.
I just initialized it using the skeleton project option
together with TypeScript, Pretty, and ESLint.
And I added Tailwind.
That's about it.
And I added like a bare bones page, which looks like this.
And here in the middle, there's this graphics missing
that I'd like to implement with you.
We first need to install Threlts.
And for that, let's check out the new Threlt website.
This is the new website of Threlt 6.
And if we jump to documentation and then installation,
we see that we get to choose the packages that we actually
want to use.
And of course, we cannot use Thread without ThreadCore.
But for this example, we are also
using ThreadExtras, which is a library
with useful abstractions on top of three JS classes that--
or other classes, actually--
that we can use to our advantage.
And we also want to use TypeScript,
so we install the 3.js types as well.
We copy the install command
and install the packages.
Also, what we do is
we need to adapt the Vite configuration.
So we copy that snippet over to here.
And we are all set up. Let's jump into the page. This is the page we're looking at and
this is how it looks now. And here we want to display our digital ticket. So we have this
div here which spans basically the whole viewport and we want to use that. So we are going to place
is the canvas element here,
and, or the canvas component, actually.
It's not the canvas element.
The canvas component sets up the basics of our whole app.
It creates a renderer for us.
It sets sensible defaults.
It sets up all the context stuff that's happening
because these components that we use with Throat
cannot rely on like the hierarchy of the DOM.
We have to set that up as ourselves,
So basically, Threlt has to set that up for us.
And it relies heavily on the context,
on the context API of Svelte.
And therefore, it's important to keep in mind
that this component is the root of our context.
And therefore, everything we do needs
to be a child of this component.
The easiest way to make sure that we are always
acting in the context of this component
is to create a new Svelte component, which
called... I call it always "scene" or most of the times I call it "scene" and
we'll just put it here. So as long as we are in this scene component here we are
acting in the context of our Thread app. Now the canvas component automatically
resizes to fit the parent element. In this case the div that spans our whole
viewport. Let's start out with something like a "Hello World" in 3.js. That is a
camera and a cube and we see how we can use props to transform or modify the
attributes of our cube or our camera. The first thing we are going to do is
implement our camera. Threlt already provides a default camera for us, but in
order to have complete control over the camera's position and field of view and
stuff like that, it's a good practice to start on a scene with implementing our
own camera. For that I opened up the 3js documentation on the right, and you
see that there is this perspective camera. And normally you can also see
that this is a class that needs to be instantiated with the new keyword. We are
also going to learn how we can use the probably most important component which
is called T to implement basically any 3JS class. Let's first import and
and implement the camera as we would do with vanilla 3js.
And after that, we are going to use the T component
to implement it the Threadway and see
what the differences are.
So in the first step, I'm going to import the perspective
camera from 3.
And to instantiate the camera, I would use the new keyword
to make a new perspective camera.
You can also see that all constructor arguments
So just like that, I'm creating a new camera.
Keep in mind that this camera is not part of our scene graph
right now, so we would need to add it to the scene
or to a parent object.
And we would also need to somehow start
rendering with that camera. Now let's see how we can implement a camera with
Throat. So first of all I'm going to delete everything that I just did and I
am using the component T to make a new camera. And I will reference the class
that I want to instantiate by dot notation. So I am using a dot perspective
camera to tell the component T what it
actually should be. In this
case a perspective camera. By doing this
I not only instantiated a perspective
camera but I also added it to the scene
graph. So this part of adding it to the
scene or to a parent object is taken
care of for me. So how would we start
rendering with that camera then. Because
Threlt creates a renderer for us which
automatically renders our scene, we
also need to tell it what camera to use
and we do that by using the property
"makeDefault". So right now we are
rendering our scene with this camera. Let's
say we want to transform the camera to be
at a different position. We can also use
a property for that. In this case we use
the position property and we have to provide it three numbers, an array, a
tuple of three numbers, x, y, and z. In this case I'd set it to x zero, y zero, and z
to 40. I'm just going to rearrange the windows here. Let's stop there for a
second. Why can we use the property position on this component and where
does the prop type come from? At this point it's important to note that this
specific property position is not hardwired into the component T, but it is
inferred from the class PerspectiveCamera that this component is
instantiating. So if we have a look at the documentation of this PerspectiveCamera,
camera, you will see that the base class for it is the class "Camera", which is in
turn derived from the class "Object3D". On the class "Object3D" you will find a
property called "Position", which is of type "Vector3". "Vector3" again is also a
class with a method called "set". This method takes three arguments, and in
this case this resembles our tuple with three numbers x, y, and z. So if there is a
method called "set" on the value with the name of a certain component prop, it will
be called with the values of the property, and if not, the property will be
set as is. That's nearly all there is to it and it sounds more complicated than it is.
Let's change the field of view of the camera. We can use IntelliSense to check for available
properties on the components and we use the property "fov" to change the field of view of
our camera to 10 degrees. The camera is all set up but we don't have anything in our scene yet.
We're going to use the 3JS class Mesh to display an object in our scene.
A 3JS Mesh is comprised of a geometry and a material.
The geometry being the shape and the material being the appearance of the object.
We can make use of Threl's Attach API to attach objects to a parent property.
In this case, we are attaching a geometry to the property Geometry
of the parent object which is a mesh and we are attaching the mesh basic material
to the property material of the parent which is also the mesh. And that's what we end up with.
It's not impressive but it's a good start. And of course this is not yet a ticket. That's where
a software like Blender comes in. I prepared a model of a ticket how I imagined it. It's partly
shiny, which makes it resemble a film laminated surface. We can export that
model to a GLB file, which is the current standard for files containing 3D models
and even complete scenes. There are several ways to use this model in
Threlt, and I'd like to show you the newest entry. It's a CLI to transform a
GLB model to a ready-to-use Threlt component. For that, we navigate to our
static directory and run the command "npx add Threlt/gltf" and because right
Right now this tool is only available with the tag @next.
We also have to provide that.
Then we reference the GLB file we want to transform.
And in this instance, we also want to generate TypeScript types.
And that's why we also provide the flag T.
And we see that the CLI transformed our GLB file into a ready-to-use thread component.
The tool supports generating TypeScript types.
It can automatically add props to render shadows.
It can transform and compress the model for web use and much more.
It's actually a pretty vital part in the Threlt ecosystem.
When we have a look at the generated component, we see that it makes use of the hook "use
gltf", a hook provided by the package "addthrelt-extras".
And we also see that it's a 3JS mesh, just like our cube, only that the geometry and
the material are extracted from the parsed and cached gltf file.
First, we copy the component to our source directory next to our scene.
Then, we can use our ticket component just like any other Swirlt component.
For that, we get rid of our cube and insert the ticket instead.
When we check how our ticket looks like on our page, we will notice that it's not visible
That is because we don't have any light sources in our scene yet.
We could, of course, add an ambient light or a directional light, but for this example,
I'd like to use IBL, which stands for Image Based Lighting.
This technique is mostly used to create realistic lighting,
but we will use it to create really colorful and vibrant reflections.
We insert a component called "Environment" from the package "Thread Extras"
and provide a path to an image from the static directory.
This is the image I prepared, but generally you just want to have a really colorful image.
We're getting closer, but the lighting is too dim.
Let's go to our Ticket component and bump up the EnvMapIntensity of the Ticket's material.
We do that with another trick that Throat has up its sleeve.
We can use dot notation to access and set sub-properties of properties.
So we type "Material.EnvMapIntensity" and we dial that up to 11.
That already looks way better, but we could have also used a regular image for that.
Let's add simple animations as well as the attendees name and the ticket number.
We start out with the attendees name.
For that, we go back to our scene and insert the text component from the package Thread
We provide several properties, for example, the text that we want to display, the font
file that we want to use, and we want to adjust the font size,
make it a little bit bigger.
And since this object is also positioned in 3D space,
we also want to set the position of it.
Now, you have to fiddle around with that a little bit.
I made measurements in Blender to get the correct position
of the text.
And to let the text wrap at new lines,
also have to set the property "maxWidth". In this case I'm gonna set it to 1.5.
This already looks pretty good, but we also see that the material's appearance
is slightly different than the one from the ticket. Let's fix that. By default the
component text uses a mesh basic material, which does not react to image
based lighting. We can however use Threll's attach API to attach a new mesh
standard material to our text. We instantiate a new MeshStandardMaterial
with the T component and set the EnvMapIntensity to 11. Also notice that
we do not need to explicitly write out "attach = material" because
materials automatically attach to the parent's material property. Let's do that
again with the ticket number. This time we move the text up and use a black
MASH standard material without any roughness. We use the props "anchorX" and
"anchorY" to center the text.
It's time for a simple animation. Thread provides a hook called "useFrame" that
will run a callback on every frame. We can use that callback to alter a
variable that will slowly rotate the ticket. Let's declare a variable called "rotationY"
and set its initial value to zero. The callback of the hook use frame receives two arguments,
the latter being the time since the last frame. We can use this number to adapt the speed of the
rotation to different frame rates. We can now use the variable "rotationY" to rotate our ticket.
There's one problem though. Because the text elements and the ticket itself are different
entities, we first have to group them. For that, again, we use the T-Component to instantiate
a 3js group and add the ticket as well as the text elements as children to it. Now we
use the dot notation to rotate that group around the y-axis.
That's better, but there are some things that still need work. For example, the attendee's
name and the ticket number are only visible on one side of the ticket.
Naturally, you would also add the labels on the back side of the ticket.
But I rather only ever want to show the front side of the ticket.
We add a check in our callback that subtracts 180 degrees if the rotation is greater than
90 degrees, so if the ticket side faces the camera.
Therefore, only the front side of the ticket is visible.
Next, we add some basic interaction.
We do this by invoking the Threll plugin called "Interactivity" from Threll Extras.
This allows us to use DOM-like event handling with Threll components.
I'd like to add an interaction where the ticket slightly scales up when we hover over it.
Let's create a Svelte Spring Store with an initial value of 1.
We create event handlers for the events "PointerEnter" and "PointerLeave" and set the spring values
to 1.1 and 1.
Note that we added the event handlers on the 3.js group, which doesn't have a visual representation.
Thread supports event propagation similarly to how events propagate in the DOM.
Next, we use the stores value to scale the group.
And there we have it.
digital ticket for the Threat Conference 2023. That's it for today's example. If you want to
have a look at the source code, you can find it below. Also, feel free to check out the website
for any updates on Threat 6 and join our active Discord community, where you can always find
people that can help you with your project and that share their inspiring projects with you.
We are also always looking for active maintainers and contributions. Thank you for listening and
And thank you to the members of the Svelte Society for organizing this summit.
And also thank you to the people behind Svelte itself for making such an awesome product.