Building Your First Browser Game with Three.js and React: Part 2 - Implementing 3D Models
Introduction
Welcome back to our series on building a 3D basketball game in the browser! In Part 1, we set up our project with Vite and created a basic 3D scene with a cube. Today, we're adding key elements to our game: the basketball and the table (or hoop stand). By the end of this tutorial, you'll see these core game components come to life in our scene.
Step 1: Preparing Your 3D Models
Before we start coding, ensure you have the 3D models for the basketball and the table. We're using models prepared for Three.js (just created in Blender and exported in the right format), which are available for download below (If these links don't lead directly to downloads, simply copy the entire page and paste the content into the files mentioned later.)
- Download the basketball model (
basketball.glb
) - Download the table model (
table.gltf
)
Create a new folder named models/
within the public/
directory and place the two models in it.
Now, the project contains these files: public/models/basketball.glb
and public/models/table.gltf
.
Step 2: Creating the Ball Component
We will create two components using the GLTFJSX tool. This tool transforms 3D models (in gltf format or others) into ready-to-use components for your React Three Fiber project. Try it yourself with our two models!
Let's start by adding the basketball to our scene. We'll use the useGLTF
hook from @react-three/drei
to load our model.
You can find everything drei has to offer on the github of the project, don't hesitate to go on the documentation if you want more explanation about what we use!
First, create a new Components
folder within the src/
directory to house the Ball and Table components and create a new Ball.jsx
file inside:
// src/Components/Ball.jsx
import React from 'react';
import { useGLTF } from "@react-three/drei";
const Ball = ({ position }) => {
const { nodes, materials } = useGLTF("/models/basketball.glb");
return (
<mesh
position={position}
geometry={nodes.Sphere.geometry}
material={materials["Material.001"]}
castShadow
receiveShadow
/>
);
};
useGLTF.preload("/models/basketball.glb");
export default Ball;
In this component, we're loading the basketball model and allow us to placing it at a specified position in the scene with the position
prop.
The preload function ensures the model is lazy loaded before it's needed, improving the performance.
This component is straightforward, containing just a single mesh—a 3D object made up of vertices and polygons—equipped with specific geometry and material. These parameters can be found by logging nodes
and materials
, the two parameters that the function useGLTF
give us. Additionally, we've included parameters such as castShadow
and receiveShadow
to control how the mesh casts and receives shadows from other objects.
Step 3: Creating the Table Component
Next, we'll add the table. This is a significant component as it's more complex than the ball.
Create a new component named Table.jsx
:
// src/Components/Table.jsx
import React, { useRef } from 'react'
import { MeshTransmissionMaterial, useGLTF } from '@react-three/drei'
const Table = (props) => {
const { nodes, materials } = useGLTF('/models/table.gltf');
return (
<group {...props} dispose={null}>
<mesh
castShadow
receiveShadow
geometry={nodes.Table.geometry}
material={materials.Wood}
position={[0, 0.068, 0]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Controls.geometry}
material={materials.Wood}
position={[4.135, 0.092, -0.003]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Control_A.geometry}
material={materials.Red}
position={[4.184, 0.129, 0.744]}>
<mesh
castShadow
receiveShadow
geometry={nodes.Control_A_Text.geometry}
material={materials.White}
position={[0.237, 0.046, 0.21]}
rotation={[Math.PI / 2, 1.179, -Math.PI / 2]}
/>
</mesh>
<mesh
castShadow
receiveShadow
geometry={nodes.Control_B.geometry}
material={materials.Green}
position={[4.183, 0.128, -0.754]}>
<mesh
castShadow
receiveShadow
geometry={nodes.Control_B_Text.geometry}
material={materials.White}
position={[0.25, 0.043, 0.207]}
rotation={[Math.PI / 2, 1.184, -Math.PI / 2]}
/>
</mesh>
<mesh
castShadow
receiveShadow
geometry={nodes.Thruster_B.geometry}
material={materials.Black}
position={[2.259, -0.189, -0.764]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Thruster_A.geometry}
material={materials.Black}
position={[2.259, -0.189, 0.765]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Hide_Thruster.geometry}
material={materials.Black}
position={[2.257, -0.047, 0]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Base.geometry}
material={materials.Wood}
position={[-2.235, 0.565, 0]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Cylinder.geometry}
material={materials.Red}
position={[-2.235, 1.177, 0]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Panel.geometry}
material={materials.Wood}
position={[-2.234, 1.814, 0]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Ring.geometry}
material={materials.Red}
position={[-1.686, 1.46, 0]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Glass.geometry}
position={[0.497, 1.54, 0.005]}
>
<MeshTransmissionMaterial anisotropy={0.1} chromaticAberration={0.04} distortionScale={0} temporalDistortion={0} />
</mesh>
</group>
)
}
useGLTF.preload('/models/table.gltf');
export default Table;
This component loads the table model and all of its parts, such as the basket hop ring, controls, thrusters, and so on. You can identify each component by examining the geometry prop of each mesh. For instance, nodes.Ring.geometry
corresponds to the ring of the basketball hoop!
We simply modify the glass part of the model (The mesh with the nodes.Glass.geometry
geometry) by adding a custom material: the MeshTransmissionMaterial
to achieve appealing glass aesthetics! To achieve this, we simply add a child to the mesh that contains the Glass geometry and remove the existing material from this mesh. This way, the mesh will use the material specified in its child.
We give it some default parameter for a good looking glass.
Step 4: Integrating the Components into the Scene
Now, let's add both the Ball and Table components into our existing scene.
Update your Experience.jsx
file to incorporate these new components and implement a few minor changes.
import React from 'react';
import { Box, Center, Environment, OrbitControls } from '@react-three/drei';
import Table from './Components/Table';
import Ball from './Components/Ball';
const Experience = () => {
return (
<>
<color attach="background" args={["#ddc28d"]} />
<ambientLight />
<directionalLight position={[0, 1, 2]} intensity={1.5} />
<Environment preset="city" />
<OrbitControls makeDefault />
<Center>
<Table position={[0, 0, 0]} />
<Ball position={[0.25, 1.5, 0]} />
</Center>
</>
);
}
export default Experience;
Now we have added these two new components inside another component from drei called Center
.
As the name suggests, this component is very useful for centering 3D objects on the screen.
We also include a <color>
component to set the scene's background color.
The transmission of our table's glass uses this to determine its background color.
We make slight adjustments to the lighting by replacing the point light with a directional light,
and we remove the parameter of the ambient light, opting to use the default one for a better view of our scene.
Lastly, we add an Environment
component from drei to enhance the reflection on our table model.
Step 5: Testing Your Scene
Launch your development server (if it's not already running) and navigate to your app. You should see the basketball and table rendered in the scene like in the screenshot bellow.
Conclusion
Great job! You've successfully incorporated complex 3D models into your basketball game. This progress brings us one step closer to finalizing our interactive game.
In the next part of our series, we'll introduce interactivity and physics.
Stay tuned, and happy coding! 🏀