[threejs-journey] Part 6

22 Custom model with Blender

Difficulty Hard

Introduction

Now that we know how to import a model into our scene; let’s learn how to create our own model using a 3D software.

Choosing a software

There are many software like Cinema 4D, Maya, 3DS Max, Blender, ZBrush, Marmoset Toolbag, Substance Painter, etc. These are great, and they differ on divers criteria like the UX, the performance, the features, the compatibility, the price, etc.

In this lesson, we are going to use Blender because it’s free, the performances are remarkable, it works with all primary OS, it has many features, it has a vast community, and it became a lot easier since the 2.8 version.

Be aware that you won’t be a Blender expert at the end of the lesson. It would take a full course to learn all its aspects, and they are already many great resources to learn. The idea is to understand the basics and de-mystify the software to have sufficient baggage to create simple models.

At the start, we will discover all the basics. That will be a lot to take in but don’t worry; we will repeat most of the shortcuts, the mechanics, and the features multiple times.

If you pressed a wrong shortcut at some point, you lost your scene, or the interface is completely messed up, just close and re-open Blender.

Downloading Blender

Go to the blender website download page and download the latest version: https://www.blender.org/download/

The software is pretty light, and it shouldn’t take more than a few minutes.

Once downloaded, simply install it.

The lesson has been written and recorded with Blender 2.83.5 . While there shouldn’t be major differences, keep an eye on potential changes.

Interface

Splash screen

The splash screen gives you access to some useful links, templates, and recently opened files.

The image changes with the Blender version, so don’t be surprised if you have a different one.

You can also see the exact version on the top right of it.

Click anywhere out of the splash screen to close it.

Areas

The different parts of the interface are called areas. Areas are very flexible, and you can create the layout that you want.

Default areas

By default, you have the main area called the 3D Viewport :

The Timeline to create animations:

The Outliner to see and manage the scene graph (or scene tree):

The Properties to manage the properties of the active object (selection) and the environment:

Change an area

To change what an area is displaying, click on the top-left button of that area. Here, we are going to change the Timeline area.

/assets/lessons/22/06.png

We are going to change the Timeline area for another 3D Viewport :

Resize an area

To resize an area, position your cursor between two areas and drag & drop:

Create new areas

To create a new area, first, we must decide what area we want to split. Then, we must position our cursor in one of the four corners of our area (a few pixels inside the area):

/assets/lessons/22/09.png

Finally, we drag and drop in the area we want to split:

Remove an area

Removing an area is a little tricky, and you might end up with dozens of unwanted areas. Don’t worry, once you get the idea, it will be fine.

In a way, we don’t remove an area; we un-split two areas. First, you must decide which one of the two areas is going to take over the other. If you want to remove the right area, start from the left area. Then place your cursor in one of the two corners adjacent to the area we want to remove (few pixels inside the area that is supposed to take over the other):

/assets/lessons/22/11.png

And then drag & drop (just like we did to create an area) but this time in the opposite direction (toward the area we want to remove):

It can take a few tries but you’ll get it.

Shortcuts

One of the strengths of Blender is its shortcuts. There are tons of them, and once you master the basics, you can be very efficient. Don’t worry; you can use all the shortcut actions through the interface or with the search panel that we will see later. Throughout this lesson, we will discover some of the more critical shortcuts.

Here’s a non-exhaustive list of shortcuts: https://docs.google.com/document/d/1wZzJrEgNye2ZQqwe8oBh54AXwF5cYIe56EGFe2bb0QU/edit?usp=sharing

One important thing to understand is that shortcuts are hovered area sensitive. That means that the same shortcut can have different actions depending on what’s behind our cursor. Most of the shortcuts we will see in this lesson concern the 3D Viewport . You should always make sure to keep your cursor above one of these areas when pressing the shortcut keys.

The shortcuts are also mode sensitive, but we will talk about modes later.

Only one or two shortcuts that we will see are different between Mac and Windows. If there is a difference, both versions will be cited. If the shortcut includes the CTRL key and you are using a Mac, do not assume it’s CMD . Use the CTRL key instead.

View

As you can see, you can move the view in every possible direction. While you can use a trackpad, I recommend using a mouse with a wheel that you can press (or a third button) for productive reasons. From now, we will refer to the wheel button (the one that we can press) as MIDDLE MOUSE .

If you are using a trackpad, you can use two fingers.

If you are using a Magic Mouse, you can replicate the MIDDLE MOUSE . Go to the preferences through Edit > Preferences . Using the navigation menu on left, choose the Input section. Check the Emulate 3 Button Mouse checkbox.

It’s also better to have a numeric pad.

Orbit rotate

We can rotate the view by pressing the MIDDLE MOUSE and drag & drop in the 3D Viewport .

Or we can use the gizmo on the top-right part of each 3D Viewport :

/assets/lessons/22/13.png

We call this rotation orbit (like the Three.js OrbitControl) because the view rotates around an invisible center called view-point. We will talk about that point later.

Truck and pedestal

Truck is when the view moves on the left and right, while pedestal is when the view moves up and down. We can do both simultaneously by pressing the MIDDLE MOUSE again, but this time with the SHIFT key pressed.

Or we can use the hand icon on the top right:

/assets/lessons/22/14.png

Truck is also called track.

Some might also wrongly call those movements “pan”.

Dolly

Dolly is when the view moves forward and backward. We can use the WHEEL to achieve that.

Or we can use the magnifier icon on the top right:

/assets/lessons/22/15.png

