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:
- Fill circle and set color based on the distance from the center.
- Crown is a circle with Sun radius + 5px. Points are placed in a loop from
0
to2*PI
. Each point is displaced by a bunch ofsin
noise made from angle and time. - Fill the internal part of the crown with a floodfill algorithm.
- 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:
- React
- React-three-fiber
- Three
- 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
- Easy to add 3D scene to existed react project
- Any react project generates a huge ecosystem, so it is likely to find a module for any problem
- THREE.js is a great 3D library for people that do not want to dive deep into WebGL. Three-fiber abstracts 3D even more
Cons
- Need to know Three.js. Three-fiber is not a silver bullet — you should know internals of Three.js for anything more complex than just rendering a single 3D model.
- All examples try to make some work around to stop react from updating components state. For example,
useFrame
is not a classic react hook, it lives in a separate loop and best practice is to useref
to Three mesh and manipulate that structure manually in object mutation way. - Bundle size. Simple empty project with React+Three wights over 1 mb. Today it is not a big problem on PC, but mobile devices over 4G would still struggle.
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.