00:00/00:00
3:22
00:03:22

Shortcuts ⌨️

  • SPACE to play / pause
  • ARROW RIGHT or L to go forward
  • ARROW LEFT or J to go backward
  • ARROW UP to increase volume
  • ARROW DOWN to decrease volume
  • F to toggle fullscreen
  • M to toggle mute
  • 0 to 9 to go to the corresponding part of the video
  • SHIFT + , to decrease playback speed
  • SHIFT + . or ; to increase playback speed

⚠️ Update

<SoftShadows> attributes have changed.

<SoftShadows size={ 25 } samples={ 10 } focus={ 0 } />
  • size: radius of the softness
  • samples: quality (more samples = less visual noise but worse performance)
  • focus: distance where the shadow is the sharpest

⚠️ Update

In the latest versions of Three.js, the whole AccumulativeShadow will look darker with this setting.

You can ignore that because we are going to replace it right after.

⚠️ Update

In the latest versions of Three.js, lights require a higher intensity and it’s also the case for <RandomizedLight>

<RandomizedLight
    amount={ 8 }
    radius={ 1 }
    ambient={ 0.5 }
    intensity={ 3 }
    position={ [ 1, 2, 3 ] }
    bias={ 0.001 }
/>

⚠️ Update

Use version 0.9.34 of leva:

npm install leva@0.9.34

⚠️ Update

In the latest versions of Three.js, we can update the envMapIntensity of the all materials without having to set it manually on each one by updating the envMapIntensity property of Scene.

Import useThree from @react-three/fiber:

import { useThree, useFrame } from '@react-three/fiber'

Right after the useControls() for envMapIntensity, retrieve the scene:

const scene = useThree(state => state.scene)

Import useEffect from react:

import { useEffect, useRef } from 'react'

Using useEffect, only assign envMapIntensity to scene.environmentIntensity when envMapIntensity changes:

const scene = useThree(state => state.scene)
useEffect(() =>
{
    scene.environmentIntensity = envMapIntensity
}, [ envMapIntensity ])

⚠️ Update

<Stage> now supports Accumulative Shadows.

For this reasons, the syntax to tweak the shadows has changed a little.

Here’s an example:

<Stage
    shadows={ {
        type: 'contact',
        opacity: 0.2,
        blur: 3
    } }
>

⚠️ Update

The intensity will also update the scene.envMapIntensity like we did earlier. To prevent conflicts, we are going to comment our implementation.

Comment the useThree and useEffect part:

// const scene = useThree(state => state.scene)
// useEffect(() =>
// {
//     scene.environmentIntensity = envMapIntensity
// }, [ envMapIntensity ])

Send the envMapIntensity to the intensity prop:

<Stage
    shadows={ { type: 'contact', opacity: 0.2, blur: 3 } }
    environment="sunset"
    preset="portrait"
    intensity={ envMapIntensity }
>

Unlock content 🔓

To get access to 93 hours of video, a members-only Discord server, subtitles, lesson resources, future updates and much more join us for only $95!

Want to learn more? 👋

86%

That's the end of the free part 😔

To get access to 93 hours of video, a members-only Discord server and future updates, join us for only $95!

Next lesson
58.

Environment and Staging

Difficulty Medium

Introduction 00:00

Until now, we’ve been using a very simple environment with one directional light source and one ambient light source.

In this lesson, we are going to see some of the many environmental features available and how to implement them in R3F.

Setup 00:45

The starter is very similar to the previous lesson with a sphere, a cube and a green floor.

In addition, the cube is rotating thanks to the useFrame and we’ve also added <Perf /> from r3f-perf in order to keep an eye on performance.

The @react-three/drei dependency is already installed within the project and we are using the OrbitControls helper to be able to move the camera around.

Background color 01:32

For the sake of learning, we are going to discover multiple ways of changing the background color.

Note that if all you want is a uniform color, any of those techniques are viable solutions. It’s up to you and your preferences. In some very specific cases, like when using postprocessing, the technique you chose to use can have a different result, but we are going to see that in a future lesson.

With CSS

The default background color seems to be white, but as we saw in a previous lesson, it’s actually transparent and what we are seing is the page HTML background.

This means that we can change the color directly in CSS.

In /src/style.css, change set the background-color (or just background) to red:

html,
body,
#root
{
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: red;
}

And that’s all. Quite easy, right? But let’s discover the other techniques.

Comment or remove the background property:

html,
body,
#root
{
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    // background: red;
}

With setClearColor on the renderer

The WebGLRenderer has a method named setClearColor. It’s a way of filling the <canvas> with a color before rendering the various objects in the scene.

To use setClearColor, we need to have access to the renderer and we need to do that only once when the renderer has been created.

In index.jsx create a created function and send it to the <Canvas> attribute named onCreated:

const created = () =>
{
    console.log('created')
}

root.render(
    <Canvas
        camera={ {
            fov: 45,
            near: 0.1,
            far: 200,
            position: [ - 4, 3, 6 ]
        } }
        onCreated={ created }
    >
        <Experience />
    </Canvas>
)