Be careful though; zooming isn’t exactly like going forward and backward. We are getting closer or further from the view-point we talked about in the Orbit section, but we cannot zoom beyond that point, and zooming too much will result in a stuck view.

To resolve this zoom limit issue, we can move forward and backward by pressing SHIFT + CTRL + MIDDLE MOUSE and drag & drop in the 3D Viewport (won’t work with the two fingers technique with a trackpad). This way, we won’t get stuck at the view-point.

There is no icon in the interface to do that.

Tilt and pan

Tilt and pan are simple rotation on the camera point.

We have to go to the Walk mode —also called Fly mode— to use those moves.

To do that, if you are using a QWERTY keyboard, press SHIFT + BACK QUOTE . The back quote —also called backtick, acute or left quote— is an inclined simple quote that can be used to add a grave accent.

Finding that character might be a little hard because its position changes a lot depending on your keyboard. You can find it on the top right corner, on the bottom left corner or very close to the ENTER key.

If you are using an AZERTY keyboard, the shortcut won’t work. We need to change the keymap. Go to the preferences through Edit > Preferences . Using the navigation menu on left, choose the Keymap section. In the search input, write view navigation and change the shortcut for View Navigation (Walk/Fly) to SHIFT + F .

That’s it, you can use the walk mode with SHIFT + F .

You can also go forward, backward, and on the sides, in the walk mode with the ARROWS or WASD if using a QWERTY .

Perspective / Orthographic

The default view uses perspective. We can toggle with the orthographic version with NUMPAD 5 or the grid icon on the top right:

/assets/lessons/22/16.png

Axes

We can align the camera on the X , Y and Z axes by pressing the NUMPAD 1 , NUMPAD 3 , and NUMPAD 7 . We are talking about the numeric pad numbers, not the numbers at the top of the keyboard.

To position the camera at the opposite, press the same keys but with CTRL .

Or we can use the gizmo and click on the axes:

/assets/lessons/22/13%201.png

In Blender, we consider the top axes as the Z , unlike in Three.js, where it’s Y .

Camera

You might have seen the camera in the 3D Viewport . To get the camera viewpoint, press NUMPAD 0 :

/assets/lessons/22/17.png

The camera will be used when doing renders. You can’t render the scene without a camera.

Reset

Sometimes, we get lost, and we don’t even know where our scene is. We can focus back on our scene by pressing SHIFT + C .

Focus

To focus the camera on an object, select the object with LEFT MOUSE , then press NUMPAD , (we are talking about the comma on the numeric pad and it might be a dot depending on the keyboard you are using).

We can also focus on an object and hide everything else with NUMPAD / . Use the same shortcut to leave the focus mode.

Selecting

As we just saw, we can select objects with the LEFT MOUSE . We can also select multiple objects with SHIFT + LEFT MOUSE .

You might have noticed that one of the objects always has a brighter outline. This object isn’t just selected; it’s also the active one. We will see more about that later.

To undo a selection, press CMD + Z ( CTRL + Z on Windows). Yes, selecting objects is considered as an action we can undo. While this might seem strange, it’s really helpful when you miss-click.

To unselect an object, use SHIFT + LEFT MOUSE again. If it’s not the active one, it’ll become active, and if it’s active, it’ll be unselected.

To select everything, press A .

To unselect everything, double press A .

To select a rectangle area, press B .

To select like if you were painting, press C . While in this mode, use WHEEL to change the radius.

Creating objects

To create objects, with the cursor above a 3D Viewport , press SHIFT + A . A menu should open behind your cursor. Navigate through this menu to create various objects. Most of what we will see in the lessons are in the Mesh sub-menu:

When you create an object, a small button should appear in the bottom left corner:

/assets/lessons/22/screenshot-19.png

Click on it to change various properties like the size, the subdivision, and many other properties related to the object you’re trying to make:

If you click anywhere else, you’ll lose that menu. You can re-open it with F9 but if you start doing modifications on the geometry, you won’t be able to re-open this menu.

Deleting objects

To delete an object, select it and, with the cursor above a 3D Viewport , press X . A confirmation menu should open behind the cursor. Click on it to delete the object:

/assets/lessons/22/screenshot-21.png

Hiding objects

To hide selected objects, press H .

To show hidden objects, press ALT + H .

To hide non-selected objects, press SHIFT + H .

You can also manage this in the Outliner with the eye icon.

/assets/lessons/22/screenshot-21_a.png

Transforming objects

There are 3 types of transformations —as in Three.js. We can change the position, the rotation, and the scale.

We can use the menu on the left to activate each separately or activate them even all together with the fourth button:

/assets/lessons/22/screenshot-22.png

Or we can use shortcuts:

  • G for the position
  • R for the rotation
  • S for the scale

After activating one of these shortcuts, you can force the transformation to operate in a specific axis by pressing the corresponding key ( X , Y , and Z ).

Modes

We are currently in Object Mode , where we can create, delete, and transform objects. There are many other modes.

Change mode

We can change the mode with the menu on the top-left select button of any 3D Viewport area:

/assets/lessons/22/screenshot-23.png

Or we can press CTRL + TAB to open a wheel menu (aka, the coolest kind of menu):

We won’t cover all the modes, but we are going to use the Edit Mode . Select a mesh (or create one) and switch to the Edit Mode .

Edit Mode

There is actually a shortcut to toggle the Edit Mode . Simply press TAB .

