DEV Community

Romain
Romain

Posted on • Originally published at rherault.dev

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.)

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

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! 🏀

Top comments (0)