Three.js Tutorial for Beginners: Create a 3D Solar System Step by Step
Andrew Saeed

Andrew Saeed @andrew-saeed

About: I will help you to create a dynamic and interactive website using Django, Alpine.js, and Tailwind. Develop Vue.js admin panels & dashboards with TanStack Query, and custom Static Websites using Astro.

Location:
Egypt
Joined:
Jul 20, 2024

Three.js Tutorial for Beginners: Create a 3D Solar System Step by Step

Publish Date: Jul 16
0 0

Content


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

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

📌 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()
Enter fullscreen mode Exit fullscreen mode

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

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

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 and alpha: 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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 entire scene from the perspective of your camera.

  • controls.update(): This is essential if you have controls.enableDamping set to true, 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 your rendering 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'
    }
]
Enter fullscreen mode Exit fullscreen mode

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 own name, radius, distance, speed, and color.
  • 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
});
Enter fullscreen mode Exit fullscreen mode

Let's break down the code:

✅ Initialize TextureLoader & SphereGeometry:

const textureLoader = new TextureLoader()
const sphereGeometry = new SphereGeometry(1, 32, 32)
Enter fullscreen mode Exit fullscreen mode
  • 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) => { /* ... */ })
Enter fullscreen mode Exit fullscreen mode

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) })
}
Enter fullscreen mode Exit fullscreen mode
  • 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 the bg (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 the bg 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
Enter fullscreen mode Exit fullscreen mode
  • A Mesh in Three.js is the actual 3D object visible in your scene. It combines a Geometry (the shape, like our sphereGeometry) with a Material (how it looks, like our baseMaterial).
  • baseMesh.scale.setScalar(ele.radius): We scale the default unit sphere (radius: 1) to the radius specified in our solarSystemData, giving each planet its correct size.
  • baseMesh.position.x = ele.distance: Initially, we place each planet along the X-axis at its specified distance 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 our solarSystemMesh 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 ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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 a MeshStandardMaterial (using its color 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
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • ele.mesh.rotation.y += ele.speed: This line is the heart of the orbital movement. We're simply incrementing the y rotation of the planet's mesh by its speed value. While rotation.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() and Math.cos(): These trigonometric functions are ideal for generating circular orbital paths. As the rotation.y value (our "angle") continuously increases, Math.sin() will output values that go up and down, and Math.cos() will output values that go left and right, tracing a circle.
  • ele.distance: By multiplying the sin and cos results by the planet's distance 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
    })
}
Enter fullscreen mode Exit fullscreen mode

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

✅ 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()
Enter fullscreen mode Exit fullscreen mode

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.

Full Code
Demo
☕ Buy Me Coffee

Comments 0 total

    Add comment