The Edit Mode is very similar to the Object Mode but we can edit the vertices, the edges and the faces. By default, we can change the vertex. Try to select vertices, transform them with the usual shortcuts — G for position, R for rotation, S for scale.

To switch to edges and faces, we can use the buttons on the top-right part of the 3D Viewport :

/assets/lessons/22/screenshot-25.png

Or we can press the first three numbers on top of the keyboard 1 , 2 , and 3 .

Once you finish editing the object, leave the Edit Mode with TAB .

Shading

Shading is how you see objects in the 3D Viewport .

We are currently in Solid shading. This shading lets you see the objects with a default material and no light support. It’s performant and convenient.

We can change the shading with the buttons on the top-right corner of the 3D Viewport :

/assets/lessons/22/screenshot-26.png

Or we can press Z to open a wheel menu:

  • Solid : The default with the same material for every objects.
  • Material : Like the Solid shading, but with a preview of the materials and their textures.
  • Wireframe : All the geometries are in wireframe.
  • Renderer : Low quality render —it’s the most realistic but the least performant.

If you can’t see much in the Render , it might be because you don’t have enough lights in your scene. Just add one or two by pressing SHIFT + A to open the Add menu.

Properties

Unless you changed the layout, the bottom right area, named Properties , lets you play with render properties, environment properties and the active object’s properties. Remember that the active object is the one with the brightest outline.

We won’t cover all these tabs but the top ones deal with the renders and the environment:

/assets/lessons/22/29.png

The ones below relate to the active object, and they can differ depending on the type of active object because we don’t have the same properties for a cube as for a light:

/assets/lessons/22/30.png

Object Properties

The Object Properties let you change with accuracy properties such as transformations:

/assets/lessons/22/31.png

Modifier Properties

The Modifiers Properties let you add what we call modifiers. These are non-destructive modifications. You can subdivide, bend, grow, shrink, etc. and turn them off as you please:

/assets/lessons/22/32.png

Material Properties

The Material Properties let you play with materials:

/assets/lessons/22/33.png

By default, you have access to one material named Material , and it should be applied to the default cube if you didn’t delete it:

/assets/lessons/22/screenshot-34.png

You can remove the material with the - button and combine them with the + button, but we usually use only one material per mesh:

/assets/lessons/22/screenshot-35.png

If there is no material on the mesh, we can choose an existing one, or we can create a new one with the New button:

We can have different types of surfaces for one material. The default one is called Principled BSDF and this type of surface uses the PBR principles as the MeshStandardMaterial does in Three.js. This means that we should get a very similar result if we export this kind of material to a Three.js scene.

We won’t see other types of materials in this lesson.

Render engines

Go to the Render Properties tab:

/assets/lessons/22/37.png

In this panel, we can change the Render Engine :

/assets/lessons/22/screenshot-38.png

There are 3 types of render engine:

  • Eevee : A real-time render engine. It uses the GPU just like Three.js, it’s very performant, but it has limitations like realism, light bounce, reflection, and refraction.
  • Workbench : A legacy render engine that we don’t use a lot anymore. Its performance is pretty good, but the result isn’t very realistic.
  • Cycles : A raytracing engine. It’s very realistic. It handles light bounce, deep reflection, deep refraction, and many other features, but it’s very sluggish, and you might end up waiting hours or even days to render your scene.

You can change this property and see if anything changes in the scene. Make sure to use the Renderer shading —press Z to change the shading. Blender developers did an excellent job of keeping a very similar result between render engines.

The default one is Eevee and it’s perfect in our case because it’s real-time rendering, and Three.js is also real-time rendering.

If you want to render the scene, press F12 . The render will be seen from the camera —make sure to have a camera well oriented in your scene.

Search

There are so many possible actions that we cannot remember how to trigger them all. The right solution when we can’t figure out where the button is, or the shortcut is to use the Search panel.

To open the Search panel, press F3 (you might need to add the fn key depending on your keyboard and OS) and write the action name:

Save our setup

Create a solid setup. It’s up to you, but because we are using Blender to export for Three.js, we don’t need a camera. We can also create two side views below the main 3D Viewport on the Z and Y axis with Wireframe shading:

Once you’re happy with your setup, go to File > Defaults > Save Startup File . That will set your current setup as the default one when opening Blender:

Be careful when clicking on Save Startup File ; a confirmation menu should open and if you move the mouse out of it, you’ll lose it. Click again to confirm:

/assets/lessons/22/screenshot-43.png

Hamburger time

It’s time to create our own model. In this lesson, we are going to make a hamburger and import it into Three.js.

Like in Three.js, a good practice is to decide on a unit scale. If you look at the grid, one square truly represents one unit, and by default, Blender considers that one unit means one meter:

/assets/lessons/22/44.png

We could create a giant hamburger, but sorry, we better start with a regular-sized sandwich. We could just think of 1m as 1cm but if you don’t like seeing this m you can remove it in the Scene Properties tab of the Properties area by choosing None as our Unit System :

/assets/lessons/22/screenshot-45.png

We can now consider 1 as 1cm .

Bottom bun

For our bottom bun, as strange as it may sound, we will start with a 10cm cube:

/assets/lessons/22/screenshot-46.png

Then, after making sure that our cube is active, go to the Modifier Properties tab and add the Subdivision Surface modifier:

/assets/lessons/22/screenshot-47.png

The cube now somewhat resembles a sphere.

The Subdivision Surface is one of the most famous modifiers. It subdivides the geometry but smoothes the angles at the same time.

Add more subdivision in the Subdivisions > viewport field:

