diff --git a/src/Body.java b/src/Body.java new file mode 100644 index 0000000..4d45bf2 --- /dev/null +++ b/src/Body.java @@ -0,0 +1,126 @@ +import codedraw.CodeDraw; + +/** + * This class represents celestial bodies like stars, planets, asteroids, etc... + */ +public class Body { + private final double mass; + private Vector massCenter; // position of the mass center. + private Vector currentMovement; + + public Body(double mass, Vector massCenter, Vector currentMovement) { + this.mass = mass; + this.massCenter = massCenter; + this.currentMovement = currentMovement; + } + + public Body(double mass, Vector massCenter) { + this(mass, massCenter, new Vector()); + } + + public Body(Body other) { + this(other.mass, new Vector(other.massCenter), new Vector(other.currentMovement)); + } + + /** + * Returns the distance between the mass centers of this body and the specified body 'b'. + */ + public double distanceTo(Body b) { + return massCenter.distanceTo(b.massCenter); + } + + /** + * Returns a vector representing the gravitational force exerted by 'b' on this body. + * The gravitational Force F is calculated by F = G*(m1*m2)/(r*r), with m1 and m2 being the + * masses of the objects interacting, r being the distance between the centers of the masses + * and G being the gravitational constant. + * Hint: see simulation loop in Simulation.java to find out how this is done. + */ + public Vector gravitationalForce(Body b) { + Vector direction = b.massCenter.minus(massCenter); + double distance = direction.length(); + direction.normalize(); + double force = Simulation.G * mass * b.mass / (distance * distance); + return direction.times(force); + } + + /** + * Moves this body to a new position, according to the specified force vector 'force' exerted + * on it, and updates the current movement accordingly. + * (Movement depends on the mass of this body, its current movement and the exerted force.) + * Hint: see simulation loop in Simulation.java to find out how this is done. + */ + public void move(Vector force) { + // F = m*a -> a = F/m + Vector newPosition = massCenter.plus(force.times(1.0 / mass)).plus(currentMovement); + + // new minus old position. + Vector newMovement = newPosition.minus(massCenter); + + // update body state + massCenter = newPosition; + currentMovement = newMovement; + } + + /** + * Returns the approximate radius of this body. + * (It is assumed that the radius r is related to the mass m of the body by r = m ^ 0.5, + * where m and r measured in solar units.) + */ + public double radius() { + return SpaceDraw.massToRadius(mass); + } + + /** + * Return the mass of the Body. + */ + public double mass() { + return mass; + } + + public Vector massCenter() { + return massCenter; + } + + public boolean collidesWith(Body body) { + return this.distanceTo(body) < this.radius() + body.radius(); + } + + /** + * Returns a new body that is formed by the collision of this body and 'b'. The impulse + * of the returned body is the sum of the impulses of 'this' and 'b'. + */ + public Body merge(Body body) { + double totalMass = this.mass + body.mass; + return new Body( + totalMass, + this.massCenter.times(this.mass).plus(body.massCenter.times(body.mass)).times(1.0 / totalMass), + this.currentMovement.times(this.mass).plus(body.currentMovement.times(body.mass)).times(1.0 / totalMass) + ); + } + + /** + * Draws the body to the specified canvas as a filled circle. + * The radius of the circle corresponds to the radius of the body + * (use a conversion of the real scale to the scale of the canvas as + * in 'Simulation.java'). + * Hint: call the method 'drawAsFilledCircle' implemented in 'Vector'. + */ + public void draw(CodeDraw cd) { + cd.setColor(SpaceDraw.massToColor(mass)); + massCenter.drawAsFilledCircle(cd, SpaceDraw.massToRadius(mass)); + } + + /** + * Returns a string with the information about this body including + * mass, position (mass center) and current movement. Example: + * "5.972E24 kg, position: [1.48E11,0.0,0.0] m, movement: [0.0,29290.0,0.0] m/s." + */ + @Override + public String toString() { + return String.format( + "%g kg, position: %s m, movement: %s m/s.", + mass, massCenter.toString(), currentMovement.toString() + ); + } +} diff --git a/src/Octree.java b/src/Octree.java new file mode 100644 index 0000000..f33f6a1 --- /dev/null +++ b/src/Octree.java @@ -0,0 +1,167 @@ + +/** + * + -> >= 0 + * - -> < 0 + * 0 -> x+ y+ z+ + * 1 -> x- y+ z+ + * 2 -> x+ y- z+ + * 3 -> x- y- z+ + * 4 -> x+ y+ z- + * 5 -> x- y+ z- + * 6 -> x+ y- z- + * 7 -> x- y- z- + */ +public class Octree { + private OctreeItem root; + private final Vector center; + private final double size; + + public Octree(double size) { + this(new Vector(), size); + } + + public Octree(Vector center, double size) { + this.center = center; + this.size = size; + } + + public void add(Body body) { + if (root == null) { + root = new OctreeLeaf(center, size, body); + } else { + root = root.add(body); + } + } + + public void applyForces(Body[] bodies, double t) { + if (root == null) return; + root.preCalc(); + Vector[] forces = new Vector[bodies.length]; + for (int i = 0; i < bodies.length; i++) { + forces[i] = root.getForcesOnBody(bodies[i], t); + } + for (int i = 0; i < bodies.length; i++) { + bodies[i].move(forces[i]); + } + } +} + +abstract class OctreeItem { + protected Vector center; + protected double size; + + protected Body pseudoBody; + + protected OctreeItem(Vector center, double size) { + this.center = center; + this.size = size; + } + + abstract protected OctreeNode add(Body body); + + abstract protected void preCalc(); + + abstract protected Vector getForcesOnBody(Body body, double t); +} + +class OctreeNode extends OctreeItem { + private final OctreeItem[] children = new OctreeItem[8];; + + protected OctreeNode(Vector center, double size) { + super(center, size); + } + + @Override + protected OctreeNode add(Body body) { + int num = getOctantNum(body.massCenter()); + if (num < 0) return this; + OctreeItem oct = children[num]; + if (oct == null) { + children[num] = new OctreeLeaf(getOctantCenter(num), size / 2.0, body); + } else { + children[num] = oct.add(body); + } + return this; + } + + @Override + protected void preCalc() { + double mass = 0; + for (OctreeItem oct : children) { + if (oct == null) continue; + oct.preCalc(); + mass += oct.pseudoBody.mass(); + } + Vector massCenter = new Vector(); + for (OctreeItem oct : children) { + if (oct == null) continue; + massCenter = massCenter.plus(oct.pseudoBody.massCenter().times(oct.pseudoBody.mass() / mass)); + } + this.pseudoBody = new Body(mass, massCenter); + } + + @Override + protected Vector getForcesOnBody(Body body, double t) { + double r = pseudoBody.massCenter().distanceTo(body.massCenter()); + if (r == 0) { + return new Vector(); + } else if (size / r < t) { + return body.gravitationalForce(pseudoBody); + } + Vector force = new Vector(); + for (OctreeItem child : children) { + if (child == null) continue; + force = force.plus(child.getForcesOnBody(body, t)); + } + return force; + } + + private int getOctantNum(Vector bodyPos) { + Vector pos = bodyPos.minus(this.center); + if (!isInOctant(bodyPos)) return -1; + return ((pos.getX() < 0) ? 1 : 0) | ((pos.getY() < 0) ? 2 : 0) | ((pos.getZ() < 0) ? 4 : 0); + } + + private boolean isInOctant(Vector pos) { + Vector p1 = center.plus(new Vector(size / 2)); + Vector p2 = center.minus(new Vector(size / 2)); + return pos.getX() <= p1.getX() && pos.getY() <= p1.getY() && pos.getZ() <= p1.getZ() && + pos.getX() >= p2.getX() && pos.getY() >= p2.getY() && pos.getZ() >= p2.getZ(); + } + + private Vector getOctantCenter(int octNum) { + return this.center.plus(new Vector( + ((octNum & 1) != 0) ? -0.5 : 0.5, + ((octNum & 2) != 0) ? -0.5 : 0.5, + ((octNum & 4) != 0) ? -0.5 : 0.5) + .times(this.size)); + } +} + +class OctreeLeaf extends OctreeItem { + private final Body body; + + public OctreeLeaf(Vector center, double size, Body body) { + super(center, size); + this.body = body; + } + + @Override + protected OctreeNode add(Body body) { + OctreeNode replace = new OctreeNode(this.center, this.size); + replace.add(this.body); + replace.add(body); + return replace; + } + + @Override + protected void preCalc() { + System.out.println(this.body); + this.pseudoBody = new Body(this.body); + } + + @Override + protected Vector getForcesOnBody(Body body, double t) { + return body.gravitationalForce(pseudoBody); + } +} diff --git a/src/Simulation.java b/src/Simulation.java index 9c944a9..c435283 100644 --- a/src/Simulation.java +++ b/src/Simulation.java @@ -1,8 +1,80 @@ +import codedraw.CodeDraw; + +import java.awt.*; +import java.util.Random; + public class Simulation { + // gravitational constant + public static final double G = 6.6743e-11; + + // one astronomical unit (AU) is the average distance of earth to the sun. + public static final double AU = 150e9; // meters + + // one light year + public static final double LY = 9.461e15; // meters + + // some further constants needed in the simulation + public static final double SUN_MASS = 1.989e30; // kilograms + public static final double SUN_RADIUS = 696340e3; // meters + public static final double EARTH_MASS = 5.972e24; // kilograms + public static final double EARTH_RADIUS = 6371e3; // meters + + // set some system parameters + public static final double SECTION_SIZE = 2 * AU; // the size of the square region in space + public static final int NUMBER_OF_BODIES = 22; + public static final double OVERALL_SYSTEM_MASS = 20 * SUN_MASS; // kilograms + public static void main(String[] args) { - //TODO: please use this class to run your simulation + CodeDraw cd = new CodeDraw(); + Body[] bodies = new Body[NUMBER_OF_BODIES]; + Random random = new Random(2022); + + for (int i = 0; i < Simulation.NUMBER_OF_BODIES; i++) { + bodies[i] = new Body( + Math.abs(random.nextGaussian()) * Simulation.OVERALL_SYSTEM_MASS / Simulation.NUMBER_OF_BODIES, + new Vector( + 0.2 * random.nextGaussian() * Simulation.AU, + 0.2 * random.nextGaussian() * Simulation.AU, + 0.2 * random.nextGaussian() * Simulation.AU + ), + new Vector( + 0 + random.nextGaussian() * 5e3, + 0 + random.nextGaussian() * 5e3, + 0 + random.nextGaussian() * 5e3 + ) + ); + } + + long seconds = 0; + while (true) { + seconds++; + + /* + BodyLinkedList mergedBodies = new BodyLinkedList(); + for (Body b1 : bodies) { + BodyLinkedList colliding = bodies.removeCollidingWith(b1); + for (Body b2 : colliding) { + b1 = b1.merge(b2); + } + mergedBodies.addLast(b1); + } + bodies = mergedBodies; + */ + Octree octree = new Octree(new Vector(AU), SECTION_SIZE); + for (Body body : bodies) { + octree.add(body); + } + octree.applyForces(bodies, 1); + + if ((seconds % 3600) == 0) { + cd.clear(Color.BLACK); + for (Body body : bodies) { + body.draw(cd); + } + cd.show(); + } + } } - } diff --git a/src/SpaceDraw.java b/src/SpaceDraw.java new file mode 100644 index 0000000..493d13b --- /dev/null +++ b/src/SpaceDraw.java @@ -0,0 +1,63 @@ +import java.awt.*; + +public class SpaceDraw { + + /** + * Returns the approximate radius of a celestial body with the specified mass. + * (It is assumed that the radius r is related to the mass m of the body by r = m ^ 0.5, + * where m and r measured in solar units.) + */ + public static double massToRadius(double mass) { + return Simulation.SUN_RADIUS * (Math.pow(mass / Simulation.SUN_MASS, 0.5)); + } + + /** + * Returns the approximate color of a celestial body with the specified mass. The color of + * the body corresponds to the temperature of the body, assuming the relation of mass and + * temperature of a main sequence star. + */ + public static Color massToColor(double mass) { + Color color; + if (mass < Simulation.SUN_MASS / 10) { + // not a star-like body below this mass + color = Color.LIGHT_GRAY; + } else { + // assume a main sequence star + color = SpaceDraw.kelvinToColor((int) (5500 * mass / Simulation.SUN_MASS)); + } + + return color; + } + + /** + * Returns the approximate color of temperature 'kelvin'. + */ + private static Color kelvinToColor(int kelvin) { + double k = kelvin / 100D; + double red = k <= 66 ? 255 : 329.698727446 * Math.pow(k - 60, -0.1332047592); + double green = k <= 66 ? 99.4708025861 * Math.log(k) - 161.1195681661 : 288.1221695283 * Math.pow(k - 60, -0.0755148492); + double blue = k >= 66 ? 255 : (k <= 19 ? 0 : 138.5177312231 * Math.log(k - 10) - 305.0447927307); + + return new Color( + limitAndDarken(red, kelvin), + limitAndDarken(green, kelvin), + limitAndDarken(blue, kelvin) + ); + } + + /** + * A transformation used in the method 'kelvinToColor'. + */ + private static int limitAndDarken(double color, int kelvin) { + int kelvinNorm = kelvin - 373; + if (color < 0 || kelvinNorm < 0) { + return 0; + } else if (color > 255) { + return 255; + } else if (kelvinNorm < 500) { + return (int) ((color / 256D) * (kelvinNorm / 500D) * 256); + } else { + return (int) color; + } + } +} diff --git a/src/Vector.java b/src/Vector.java new file mode 100644 index 0000000..71fa038 --- /dev/null +++ b/src/Vector.java @@ -0,0 +1,125 @@ +import codedraw.CodeDraw; + +/** + * This class represents vectors in a 3D vector space. + */ +public class Vector { + private double x; + private double y; + private double z; + + public Vector() { + this(0); + } + + public Vector(double v) { + this(v, v, v); + } + + public Vector(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector(Vector other) { + this(other.x, other.y, other.z); + } + + public double getX() { + return this.x; + } + + public double getY() { + return this.y; + } + + public double getZ() { + return this.z; + } + + /** + * Returns the sum of this vector and vector 'v'. + */ + public Vector plus(Vector v) { + return new Vector(x + v.x, y + v.y, z + v.z); + } + + /** + * Returns the product of this vector and 'd'. + */ + public Vector times(double d) { + return new Vector(x * d, y * d, z * d); + } + + /** + * Returns the sum of this vector and -1*v. + */ + public Vector minus(Vector v) { + return new Vector(x - v.x, y - v.y, z - v.z); + } + + /** + * Returns the Euclidean distance of this vector + * to the specified vector 'v'. + */ + public double distanceTo(Vector v) { + double dX = x - v.x; + double dY = y - v.y; + double dZ = z - v.z; + return Math.sqrt(dX * dX + dY * dY + dZ * dZ); + } + + /** + * Returns the length (norm) of this vector. + */ + public double length() { + return distanceTo(new Vector()); + } + + /** + * Normalizes this vector: changes the length of this vector such that it becomes 1. + * The direction and orientation of the vector is not affected. + */ + public void normalize() { + double length = length(); + x /= length; + y /= length; + z /= length; + } + + public double getScreenX(CodeDraw cd) { + return cd.getWidth() * (this.x + Simulation.SECTION_SIZE / 2) / Simulation.SECTION_SIZE; + } + + public double getScreenY(CodeDraw cd) { + return cd.getWidth() * (this.y + Simulation.SECTION_SIZE / 2) / Simulation.SECTION_SIZE; + } + + /** + * Draws a filled circle with a specified radius centered at the (x,y) coordinates of this vector + * in the canvas associated with 'cd'. The z-coordinate is not used. + */ + public void drawAsFilledCircle(CodeDraw cd, double radius) { + radius = cd.getWidth() * radius / Simulation.SECTION_SIZE; + cd.fillCircle(getScreenX(cd), getScreenY(cd), Math.max(radius, 1.5)); + } + + /** + * Returns the coordinates of this vector in brackets as a string + * in the form "[x,y,z]", e.g., "[1.48E11,0.0,0.0]". + */ + @Override + public String toString() { + return String.format("[%g,%g,%g]", x, y, z); + } + + @Override + public boolean equals(Object other) { + if (other.getClass() != Vector.class) { + return false; + } + Vector v = (Vector) other; + return this.x == v.x && this.y == v.y && this.z == v.z; + } +}