3D Solar System #
Now that we have seen how the transformations work in 2D, we can apply them to a 3D model. In addition to that, we will add a bit more of interactivity to the model, so that we can move around the solar system and see it from different perspectives.
A 3D model involves way more transformations than a 2D one, some of which may be really unintuitive, so we will have to be careful with the order in which we apply them.
How does it work? #
Similar to the 2D model, we need a class to manage the planets and their atributes, but now, we will be more specific. First, we will define a class called Star
for the sun and another class called CelestiaBody
for the planets and moons. The Star
class will be as follows:
class Star
class Star {
constructor(radius, color) {
this.radius = radius;
this.color = color;
this.planets = [];
}
show() {
push();
noStroke();
fill(this.color);
sphere(this.radius);
for (let planet of this.planets) {
rotateX(planet.orbitTilt)
planet.show();
planet.update();
}
pop();
}
}
So basically, we have a constructor that takes the radius and color of the sun, and an array of planets. The show()
method will draw the sun and then, for each planet, it will call their own show()
method (which we will see in a moment) and also rotate the plane in which the planet orbitates if it has an orbitTilt. Note that we do this transformation inside the push()
and pop()
functions, so that it only affects the planets and not the sun.
Now, the CelestialBody
class will contain more information that will be shared by the classes Planet
and Moon
. It will first have a contructor with the common atributes like the radius, color, distance to the sun, and the speed at which it rotates around the sun. It will also have a show()
method that will draw the planet and rotate it around the sun. Lastly, the update()
method will be used to update the angle of the planet in the orbit. There are wo other things we’re adding here: first, a reference to the absolute position of the planet in the canvas with the three atributes x
, y
and z
(this will be useful for the camera positioning), and secondly, a factor scaling to the radius and speed atributes to add more interactivity.
CelestialBody.update()
update() {
this.radius = this.orginalRadius*sizeFactor;
this.orbitSpeed = this.originalOrbitSpeed*speedFactor;
this.rotationSpeed = this.originalRotationSpeed*speedFactor;
this.angle += this.orbitSpeed;
this.x = this.distance * cos(this.angle);
this.y = this.distance * sin(this.angle) * sin(this.orbitTilt);
this.z = this.distance * -sin(this.angle) * cos(this.orbitTilt);
}
Note how we get the absolute position by usingcos
andsin
functions, and also how we multiply they
andz
coordinates by thesin
of theorbitTilt
angle.
The Planet
class will follow a similar logic to the Star
class for showing its moons. The main feature inside this class is the hasRings
atributte, which will be used to draw the rings around the planet. The show()
method will contain the following:
Planet.show()
show() {
super.show(); // Call the show() method of its parent class
for (let moon of this.moons) {
push();
rotateY(this.angle);
rotateX(moon.orbitTilt); // Moon's orbit tilt
translate(this.distance, 0, 0);
moon.show();
pop();
moon.update()
}
if (this.hasRings) {
push();
rotateY(this.angle);
rotateX(this.rotationTilt); // Ring's rotation tilt
translate(this.distance, 0, 0);
rotateX(PI / 2);
noFill();
stroke(115,100,90); // Ring's color
strokeWeight(this.ringThickness); // Border thickness
for (let i = 0; i < this.ringSegments; i++) {
let radius = random(this.radius*this.innerRadius, this.radius*this.outerRadius); // Radius of the ring
circle(0, 0, radius * 2, radius * 2);
}
pop();
}
}
The Moon
does not add any new info to it parent. We also declare one final class which is the AsteroidBelt
which is shown is the following way.
AsteroidBelt.show()
show() {
push();
noFill();
stroke(this.color);
strokeWeight(this.asteroidSize);
for (let i = 0; i < this.density; i++) {
let angle = map(i, 0, this.density, 0, TWO_PI);
let x = cos(angle) * random(this.innerRadius, this.outerRadius);
let y = cos(angle) * random(-5, 5);
let z = sin(angle) * random(this.innerRadius, this.outerRadius);
point(x, y, z);
}
pop();
}
After this, we just instantiate the planets, add them to the Sun’s planets array and the call the show()
method inside the draw()
function. Additionally, we include the updateCamera
function to allow the user to change the camera view around the solar system. We also include Pluto as a planet, because why not.
updateCamera()
function updateCamera() {
if (selectedPlanet) {
let distance = selectedPlanet.radius*5 + 50; // Distancia de la c谩mara desde el planeta (ajustable)
let x = selectedPlanet.x;
let y = selectedPlanet.y;
let z = selectedPlanet.z;
camera(x+distance, y-distance, z+distance, x, y, z, 0, 1, 0);
}
}
Here we use the camera()
function to set the camera position. The first three parameters are the camera position, the next three are the camera target, and the last three are the camera orientation. You can find more info about this function on the p5 camera doc.
And that’s it! You can see the final result in the following sketch.
Result #
Move the sliders to change the planets size
and speed
and check the asteroid belt
option to show it. You can also select a planet to follow it with the camera and see it with more detail. To move freely on the canvas select Free
. Try to find Pluto from the Sun view!
The distances and sizes of the planets are not fully to scale (they would be too small or too far), but the speeds are.
Of course, this is just a simple example of what you can do with the p5.js
library. You can add more planets, moons, rings, etc. You can also add more features to the planets, like clouds, or even add a spaceship to move around the solar system. The possibilities are endless!
Full Code #
3d_solar_system.js
let sizeFactor = 1;
let speedFactor = 1;
let asteroidBeltCheckbox = false;
let selectedPlanet = null;
class Star {
constructor(radius, color) {
this.radius = radius;
this.color = color;
this.planets = [];
}
show() {
push();
noStroke();
fill(this.color);
sphere(this.radius);
for (let planet of this.planets) {
rotateX(planet.orbitTilt)
planet.show();
planet.update();
}
pop();
}
}
class CelestialBody {
constructor(radius, distance, orbitSpeed, rotationSpeed, color, orbitTilt = 0, rotationTilt = 0) {
this.radius = radius;
this.distance = distance;
this.orbitSpeed = orbitSpeed;
this.rotationSpeed = rotationSpeed;
this.orbitTilt = orbitTilt;
this.rotationTilt = rotationTilt;
this.angle = random(360);
this.color = color;
this.orginalRadius = radius;
this.originalOrbitSpeed = orbitSpeed;
this.originalRotationSpeed = rotationSpeed;
this.x = 0;
this.y = 0;
this.z = 0;
}
update() {
this.radius = this.orginalRadius*sizeFactor;
this.orbitSpeed = this.originalOrbitSpeed*speedFactor;
this.rotationSpeed = this.originalRotationSpeed*speedFactor;
this.angle += this.orbitSpeed;
this.x = this.distance * cos(this.angle);
this.y = this.distance * sin(this.angle) * sin(this.orbitTilt);
this.z = this.distance * -sin(this.angle) * cos(this.orbitTilt);
}
show() {
push();
rotateY(this.angle);
translate(this.distance, 0,0);
rotateY(frameCount * this.rotationSpeed);
noStroke();
fill(this.color);
sphere(this.radius);
pop();
}
}
class Planet extends CelestialBody {
constructor(radius, distance, orbitSpeed, rotationSpeed, color, orbitTilt, rotationTilt, hasRings = false, innerRadius = 0, outerRadius = 0, ringThickness = 0, ringSegments = 0) {
super(radius, distance,orbitSpeed, rotationSpeed, color, orbitTilt, rotationTilt);
this.moons = [];
this.hasRings = hasRings;
this.innerRadius = innerRadius;
this.outerRadius = outerRadius;
this.ringThickness = ringThickness; // Grosor del anillo
this.ringSegments = ringSegments; // N煤mero de segmentos del anillo (c铆rculos vac铆os)
}
show() {
super.show(); // Llama al m茅todo show() de la clase base
for (let moon of this.moons) {
push();
rotateY(this.angle);
rotateX(moon.orbitTilt); // Inclinaci贸n de la 贸rbita de la luna
translate(this.distance, 0, 0);
moon.show();
pop();
moon.update()
}
if (this.hasRings) {
push();
rotateY(this.angle);
rotateX(this.rotationTilt); // Inclinaci贸n de la 贸rbita del anillo
translate(this.distance, 0, 0);
rotateX(PI / 2); // Rotar el anillo para que est茅 en el plano XY
noFill();
stroke(115,100,90); // Color del anillo
strokeWeight(this.ringThickness); // Grosor del borde
for (let i = 0; i < this.ringSegments; i++) {
let radius = random(this.radius*this.innerRadius, this.radius*this.outerRadius); // Radio del c铆rculo vac铆o
circle(0, 0, radius * 2, radius * 2);
}
pop();
}
}
}
class Moon extends CelestialBody {
constructor(radius, distance, orbitSpeed, rotationSpeed, color, orbitTilt) {
super(radius, distance, orbitSpeed, rotationSpeed, color, orbitTilt);
}
}
class AsteroidBelt {
constructor(innerRadius, outerRadius, density, color, asteroidSize) {
this.innerRadius = innerRadius;
this.outerRadius = outerRadius;
this.density = density;
this.color = color;
this.asteroidSize = asteroidSize
}
show() {
push();
noFill();
stroke(this.color);
strokeWeight(this.asteroidSize);
for (let i = 0; i < this.density; i++) {
let angle = map(i, 0, this.density, 0, TWO_PI);
let x = cos(angle) * random(this.innerRadius, this.outerRadius);
let y = cos(angle) * random(-5, 5);
let z = sin(angle) * random(this.innerRadius, this.outerRadius);
point(x, y, z);
}
pop();
}
}
let sun, mercury, venus, earth, moon, mars, asteroidBelt, jupiter, saturn, uranus, neptune, pluto;
function setup() {
createCanvas(700, 700, WEBGL);
camera(0, -300, 1000, 0, 0, 0, 0, 1, 0);
sun = new Star(70, color('#EAD740'));
mercury = new Planet(0.8, 250, 0.00479, 0.02, color('#999999'));
sun.planets.push(mercury);
venus = new Planet(2.4, 450, 0.0035, 0.03, color('#B1894F'));
sun.planets.push(venus);
earth = new Planet(2.4, 550, 0.00298, 0.04, color('#06429F'));
sun.planets.push(earth);
moon = new Moon(0.6, 16, 0.1, 0.1, color(200), 50, PI/8);
earth.moons.push(moon);
mars = new Planet(1.4, 1000, 0.00241, 0.05, color('#D95B2E'));
sun.planets.push(mars);
asteroidBelt = new AsteroidBelt(1250, 2000, 300, color(150), 0.6);
jupiter = new Planet(14, 2200, 0.00131, 0.06, color('#D39E7B'));
sun.planets.push(jupiter);
saturn = new Planet(12, 3000, 0.00097, 0.07, color('#CAAA67'), 0, PI/8, true, 1.5, 2.5, 0.2,150);
sun.planets.push(saturn);
uranus = new Planet(5, 4000, 0.00068, 0.08, color('#C4EAEE'), 0, PI/2, true, 1.3,1.8, 0.08, 60);
sun.planets.push(uranus);
neptune = new Planet(4.9, 4500, 0.00054, 0.09, color('#4575FF'), 0, PI/5, true, 1.3,1.5, 0.01, 30);
sun.planets.push(neptune);
pluto = new Planet(0.4, 5000, 0.00046, 0.1, color('#B5AA9F'), PI/8, 0);
sun.planets.push(pluto);
// Crea el slider para ajustar el tama帽o de los planetas
sizeSlider = createSlider(1, 4, 1, 0.1);
sizeSlider.position(20, 20);
sizeSlider.style('width', '150px');
sizeSlider.input(updateSize); // Llama a la funci贸n updateSize cuando se cambia el valor del slider
// Crea el elemento de p谩rrafo para el t铆tulo del tama帽o
sizeLabel = createP('Size');
sizeLabel.position(175, 10);
sizeLabel.style('font-size', '14px');
sizeLabel.style('color', 'white');
// Crea el slider para ajustar la velocidad de los planetas
speedSlider = createSlider(0.1, 15, 1, 0.1);
speedSlider.position(20, 50);
speedSlider.style('width', '150px');
speedSlider.input(updateSpeed); // Llama a la funci贸n updateSpeed cuando se cambia el valor del slider
// Crea el elemento de p谩rrafo para el t铆tulo de la velocidad
speedLabel = createP('Speed');
speedLabel.position(175, 40);
speedLabel.style('font-size', '14px');
speedLabel.style('color', 'white');
// Crea el checkbox para el cintur贸n de asteroides
asteroidBeltCheckbox = createCheckbox('Asteroid Belt', false);
asteroidBeltCheckbox.position(20, 80);
asteroidBeltCheckbox.style('color', 'white');
// Crea el select para seleccionar el planeta
planetSelector = createSelect();
planetSelector.position(620, 20);
planetSelector.option('Free')
planetSelector.option('Sun');
planetSelector.option('Mercury');
planetSelector.option('Venus');
planetSelector.option('Earth');
planetSelector.option('Mars');
planetSelector.option('Jupiter');
planetSelector.option('Saturn');
planetSelector.option('Uranus');
planetSelector.option('Neptune');
planetSelector.option('Pluto');
planetSelector.changed(updateSelectedPlanet); // Llama a la funci贸n updateCamera cuando cambia la selecci贸n
}
function updateSelectedPlanet() {
const planetMap = {
Free: null,
Sun: sun,
Mercury: mercury,
Venus: venus,
Earth: earth,
Mars: mars,
Jupiter: jupiter,
Saturn: saturn,
Uranus: uranus,
Neptune: neptune,
Pluto: pluto
};
const selected = planetSelector.value();
selectedPlanet = planetMap[selected];
}
function updateCamera() {
if (selectedPlanet) {
let distance = selectedPlanet.radius*5 + 50; // Distancia de la c谩mara desde el planeta (ajustable)
let x = selectedPlanet.x;
let y = selectedPlanet.y;
let z = selectedPlanet.z;
camera(x+distance, y-distance, z+distance, x, y, z, 0, 1, 0);
}
}
// Funci贸n que se llama cuando se cambia el valor del slider de tama帽o
function updateSize() {
sizeFactor = sizeSlider.value();
}
// Funci贸n que se llama cuando se cambia el valor del slider de velocidad
function updateSpeed() {
speedFactor = speedSlider.value();
}
function draw() {
background(0);
sun.show();
if (selectedPlanet === sun) {
camera(0, -300, 1000, 0, 0, 0, 0, 1, 0);
} else if (selectedPlanet) {
updateCamera();
}
else {
orbitControl();
}
if (asteroidBeltCheckbox.checked()){
asteroidBelt.show();
}
}