/assets/lessons/22/screenshot-48.png

The faces are flat but we want a smooth surface. To achieve this, right-click on the sphere and choose Shade Smooth :

/assets/lessons/22/screenshot-50.png

The good thing with modifiers is that you preserve the original geometry. Go to the Edit Mode by pressing TAB and move the vertices until you get a pebble right above the ground:

So it’s still not a bun. To get the right shape, we must add more vertices at the edge of the bun. What we need is a loop cut. Press CTRL + R while in Edit Mode and move the mouse on one of the four vertical edges:

Click once to create the cut but do not click again. At this point, you can move the loop cut up and down. Put the loop cut closer to the top edge:

Repeat the process for the down part of the bun. We want our bun to be flat at the bottom, but not too flat:

Improve your bun until you are satisfied but don’t spend too much time on it. Don’t worry, we’ll come back to it once we have the whole scene.

Save

It’s time to save. Press CMD + S ( CTRL + S on Windows) or go to Files > Save :

/assets/lessons/22/screenshot-55.png

Save anywhere you want.

If you make some changes and you save again, you’ll see a .blend1 file in the same folder as your original .blend file. That is a backup of the latest save automatically generated by Blender. There was a time where Blender wasn’t as stable as it is today. Saving your file could result in corrupting it. That is why they created those automatic backups. Today, Blender is very stable, but they keep the backup just in case or because the user can make a mistake like deleting an object, saving, and closing Blender. Fortunately, you only need to rename the .blend1 file .blend to access your previous save.

Meat

Repeat the same process for the meat. You can also duplicate the bottom bun with SHIFT + D in Object Mode (the default one)

Don’t forget to save.

Cheese

For the cheese, start with a plane right above the meat:

Go to Edit Mode (where you can edit the vertices, edges and faces) and subdivide the geometry. Select the only face, right click on it and select Subdivide . Set the Number of Cuts to 10 :

We are going to melt the cheese corners.

While we’re still in Edit Mode , select one of the cheese corners. If we were to move one vertex at a time, it would take too long, and the result would probably look disappointing. What we can do is use the Proportional Editing . Press O —the letter— or click on the icon at the 3D Viewport top center:

/assets/lessons/22/screenshot-59.png

Now that the Proportional Editing is active, transforming one vertex will also transform the neighbor vertices. Increase the area of effect by using the WHEEL after pressing a transformation key —like G to move:

The way the neighbor vertices move isn’t correct. To change that click on the Proportional Editing Falloff icon and choose Sharp :

/assets/lessons/22/screenshot-61.png

Now play with the vertices and the Proportional Editing size until you get a nice cheesy shape:

In Object Mode , right click and choose Shade Smooth for a smooth surface:

Our cheese doesn’t look thick enough. To solve this, go to the Modifier Properties and add the Solidify modifier:

/assets/lessons/22/screenshot-64.png

Then increase the thickness to 0.08 :

/assets/lessons/22/screenshot-65.png

Change the shading to wireframe if you want to appreciate the result even more —press Z to open the shading wheel menu:

Not to sound too picky but, the Shade Smooth on the edges looks kind of strange:

To fix that, go to the Object Data Properties panel, then the Normals section, and check Auto Smooth :

/assets/lessons/22/screenshot-68.png

This feature will smooth the surface only if its edge’s angle is inferior to 30°. At that point, you should get a realistic and appetizing melted cheese:

Top bun

Add the top bun just like we did for the bottom one but make it rounder:

Final tweaks

Now that we have all the ingredients, we can fix the proportions, go back to the previous ingredients, and improve the general shape.

Don’t forget to save.

Materials

It’s time to add some colors and surface properties to our hamburger. For simplicity reasons, we won’t apply textures or use painting.

We need 3 materials —one for the buns, one for the meat and one for the cheese.

Select the bottom bun and go to the Material Properties panel:

/assets/lessons/22/71.png

You should have no material associated with the bun. If you have one, click on it and press the - button.

To create a new material, click on the New button:

/assets/lessons/22/screenshot-72.png

It’s good practice to rename materials. Double click on its name and call it BunMaterial :

/assets/lessons/22/73.png

Now, tweak the properties to get a tasty color. We only need to change the Base Color and the Roughness :

If you can’t see the color on the 3D Viewport , it might be because you are not using proper shading. To change the shading, press Z and choose Renderer :

If the hamburger looks really dark, it’s probably because of the light. Select the light, position it a little further away from the hamburger, go to the Object Data Properties panel, and increase its power to 5000 :

/assets/lessons/22/screenshot-76.png

Repeat the material process for the meat and the cheese:

For the top bun, you only need to select the BunMaterial you already worked on:

Take your time, delete and try again, until you get a delicious looking hamburger.

/assets/lessons/22/screenshot-79.png

You can even add other ingredients like salad, tomatoes, maybe a pickle slice if you’re one of these people— but does anybody even likes pickles?

Don’t forget to save.

Export

It’s finally time to export our hamburger. Download and run the starter. The Three.js scene is ready, and the only thing left is to provide the model.

Select all the ingredients of the hamburger. If you select other elements like lights or cameras, you might export them as well. That can be useful but not in our case.

Go to File > Export > glTF 2.0 (.glb/.gltf) :

A panel should open in the middle of the screen. Use it to navigate to the /static/models/ folder of the project (ignore the .gitkeep file if you see it).

In the bottom part of the panel, choose a name for your file. We will go for hamburger . Do not add the extension:

