Blog Details
KALIN CHAKMA
16 Oct 2024
5 min read
Three.js is a robust JavaScript library designed for creating and displaying 3D graphics in web browsers. It abstracts the complexities of WebGL, enabling developers to create 3D scenes with relative ease. With features like lighting, shadows, textures, and animations, Three.js opens the door to visually stunning applications, from simple 3D models to fully immersive environments.
React is a widely-used JavaScript library developed by Facebook for building user interfaces, particularly single-page applications. Its component-based architecture allows developers to create reusable UI elements, manage state efficiently, and build dynamic applications that respond quickly to user interactions. React’s declarative approach simplifies the development process, making it easier to maintain and scale applications.
Integrating Three.js with React brings together the strengths of both libraries, creating a powerful toolkit for web developers. By combining React’s efficient UI management with the 3D capabilities of Three.js, developers can build applications that are not only visually captivating but also highly interactive and responsive.
This integration allows for:
Before diving into the integration of Three.js with React, it’s helpful to have a few foundational skills and knowledge areas. While you don’t need to be an expert, a basic understanding of the following will make the learning process smoother
A fundamental knowledge of React is important for integrating Three.js effectively. You should be comfortable with concepts such as:
useEffect
and useState
will help you manage the rendering and updates of your 3D scenes.Three.js is a powerful JavaScript library that simplifies the process of creating 3D graphics in the web browser.
Scene
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
// Create renderer for scene
const renderer = new THREE.WebGLRenderer();
// set renderer size as window height and width
renderer.setSize( window.innerWidth, window.innerHeight );
// you can also append renderer to any html element
document.body.appendChild( renderer.domElement );
Camera
const camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
Renderer
<canvas>
).Mesh
const geometry = new THREE.PlaneGeometry( 1, 1 );
const material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
const plane = new THREE.Mesh( geometry, material );
scene.add( plane );
Geometries
Materials
Lights
Step 1: Creating project with vite:
npm create vite@latest vanilla_3d_web --template vanilla
Step 2: Navigate to the Project Directory:
cd vanilla_3d_web
Step 3: Install Dependencies:
npm install
Step 4: Start the Development Server:
npm run dev
Step 5: Edit i"/index.html":
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D web view with vanilla js + Three.js</title>
</head>
<body>
<canvas id="main"></canvas>
<script type="module" src="/main.js"></script>
</body>
</html>
Step 6: Reset canvas styles and position to window: "/styles.css"
* {
margin: 0;
padding: 0;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
Step 7: writting Three.js to render 3D view "/main.js":
import './style.css'
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/Addons.js';
// main scene
const scene = new THREE.Scene();
// main camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// main renderer
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("main")
})
// set pixel ratio of renderer
renderer.setPixelRatio(window.devicePixelRatio);
// set renderer witdth and height
renderer.setSize( window.innerWidth, window.innerHeight );
// set camera position
camera.position.setZ(40);
// create opject
const geometry = new THREE.TorusGeometry(10, 3, 16, 100);
// the Standard Mesh Material react to light source
const material = new THREE.MeshStandardMaterial({ color: 0xFF6347 });
const torus = new THREE.Mesh(geometry, material);
// add torus to scene
scene.add(torus);
// add light source
const pointLight = new THREE.PointLight(0xffffff);
// set the position
pointLight.position.set(20,20,20);
// ambient light
const ambientLight = new THREE.AmbientLight(0xffffff);
// add point light to scene
scene.add(pointLight, ambientLight);
// light helper which help to locate where the light is
const lightHelper = new THREE.PointLightHelper(pointLight);
scene.add(lightHelper);
// add grid helper
// const gridHelper = new THREE.GridHelper(200, 50);
// scene.add(gridHelper);
// orbit controls that use to explore scene with mouse
const controls = new OrbitControls(camera, renderer.domElement);
// rendom star adding to scene
function addStar() {
const geometry = new THREE.SphereGeometry(0.25, 24, 24);
const material = new THREE.MeshStandardMaterial( {color: 0xfffff});
const star = new THREE.Mesh( geometry, material );
// give the star a random postion
const [x, y, z] = Array(3).fill().map(() => THREE.MathUtils.randFloatSpread(100));
star.position.set(x, y, z);
scene.add(star);
}
// add 200 random start
Array(200).fill().forEach(addStar)
// rerender like game loop function
function animate() {
requestAnimationFrame( animate );
torus.rotation.x += 0.01;
torus.rotation.y += 0.005;
torus.rotation.z += 0.01;
controls.update()
renderer.render(scene, camera);
}
animate()
Result:
npx create-react-app 3d_web
cd 3d_web
npm start
Install Vite (if you haven't already): Open your terminal and run:
npm create vite@latest 3d_web --template react
Navigate to Your Project Directory:
cd 3d_web
Install Dependencies:
npm install
Start the Development Server:
npm run dev
npm install three @react-three/fiber
npm install @react-three/drei
import { OrbitControls, useTexture } from "@react-three/drei"
import { Canvas } from "@react-three/fiber"
const App = () => {
return (
<div style={{background: "grey", height: "100vh", width: "100vw"}}>
<Canvas />
</div>
)
}
Here, we are using "Canvas" from "@react-three/fiber" that automatically setup Scenes, Cameras, and Renderers for us
import { OrbitControls, } from "@react-three/drei"
import { Canvas } from "@react-three/fiber"
const App = () => {
return (
<div style={{background: "grey", height: "100vh", width: "100vw"}}>
<Canvas>
{/* ambientLight for illuminates all objects equally. */}
<ambientLight intensity={0.5}/>
{/* orbitcontrols to move around with mouse */}
<OrbitControls />
<mesh position={[0, 0, 0]} rotation={[Math.PI * -0.5, 0, 0]}>
<planeGeometry args={[500, 500]} />
<meshStandardMaterial color={"#18d6b6"} />
</mesh>
{/* orbitcontrols to move around with mouse */}
<OrbitControls />
</Canvas>
</div>
)
}
import {useTexture} from "@react-three/drei"
const BreakGround = () => {
const textureMap = useTexture("./textures/ground/red_brick_diff.png");
return <mesh position={[0, 0, 0]} rotation-x={Math.PI * -0.5}>
<planeGeometry args={[500, 500]} />
<meshStandardMaterial map={textureMap} />
</mesh>
}
Creating InteractiveBall component that can be interact with click event:
import { Color, Vector3 } from "three";
const InteractiveBall = ({onClick, color, position}: {onClick: () => void, color: Color, position: Vector3}) => {
return <mesh
onClick={() => {
onClick()
}}
position={position}
>
<sphereGeometry args={[10 ,10, 10]} />
<meshStandardMaterial color={color} />
</mesh>
}
Creating helper function that can helper function to interact with ball:
// generate random reward 1000 to 100000
const getRandomNumber = () => {
return Math.abs(Math.floor(Math.random() * (1000 - 100000 + 1)) + 1000);
}
// generate random color for ball
const getRandomColor = () => {
const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
return new THREE.Color(randomColor);;
}
Creating simple state screen:
<div style={{
position: "absolute",
top: "0px",
left: "0px",
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<div style={{
width: "300px",
height: "300px",
backgroundColor: "rgb(249 215 220 / 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
rowGap: "10px"
}} >
<h1 style={{
fontSize: "30px",
color: "#effa16",
fontWeight: "900"
}}>Congrats!</h1>
<p style={{
fontSize: "50px",
color: "#84fa16",
fontWeight: "900",
textAlign: "center"
}}>You won <span style={{
color: "#f76fee"
}}>${getRandomNumber()}</span> </p>
<button style={{
padding: "8px 20px",
backgroundColor: "#fa78e4",
color: "#fff",
fontWeight: "700",
borderRadius: "0.75rem"
}}
onClick={() => {
setGameState(false)
setClickCounter(0);
}}
>Play again</button>
</div>
</div>
Putting it all togather:
import { OrbitControls, useTexture } from "@react-three/drei"
import { Canvas } from "@react-three/fiber"
import { useEffect, useState } from "react";
import { Color, Vector3 } from "three";
import * as THREE from "three";
const BreakGround = () => {
const textureMap = useTexture("./textures/ground/red_brick_diff.png");
return <mesh position={[0, 0, 0]} rotation-x={Math.PI * -0.5}>
<planeGeometry args={[500, 500]} />
<meshStandardMaterial map={textureMap} />
</mesh>
}
const InteractiveBall = ({onClick, color, position}: {onClick: () => void, color: Color, position: Vector3}) => {
return <mesh
onClick={() => {
onClick()
}}
position={position}
>
<sphereGeometry args={[10 ,10, 10]} />
<meshStandardMaterial color={color} />
</mesh>
}
const getRandomNumber = () => {
return Math.abs(Math.floor(Math.random() * (1000 - 100000 + 1)) + 1000);
}
const getRandomColor = () => {
const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
return new THREE.Color(randomColor);;
}
const App = () => {
const [ball_1_Color, setBall_1_Color] = useState<Color>(getRandomColor);
const [ball_2_color, setBall_2_color] = useState<Color>(getRandomColor);
const [gameState, setGameState] = useState<boolean>(false);
const [clickCounter, setClickCounter] = useState<number>(0);
useEffect(() => {
if (ball_1_Color.equals(ball_2_color)) {
setGameState(true);
}
if (clickCounter === 10) {
setBall_1_Color(ball_1_Color);
setBall_2_color(ball_2_color);
setGameState(true);
}
}, [ball_1_Color, ball_2_color, clickCounter])
return (
<div style={{
position: "relative"
}}>
<div style={{background: "grey", height: "100vh", width: "100vw", position: "fixed", left: "0px", top: "0px"}}>
<Canvas camera={{fov: 75, position: [10, 35, 50]}}>
<ambientLight intensity={0.5}/>
<spotLight castShadow position={[0, 2, 0]} />
<directionalLight position={[0, 10, 10]} />
{/* orbitcontrols to move around with mouse */}
<OrbitControls />
<BreakGround />
<InteractiveBall onClick={() => {
setClickCounter((state) => state += 1)
setBall_1_Color(getRandomColor())
}} color={ball_1_Color} position={new Vector3(0, 10, 0)} />
<InteractiveBall onClick={() => {
setBall_2_color(getRandomColor())
setClickCounter((state) => state += 1)
}}
color={ball_2_color}
position={new Vector3(20, 10, 0)}
/>
</Canvas>
</div>
{/* win screen */}
{
gameState &&
<div style={{
position: "absolute",
top: "0px",
left: "0px",
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<div style={{
width: "300px",
height: "300px",
backgroundColor: "rgb(249 215 220 / 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
rowGap: "10px"
}} >
<h1 style={{
fontSize: "30px",
color: "#effa16",
fontWeight: "900"
}}>Congrats!</h1>
<p style={{
fontSize: "50px",
color: "#84fa16",
fontWeight: "900",
textAlign: "center"
}}>You won <span style={{
color: "#f76fee"
}}>${getRandomNumber()}</span> </p>
<button style={{
padding: "8px 20px",
backgroundColor: "#fa78e4",
color: "#fff",
fontWeight: "700",
borderRadius: "0.75rem"
}}
onClick={() => {
setGameState(false)
setClickCounter(0);
}}
>Play again</button>
</div>
</div>
}
</div>
)
}
Don’t worry, we don’t spam!