Solar System

I want to build a solar system with all planets, the Sun, comets and stars!

First of all let's collect the data about each solar system body:

Name Distance Radius Mass Cycle Rings Moons Color
Sun 0 169 634 333 000 Me Nope Nope 0
#FFFF00
Yellow
Mercury 46–88 m km 2 440 0.055 Me 88 d Nope 0
#5A5A5A
Gray
Venus 107–109 m km 6 052 0.815 Me 225 d Nope 0
#e6e6e6
Light Gray
Earth 147–152 m km 6 371 1 Me 365.24 d Nope 1
#2f6a69
Blue-Green
Mars 206–249 m km 3 390 0.107 Me 687 d Nope 2
#b07f35
Red
Jupiter 740–816 m km 69 911 318 Me 12 y True A lot
#e1a27a
Red
Saturn 1 350 m km 58 232 95 Me 29.5 y True A lot
#b08f36
Brown
Uranus 3 006 m km 25 362 15 Me 84 y True A lot
#5580aa
Blue
Neptune 4 537 m km 24 622 17 Me 165 y False Some
#eee7a7
Light Yellow
Pluto 5 900 m km 1 188 0.18 Me 248 y False 0
#366896
Blue

The Sun

Let's make a gradient from the center and add some dizzy crown with sparkles.

How it is made:

  1. Fill circle and set color based on the distance from the center.
  2. Crown is a circle with Sun radius + 5px. Points are placed in a loop from 0 to 2*PI. Each point is displaced by a bunch of sin noise made from angle and time.
  3. Fill the internal part of the crown with a floodfill algorithm.
  4. Draw rays — their positions are also based on angle, time and a bunch of sin functions.

Draw all planets

All data from the above table would be used. As an example below code would show one planet (Earth) logic.

var planets = {}; planets.Earth = { distance: [147, 152], radius: 6371, color: 0x2f6a69, moons: [ { distance: [15, 15], radius: 631, cycle: 28, color: 0xA0A0A0 } ], cycle: 365.24 }; var sunPosition = center.addClone(0,0); var distanceScale = function(distance){ return pow(distance,1.08) }; var planetScale = function(radius){ return pow(radius, 0.35)/4; }; var sunSize = 40; var drawPlanet = function(planet, center, t){ var scaledRadius = planetScale(planet.radius); var time = (t+ 666666) / planet.cycle*100; var position = center.addClone( cos(time)* distanceScale(planet.distance[1]), sin(time)* distanceScale(planet.distance[0])/1.5 ); if(planet.moons) planet.moons.forEach(function( moon ) { drawPlanet(moon, position, t); }); var lastOrbitPoint, orbitColor = darker(planet.color, 1.5); for(var angle = 0; angle < PI * 2+0.15; angle += 0.15){ orbitPoint = center.addClone( cos(angle)* distanceScale(planet.distance[1]), sin(angle)* distanceScale(planet.distance[0])/1.5 ); if(lastOrbitPoint){ line(lastOrbitPoint, orbitPoint, orbitColor); } lastOrbitPoint = orbitPoint; } fillCircle(position.x, position.y, scaledRadius, planet.color); }; update = function(dt, t){ rect(0,0,w,h, background, 0); for(var planetName in planets){ drawPlanet(planets[planetName], sunPosition, t); } drawSun(sunPosition, sunSize, t); };
var distanceScale = function(distance){ return pow(distance, 0.63) }; var planetScale = function(radius){ return pow(radius, 0.35)/7; }; var sunSize = 10; planets.Mercury = { distance: [46, 88], radius: 2440, color: 0x5A5A5A, cycle: 88 }; planets.Venus = { distance: [107, 109], radius: 6052, color: 0xe6e6e6, cycle: 225 }; planets.Mars = { distance: [206, 249], radius: 3390, color: 0xb07f35, cycle: 687 }; planets.Jupiter = { distance: [740, 816], radius: 69911, color: 0xe1a27a, cycle: 12 *365 }; planets.Saturn = { distance: [1350, 1350], radius: 58232, color: 0xb08f36, cycle: 29.5 *365 }; planets.Uranus = { distance: [3006, 3006], radius: 25362, color: 0x5580aa, cycle: 84 *365 }; planets.Neptune = { distance: [4537, 4537], radius: 24622, color: 0xeee7a7, cycle: 165 *365 }; planets.Pluto = { distance: [5900, 5900], radius: 1188, color: 0x366896, cycle: 248 *365 };