The state will be sent as an argument of the function and the renderer will be available in the gl property:

const created = (state) =>
{
    console.log(state.gl)
}

And if you like to destructure:

const created = ({ gl }) =>
{
    console.log(gl)
}

We can then call the setClearColor with the color as the first parameter and the alpha as the second parameter:

const created = ({ gl }) =>
{
    gl.setClearColor('#ff0000', 1)
}

With the scene background

That was a viable solution, but let's check out another one.

Instead of doing it on the renderer, we can do it on the scene and we can access it in the created function the same way:

const created = ({ scene }) =>
{
    console.log(scene)
}

We can now instantiate a Color using Three.js and assign it to the background property (don’t forget to import THREE or just Color from three)

import * as THREE from 'three'

// ...

const created = ({ scene }) =>
{
    scene.background = new THREE.Color('#ff0000')
}

With R3F color

That’s also a good solution, but wait, there is an even better alternative.

Instead of listening to events and importing Three.js, we can create the color directly in the JSX.

First, let’s do some clean-up and comment or remove the THREE import, the created function and the onCreated attribute:

import './style.css'
import ReactDOM from 'react-dom/client'
import { Canvas } from '@react-three/fiber'
import Experience from './Experience.jsx'

const root = ReactDOM.createRoot(document.querySelector('#root'))

root.render(
    <Canvas
        camera={ {
            fov: 45,
            near: 0.1,
            far: 200,
            position: [ - 4, 3, 6 ]
        } }
    >
        <Experience />
    </Canvas>
)

Then, create a <color> inside the <Canvas>:

<Canvas
    camera={ {
        fov: 45,
        near: 0.1,
        far: 200,
        position: [ 1, 2, 6 ]
    } }
>
    <color args={ [ '#ff0000' ] } />
    <Experience />
</Canvas>

Doing so will automatically create a Color instance, but that instance isn’t assigned to the scene.background.

And here’s the trick.

We can add an attach attribute to specify what that component should be attached to.

<color args={ [ '#ff0000' ] } attach="background" />

Here, the scene is implied because it’s the only parent.

We can actually put that code anywhere, as long as the direct parent is the scene which is still the case if we put it in Experience:

export default function Experience()
{
    // ...

    return <>

        <color args={ [ '#ff0000' ] } attach="background" />

        {/* ... */}

    </>
}

Let’s change it to a less annoying color like 'ivory':

<color args={ [ 'ivory' ] } attach="background" />

(The following screenshots aren’t using this background color which is why they will look more white).

Lights 13:43

All default Three.js lights are supported in R3F:

  • <ambientLight />
  • <hemisphereLight />
  • <directionalLight />
  • <pointLight />
  • <rectAreaLight />
  • <spotLight />

We are not going to test them since we already saw how they work in the Lights lesson and they work just the same with R3F.

Light Helpers

We can still use Three.js light helpers too.

To do so, we are going to use useHelper from drei, but first, we need a reference to the <directionalLight>.

useRef is already import from react to animate the cube.

Create a directionalLight reference:

export default function Experience()
{
    const directionalLight = useRef()

    // ...
}

Associate it with the <directionalLight> using the ref attribute:

<directionalLight ref={ directionalLight } position={ [ 1, 2, 3 ] } intensity={ 4.5 } />

Import useHelper from @react-three/drei:

import { useHelper, OrbitControls } from '@react-three/drei'

The first parameter of useHelper is the reference to the light source and the second parameter is the helper class we want to use from Three.js.

This means that we first need to import THREE in order to get access to the DirectionalLightHelper class:

import * as THREE from 'three'

(We could also have imported the DirectionalLightHelper class only).

Finally, we can call useHelper() with the reference (directionLight), the helper class (THREE.DirectionalLightHelper) and the parameters of the helper (in our case, the size):

export default function Experience()
{
    const directionalLight = useRef()
    useHelper(directionalLight, THREE.DirectionalLightHelper, 1)

    // ...
}

It’s barely visible since it’s composed of white lines, but move the camera and you should see it.

useHelper isn’t just for the light, as we could have used it for the camera with CameraHelper as an example.

Shadows 18:28

To make the scene more realistic, we need shadows.

We are going to start with the default Three.js shadows system, but then we are going to see other shadow solutions made easier thanks to R3F and drei.

Want to learn more?

That's the end of the free part 😔

To get access to 93 hours of video, a members-only Discord server and future updates, join us for only $95!

How to use it 🤔

  • Download the Starter pack or Final project
  • Unzip it
  • Open your terminal and go to the unzip folder
  • Run npm install to install dependencies
    (if your terminal warns you about vulnerabilities, ignore it)
  • Run npm run dev to launch the local server
    (project should open on your default browser automatically)
  • Start coding
  • The JS is located in src/script.js
  • The HTML is located in src/index.html
  • The CSS is located in src/style.css

If you get stuck and need help, join the members-only Discord server:

Discord