Build 3D Apps with React | Animated Solar System | Part 1
--
Hello everyone, In this article I’ll walk you through how to create a basic 3D application with React using React Three Fiber. React Three Fiber (AKA r3f) is a React renderer for Three.js created by Paul Henschel. Although you can use three.js directly with React, React Three Fiber makes it way easier to to create 3D applications without too much code.
In this blog we will use a lot of core concepts in three.js. Even if you are not familiar with those, you can still follow along. If you want to learn the basics of three.js, head over to threejsfundamentals.org and you will find some pretty good tutorials there.
Initial Setup
Let’s get things started by installing the dependencies.
npm i three @react-three/fiber @react-three/drei
React Three Fiber depends on three.js and @react-three/drei has some additional functionality we can use with React Three Fiber.
In the App.js file, we will import Canvas component from the React Three Fiber library. Just like a real canvas, which is used to draw paintings on, Canvas component here will be the parent component in which we will put all our 3D objects.
import React from 'react';
import { Canvas } from '@react-three/fiber';export default function App() {
return <Canvas></Canvas>;
}
Adding Objects
Now we can add some 3D objects to the scene. Adding 3D objects to the scene in react three fiber is pretty easy. Just like in three.js , to create a 3D object we need to create a mesh with specific geometry and material. All the geometries and materials available for three.js are available for react three fiber as well.
You can find list of available three.js geometries here and a list of available materials here.
Now how do you take a geometry or a material from three.js docs and convert it into a react three fiber component?
Easy! Just follow these steps.
- Convert three.js material/geometry name to camel case and that will be your component name.
- For the geometry pass all the arguments that you would normally pass to the constructor as an array using the args prop on the geometry component. For the material, pass each of the constructor arguments as a prop.
To create a mesh using this material and geometry, You just need to wrap these two components by a mesh component.
Let’s take the cube example from the three.js docs.
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
And here is the same example converted into react three fiber.
<mesh>
<sphereGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={0x00ff00} />
</mesh>
Now back to our solar system example. Let’s create our sun at the center of our scene.
export default function App() {
return (
<Canvas>
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="#E1DC59" />
</mesh>
</Canvas>
);
}
Let There Be Light(s)
Hmmm. The sphere is there. But why is it black instead of yellow ? This is because meshStandardMaterial is affected by lights. Since we don’t have any lights, it does not have any lights to reflect from. So let’s add some lights.
export default function App() {
return (
<Canvas>
<ambientLight />
<pointLight position={[0, 0, 0]} />
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="#E1DC59" />
</mesh>
</Canvas>
);
}
Ambient light globally illuminates all objects in the scene equally while a point light is a light that sits at a point and shoots light in all directions from that point. Sounds like what a sun would do huh!
We have lights now!. Next, we can add our first planet and place it slightly to the right from the center. To move a mesh we can use the position prop on the mesh component. We can pass x,y,z position values as an array to this prop. Similarly if we want to rotate or scale a mesh, we can change scale and rotation props on the mesh.
export default function App() {
return (
<Canvas>
<ambientLight />
<pointLight position={[0, 0, 0]} />
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="#E1DC59" />
</mesh>
<mesh position={[4, 0, 0]}>
<sphereGeometry args={[0.5, 32, 32]} />
<meshStandardMaterial color="#78D481" />
</mesh>
</Canvas>
);
}
Adding controls
Wouldn’t it be nice if we had the ability to move the scene around with the mouse to look around. Enter Orbit Controls. There are few different controls available in three.js. We will use orbit controls in our solar system example. Controls are not included in the core react three fiber package. So we have to import it from @react-three/drei.
...
import { OrbitControls } from '@react-three/drei';export default function App() {
return (
<Canvas>
...
<OrbitControls />
</Canvas>
);
}
Lets also adjust our camera a little bit so we can see our scene better when the app load. To adjust the camera you can use the camera prop on canvas.
export default function App() {
return (
<Canvas camera={{ position: [0, 20, 25], fov: 45 }}>
...
</Canvas>
);
}
Little bit of code refactor…
We can create separate sun, planet and light components so that our code will look more structured and that will also help us when we control each component separately later.
export default function App() {
return (
<Canvas camera={{ position: [0, 20, 25], fov: 45 }}>
<Sun />
<Planet />
<Lights />
<OrbitControls />
</Canvas>
);
}function Sun() {
return (
<mesh>
<sphereGeometry args={[2.5, 32, 32]} />
<meshStandardMaterial color="#E1DC59" />
</mesh>
);
}function Planet() {
return (
<mesh position={[8, 0, 0]}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="#78D481" />
</mesh>
);
}function Lights() {
return (
<>
<ambientLight />
<pointLight position={[0, 0, 0]} />
</>
);
}
The ecliptics
To create an ecliptic we can use the LineBasicMaterial from three.js. To create the geometry for that we need to calculate some points on an ellipse. There are multiple ways to get the points on an ellipse. The easiest is to get points on a circle and scale each axis differently.
const points = [];
for (let index = 0; index < 64; index++) {
const angle = (index / 64) * 2 * Math.PI;
const x = xRadius * Math.cos(angle);
const z = zRadius * Math.sin(angle);
points.push(new THREE.Vector3(x, 0, z));
}points.push(points[0]);
We push the initial point again as the last index of the array so that we get a closed curve. Since our solar system is perpendicular to the y-axis, we are calculating elliptical point values for x and z .To create the line we can use the line component from react three fiber.
function Ecliptic({ xRadius = 1, zRadius = 1 }) {
const points = [];
for (let index = 0; index < 64; index++) {
const angle = (index / 64) * 2 * Math.PI;
const x = xRadius * Math.cos(angle);
const z = zRadius * Math.sin(angle);
points.push(new THREE.Vector3(x, 0, z));
}points.push(points[0]);const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
return (
<line geometry={lineGeometry}>
<lineBasicMaterial attach="material" color="#BFBBDA" linewidth={10} />
</line>
);
}
Notice how we passed the geometry as a prop instead of a child component ?. You can always pass geometry or material as a prop to the mesh when you need more customizability.
We will add the ecliptic to the planet component so that when we generate multiple planets. Each planet will have it’s own ecliptic.
function Planet() {
return (
<>
<mesh position={[8, 0, 0]}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="#78D481" />
</mesh>
<Ecliptic xRadius={8} zRadius={4} />
</>
);
}
Generating random planets
Now we can use our planet component to create multiple planets, let’s create some random planets. We will store each planet’s color, size and elliptic radius values in an array so that we can iterate through it to generate the planets.
First we will need couple of utility functions for generating random values.
const random = (a, b) => a + Math.random() * b;
const randomInt = (a, b) => Math.floor(random(a, b));const randomColor = () =>
`rgb(${randomInt(80, 50)}, ${randomInt(80, 50)}, ${randomInt(80, 50)})`;
We can use these functions to create a random set of planet config. If you are wondering about the random color range I picked 80–120 so that we will get colors that are not too bright or dark. Let’s create the planet set.
const planetData = [];
const totalPlanets = 6;for (let index = 0; index < totalPlanets; index++) {
planetData.push({
id: i,
color: randomColor(),
xRadius: (i + 1.5) * 4,
zRadius: (i + 1.5) * 2,
size: random(0.5, 1),
});
}
How do you choose all those numbers that are used in random number generators ? Trial and error really ! You can try with different numbers until you get a look that you really like.
Let’s change the Planet component so it can get all these details from props.
function Planet({ planet: { color, xRadius, zRadius, size } }) {
return (
<>
<mesh position={[xRadius, 0, 0]}>
<sphereGeometry args={[size, 32, 32]} />
<meshStandardMaterial color={color} />
</mesh>
<Ecliptic xRadius={xRadius} zRadius={zRadius} />
</>
);
}
Now we can loop through the planet list to create multiple planets.
export default function App() {
return (
<Canvas camera={{ position: [0, 20, 25], fov: 45 }}>
<Sun />
{planetData.map((planet) => (
<Planet planet={planet} key={planet.id} />
))}
<Lights />
<OrbitControls />
</Canvas>
);
}
Now if your are following along you will get something like this.
There will be more
I’ll stop here since this article is getting too long and there are couple more things that I am planning to cover including the animations. If you liked this article so far, don’t forget to check out the next one that I will publish in a couple of days!
If you got stuck with the code. Here is a codesandbox example that you can use as a reference.