Three-fiber

Now we have the core mechanics and it's time to go 3D. I would use a crazy stack:

  1. React
  2. React-three-fiber
  3. Three
  4. TypeScript

But the logics would remain the same.

First of all I would post some great shots!

Three fiber is a library that gives an ability to describe Three.js 3D scenes as react virtual dom components tree.

For example, placing a sphere can be as simple:

import { Canvas } from "@react-three/fiber";

export default function App() {
  return (
    <Canvas>
      <mesh>
        <sphereBufferGeometry args={[3, 24, 24]} />
        <meshStandardMaterial color={"#FF0000"} />
      </mesh>
    </Canvas>
  );
}

All objects are intractable and event handlers can be caught on any of those events:

<mesh
  onClick={(e) => console.log('click')}
  onContextMenu={(e) => console.log('context menu')}
  onDoubleClick={(e) => console.log('double click')}
  onWheel={(e) => console.log('wheel spins')}
  onPointerUp={(e) => console.log('up')}
  onPointerDown={(e) => console.log('down')}
  onPointerOver={(e) => console.log('over')}
  onPointerOut={(e) => console.log('out')}
  onPointerEnter={(e) => console.log('enter')} // see note 1
  onPointerLeave={(e) => console.log('leave')} // see note 1
  onPointerMove={(e) => console.log('move')}
  onPointerMissed={() => console.log('missed')}
  onUpdate={(self) => console.log('props have been updated')}
/>

I made a base planet object (like in 2D code). It took planet properties and draw a moving sphere with some radius, it's orbit, and it's moons. Moons is a hacky entity — they can be moons, or any other object (for example Saturn rings are created in this way).

Base planet code:

import {JSX, useRef, useState} from "react";
import * as three from "three";
import {useFrame} from "@react-three/fiber";
import {ShaderMaterial} from "three";

export const Planet = function(cfg){
  const sphere = useRef(null);
  
  useFrame((state, delta) => {
    let time = state.clock.getElapsedTime();
    var d = +new Date()/40000;
    var yearLength = cfg.cycle / 365;
    sphere.current.position.x = cfg.distanceMin/30*Math.cos(d/yearLength);
    sphere.current.position.z = cfg.distanceMax/30*Math.sin(d/yearLength);
    if(cfg.material) {
      sphere.current.material.uniforms.iTime.value = time + 20;
    }
  });
  
  const [active, setActive] = useState(false);

  return (
    <mesh scale={scale} ref={sphere} position={[cfg.distanceMin/30,0,0]} 
          onClick={(e)=>{
            if(cfg.interactive !== false) {
              cfg.setCameraTarget && cfg.setCameraTarget(sphere.current)
            }
          }}
          onPointerEnter={()=>cfg.interactive !== false  && setActive(true)}
          onPointerLeave={()=>cfg.interactive !== false && setActive(false)}
          castShadow={cfg.noShadow?false:true} receiveShadow={cfg.noShadow?false:true}>
      <sphereBufferGeometry args={[cfg.radius**0.65/1500, 32*2, 16*2]} />
      {cfg.material?cfg.material:<meshStandardMaterial color={cfg.color} emissive={cfg.emissive || '#000000'}/>}
      {cfg.moons?cfg.moons:null}
    </mesh>
  );
};

I didn't use any textures, so all planets are just colored spheres or glsl shaders.

Pros

Cons

Live Demo

Anyway, it was fun to make this project.

Move around with left mouse button. Zoom with scroll. On object hover — it is scaled up. And if you click on it — camera focus point would be bind to that object.

Repository

Playground