/assets/lessons/22/81.png

In the right section, we can choose the Format . The choices correspond to the GLTF formats we talked about in the previous lesson. We will go for glTF Binary because we don’t have any texture, and we want the smallest possible file:

/assets/lessons/22/screenshot-82.png

For the rest of the properties, here are the settings:

Most of those are obvious but here’s the list

  • Remember Export Settings : To keep these settings the next time we export.
  • Include:
    • Selected Objects : Only the selected objects.
    • Custom Properties : A text information that we can add to any object in Blender, but we didn’t add any.
    • Cameras : The cameras.
    • Punctual Lights : The lights.
  • Transform:
    • +Y Up : Convert the axes so that positive Y is the upper one —the default property for Three.js.
  • Geometry:
    • Apply Modifiers : Applies the modifiers like the Subdivision Surface . If you don’t check this one, you’ll end up with the original cubes instead of your round buns.
    • UVs : Adds the UV coordinates. These are the coordinates that Three.js —and other environments— use to apply textures on a geometry. Because we don’t have textures, we don’t need those coordinates.
    • Normals : The outside direction of each face. We do need this one if we want the light to work properly.
    • Tangents : Works in tandem with the normals, but they are perpendicular to them.
    • Vertex Colors : The color associated with each vertex. Useful if we painted the vertices, but we didn’t.
    • Materials : The different materials we used.
    • Compression : The legendary Draco compression.
  • Animation
    • Everything related to the animations, but we don’t need any of those because we don’t have animations.

Click Export glTF 2.0 and check the file. The file size should be pretty light. Try to export the same hamburger without the compression , and it should be much bigger. In a real project, this would be when you have to decide between using the compressed version and win a few kB load the whole Draco decoder or go for the uncompressed version.

It doesn’t matter as long as we have our file.

Test in Three.js

Run the project and change the hamburger path if needed:

gltfLoader.load(
    '/models/hamburger.glb',

// ...

JavaScript

Copy

You should see your burger in your Three.js scene.

Regrettably, our hamburger appears way too orange compared to the Blender version. That is due to Three.js settings, and we have to do some tweaks. That will be the next lesson’s topic, but feel free to come back to your burger once you finish applying the features we will learn.

Go further

There are tons of great online tutorials, and most of them are free. Here’s a non-exhaustive list of ressources you can use:

Make sure always to follow tutorials using at least version 2.8 of Blender.

23 Realistic render

Difficulty Hard

Introduction

When we imported our hamburger in the previous lesson, the colors were off. To put it in a nutshell: many things participate in a wrong looking model.

Sometimes, we want a very realistic render. Maybe it’s because we want to showcase a real-life product on our website. Or perhaps we are 3D artists, and we want to show off our work with the best possible result. Anyway, we need a render as real as possible.

In this lesson, we will learn many techniques to improve our model looks once rendered in Three.js. Be careful though, some of those techniques can have a performance impact, and some techniques depend on your model. You’ll have to adapt according to the situation.

Setup

We could use our hamburger, but it’s better to try a more realistic model with textures, normal maps, etc. We will use the Flight Helmet from the GLTF Sample Models repository. You can find the model in the /static/models/ folder.

This lesson is also the perfect opportunity to revise what we already learned. That is why there isn’t much code in the starter. We will have to instantiate the loaders, the lights, the shadows, etc., all by ourselves.

We will also use Dat.GUI to tweak as many parameters as possible. That is required if we want to create the perfect environment.

For now, all we have in our scene is a white sphere and an instance of Dat.GUI.

This sphere is just a placeholder to make sure that the starter is working, but we can use it to set up the lights. Change the material of testSphere to MeshStandardMaterial to see the lights we are about to add:

const testSphere = new THREE.Mesh(
    new THREE.SphereBufferGeometry(1, 32, 32),
    new THREE.MeshStandardMaterial()
)

JavaScript

Copy

As you can see, everything has gone black.

Lights

We are going to use only one DirectionalLight. But how can we have a realistic render with only one light? The environment map will do most of the heavy leverage and simulate light bounce. We could get rid of any light, but the DirectionalLight is important if we want to have more control over the lighting but also to create shadows:

const directionalLight = new THREE.DirectionalLight('#ffffff', 1)
directionalLight.position.set(0.25, 3, - 2.25)
scene.add(directionalLight)

JavaScript

Copy

Let’s add some parameters to our Dat.GUI:

gui.add(directionalLight, 'intensity').min(0).max(10).step(0.001).name('lightIntensity')
gui.add(directionalLight.position, 'x').min(- 5).max(5).step(0.001).name('lightX')
gui.add(directionalLight.position, 'y').min(- 5).max(5).step(0.001).name('lightY')
gui.add(directionalLight.position, 'z').min(- 5).max(5).step(0.001).name('lightZ')

JavaScript

Copy

We can now control the position and intensity .

Default Three.js light intensity values aren’t realistic. They are based on an arbitrary scale unit and don’t reflect real-world values. You could say it doesn’t matter, but it’s better to base our scene on realistic and standard values. It might be more comfortable to reproduce real-life conditions that way.

To change Three.js lights for more realistic values, switch the physicallyCorrectLights property of the WebGLRenderer instance (the renderer ) to true :

renderer.physicallyCorrectLights = true

JavaScript

Copy

Our light appears dimmed. Let’s increase its intensity to 3 :

const directionalLight = new THREE.DirectionalLight('#ffffff', 3)

JavaScript

Copy

Model

Let’s load our model instead of that test sphere.

First, instantiate the GLTFLoader. We will regroup the different loaders together. There is no particular reason for that but to regroup things together:

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

// ...

/**
 * Loaders
 */
const gltfLoader = new GLTFLoader()

JavaScript

Copy

We don’t need the DRACOLoader because the model isn’t compressed. But if you load a Draco compressed model, instantiate the DRACOLoaderas we did in a previous lesson.

We can now load our model located in /static/models/FlightHelmet/glTF/FlightHelmet.gltf :

/**
 * Models
 */
gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        console.log('success')
        console.log(gltf)
    }
)

