In this chapter, we set up the drawing canvas, the camera, and the lights to render a box. We use an orthographic camera with both ambient and directional lights. Then, we frame the scene to ensure we always see the same content regardless of the screen size.
Initializing the Project
There are many ways to set up a project with React Three Fiber. I recommend using Vite. It’s a fast and opinionated build tool that makes it easy to get started.
Run npm create vite to create a new project. When asked for a framework, pick React. Choose TypeScript or JavaScript as you prefer. This tutorial guides you through both languages (use the language selector above).
Navigate to the project folder Vite created for you.
At the time of writing this article, Vite still uses React 18. Meanwhile, React 19 is out, and the latest version of React Three Fiber is only compatible with React 19. So let’s update react and react-dom.
As we go through the tutorial, we will have some variables that we don’t use right away. ESlint, however, doesn’t like unused variables, and it marks them as errors. Let’s reduce the noise by downgrading the severity of the no-unused-vars rule to a warning.
Vite set up ESlint for us, which is great. However, it also set up some rules that we don’t want for this project. Let’s turn them off.
react/prop-types: This rule makes it mandatory to define the types of the props. If you care about types, follow the TypeScript version of this tutorial.
react/no-unknown-property: This rule ensures that we don’t use unknown properties in JSX. It doesn’t play nicely with React Three Fiber, so we need to turn it off.
no-unused-vars: This rule ensures that we don’t have unused variables. As we go through the tutorial, we will have some variables that we don’t use right away. We reduce the noise by downgrading the severity of this rule to a warning.
Let’s create a new file, src/Game.tsx. This component will be the root of our game.
Let’s create a new component, src/Game.jsx. This will be the root of our game.
The Scene component will contain the drawing canvas, the camera, and the lights. We pass on the Player component as its child, which will render a box. Later, we will add the Map component, including the trees, cars, and trucks. This component is also where the score indicator and the controls come later.
src/Game.tsx
import { Scene } from"./components/Scene";
import { Player } from"./components/Player";
exportdefaultfunctionGame() {
return (
<Scene>
<Player />
</Scene>
);
}
src/Game.jsx
import { Scene } from"./components/Scene";
import { Player } from"./components/Player";
exportdefaultfunctionGame() {
return (
<Scene>
<Player />
</Scene>
);
}
Setting the Game Component as the Root
To use the new Game component as our root, we need to replace the original App component in the src/main.jsxsrc/main.tsx file.
This will give you an error for now because we didn’t implement the Scene and Player components.
Now that we replaced the App component, we can delete the original App.tsx, App.css, and the assets folder.
Now that we replaced the App component, we can delete the original App.jsx, App.css, and the assets folder.
We can also get rid of most things in the CSS file. All we need is that the body element and the root div fills the whole screen.
Now, let’s look into the different camera and light options.
src/index.css
body {
margin: 0;
display: flex;
min-height: 100vh;
}
#root {
width: 100%;
}
Orthographic vs Perspective Camera
There are two main camera options: the perspective camera (left) and the orthographic camera (right). The perspective camera is the default camera in Three.js. It is the most common camera type and is used to create perspective projections, which make things further away appear to be smaller.
The orthographic camera creates parallel projections, which means that objects are the same size regardless of their distance from the camera. We use an orthographic camera to give our images a more arcade, boxy look. Grab the scenes below to see how these projections work.
Camera Position and Target
We use a coordinate system in which the ground is the xy-plane, and the z-axis points up. This way, the player can move left and right along the x-axis, forward and backward along the y-axis, and when it jumps, it goes up along the z-axis.
The camera is positioned to the right along the x-axis, behind the player along the y-axis, and above the ground. The camera looks at the origin of the coordinate system, to the 0,0,0 coordinate, where the player is initially.
Ambient and Directional Light
There are many types of lights in Three.js. We use an ambient light and a directional light. Turn around the scenes below to see how the light affects the objects.
The ambient light brightens the entire scene. It doesn’t have a specific position or direction. You can think of it as the light on a cloudy day when it is bright, but there are no shadows. The ambient light is used to simulate indirect lighting. The downside is that, as a result, every side of our objects is equally bright, and every side of the boxes in the scene has the same color. We don’t see the edges of the objects.
The directional light has a position and a target. The position is misleading, though. It doesn’t shine from one point in every direction like a point light. The position and target are used to determine its direction. You can think of it as the sun, which shines from very far away, and all the light rays are in parallel. The directional light can cast shadows. Its downside is that the sides of the object that don’t receive light are completely black.
That’s why we are combining the two lights. The ambient light gives the whole scene a baseline brightness, and the directional light will highlight some sides of our shapes.
Defining the Scene
After reviewing the different camera and light options, let’s put them together in the Scene component. We set up the canvas with an orthographic camera and lights.
We use the Canvas component from @react-three/fiber. This component will contain every 3D object on the scene, so it has a children prop.
We set the orthographic prop to true to use an orthographic camera and the camera prop to define the camera’s position and orientation. The camera props require vectors or coordinates that are defined by the x, y, and z values.
The up prop sets the camera’s up vector. We set it to [0, 0, 1] to make the z-axis the up vector. The position prop sets the camera’s position. We move the camera to the right along the x-axis, backward along the y-axis, and up along the z-axis.
We also add the lights. We can use React Three Fiber-specific elements within the Canvas element. We add the ambientLight and directionalLight components to add lights to the scene. We position the directional light to the left along the x-axis, backward along the y-axis, and up along the z-axis.
src/components/Scene.tsx
import { Canvas } from"@react-three/fiber";
exportconstScene= ({
children,
}: {
children:React.ReactNode;
}) => {
return (
<Canvas
orthographic={true}
camera={{
up: [0, 0, 1],
position: [300, -300, 300],
}}
>
<ambientLight />
<directionalLightposition={[-100, -100, 200]} />
{children}
</Canvas>
);
};
src/components/Scene.jsx
import { Canvas } from"@react-three/fiber";
exportconstScene= ({ children }) => {
return (
<Canvas
orthographic={true}
camera={{
up: [0, 0, 1],
position: [300, -300, 300],
}}
>
<ambientLight />
<directionalLightposition={[-100, -100, 200]} />
{children}
</Canvas>
);
};
Drawing the Player
We added many things to the scene already, but we still don’t have any visible objects. Let’s define the player as a simple box.
The Player
Then, let’s define the player. In the Game component, we passed on the Player component as a child to the Scene component.
Initially, the player will be a simple box. To draw a 3D box, we define a geometry and a material. The geometry defines the object’s shape, and the material defines its appearance.
Here, we use boxGeometry to define a box. The boxGeometry takes three arguments: the width, depth, and height of the box along the x, y, and z-axes.
There are different types of materials in Three.js. The main difference between them is how they react to light. We use the meshLambertMaterial, a simple material that responds to light. We set the color prop to white with a hexadecimal value.
We wrap the geometry and the material into a mesh. A mesh is an object that we can add to the scene. We can also position a mesh. We set the box position with the x, y, and z positions. In the case of a box, these set the center position. Here, we elevate the player above the ground by half its height. This way, the bottom of the box is on the ground.
By default, one unit in the scene matches one pixel on the screen (if the shape faces the camera and we use an orthographic camera). This is even true when we resize the screen. If we enlarge the browser window, the size of the objects within the canvas doesn’t change. Instead, the camera exposes more of the scene.
This is not what we want. We want to show the same content regardless of the screen size. The close surrounding of the player. Resize this screen to see the difference between the default and the desired behavior.
Using Bounds
We frame the scene using the Bounds component from @react-three/drei. This calculates a boundary box and frames the scene according to the margin prop. We want to frame the player, so we wrap it in the Bounds component.
This component calculates the size of its children - this time, the player - multiplies it by the margin prop, and that will be the size of the visible area. If the margin is 1, the child components fill the whole canvas as much as possible (while keeping their proportion). If the margin is 1.2, then there’s a 20% margin around them. If the margin is 10, like in this case, the visible area is ten times the size of the child components. It then sets the camera target to the center of the bounding box.