Content
- Introduction
- Setup
- Add Some Data
- Solar 3D Elements
- Animate Elements
- Stage and Render
- Final code and demo
Introduction
Getting started with Three.js can feel overwhelming when you’re new to 3D graphics, but it doesn’t have to be. In this tutorial, you’ll take your first practical steps into Three.js by building a small project: a 3D Solar System.
Together, we will explore the essentials of Three.js: scenes, cameras, lighting, geometry, materials, and animation.
So, what is Three.js?
Three.js is a powerful JavaScript library that simplifies creating 3D graphics right in your web browser. Think of it as a helpful layer that makes working with WebGL – the browser's native 3D graphics API – much easier by handling a lot of the complex details for you.
Setup
Every Three.js project needs a foundational setup involving HTML, CSS, and JavaScript. Let's get these ready.
📌 HTML
First, you'll need a basic HTML structure. We'll include a <div>
for a starry background and, crucially, a <canvas>
element where our 3D scene will be rendered.
<!doctype html>
<html lang="en">
<head>
...
</head>
<body>
<div class="bg bg-stars"></div>
<canvas id="main"></canvas>
<script type="module" src="/src/main.js"></script>
</body>
</html>
📌 CSS
Next, let's add some CSS to ensure our canvas fills the screen and to set up a nice starry background.
body {
margin: 0;
padding: 0;
overflow: hidden; /* Prevents scrollbars */
canvas {
display: block; /* Removes extra space below the canvas */
}
.bg {
position: absolute;
z-index: -1; /* Puts the background behind the canvas */
top: 0;
left: 0;
width: 100vw;
height: 100vh;
&.bg-stars {
background: url('bg.jpg'); /* Your starry background image */
background-size: cover;
}
}
}
📌 JavaScript
Now for the heart of our setup: the JavaScript. This setup initializes your 3D environment, adds controls, and starts your render loop.
✅ Create a Scene
import { Scene } from 'three'
const scene = new Scene()
Think of the scene as your 3D stage. It's where all your planets, moons, lights, and cameras will eventually reside. Nothing appears in Three.js without being added to the scene!
✅ Set Up the Camera
import { PerspectiveCamera } from 'three'
const camera = new PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 400)
camera.position.z = 100
scene.add(camera)
No scene is viewable without a camera. We're using a PerspectiveCamera here because it mimics how the human eye sees the world, adding depth and realism.
35
: This is our field of view (FOV) in degrees. A lower number "zooms in" while a higher one "zooms out".window.innerWidth / window.innerHeight
: This calculates the aspect ratio of our browser window, crucial for preventing distortion.0.1 and 400
: These are the near and far clipping planes. Objects closer than 0.1 units or farther than 400 units from the camera won't be rendered.
We then pull the camera back along the Z-axis camera.position.z = 100
so it has a good vantage point to view our upcoming solar system, and finally, we add it to our scene.
✅ Prepare the Renderer
import { WebGLRenderer } from 'three'
const canvas = document.querySelector('#main')
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
renderer.setClearColor(0x000000, 0)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
The WebGLRenderer
is what actually draws your 3D scene onto the 2D canvas in your browser.
- We first grab our
<canvas id="main">
element. - When creating the renderer, we enable
antialias: true
for smoother edges on our 3D objects andalpha: true
to allow for a transparent background. -
renderer.setClearColor(0x000000, 0)
sets the background to a transparent black. -
renderer.setSize()
ensures the renderer fills the entire browser window. -
renderer.setPixelRatio()
makes sure your scene looks crisp on high-DPI (Retina) displays.
✅ Add OrbitControls
import { OrbitControls } from 'three/examples/jsm/Addons.js'
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
OrbitControls
are a fantastic utility for development. They allow you to click and drag with your mouse to rotate, zoom, and pan around your 3D scene effortlessly.
Setting controls.enableDamping = true
adds a nice, smooth deceleration effect when you stop moving the camera.
✅ Add Lighting
import { AmbientLight, PointLight} from 'three'
// Soft ambient light to prevent completely black shadows
const ambientLight = new AmbientLight(0xffffff, 0.1)
scene.add(ambientLight)
// Point light at the Sun's position to simulate sunlight
const pointLight = new PointLight(0xffffff, 500)
scene.add(pointLight)
Since we're using MeshStandardMaterial
for our planets, we need light sources for them to be visible:
-
AmbientLight
: Provides soft, uniform lighting from all directions -
PointLight
: Simulates the Sun, casting directional light and shadows
✅ Handle Window Resizing
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
This event listener makes your 3D scene fully responsive. Whenever the browser window is resized, we:
- Update the
camera.aspect
ratio to prevent stretching or squashing of your scene. - Call
camera.updateProjectionMatrix()
to apply the aspect ratio change. - Adjust the
renderer.setSize()
to match the new window dimensions.
✅ Add an Axes Helper
import { AxesHelper } from 'three'
const axesHelper = new AxesHelper(2)
scene.add(axesHelper)
The AxesHelper
is an incredibly useful debugging tool. It displays a small set of colored lines at the origin (0,0,0) of your scene: red for the X-axis, green for the Y-axis, and blue for the Z-axis.
This helps you quickly understand the orientation of your 3D space.
You'll likely remove this in a final project, but it's invaluable during development!
✅ Create the Render Loop
const rendering = () => {
renderer.render(scene, camera)
controls.update()
window.requestAnimationFrame(rendering)
};
rendering()
This is the heart of any animated Three.js scene – the render loop.
renderer.render(scene, camera)
: This is the core command that draws (or "renders") your entirescene
from the perspective of yourcamera
.controls.update()
: This is essential if you havecontrols.enableDamping
set totrue
, as it smoothly updates the camera's position after user interaction.window.requestAnimationFrame(rendering)
: This method tells the browser you want to perform an animation and requests that the browser calls yourrendering
function before the next repaint. This creates a continuous, efficient animation loop that's optimized for the browser.
Add Some Data
Let's add the essential data for the Sun, planets, and their moons, which will allow us to visualize them in our 3D scene.
We'll use a JavaScript array named solarSystemData
to store the properties for each celestial body.
const solarSystemData = [
{
name: 'Sun',
radius: 5,
distance: 0,
speed: 0,
color: 'yellow',
moons: [],
animate: null,
bg: 'alectrona_map_2k_by_greaterhtrae_dik6ipy-pre.jpg'
},
// ... (other planets will go here)
{
name: 'Uranus',
radius: 1.7,
distance: 65,
speed: -0.003,
color: 0x66ccff,
moons: [
{ name: 'Miranda', radius: 0.15, distance: 1.5, speed: -0.03, color: 0xcccccc },
{ name: 'Ariel', radius: 0.15, distance: 2, speed: 0.028, color: 0xaaaaaa },
{ name: 'Umbriel', radius: 0.15, distance: 2.5, speed: -0.026, color: 0x999999 },
{ name: 'Titania', radius: 0.15, distance: 3, speed: 0.024, color: 0x888888 },
{ name: 'Oberon', radius: 0.15, distance: 3.5, speed: -0.022, color: 0x777777 }
],
animate: true,
bg: 'uranus_map_2k_by_greaterhtrae_dhlumbg-pre.jpg'
}
]
Each object within this array will contain details like:
-
name
: The name of the planet (e.g., 'Earth', 'Mars'). -
radius
: How large the planet will appear in our 3D space. -
distance
: Its distance from the Sun (or from its parent planet, in the case of moons). -
speed
: How fast it orbits around its parent body. -
color
: A fallback color, useful for placeholder or simpler materials. -
moons
: An array of objects, each describing a moon with its ownname
,radius
,distance
,speed
, andcolor
. -
animate
: A boolean flag to determine if the object should be animated. -
bg
: The filename for the texture image that will be mapped onto the planet's surface.
Solar 3D Elements
With our solarSystemData
ready, it's time to bring our celestial bodies into the 3D world!
In this step, we'll create the actual 3D models for the Sun, planets, and their moons, apply their unique textures, position them correctly in space, and set up the parent-child relationships for moons orbiting their planets.
We'll achieve this by iterating over our solarSystemData
array and dynamically generating Mesh
objects for each element.
// Ensure these imports are at the top of your main.js file
import { TextureLoader, SphereGeometry, MeshBasicMaterial, MeshStandardMaterial, Mesh } from 'three';
const textureLoader = new TextureLoader();
const sphereGeometry = new SphereGeometry(1, 32, 32); // A reusable sphere geometry
const solarSystemElements = solarSystemData.map((ele) => {
// This object will hold our Three.js mesh and its related animation properties
const solarSystemMesh = {
mesh: null, // The main Three.js Mesh for the planet/sun
moons: [], // Array to store moon meshes and their animation data
distance: ele.distance,
speed: ele.speed,
animate: ele.animate,
};
let baseMaterial; // Declare material outside the conditional for scope
// The Sun is self-illuminated, so it doesn't need to react to external lights.
// Planets, however, need to reflect light.
if (ele.name === 'Sun') {
baseMaterial = new MeshBasicMaterial({ map: textureLoader.load(ele.bg) });
} else {
baseMaterial = new MeshStandardMaterial({ map: textureLoader.load(ele.bg) });
}
// Create the main mesh (planet or sun)
const baseMesh = new Mesh(sphereGeometry, baseMaterial);
baseMesh.scale.setScalar(ele.radius); // Set the size based on the data's radius
baseMesh.position.x = ele.distance; // Position it along the X-axis
solarSystemMesh.mesh = baseMesh; // Store the Three.js mesh
// Check if this celestial body has moons
if (ele.moons?.length) { // The '?' is optional chaining, ensures it only runs if 'moons' exists
for (let moon of ele.moons) {
// Create material for the moon (we'll just use color for moons for simplicity)
const followerMaterial = new MeshStandardMaterial({ color: moon.color });
const followerMesh = new Mesh(sphereGeometry, followerMaterial);
followerMesh.scale.setScalar(moon.radius); // Set moon's size
followerMesh.position.x = moon.distance; // Position moon relative to its planet
// IMPORTANT: Add the moon mesh as a child of the planet mesh.
// This makes the moon automatically move and rotate with its parent planet!
solarSystemMesh.mesh.add(followerMesh);
// Store moon's mesh and animation data for later use
solarSystemMesh.moons.push({
mesh: followerMesh,
speed: moon.speed,
distance: moon.distance
});
}
}
return solarSystemMesh; // Return the structured object for each planet/sun
});
Let's break down the code:
✅ Initialize TextureLoader
& SphereGeometry
:
const textureLoader = new TextureLoader()
const sphereGeometry = new SphereGeometry(1, 32, 32)
- The
TextureLoader
is a built-in Three.js utility that lets us load images (like our planet surface maps) to use as textures on our 3D objects. -
SphereGeometry(1, 32, 32)
creates the basic sphere shape.-
1
: This is the default radius of the sphere. We'll scale it later. -
32, 32
: These values define the horizontal and vertical segment counts. More segments mean a smoother sphere, but also more polygons and potentially lower performance. 32x32 is a good balance for planets.
-
✅ Map Over Data to Create Planets and Moons:
solarSystemData.map((ele) => { /* ... */ })
We use the map
array method to iterate through each entry in our solarSystemData
. For every ele
(element, representing a planet or the Sun), we'll construct its corresponding Three.js 3D object and store its relevant animation properties.
✅ Choose the Right Material:
if (ele.name === 'Sun') {
baseMaterial = new MeshBasicMaterial({ map: textureLoader.load(ele.bg) })
} else {
baseMaterial = new MeshStandardMaterial({ map: textureLoader.load(ele.bg) })
}
-
MeshBasicMaterial
: Used for the Sun. This material is not affected by lights; it's always illuminated at its full color. Perfect for a self-luminous star! We load thebg
(background/texture) image directly onto it. -
MeshStandardMaterial
: Used for all planets and moons. This is a PBR (Physically Based Rendering) material, meaning it interacts realistically with light sources in your scene. This will allow our planets to have shadows and highlights, making them look much more realistic. Again, we apply thebg
image as its texture map.
✅ Create the Mesh and Position It:
const baseMesh = new Mesh(sphereGeometry, baseMaterial)
baseMesh.scale.setScalar(ele.radius)
baseMesh.position.x = ele.distance
solarSystemMesh.mesh = baseMesh
- A
Mesh
in Three.js is the actual 3D object visible in your scene. It combines aGeometry
(the shape, like oursphereGeometry
) with aMaterial
(how it looks, like ourbaseMaterial
). -
baseMesh.scale.setScalar(ele.radius)
: We scale the default unit sphere (radius: 1
) to theradius
specified in oursolarSystemData
, giving each planet its correct size. -
baseMesh.position.x = ele.distance
: Initially, we place each planet along the X-axis at its specifieddistance
from the origin (which will be the Sun's position). This starting position is crucial for our later animation. -
solarSystemMesh.mesh = baseMesh
: We store this newly created Three.js mesh in oursolarSystemMesh
object, along with other data we'll need for animation.
✅ Handle Moons (Nesting Objects):
if (ele.moons?.length) {
for (let moon of ele.moons) {
// ... create moon mesh ...
solarSystemMesh.mesh.add(followerMesh)
// ... store moon data ...
}
}
This is a powerful Three.js concept: object hierarchy.
- If a planet has moons (checked by
ele.moons?.length
), we loop through each one. - For each moon, we create a small
SphereGeometry
and aMeshStandardMaterial
(using itscolor
property). - We scale and position the moon relative to its parent planet (not the global origin).
-
solarSystemMesh.mesh.add(followerMesh)
: This is the critical part! By adding the moon's mesh as a child of the planet's mesh, the moon will automatically inherit the planet's position and rotation. This means when the planet moves, its moons move with it, simplifying our animation logic significantly. - We then store the moon's mesh and its unique animation properties in the
solarSystemMesh.moons
array.
Animate Elements
Now for the exciting part – bringing our static planets and moons to life!
We'll animate them to orbit around the Sun and for moons to gracefully circle their parent planets. This is where the speed
and distance
properties from our solarSystemData
truly come into play.
We'll achieve this with a dedicated animateSolarEle
helper function:
const animateSolarEle = (ele) => {
// Animate the main body (planet or sun)
ele.mesh.rotation.y += ele.speed // Increment the Y-axis rotation for orbital speed
// Calculate new X and Z positions for circular orbit
ele.mesh.position.x = Math.sin(ele.mesh.rotation.y) * ele.distance
ele.mesh.position.z = Math.cos(ele.mesh.rotation.y) * ele.distance
// Animate any moons this body might have
if (ele.moons?.length) { // Check if there are moons
ele.moons.forEach((moon) => {
moon.mesh.rotation.y += moon.speed // Increment moon's Y-axis rotation for its orbital speed
// Calculate new X and Z positions for moon's orbit around its parent
moon.mesh.position.x = Math.sin(moon.mesh.rotation.y) * moon.distance
moon.mesh.position.z = Math.cos(moon.mesh.rotation.y) * moon.distance
})
}
}
Let's break down how this function makes our solar system move:
✅ Rotate and Orbit the Planet
ele.mesh.rotation.y += ele.speed
ele.mesh.position.x = Math.sin(ele.mesh.rotation.y) * ele.distance
ele.mesh.position.z = Math.cos(ele.mesh.rotation.y) * ele.distance
-
ele.mesh.rotation.y += ele.speed
: This line is the heart of the orbital movement. We're simply incrementing they
rotation of the planet's mesh by itsspeed
value. Whilerotation.y
directly controls the mesh's orientation (spinning), we're cleverly repurposing this value as an angle for our orbital calculations. Think of it as tracking the angle around its parent. -
Math.sin()
andMath.cos()
: These trigonometric functions are ideal for generating circular orbital paths. As therotation.y
value (our "angle") continuously increases,Math.sin()
will output values that go up and down, andMath.cos()
will output values that go left and right, tracing a circle. -
ele.distance
: By multiplying thesin
andcos
results by the planet'sdistance
from the Sun, we scale the circle to the correct orbital radius.
✅ Animate the Moons
if (ele.moons?.length) {
ele.moons.forEach((moon) => {
moon.mesh.rotation.y += moon.speed
moon.mesh.position.x = Math.sin(moon.mesh.rotation.y) * moon.distance
moon.mesh.position.z = Math.cos(moon.mesh.rotation.y) * moon.distance
})
}
This part applies the exact same orbital logic to each moon.
The crucial difference is that because we added the moon meshes as children of their parent planet's mesh in the previous step, their movements are now relative!
Stage and Render
We're almost there! With all our solar system elements created and our animation logic defined, the final step is to put everything into the Three.js scene and set up the continuous animation.
✅ Add Elements to the Scene
Before anything can appear on screen, each of our carefully crafted planet and moon meshes needs to be added to the Three.js scene
.
solarSystemElements.forEach(ele => scene.add(ele.mesh))
✅ Create the Render Loop
Finally, let's assemble our render loop. This function will be called repeatedly, allowing us to update the positions of our planets and moons, and then redraw the scene.
const rendering = () => {
// Apply animations to eligible solar system elements
solarSystemElements.forEach(ele => {
if (ele.animate) { // Check if this element is set to animate
animateSolarEle(ele) // Call our animation function
}
})
// Render the scene from the camera's perspective
renderer.render(scene, camera)
// Update orbit controls (important for smooth damping)
controls.update()
// Request the next animation frame to keep the loop going
window.requestAnimationFrame(rendering)
};
// Start the animation loop!
rendering()
Final Code and Demo
We've just built a complete 3D solar system using Three.js. We've covered all the fundamental concepts necessary to start creating awesome interactive 3D web experiences.