JavaScript

Copy

As always, go slow, make sure that the model is well loaded with no error, and study the imported result.

Because it’s a complex model, we will simply add the gltf.scene group to our own scene:

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        scene.add(gltf.scene)
    }
)

JavaScript

Copy

If you can’t see it but don’t get any error, Remove your testSphere and zoom a little. The explanation is simple: the loaded model is too small.

Increase its scale, move it down a little, and rotate it so it fits our camera view better:

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(10, 10, 10)
        gltf.scene.position.set(0, - 4, 0)
        gltf.scene.rotation.y = Math.PI * 0.5
        scene.add(gltf.scene)
    }
)

JavaScript

Copy

Let’s also add a tweak to rotate the whole model in our Dat.GUI:

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        // ...

        gui.add(gltf.scene.rotation, 'y').min(- Math.PI).max(Math.PI).step(0.001).name('rotation')
    }
)

JavaScript

Copy

Environment map

We can’t see much of our model because we have only one weak DirectionalLight. As we said previously, the lighting will be taken care of by the environment map.

We already talked about the environment map in the Materials lesson. An environment map is like a photo of the surrounding. It can be a 360 photo or 6 photos that, once put together, compose a cube.

We will use the environment map both for the background and to illuminate our model.

Load the environment map

First, let’s load our environment map. There are multiple textures located in the /static/textures/environmentMaps/ folder. We are going to use the first one.

Because these textures are composed of 6 images (a cube), we have to use a CubeTextureLoader.

Add the CubeTextureLoader to our loaders:

const cubeTextureLoader = new THREE.CubeTextureLoader()

JavaScript

Copy

Now we can load the textures. The order is positive x , negative x , positive y , negative y , positive z , and negative z .

Add it after creating the scene :

/**
 * Environment map
 */
const environmentMap = cubeTextureLoader.load([
    '/textures/environmentMaps/0/px.jpg',
    '/textures/environmentMaps/0/nx.jpg',
    '/textures/environmentMaps/0/py.jpg',
    '/textures/environmentMaps/0/ny.jpg',
    '/textures/environmentMaps/0/pz.jpg',
    '/textures/environmentMaps/0/nz.jpg'
])

JavaScript

Copy

Nothing should have change because we are loading the environment map but we don’t use it yet.

Check the logs for potential errors.

Apply the environment map to the background

To add the environment map in our scene’s background, we could create a massive cube around the scene, set its face to be visible on the inside, and apply its texture. It should work and looks ok, but instead, let’s use a feature included in Three.js.

To apply an environmentMap on a scene, use the cube texture on the Scene’s background property. Make sure to do this after creating the environmentMap and the scene :

scene.background = environmentMap

JavaScript

Copy

And that’s all. You should see the environment map in the background.

Apply the environment map to the model

One essential feature to get a realistic render is to use our environment map to lighten our model.

We have already covered how to apply an environment map to a MeshStandardMaterial with the envMap property. The problem is that our model is composed of many Meshes. What we can do is use the traverse(...) method available on every Object3D —and classes that inherit from it like Group and Mesh.

Instead of doing it in the success callback function, we will create a updateAllMaterials function that will get handy later. Create this function before the environment map:

/**
 * Update all materials
 */
const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        console.log(child)
    })
}

JavaScript

Copy

Now call it when the model is loaded and added to the scene:

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        // ...

        updateAllMaterials()
    }
)

JavaScript

Copy

You should see all the children and grand children in the logs.

Instead of logging the children, we want to apply the environment map to each material that should have it.

It would make no sense to apply the environment map to the lights, the camera, the group, etc. We only want to apply the environment map to the Meshes that have a MeshStandardMaterial.

What we can do is test if the child is an instance of THREE.Mesh and if its material is an instance of THREE.MeshStandardMaterial :

const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            console.log(child)
        }
    })
}

JavaScript

Copy

We now only log the children that support environment maps. Let’s change their envMap property in the material property:

const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            child.material.envMap = environmentMap
        }
    })
}

JavaScript

Copy

Can’t see much of a difference? Increase the envMapIntensity to 5 :

const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            child.material.envMap = environmentMap
            child.material.envMapIntensity = 5
        }
    })
}

JavaScript

Copy

That’s better. We get a nice and realistic lighting.

For more control, let’s add the envMapIntensity property to our Dat.GUI. The problem is that we need only one property to tweak, and, when changed, this value should be applied to all children materials.

We can, however, use the debugObject technique as we did in a previous lesson. Right after instantiating Dat.GUI, create a debugObject :

const gui = new dat.GUI()
const debugObject = {}

JavaScript

Copy

Then, in the environment map code section, add an envMapIntensity property to that object as well as to your Dat.GUI:

debugObject.envMapIntensity = 5
gui.add(debugObject, 'envMapIntensity').min(0).max(10).step(0.001) 

JavaScript

Copy

