Editor’s note: We want to share more of the web dev and design community directly here on Codrops, so we’re very happy to start featuring Yuriy’s newest live coding sessions!
In this live stream of ALL YOUR HTML, we will be coding a Raymarching demo in Three.js from scratch. We’ll be using some cool Matcap textures, and add a wobbly animation to the scene, and all will be done as math functions. The scene is inspired by this demo made by Luigi De Rosa.
This coding session was streamed live on December 6, 2020.
Editor’s note: We want to share more of the web dev and design community directly here on Codrops, so we’re very happy to start featuring Yuriy’s newest live coding sessions!
In this ALL YOUR HTML coding session, you’ll learn how to create a beautiful shape with parametric functions, a hyperbolic helicoid, inspired by J. Miguel Medina’s artwork. We’ll be upgrading MeshPhysicalMaterial with some custom shader code, making an infinite animation.
This coding session was streamed live on January 10, 2021.
All things we see around are 3D shapes. Some of them are nice looking, some of them you’d like to touch and squeeze. But because it’s still quarantine time, and I don’t have many things around, I decided to create some virtual things to twist around. Here’s what I ended up with:
How to do that?
To render everything I used Three.js, just like Mario Carillo in his amazing demo, go check it out! Three.js is the best and most popular library to do things with WebGL at the moment. And if you are just starting your 3D path, it is definitely the easiest way to get something working.
If you just rotate the shape, its nothing special, its just rotating.
Interesting things start to happen when you rotate different parts of the shape with different speeds. Let’s say I want the bottom part of the object to rotate first, and than the rest, let’s see whats going to happen.
To do something like that, you would need to use a Vertex shader. I used a little bit of math and rotated parts of the object depending on the Y or Z coordinate. The simplified code looks like this:
So with a coordinate change, the rotation would change as well. The rotate function itself includes a lot of Sine-Cosine’s and matrices. All the math you need to rotate a point around some axis. To give it a more natural feel, I also change the easing function, so it looks like elastic rubber.
Visuals, MatCap
To create a nice look for objects, we could have used lights and shadows and complicated materials. But because there’s not so much in the scene, we got away with the most simple kind of material: MatCaps. So instead of making complicated calculations, you just use a simple image to reference the lighting and color. Here is how it looks like:
And here is how the torus shape looks like with this texture applied to it:
Amazing, isn’t it? And so simple.
So combining Matcaps and a rotation method, we can create a lot of cool shape animations that look great, and have this rubber feeling.
Go and create your shape with this technique and make sure to share it with us! Hope this simple technique will inspire you to create your own animation! Have a good day!
Today I’d like to share another thumbnail to full view animation with you. This time we have an initial grid with thumbnails that move with the mouse in 3D and have a magnetic effect including a little tooltip on hover. Once clicked, all items animate out, giving way to a content preview with more details.
The grid interaction looks as follows:
Here’s how the grid to content animation looks like:
I really hope you find this useful! Thanks for visiting!
Some months ago, I wrote about how to achieve a hover effect for a menu where an image would appear for each item. Then we saw how to apply this in Exploring Animations for Menu Hover Effects. When I discovered the fantastic site of One up Studio I wanted to combine their cool 3D-ish menu hover effect with that previous animation, so this is a little fusion between the two.
The 3D motion is slightly different and an image is shown in an angle, creating a playful look.
I really hope you like this little experiment and find it useful. Thanks for visiting!
Three.js is a JavaScript library for drawing in 3D with WebGL. It enables us to add 3D objects to a scene, and manipulate things like position and lighting. If you’re a developer used to working with the DOM and styling elements with CSS, Three.js and WebGL can seem like a whole new world, and perhaps a little intimidating! This article is for developers who are comfortable with JavaScript but relatively new to Three.js. Our goal is to walk through building something simple but effective with Three.js — a 3D animated figure — to get a handle on the basic principles, and demonstrate that a little knowledge can take you a long way!
Setting the scene
In web development we’re accustomed to styling DOM elements, which we can inspect and debug in our browser developer tools. In WebGL, everything is rendered in a single <canvas> element. Much like a video, everything is simply pixels changing color, so there’s nothing to inspect. If you inspected a webpage rendered entirely with WebGL, all you would see is a <canvas> element. We can use libraries like Three.js to draw on the canvas with JavaScript.
Basic principles
First we’re going to set up the scene. If you’re already comfortable with this you can skip over this part and jump straight to the section where we start creating our 3D character.
We can think of our Three.js scene as a 3D space in which we can place a camera, and an object for it to look at.
We can picture our scene as a giant cube, with objects placed at the center. In actual fact, it extends infinitely, but there is a limit to how much we can see.
First of all we need to create the scene. In our HTML we just need a <canvas> element:
<canvas data-canvas></canvas>
Now we can create the scene with a camera, and render it on our canvas in Three.js:
const canvas = document.querySelector('[data-canvas]')
// Create the scene
const scene = new THREE.Scene()
// Create the camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 1000)
scene.add(camera)
// Create the renderer
const renderer = new THREE.WebGLRenderer({ canvas })
// Render the scene
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.render(scene, camera)
For brevity, we won’t go into the precise details of everything we’re doing here. The documentation has much more detail about creating a scene and the various camera attributes. However, the first thing we’ll do is move the position of our camera. By default, anything we add to the scene is going to be placed at co-ordinates (0, 0, 0) — that is, if we imagine the scene itself as a cube, our camera will be placed right in the center. Let’s place our camera a little further out, so that our camera can look at any objects placed in the center of the scene.
Moving the camera away from the center allows us to see the objects placed in the center of the scene.
We can do this by setting the z position of the camera:
camera.position.z = 5
We won’t see anything yet, as we haven’t added any objects to the scene. Let’s add a cube to the scene, which will form the basis of our figure.
3D shapes
Objects in Three.js are known as meshes. In order to create a mesh, we need two things: a geometry and a material. Geometries are 3D shapes. Three.js has a selection of geometries to choose from, which can be manipulated in different ways. For the purpose of this tutorial — to see what interesting scenes we can make with just some basic principles — we’re going to limit ourselves to only two geometries: cubes and spheres.
Let’s add a cube to our scene. First we’ll define the geometry and material. Using Three.js BoxGeometry, we pass in parameters for the x, y and z dimensions.
// Create a new BoxGeometry with dimensions 1 x 1 x 1
const geometry = new THREE.BoxGeometry(1, 1, 1)
For the material we’ll choose MeshLambertMaterial, which reacts to light and shade but is more performant than some other materials.
// Create a new material with a white color
const material = new THREE.MeshLambertMaterial({ color: 0xffffff })
Then we create the mesh by combining the geometry and material, and add it to the scene:
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
Unfortunately we still won’t see anything! That’s because the material we’re using depends on light in order to be seen. Let’s add a directional light, which will shine down from above. We’ll pass in two arguments: 0xffffff for the color (white), and the intensity, which we’ll set to 1.
const lightDirectional = new THREE.DirectionalLight(0xffffff, 1)
scene.add(lightDirectional)
By default, the light points down from above
If you’ve followed all the steps so far, you still won’t see anything! That’s because the light is pointing directly down at our cube, so the front face is in shadow. If we move the z position of the light towards the camera and off-center, we should now see our cube.
const lightDirectional = new THREE.DirectionalLight(0xffffff, 1)
scene.add(lightDirectional)
// Move the light source towards us and off-center
lightDirectional.position.x = 5
lightDirectional.position.y = 5
lightDirectional.position.z = 5
Moving the light gives us a better view
We can alternatively set the position on the x, y and z axis simultaneously by calling set():
lightDirectional.position.set(5, 5, 5)
We’re looking at our cube straight on, so only one face can be seen. If we give it a little bit of rotation, we can see the other faces. To rotate an object, we need to give it a rotation angle in [radians](). I don’t know about you, but I don’t find radians very easy to visualize, so I prefer to use a JS function to convert from degrees:
We can also add some ambient light (light that comes from all directions) with a color tint, which softens the effect slightly end ensures the face of the cube turned away from the light isn’t completely hidden in shadow:
const lightAmbient = new THREE.AmbientLight(0x9eaeff, 0.2)
scene.add(lightAmbient)
Now that we have our basic scene set up, we can start to create our 3D character. To help you get started I’ve created a boilerplate which includes all the set-up work we’ve just been through, so that you can jump straight to the next part if you wish.
Creating a class
The first thing we’ll do is create a class for our figure. This will make it easy to add any number of figures to our scene by instantiating the class. We’ll give it some default parameters, which we’ll use later on to position our character in the scene.
In our class constructor, let’s create a Three.js group and add it to our scene. Creating a group allows us to manipulate several geometries as one. We’re going to add the different elements of our figure (head, body, arms, etc.) to this group. Then we can position, scale or rotate the figure anywhere in our scene without having to concern ourselves with individually positioning those parts individually every time.
Next let’s write a function to render the body of our figure. It’ll be much the same as the way we created a cube earlier, except, we’ll make it a little taller by increasing the size on the y axis. (While we’re at it, we can remove the lines of code where we created the cube earlier, to start with a clear scene.) We already have the material defined in our codebase, and don’t need to define it within the class itself.
Instead of adding the body to the scene, we instead add it to the group we created.
const material = new THREE.MeshLambertMaterial({ color: 0xffffff })
class Figure {
constructor(params) {
this.params = {
x: 0,
y: 0,
z: 0,
ry: 0,
...params
}
this.group = new THREE.Group()
scene.add(this.group)
}
createBody() {
const geometry = new THREE.BoxGeometry(1, 1.5, 1)
const body = new THREE.Mesh(geometry, material)
this.group.add(body)
}
}
We’ll also write a class method to initialize the figure. So far it will call only the createBody() method, but we’ll add others shortly. (This and all subsequent methods will be written inside our class declaration, unless otherwise specified.)
createBody() {
const geometry = new THREE.BoxGeometry(1, 1.5, 1)
const body = new THREE.Mesh(geometry, material)
this.group.add(body)
}
init() {
this.createBody()
}
Adding the figure to the scene
At this point we’ll want to render our figure in our scene, to check that everything’s working. We can do that by instantiating the class.
const figure = new Figure()
figure.init()
Next we’ll write a similar method to create the head of our character. We’ll make this a cube, slightly larger than the width of the body. We’ll also need to adjust the position so it’s just above the body, and call the function in our init() method:
createHead() {
const geometry = new THREE.BoxGeometry(1.4, 1.4, 1.4)
const head = new THREE.Mesh(geometry, material)
this.group.add(head)
// Position it above the body
head.position.y = 1.65
}
init() {
this.createBody()
this.createHead()
}
You should now see a narrower cuboid (the body) rendered below the first cube (the head).
Adding the arms
Now we’re going to give our character some arms. Here’s where things get slightly more complex. We’ll add another method to our class called createArms(). Again, we’ll define a geometry and a mesh. The arms will be long, thin cuboids, so we’ll pass in our desired dimensions for these.
As we need two arms, we’ll create them in a for loop.
createArms() {
for(let i = 0; i < 2; i++) {
const geometry = new THREE.BoxGeometry(0.25, 1, 0.25)
const arm = new THREE.Mesh(geometry, material)
this.group.add(arm)
}
}
We don’t need to create the geometry in the for loop, as it will be the same for each arm.
Don’t forget to call the function in our init() method:
We’ll also need to position each arm either side of the body. I find it helpful here to create a variable m (for multiplier). This helps us position the left arm in the opposite direction on the x axis, with minimal code. (We’ll also use it rotate the arms in a moment too.)
createArms() {
for(let i = 0; i < 2; i++) {
const geometry = new THREE.BoxGeometry(0.25, 1, 0.25)
const arm = new THREE.Mesh(geometry, material)
const m = i % 2 === 0 ? 1 : -1
this.group.add(arm)
arm.position.x = m * 0.8
arm.position.y = 0.1
}
}
Additionally, we can rotate the arms in our for loop, so they stick out at a more natural angle (as natural as a cube person can be!):
arm.rotation.z = degreesToRadians(30 * m)
If our figure is placed in the center, the arm on the left will be positioned at the negative equivalent of the x-axis position of the arm on the right
Pivoting
When we rotate the arms you might notice that they rotate from a point of origin in the center. It can be hard to see with a static demo, but try moving the slider in this example.
We can see that the arms don’t move naturally, at an angle from the shoulder, but instead the entire arm rotates from the center. In CSS we would simply set the transform-origin. Three.js doesn’t have this option, so we need to do things slightly differently.
The figure on the right has arms that rotate from the top, for a more natural effect
Our steps are as follows for each arm:
Create a new Three.js group.
Position the group at the “shoulder” of our figure (or the point from which we want to rotate).
Create a new mesh for the arm and position it relative to the group.
Rotate the group (instead of the arm).
Let’s update our createArms() function to follow these steps. First we’ll create the group for each arm, add the arm mesh to the group, and position the group roughly where we want it:
createArms() {
const geometry = new THREE.BoxGeometry(0.25, 1, 0.25)
for(let i = 0; i < 2; i++) {
const arm = new THREE.Mesh(geometry, material)
const m = i % 2 === 0 ? 1 : -1
// Create group for each arm
const armGroup = new THREE.Group()
// Add the arm to the group
armGroup.add(arm)
// Add the arm group to the figure
this.group.add(armGroup)
// Position the arm group
armGroup.position.x = m * 0.8
armGroup.position.y = 0.1
}
}
To assist us with visualizing this, we can add one of Three.js’s built-in helpers to our figure. This creates a wireframe showing the bounding box of an object. It’s useful to help us position the arm, and once we’re done we can remove it.
// Inside the `for` loop:
const box = new THREE.BoxHelper(armGroup, 0xffff00)
this.group.add(box)
To set the transform origin to the top of the arm rather than the center, we then need to move the arm (within the group) downwards by half of its height. Let’s create a variable for height, which we can use when creating the geometry:
createArms() {
// Set the variable
const height = 1
const geometry = new THREE.BoxGeometry(0.25, height, 0.25)
for(let i = 0; i < 2; i++) {
const armGroup = new THREE.Group()
const arm = new THREE.Mesh(geometry, material)
const m = i % 2 === 0 ? 1 : -1
armGroup.add(arm)
this.group.add(armGroup)
// Translate the arm (not the group) downwards by half the height
arm.position.y = height * -0.5
armGroup.position.x = m * 0.8
armGroup.position.y = 0.6
// Helper
const box = new THREE.BoxHelper(armGroup, 0xffff00)
this.group.add(box)
}
}
Then we can rotate the arm group.
// In the `for` loop
armGroup.rotation.z = degreesToRadians(30 * m)
In this demo, we can see that the arms are (correctly) being rotated from the top, for a more realistic effect. (The yellow is the bounding box.)
Next we’re going to give our figure some eyes, for which we’ll use the Sphere geometry in Three.js. We’ll need to pass in three parameters: the radius of the sphere, and the number of segments for the width and height respectively (defaults shown here).
const geometry = new THREE.SphereGeometry(1, 32, 16)
As our eyes are going to be quite small, we can probably get away with fewer segments, which is better for performance (fewer calculations needed).
Let’s create a new group for the eyes. This is optional, but it helps keep things neat. If we need to reposition the eyes later on, we only need to reposition the group, rather than both eyes individually. Once again, let’s create the eyes in a for loop and add them to the group. As we want the eyes to be a different color from the body, we can define a new material:
createEyes() {
const eyes = new THREE.Group()
const geometry = new THREE.SphereGeometry(0.15, 12, 8)
// Define the eye material
const material = new THREE.MeshLambertMaterial({ color: 0x44445c })
for(let i = 0; i < 2; i++) {
const eye = new THREE.Mesh(geometry, material)
const m = i % 2 === 0 ? 1 : -1
// Add the eye to the group
eyes.add(eye)
// Position the eye
eye.position.x = 0.36 * m
}
}
We could add the eye group directly to the figure. However, if we decide we want to move the head later on, it would be better if the eyes moved with it, rather than being positioned entirely independently! For that, we need to modify our createHead() method to create another group, comprising both the main cube of the head, and the eyes:
createHead() {
// Create a new group for the head
this.head = new THREE.Group()
// Create the main cube of the head and add to the group
const geometry = new THREE.BoxGeometry(1.4, 1.4, 1.4)
const headMain = new THREE.Mesh(geometry, material)
this.head.add(headMain)
// Add the head group to the figure
this.group.add(this.head)
// Position the head group
this.head.position.y = 1.65
// Add the eyes by calling the function we already made
this.createEyes()
}
In the createEyes() method we then need to add the eye group to the head group, and position them to our liking. We’ll need to position them forwards on the z axis, so they’re not hidden inside the cube of the head:
// in createEyes()
this.head.add(eyes)
// Move the eyes forwards by half of the head depth - it might be a good idea to create a variable to do this!
eyes.position.z = 0.7
Legs
Lastly, let’s give our figure some legs. We can create these in much the same way as the eyes. As they should be positioned relative to the body, we can create a new group for the body in the same way that we did with the head, then add the legs to it:
createLegs() {
const legs = new THREE.Group()
const geometry = new THREE.BoxGeometry(0.25, 0.4, 0.25)
for(let i = 0; i < 2; i++) {
const leg = new THREE.Mesh(geometry, material)
const m = i % 2 === 0 ? 1 : -1
legs.add(leg)
leg.position.x = m * 0.22
}
this.group.add(legs)
legs.position.y = -1.15
this.body.add(legs)
}
Positioning in the scene
If we go back to our constructor, we can position our figure group according to the parameters:
Now, passing in different parameters enables us to position it accordingly. For example, we can give it a bit of rotation, and adjust its x and y position:
Alternatively, if we want to center the figure within the scene, we can use the Three.js Box3 function, which computes the bounding box of the figure group. This line will center the figure horizontally and vertically:
new THREE.Box3().setFromObject(figure.group).getCenter(figure.group.position).multiplyScalar(-1)
Making it generative
At the moment our figure is all one color, which doesn’t look particularly interesting. We can add a bit more color, and take the extra step of making it generative, so we get a new color combination every time we refresh the page! To do this we’re going to use a function to randomly generate a number between a minimum and a maximum. This is one I’ve borrowed from George Francis, which allows us to specify whether we want an integer or a floating point value (default is an integer).
const random = (min, max, float = false) => {
const val = Math.random() * (max - min) + min
if (float) {
return val
}
return Math.floor(val)
}
Let’s define some variables for the head and body in our class constructor. Using the random() function, we’ll generate a value for each one between 0 and 360:
I like to use HSL when manipulating colors, as it gives us a fine degree of control over the hue, saturation and lightness. We’re going to define the material for the head and body, generating different colors for each by using template literals to pass the random hue values to the hsl color function. Here I’m adjusting the saturation and lightness values, so the body will be a vibrant color (high saturation) while the head will be more muted:
Our generated hues range from 0 to 360, a full cycle of the color wheel. If we want to narrow the range (for a limited color palette), we could select a lower range between the minimum and maximum. For example, a range between 0 and 60 would select hues in the red, orange and yellow end of the spectrum, excluding greens, blues and purples.
We could similarly generate values for the lightness and saturation if we choose to.
Now we just need to replace any reference to material with this.headMaterial or this.bodyMaterial to apply our generative colors. I’ve chosen to use the head hue for the head, arms and legs.
We could use generative parameters for much more than just the colors. In this demo I’m generating random values for the size of the head and body, the length of the arms and legs, and the size and position of the eyes.
Part of the fun of working with 3D is having our objects move in a three-dimensional space and behave like objects in the real world. We can add a bit of animation to our 3D figure using the Greensock animation library (GSAP).
GSAP is more commonly used to animate elements in the DOM. As we’re not animating DOM elements in this case, it requires a different approach. GSAP doesn’t require an element to animate — it can animate JavaScript objects. As one post in the GSAP forum puts it, GSAP is just “changing numbers really fast”.
We’ll let GSAP do the work of changing the parameters of our figure, then re-render our figure on each frame. To do this, we can use GSAP’s ticker method, which uses requestAnimationFrame. First, let’s animate the ry value (our figure’s rotation on the y axis). We’ll set it to repeat infinitely, and the duration to 20 seconds:
We won’t see any change just yet, as we aren’t re-rendering our scene. Let’s now trigger a re-render on every frame:
gsap.ticker.add(() => {
// Update the rotation value
figure.group.rotation.y = this.params.ry
// Render the scene
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.render(scene, camera)
})
Now we should see the figure rotating on its y axis in the center of the scene. Let’s give him a little bounce action too, by moving him up and down and rotating the arms. First of all we’ll set his starting position on the y axis to be a little further down, so he’s not bouncing off screen. We’ll set yoyo: true on our tween, so that the animation repeats in reverse (so our figure will bounce up and down):
// Set the starting position
gsap.set(figure.params, {
y: -1.5
})
// Tween the y axis position and arm rotation
gsap.to(figure.params, {
y: 0,
armRotation: degreesToRadians(90),
repeat: -1,
yoyo: true,
duration: 0.5
})
As we need to update a few things, let’s create a method called bounce() on our Figure class, which will handle the animation. We can use it to update the values for the rotation and position, then call it within our ticker, to keep things neat:
/* In the Figure class: */
bounce() {
this.group.rotation.y = this.params.ry
this.group.position.y = this.params.y
}
/* Outside of the class */
gsap.ticker.add(() => {
figure.bounce()
// Render the scene
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.render(scene, camera)
})
To make the arms move, we need to do a little more work. In our class constructor, let’s define a variable for the arms, which will be an empty array:
class Figure {
constructor(params) {
this.arms = []
}
}
In our createArms() method, in addition to our code, we’ll push each arm group to the array:
createArms() {
const height = 0.85
for(let i = 0; i < 2; i++) {
/* Other code for creating the arms.. */
// Push to the array
this.arms.push(armGroup)
}
}
Now we can add the arm rotation to our bounce() method, ensuring we rotate them in opposite directions:
bounce() {
// Rotate the figure
this.group.rotation.y = this.params.ry
// Bounce up and down
this.group.position.y = this.params.y
// Move the arms
this.arms.forEach((arm, index) => {
const m = index % 2 === 0 ? 1 : -1
arm.rotation.z = this.params.armRotation * m
})
}
Now we should see our little figure bouncing, as if on a trampoline!
There’s much, much more to Three.js, but we’ve seen that it doesn’t take too much to get started building something fun with just the basic building blocks, and sometimes limitation breeds creativity! If you’re interested in exploring further, I recommend the following resources.
Resources
Three.js Journey video course by Bruno Simon (includes transcripts and starter projects every lesson)
A Generative SVG Starter Kit by George Francis — not Three.js-related, but covers some of the principles of generative art, and some handy utility functions
In a recent release of Three.js (r129 and beyond) some fabulous new features to MeshPhysicalMaterial were merged. The new features allow us to create convincing transparent, glass-like and plastic-like materials that refract and diffuse the content behind them, and are as easy-to-use as adding a couple of material properties!
About this article
This article explores some advanced properties of materials. While the results are very technically impressive, the new features that enable them are simple to use! Some experience with three and an intermediate understanding of the concept of “materials” in 3D graphics is ideal. Code examples are written for brevity, so it’s best to dive into the sandbox code (provided with each screenshot) if you’re interested in the gritty implementation details.
The physics of optics, light, reflection and refraction are not discussed in-depth here. This article approaches these effects through an aesthetic lens: aiming for convincing and visually pleasing results, even if they are not scientifically accurate.
Rather than introducing new concepts, this is primarily a walkthrough of features that exist within three and its MeshPhysicalMaterial class. I’d like to gush and shower praise upon the contributors and maintainers of three. It continues to be a core pillar of 3D in the browser. It has a vibrant community and extremely talented contributors who continue to push the boundaries of what’s possible on a humble web page.
Prior Art
Creating transparent materials, especially with texture and diffusion, has for a long time required deep technical expertise and creative problem solving. Some projects have achieved an impressive and convincing effect in WebGL through bespoke techniques:
In addition, these excellenttechnicalexamples provided the inspiration for writing this article, and further exploring what’s possible with these new features.
Three.js
three is an open-source javascript library for rendering 3D graphics in the browser. It provides a friendly API and abstractions that make working with WebGL more palatable and expressive. three has been around since 2010, is extremely well battle-tested, and is the de-facto standard for rendering 3D content on the internet. See the list of case studies on the home page, docs, examples, or source.
MeshPhysicalMaterial
MeshPhysicalMaterial is a relatively recent Physically-Based Rendering (PBR) built-in material for three. It’s an evolution and extension of the already impressive MeshStandardMaterial, providing additional features to pump the photo-realism.
This visual fidelity comes at a cost, from the docs: As a result of these complex shading features, MeshPhysicalMaterial has a higher performance cost, per pixel, than other Three.js materials. Most effects are disabled by default, and add cost as they are enabled.
Beyond the properties offered in MeshStandardMaterial, it introduces some new ones:
Transmission
transmission is the key to transparent glass-like and plastic-like effects. Traditionally when we adjust the opacity of an element to make it transparent, its visual presence is diluted as a whole. The object appears ghostly, uniformly transparent, and not realistic as a see-through object. In the real-world, transparent objects reflect light and show glare. They have a physical presence even though they may be perfectly clear.
Reflectivity properties
MeshPhysicalMaterial includes some properties that estimate refraction through the transmissible object: thickness, ior (Index-of-refraction) and reflectivity. We’ll mostly ignore ior and reflectivity (which changes ior too, but is mapped to a 0-1 range) as the defaults work great!
thickness is the magic here, as we’ll see shortly.
Clearcoat
Like a layer of lacquer, clearcoat provides an additional thin reflective layer on the surface of objects. Previously this would require a second version of the object, with a separate material, and with different parameters.
Other
There are some other additional properties on MeshPhysicalMaterial like sheen and attenuationTint which I won’t be touching on in this article.
We can expect to see more and more features added to this material in future releases.
First steps
First things first, let’s create a scene and pop something in it! We’ll start with an Icosahedron because hey, they just look cool.
I’m skipping the basic scene setup stuff here, I recommend diving into the sandbox source or three docs if you’re unfamiliar with this.
const geometry = new THREE.IcosahedronGeometry(1, 0);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh);
Looks like an Icosahedron! Let’s apply our MeshPhysicalMaterial:
const material = new THREE.MeshPhysicalMaterial({
metalness: 0,
roughness: 0
});
The options metalness and roughness are the two primary handles with PBR materials (they are on MeshStandardMaterial too). They can be used to set the stage for how our material responds to lighting and environment. Having both set at zero describes something like “A non-metallic object with a highly polished surface”.
Cool, there it is again… Now let’s make it transparent!
Call the Glazier
The transmission option is responsible for applying our transparency. It makes the “fill” or “body” of the object transparent, while leaving all lighting and reflections on the surface in-tact.
Note that we’re not using the opacity option, which applies a uniform transparency to the material as a whole. We also don’t need to include the transparent option on the material for it to appear transparent through transmission.
const material = new THREE.MeshPhysicalMaterial({
roughness: 0,
transmission: 1, // Add transparency
});
I think that’s transparent, we can see the background colour through it. Let’s pop something else behind it to be sure. We’ll add a textured plane as our “backdrop”:
const bgTexture = new THREE.TextureLoader().load("src/texture.jpg");
const bgGeometry = new THREE.PlaneGeometry(5, 5);
const bgMaterial = new THREE.MeshBasicMaterial({ map: bgTexture });
const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial);
bgMesh.position.set(0, 0, -1);
scene.add(bgMesh);
It’s transparent! It’s lacking something though. There’s nothing but a tiny flicker of movement on the corners of our geometry; as if our material is made from the most delicate and fragile of super-thin, super-clear glass.
Now here’s the magic part!
const material = new THREE.MeshPhysicalMaterial({
roughness: 0,
transmission: 1,
thickness: 0.5, // Add refraction!
});
By adding a single option: thickness to our material, we’ve now been given the gift of refraction through our object! Our background plane, which is a completely separate object, simply sitting behind our Icosahedron in the scene, now gets refracted.
This is incredible! Previous methods of achieving this required much more work and intense technical understanding. This has immediately democratised refractive materials in WebGL.
The effect is especially impressive when viewed in motion, and from an angle:
Swooping around our glass object to see it refracting the rest of the scene
Have a play by dragging around in this sandbox:
Diverse objects
While the sharp facets of our Icosahedron show a nice “cut-gem” style of refraction, we rarely see such precisely cut glass objects at any size other than tiny. This effect is greatly enhanced when geometries with smoother edges are used. Let’s increase the detail level of our Icosahedron to form a sphere:
const geometry = new THREE.IcosahedronGeometry(1, 15);
This shows some optical distortion in addition to the refraction based on the shape of the geometry!
Hot tip: with all of the PolyhedronGeometry types in three, any detail level above zero is rendered as a sphere, rather than a faceted polyhedron as far as transmission is concerned.
You may notice that the distorted content is a little pixelated, this is due to the material upscaling what’s transmitted through it to perform the distortion. We can mitigate this a bit with some other effects which we’ll cover later.
Let’s explore adding some texture to glass material:
const material = new THREE.MeshPhysicalMaterial({
roughness: 0.7,
transmission: 1,
thickness: 1
});
The roughness option on our transmissible material provides us with a “frosting” level, making light that passes through the material more diffuse.
This becomes immediately recognisable as a frosted glass object, with a fine powdery texture.
Notes on roughness:
The middle of the roughness range can display some quite noticeably pixelated transmitted content (at the time of writing). In my experience the best results are found in the low (0-0.15) and higher (0.65+) ends of the range. This can also be quite successfully mitigated with some of the things we’ll add shortly.
The distance of the transmissible object from the camera affects how roughness is rendered. It’s best to tweak the roughness parameter once you’ve established your scene
Hot tip: Using a small amount of roughness (0.05 – 0.15) can help soften aliasing on the transmitted content at the cost of a bit of sharpness.
For the rest of our examples we’ll include two additional geometries for reference: a RoundedBoxGeometry and a 3D model of a dragon (loaded as a GLTF, but only used for the geometry):
While the transmission effect is already appealing, there’s so much more we can do to make this appear truer-to-life.
The next thing we’ll do is add an environment map. It’s recommended that you always include an envMap when using MeshPhysicalMaterial, as per the docs: For best results, always specify an environment map when using this material.
Highly reflective objects show reflections, and glare, and glimpses of their surrounding environment reflected off their surface. It’s unusual for a shiny object to be perfectly unreflective; as they have been in our examples so far.
We’ll use a high quality High Dynamic Range Image (HDRI) environment map. I’ve chosen this one for its bright fluorescent overhead lighting:
const hdrEquirect = new THREE.RGBELoader().load(
"src/empty_warehouse_01_2k.hdr",
() => {
hdrEquirect.mapping = THREE.EquirectangularReflectionMapping;
}
);
const material = new THREE.MeshPhysicalMaterial({
...
envMap: hdrEquirect
});
NICE! Now that looks more realistic. The objects glint and shimmer in our bright environment; much more like the lighting challenges faced by a photographer of shiny things.
This is where our rounded geometries really shine too. Their smoother curves and edges catch light differently, really amplifying the effect of a highly polished surface.
Hot tip: Adding an envMap texture of some sort helps to resolve some rendering artifacts of this material. This is why it’s always recommended to include one (beyond the fact that it looks great!).
If you adjust the roughness level upward, you’ll notice that the reflections are diffused by the rougher frosted texture of the surface; however, we may want an object that’s semi-transparent while still having a shiny surface.
The clearcoat options allow us to include an additional reflective layer on the surface of our object (think lacquered wood, powder coatings, or plastic films). In the case of our transparent objects, we can make them from semi-transparent glass or plastic which still has a polished and reflective surface.
Adjusting the clearcoatRoughness option adjusts how highly polished the surface is; visually spanning the range from highly-polished frosted glass through to semi-gloss and matte frosted plastics. This effect is pretty convincing! You can almost feel the tack and texture of these objects.
So far we’ve been exploring objects with perfectly smooth surfaces. To really bring some texture to them, we can add a normal map:
const textureLoader = new THREE.TextureLoader();
const normalMapTexture = textureLoader.load("src/normal.jpg");
normalMapTexture.wrapS = THREE.RepeatWrapping;
normalMapTexture.wrapT = THREE.RepeatWrapping;
const material = new THREE.MeshPhysicalMaterial({
...
normalMap: normalMapTexture,
clearcoatNormalMap: normalMapTexture,
});
The interplay between the normalMap and clearcoatNormalMap is interesting. By setting the normalMap we affect the transmission through the object, adding a textured frosting that refracts light differently. By setting the clearcoatNormalMap we affect the finish on the surface of the object.
Hot tip: The additional texture added by the normalMap greatly reduces the visible pixelation on the transmitted content, effectively solving this issue for us.
As a final touch, we’ll add a post-processing pass to apply bloom to our scene. Bloom adds that extra little bit of photographic appeal by simulating volumetric glare from the bright overhead lighting bathing our objects.
I’ll leave information around implementation post-processing within three to the docs and examples. In this sandbox I’ve included the UnrealBloomPass.
Bloom always looks good.
There we have it! Convincingly transparent, textured and reflective 3D objects, rendered in real-time, and without much effort. This deserves to be celebrated, what an empowering experience it is playing with MeshPhysicalMaterial.
Drippin ice
Just for fun, let’s crank the dial on this by rendering many of our transparent objects using three‘s InstancedMesh (link).
OOOUF! YES. Instances can’t be seen through each other, which is a general limitation of transmission on MeshPhysicalMaterial (current as of r133); but in my opinion the effect is still very cool.
Explore for yourself
Finally, here’s our dragon model with a bunch of material options enabled in the GUI:
Have a play, check out metalness, play around with color to explore colourful tinted glass, tweak the ior to change our glass into crystal!
Sign-off
I’ve really only scratched the surface of what can be achieved with MeshPhysicalMaterial. There are even more options available within this material, sheen, roughnessMap, transmissionMap, attenuationTint and all sorts of other things provide inroads to many more effects. Dig deep into the docs and source if you’re interested!
This is an enabler, given the creative vision for a transparent object you can use these tools to work towards a convincing result. Transparent materials in three are here, you can start using them in your projects today.
In this ALL YOUR HTML stream and coding session we will recreate the interesting looking 3D ribbon effect seen on the website of iad-lab and made by mutoco.ch. We’ll apply some geometrical tricks and use the Three.js API.
This coding session was streamed live on November 28, 2021.
In this ALL YOUR HTML stream and coding session we’ll be creating a teleportation-like transition with Three.js using some quaternions, and fragment shaders! The original effect comes from Marseille 2021 by La Phase 5.
This coding session was streamed live on December 5, 2021.
In this ALL YOUR HTML stream and coding session we’ll be recreating the interactive pixel distortion effect seen on the website for the music video covers of “Infinite Bad Guy” made as an AI Experiment at Google and YouTube. We’ll be using Three.js and datatexture to achieve the look.
This coding session was streamed live on December 12, 2021.
In this festive ALL YOUR HTML coding session we’ll decompile the animation seen on the website of ONE-OFF using the K-d tree algorithm and Three.js shape creation. We’ll also be using GLSL to create the visuals.
This coding session was streamed live on December 26, 2021.
If you like cute little games you will love Karim Maaloul’s “The Aviator” — as a pilot you steer your aircraft across a round little ocean world, evading red “enemies” and collecting blue energy tokens to avoid crashing into the water. It runs entirely in the browser so make sure to quickly play a round to better understand what we are about to do in this tutorial.
Karim thankfully wrote about the making of and open sourced the code and while it is a fun little game there is still a lot of potential to get even more out of it. In this article we will explore some hands-on changes on how to bring the most fun based on the foundation we have here, a small browser game using Three.js.
This tutorial requires some knowledge of JavaScript and Three.js.
What makes a game fun?
While there obviously is no definitive recipe there are a few key mechanics that will maximize your chances of generating fun. There is a great compilation on gamedesigning.org, so let’s see which items apply already:
Great controls An interesting theme and visual style Excellent sound and music Captivating worlds Fun gameplay Solid level design An entertaining story & memorable characters Good balance of challenge and reward Something different
We can see there’s lots to do, too much for a single article of course, so we will get to the general game layout, story, characters and balance later. Now we will improve the gameplay and add sounds — let’s go!
Adding weapons
Guns are always fun! Some games like Space Invaders consist entirely of shooting and it is a great mechanic to add visual excitement, cool sound effects and an extra dimension to the skill requirement so we not only have the up and down movement of the aircraft.
Let’s try some simple gun designs:
The “Simple gun” (top) and the “Better gun” (bottom).
These 3D models consist of only 2–3 cylinders of shiny metal material:
const metalMaterial = new THREE.MeshStandardMaterial({
color: 0x222222,
flatShading: true,
roughness: 0.5,
metalness: 1.0
})
class SimpleGun {
static createMesh() {
const BODY_RADIUS = 3
const BODY_LENGTH = 20
const full = new THREE.Group()
const body = new THREE.Mesh(
new THREE.CylinderGeometry(BODY_RADIUS, BODY_RADIUS, BODY_LENGTH),
metalMaterial,
)
body.rotation.z = Math.PI/2
full.add(body)
const barrel = new THREE.Mesh(
new THREE.CylinderGeometry(BODY_RADIUS/2, BODY_RADIUS/2, BODY_LENGTH),
metalMaterial,
)
barrel.rotation.z = Math.PI/2
barrel.position.x = BODY_LENGTH
full.add(barrel)
return full
}
}
We will have 3 guns: A SimpleGun, then the DoubleGun as just two of those and then the BetterGun which has just a bit different proportions and another cylinder at the tip.
Guns mounted to the airplane
Positioning the guns on the plane was done by simply experimenting with the positional x/y/z values.
The shooting mechanic itself is straight forward:
class SimpleGun {
downtime() {
return 0.1
}
damage() {
return 1
}
shoot(direction) {
const BULLET_SPEED = 0.5
const RECOIL_DISTANCE = 4
const RECOIL_DURATION = this.downtime() / 1.5
const position = new THREE.Vector3()
this.mesh.getWorldPosition(position)
position.add(new THREE.Vector3(5, 0, 0))
spawnProjectile(this.damage(), position, direction, BULLET_SPEED, 0.3, 3)
// Little explosion at exhaust
spawnParticles(position.clone().add(new THREE.Vector3(2,0,0)), 1, Colors.orange, 0.2)
// Recoil of gun
const initialX = this.mesh.position.x
TweenMax.to(this.mesh.position, {
duration: RECOIL_DURATION,
x: initialX - RECOIL_DISTANCE,
onComplete: () => {
TweenMax.to(this.mesh.position, {
duration: RECOIL_DURATION,
x: initialX,
})
},
})
}
}
class Airplane {
shoot() {
if (!this.weapon) {
return
}
// rate-limit shooting
const nowTime = new Date().getTime() / 1000
if (nowTime-this.lastShot < this.weapon.downtime()) {
return
}
this.lastShot = nowTime
// fire the shot
let direction = new THREE.Vector3(10, 0, 0)
direction.applyEuler(airplane.mesh.rotation)
this.weapon.shoot(direction)
// recoil airplane
const recoilForce = this.weapon.damage()
TweenMax.to(this.mesh.position, {
duration: 0.05,
x: this.mesh.position.x - recoilForce,
})
}
}
// in the main loop
if (mouseDown[0] || keysDown['Space']) {
airplane.shoot()
}
Now the collision detection with the enemies, we just check whether the enemy’s bounding box intersects with the bullet’s box:
class Enemy {
tick(deltaTime) {
...
const thisAabb = new THREE.Box3().setFromObject(this.mesh)
for (const projectile of allProjectiles) {
const projectileAabb = new THREE.Box3().setFromObject(projectile.mesh)
if (thisAabb.intersectsBox(projectileAabb)) {
spawnParticles(projectile.mesh.position.clone(), 5, Colors.brownDark, 1)
projectile.remove()
this.hitpoints -= projectile.damage
}
}
if (this.hitpoints <= 0) {
this.explode()
}
}
explode() {
spawnParticles(this.mesh.position.clone(), 15, Colors.red, 3)
sceneManager.remove(this)
}
}
Et voilá, we can shoot with different weapons and it’s super fun!
Changing the energy system to lives and coins
Currently the game features an energy/fuel bar that slowly drains over time and fills up when collecting the blue pills. I feel like this makes sense but a more conventional system of having lives as health, symbolized by hearts, and coins as goodies is clearer to players and will allow for more flexibility in the gameplay.
In the code the change from blue pills to golden coins is easy: We changed the color and then the geometry from THREE.TetrahedronGeometry(5,0) to THREE.CylinderGeometry(4, 4, 1, 10).
The new logic now is: We start out with three lives and whenever our airplane crashes into an enemy we lose one. The amount of collected coins show in the interface. The coins don’t yet have real impact on the gameplay but they are great for the score board and we can easily add some mechanics later: For example that the player can buy accessoires for the airplane with their coins, having a lifetime coin counter or we could design a game mode where the task is to not miss a single coin on the map.
Adding sounds
This is an obvious improvement and conceptually simple — we just need to find fitting, free sound bites and integrate them.
We load all 24 sound files at the start of the game and then to play a sound we code a simple audioManager.play(‘shot-soft’). Repetitively playing the same sound can get boring for the ears when shooting for a few seconds or when collecting a few coins in a row, so we make sure to have several different sounds for those and just select randomly which one to play.
Be aware though that browsers require a page interaction, so basically a mouse click, before they allow a website to play sound. This is to prevent websites from annoyingly auto-playing sounds directly after loading. We can simply require a click on a “Start” button after page load to work around this.
Adding collectibles
How do we get the weapons or new lives to the player? We will spawn “collectibles” for that, which is the item (a heart or gun) floating in a bubble that the player can catch.
We already have the spawning logic in the game, for coins and enemies, so we can adopt that easily.
class Collectible {
constructor(mesh, onApply) {
this.mesh = new THREE.Object3D()
const bubble = new THREE.Mesh(
new THREE.SphereGeometry(10, 100, 100),
new THREE.MeshPhongMaterial({
color: COLOR_COLLECTIBLE_BUBBLE,
transparent: true,
opacity: .4,
flatShading: true,
})
)
this.mesh.add(bubble)
this.mesh.add(mesh)
...
}
tick(deltaTime) {
rotateAroundSea(this, deltaTime, world.collectiblesSpeed)
// rotate collectible for visual effect
this.mesh.rotation.y += deltaTime * 0.002 * Math.random()
this.mesh.rotation.z += deltaTime * 0.002 * Math.random()
// collision?
if (utils.collide(airplane.mesh, this.mesh, world.collectibleDistanceTolerance)) {
this.onApply()
this.explode()
}
// passed-by?
else if (this.angle > Math.PI) {
sceneManager.remove(this)
}
}
explode() {
spawnParticles(this.mesh.position.clone(), 15, COLOR_COLLECTIBLE_BUBBLE, 3)
sceneManager.remove(this)
audioManager.play('bubble')
// animation to make it very obvious that we collected this item
TweenMax.to(...)
}
}
function spawnSimpleGunCollectible() {
const gun = SimpleGun.createMesh()
gun.scale.set(0.25, 0.25, 0.25)
gun.position.x = -2
new Collectible(gun, () => {
airplane.equipWeapon(new SimpleGun())
})
}
And that’s it, we have our collectibles:
The only problem is that I couldn’t for the life of me create a heart model from the three.js primitives so I resorted to a free, low-poly 3D model from the platform cgtrader.
Defining the spawn-logic on the map in a way to have a good balance of challenge and reward requires sensible refining so after some experimenting this felt nice: Spawn the three weapons after a distance of 550, 1150 and 1750 respectively and spawn a life a short while after losing one.
Some more polish
The ocean’s color gets darker as we progress through the levels
Show more prominently when we enter a new level
Show an end game screen after 5 levels
Adjusted the code for a newer version of the Three.js library
Tweaked the color theme
More, more, more fun!
We went from a simple fly-up-and-down gameplay to being able to collect guns and shoot the enemies. The sounds add to the atmosphere and the coins mechanics sets us up for new features later on.
Make sure to play our result here! Collect the weapons, have fun with the guns and try to survive until the end of level 5.
If you are interested in the source code, you find it here on GitHub.
How to proceed from here? We improved on some key mechanics and have a proper basis but this is not quite a finalized, polished game yet.
As a next step we plan to dive more into game design theory. We will look at several of the most popular games of the endless runner genre to get insights into their structure and mechanics and how they keep their players engaged. The aim would be to learn more about the advanced concepts and build them into The Aviator.
Subway Surfer, the most successful “endless runner” game.
Infinite scrolling is a web design technique that allows users to scroll through a never-ending list of content by automatically loading new items as the user reaches the bottom of the page. Instead of having to click through to a new page to see more content, the content is automatically loaded and appended to the bottom of the page as the user scrolls. This can create a seamless and engaging browsing experience for users, as they can easily access a large amount of content without having to wait for new pages to load. Forget about reaching the footer, though!
Looping the scroll of a page refers to the process of automatically taking users back to the top of the page once they reach the end of the scroll. This means that they will be able to continuously scroll through the same content over and over again, rather than being presented with new content as they scroll down the page.
In this article, we will show some examples creative loop scrolling, and then reimplement the effect seem on Bureau DAM. We will be using Lenis by Studio Freight to implement the looping effect and GSAP for the animations.
What is loop scrolling?
When your content is limited, you can do creative things with loop scrolling, which is basically infinite scrolling with repeating content. A great example for such a creative use is the effect seen on Bureau DAM:
Some great ideas how looping the scroll can be used in a creative way and add value to a website:
It can create an immersive experience for users. By continuously scrolling through the same content, users can feel more immersed in the website and engaged with the content. This can be particularly useful for websites that want to create a strong emotional connection with their audience.
It can be used to create a game or interactive experience. By adding interactive elements to the content that is being looped, developers can create a game or interactive experience for users. For example, a website could use looping the scroll to create a scrolling game or to allow users to interact with the content in a novel way.
It can be used to create a “time loop” effect. By looping the scroll in a particular way, developers can create the illusion of a “time loop,” where the content appears to repeat itself in a continuous loop. This can be particularly effective for websites that want to convey a sense of continuity or timelessness.
A simple example
Let’s create a simple example. We’ll set up a grid of 6 images that repeat on scroll. How can we achieve this? It’s simple: we need to repeat the content in a way that when reaching the end, we can simply reset the scroll to the top without anybody noticing! We’ve previously explored this concept in our CSS-only marquee effect. In another demo we also show how to play with this kind of infinite animation.
So, for our first example we have the following markup:
Lenis comes with a handy option for making the scroll infinite. In our script we’ll make sure to repeat the visible grid items (in this case it’s 6):
const lenis = new Lenis({
smooth: true,
infinite: true,
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// repeat first six items by cloning them and appending them to the .grid
const repeatItems = (parentEl, total = 0) => {
const items = [...parentEl.children];
for (let i = 0; i <= total-1; ++i) {
var cln = items[i].cloneNode(true);
parentEl.appendChild(cln);
}
};
repeatItems(document.querySelector('.grid'), 6);
While we scroll, we can add some fancy animations to our grid items. Paired with switching the transform origin in the right time, we can get something playful like this:
We can also play with a 3D animation and an additional filter effect:
Adding a filter effect and perspective. View the demo
The Bureau DAM example
Now, let’s see how we can remake the Bureau DAM animation. As we don’t have a grid here, things get a bit simpler. Just like them, we’ll use an SVG for the typography element, as we want to stretch it over the screen:
Explore the creation of a 3D glass portal with React Three Fiber, with optimized rendering using Gaussian Splatting and integrating real-world objects.