We now have the envMapIntensity tweak, but changing the value isn’t updating the scene and its children. We should now call the updateAllMaterials function when the tweak value changes and use the debugObject.envMapIntensity value in the updateAllMaterials function:

const updateAllMaterials = () =>
{
    // ...
            child.material.envMapIntensity = debugObject.envMapIntensity
    // ...
}

// ...

debugObject.envMapIntensity = 5
gui.add(debugObject, 'envMapIntensity').min(0).max(10).step(0.001).onChange(updateAllMaterials)

JavaScript

Copy

We can now change all Meshes environment map intensity with only one tweak directly in our debug interface.

Apply the environment map as default

There is an easier way of applying the environment map to all objects. We can update the environment property of the scene just like we changed the background property:

scene.environment = environmentMap

GLSL

Copy

Unluckily, we cannot change the environment map intensity of each material directly from the scene, so we still need our updateAllMaterials function, but it’s a perfectly viable solution.

Renderer

Things are getting more and more realistic, but we still feel that it’s all fake. We need to work on the colors, and this is a matter of WebGLRenderer properties.

Output encoding

Without going too much into details, the outputEncoding property controls the output render encoding. By default, the value of outputEncoding is THREE.LinearEncoding , which looks ok, but not realistic.

The recommended value for the outputEncoding is THREE.sRGBEncoding :

renderer.outputEncoding = THREE.sRGBEncoding

JavaScript

Copy

You should see much brighter textures that will also impact the environment map.

Another possible value is THREE.GammaEncoding . This encoding has the advantage of letting you play on a value called gammaFactor that would act a little like the brightness, but we won’t use this one in the lesson.

The Gamma Encoding is a way of storing colors while optimizing how bright and dark values are stored according to human eye sensitivity. When we use the sRGBEncoding , it’s like using the GammaEncoding with a default gamma factor of 2.2 , which is the common value.

You can find out more about this topic here

While some might think that GammaEncoding is better than sRGBEncoding because we can control the gamma factor for a darker or brighter scene, this physically doesn’t seem right, and we will see how to manage the “brightness” in a better way later.

Textures encoding

You might not have noticed it, but the environment map colors are wrong. They appear grayish and toned down. Even if the effect looks pretty good, it’s more satisfying to preserve the right colors.

The problem is that our renderer outputEncoding is THREE.sRGBEncoding , yet the environment map texture is by default THREE.LinearEncoding .

The rule is straightforward. All textures that we can see directly —like the map —should have THREE.sRGBEncoding as encoding, and all other textures — such as normalMap —should have THREE.LinearEncoding .

We can see the environmentMap texture directly, so we have to change its encoding to THREE.sRGBEncoding :

environmentMap.encoding = THREE.sRGBEncoding

GLSL

Copy

But what about the model textures?

Fortunately, the GLTFLoader implemented this rule, and all the textures loaded from it will have the right encoding automatically.

Tone mapping

The tone mapping intends to convert High Dynamic Range (HDR) values to Low Dynamic Range (LDR) values. HDR is much more than the following interpretation, but you can see that like images where the color values can go beyond 1 . It’s useful if we want to store light information because light doesn’t have intensity limits.

While our assets are not HDR, the tone mapping effect can have a realistic result as if the camera was poorly adjusted.

To change the tone mapping, update the toneMapping property on the WebGLRenderer.

There are multiple possible values:

  • THREE.NoToneMapping (default)
  • THREE.LinearToneMapping
  • THREE.ReinhardToneMapping
  • THREE.CineonToneMapping
  • THREE.ACESFilmicToneMapping

Test these tone mapping:

renderer.toneMapping = THREE.ACESFilmicToneMapping

JavaScript

Copy

To appreciate the difference, let’s add the toneMapping to our Dat.GUI. We can create a dropdown tweak by sending an object with different keys and values as the third parameter of gui.add(...) :

gui.add(renderer, 'toneMapping', {
    No: THREE.NoToneMapping,
    Linear: THREE.LinearToneMapping,
    Reinhard: THREE.ReinhardToneMapping,
    Cineon: THREE.CineonToneMapping,
    ACESFilmic: THREE.ACESFilmicToneMapping
})

JavaScript

Copy

Unfortunately, changing this tweak will result in a warning in the console, looking like THREE.WebGLProgram: Unsupported toneMapping: 3 . The problem is a bug with Dat.GUI interpreting the values of the object as a String.

For example, if we use THREE.ReinhardToneMapping , the value behind this constant is 3 as a Number . But once we change the tweak for tone mapping, Dat.GUI will send '3' as a String which will result in the error previously mentioned.

We can fix that by converting the value to a Number in the change event callback:

gui
    .add(renderer, 'toneMapping', {
        No: THREE.NoToneMapping,
        Linear: THREE.LinearToneMapping,
        Reinhard: THREE.ReinhardToneMapping,
        Cineon: THREE.CineonToneMapping,
        ACESFilmic: THREE.ACESFilmicToneMapping
    })
    .onFinishChange(() =>
    {
        renderer.toneMapping = Number(renderer.toneMapping)
    })

JavaScript

Copy

Changing the tone mapping should work. Nevertheless, if you watch closely, you’ll see that the tone mapping changed for the environment map in the background, but not for the model itself.

We need to find a way to tell the materials that they need to be updated and we already did that in the updateAllMaterials function. What we can do is call this function right after changing the toneMapping :

    .onFinishChange(() =>
    {
        renderer.toneMapping = Number(renderer.toneMapping)
        updateAllMaterials()
    })

JavaScript

Copy

The materials should also update when changing the tone mapping.

We can also change the tone mapping exposure. You can see that like how much light we let in and the algorithm will handle it its way. To change this value, we must update the toneMappingExposure property directly on the renderer :

renderer.toneMappingExposure = 3

JavaScript

Copy

Let’s add it to Dat.GUI as well:

gui.add(renderer, 'toneMappingExposure').min(0).max(10).step(0.001)

JavaScript

Copy

Feel free to choose your favorite toneMapping for the rest of the lesson, but here we will go for THREE.ReinhardToneMapping .

Antialiasing

We call aliasing an artifact that might appear in some situations where we can see a stair-like effect, usually on the edge of geometries.

Our model isn’t subject to that problem because there is a lot of details, but if you have a screen with a pixel ratio of 1 . Look at the edges —especially the bright ones— rotate the camera slowly, and you might see the problem:

It’s a well-known problem. When the rendering of a pixel occurs, it tests what geometry is being rendered in that pixel. It calculates the color, and, in the end, that color appears on the screen.

But geometry edges are usually not perfectly aligned with vertical lines and horizontal lines of pixel of your screen and this is why you get this stair-like artifact named aliasing .

There are many ways of fixing that problem, and developers have been struggling with it for many years.

One easy solution would be to increase our render’s resolution, let’s say to the double. When resized to it’s normal-sized, each pixel color will automatically be averaged from the 4 pixels rendered.

This solution is called super sampling (SSAA) or fullscreen sampling (FSAA), and it’s the easiest and more efficient one. Unfortunately, that means 4 times more pixels to render, which can result in performance issues.

The other solution is called multi sampling (MSAA). Again, the idea is to render multiple values per pixel (usually 4) like for the super sampling but only on the geometries’ edges. The values of the pixel are then averaged to get the final pixel value.

The most recent GPU can perform this multi sampling anti-aliasing, and Three.js handles the setup automatically. We just need to change the antialias property to true during the instantiating — and not after:

const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true
})

JavaScript

Copy

Those aliasing artifacts should be gone now.

Using the antialias exhausts some resources. As we said earlier, screens with a pixel ratio above 1 don’t really need antialias. One right way to do this would be to activate it only for screens with a pixel ratio below 2 . We will see how to achieve that in a future lesson, along with other optimizations.

Shadows

The final touch for a realistic render is to add shadows. First, toggle the shadows on WebGLRenderer. Then, change the shadow type to THREE.PCFSoftShadowMap as we did in the Shadows lesson:

renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap

JavaScript

Copy

Activate it on the DirectionalLight:

directionalLight.castShadow = true

JavaScript

Copy

We also need to set the camera that handles the shadow for this light.

Add a CameraHelper to the directionalLight.shadow.camera :

const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)

JavaScript

Copy

We can now see accurately what the shadow camera will render. The box should already fit pretty nicely with the scene. Let’s reduce the far value:

directionalLight.shadow.camera.far = 15

JavaScript

Copy

We can remove or comment the directionalLightCameraHelper .

As we want realistic and precise shadows and because we have only one light, we can increase the shadow map size to 1024x1024 without fearing a frame rate drop.

directionalLight.shadow.mapSize.set(1024, 1024)

JavaScript

Copy

Finally, we can activate the shadows on all the Meshes of our model. As we are already traversing the scene in the updateAllMaterials function, let’s simply activate both castShadow and receiveShadow on all the children:

const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            // ...

            child.castShadow = true
            child.receiveShadow = true
        }
    })
}

JavaScript

Copy

You should now observe an accurate shadow, mostly on the wood base and inside the model.

Final tweaks

Now that we have everything in place, we can tweak the values, make sure the directionalLight corresponds to the light in the environment map, try other environment maps, test different tone mappings, add animation, etc.

It’s up to you. Take your time, stop looking at your render, and look around because you need real-life markers, make sure your screen colors are good, maybe show your work to your friends to get an external point of view until everything is correctly set.

Hamburger

Let’s try with our hamburger. A version is already located in /static/models/hamburger.glb .

This file isn’t Draco compressed. If you are using your model, make sure it’s not compressed or add the DRACOLoader to the GLTFLoader as we did in the Imported Model lesson.

Replace the path to load the hamburger and change the scale and position:

gltfLoader.load(
    '/models/hamburger.glb',
    (gltf) =>
    {
        gltf.scene.scale.set(0.3, 0.3, 0.3)
        gltf.scene.position.set(0, - 1, 0)
        scene.add(gltf.scene)

        updateAllMaterials()
    }
)

JavaScript

Copy

Your hamburger appears, but some nasty strips cover its surface.

No we didn’t let the hamburger burn on the grill.

These artifacts are called shadow acne. Shadow acne can occur on both smooth and flat surfaces for precision reasons when calculating if the surface is in the shadow or not. What’s happening here is that the hamburger is casting a shadow on its own surface.

We have to tweak the light shadow 's bias and normalBias properties to fix this shadow acne.

The bias usually helps for flat surfaces. It’s not our case here, but if you have the problem on flat surfaces, try to increase the bias slightly until the acne disappears.

The normalBias usually helps for rounded surfaces, which is our case. Let’s increase it until the shadow acne is barely visible:

directionalLight.shadow.normalBias = 0.05

JavaScript

Copy

Now you get a very decent, acne-free hamburger.

Bon appétit.

1 симпатия