diff --git a/pom.xml b/pom.xml index 39c3428..2af83d2 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ 0.0.4 3.8.1 + ThreadRipper launcher nl.andrewlalis.threadripper.ThreadRipperApplication diff --git a/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java b/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java index a039366..e95b392 100644 --- a/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java +++ b/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java @@ -1,17 +1,20 @@ package nl.andrewlalis.threadripper; import javafx.application.Application; -import javafx.scene.Group; +import javafx.beans.value.ChangeListener; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.paint.Color; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; import nl.andrewlalis.threadripper.engine.ParticleChamber; import nl.andrewlalis.threadripper.engine.Vec2; -import nl.andrewlalis.threadripper.particle.Particle; import nl.andrewlalis.threadripper.particle.ParticleFactory; +import nl.andrewlalis.threadripper.render.ParticleChamberRenderer; /** * Main application starting point. @@ -20,46 +23,34 @@ import nl.andrewlalis.threadripper.particle.ParticleFactory; public class ThreadRipperApplication extends Application { private final Canvas canvas; + private final ParticleChamber chamber; + private final Thread chamberThread; + private final ParticleChamberRenderer renderer; + private final Thread renderThread; public ThreadRipperApplication() { - this.canvas = new Canvas(1000, 1000); - this.chamber = new ParticleChamber(canvas); + this.canvas = new Canvas(800, 800); + this.chamber = new ParticleChamber(); + this.renderer = new ParticleChamberRenderer(chamber, canvas); + + ParticleFactory factory = new ParticleFactory( - 10000.0, - 10000000.0, - 0, - 0, - new Vec2(350, 250), - new Vec2(450, 400), - new Vec2(10, -2), - new Vec2(25, 2) + 0.1, 100000000000000.0, + 0, 0, + 0.5, 5, + new Vec2(0, 0), new Vec2(800, 800), + new Vec2(-50, -50), new Vec2(50, 50) ); - for (int i = 0; i < 100; i++) { + for (int i = 0; i < 50; i++) { this.chamber.addParticle(factory.build()); } -// this.chamber.addParticle(new Particle(new Vec2(400, 298), new Vec2(19, 0), 100000.0, 0.0)); -// this.chamber.addParticle(new Particle(new Vec2(400, 300), new Vec2(20, 0), 10000000000.0, 0.0)); - this.chamber.addParticle(new Particle(new Vec2(400, 500), new Vec2(0, 0), 1000000000000000.0, 0.0)); + this.chamberThread = new Thread(this.chamber); + this.chamberThread.start(); -// ParticleFactory factory1 = new ParticleFactory( -// 1000000000.0, -// 10000000000000.0, -// 0, -// 0, -// new Vec2(700, 700), -// new Vec2(900, 900), -// new Vec2(0, 0), -// new Vec2(0, 0) -// ); -// for (int i = 0; i < 500; i++) { -// this.chamber.addParticle(factory1.build()); -// } - - Thread particleChamberThread = new Thread(chamber); - particleChamberThread.setName("ParticleChamber"); - particleChamberThread.start(); + this.renderThread = new Thread(this.renderer); + this.renderThread.start(); } public static void main(String[] args) { @@ -71,22 +62,47 @@ public class ThreadRipperApplication extends Application { public void start(Stage stage) throws Exception { stage.setTitle("ThreadRipper"); - GraphicsContext gc = this.canvas.getGraphicsContext2D(); + BorderPane borderPane = new BorderPane(); + borderPane.setCenter(this.canvas); - gc.setFill(Color.GREEN); - gc.fillOval(10, 60, 30, 30); + TextField simRateField = new TextField(Double.toString(this.chamber.getSimulationRate())); + Button simRateUpdate = new Button("Apply"); + simRateUpdate.setOnMouseClicked(mouseEvent -> { + double simRate = this.chamber.getSimulationRate(); + try { + simRate = Double.parseDouble(simRateField.getText().trim()); + } catch (NumberFormatException e) { + simRateField.clear(); + } + this.chamber.setSimulationRate(simRate); + }); + HBox bottomPanel = new HBox( + new Label("Simulation Rate"), + simRateField, + simRateUpdate + ); + borderPane.setBottom(bottomPanel); + + Scene scene = new Scene(borderPane); + + ChangeListener sceneSizeListener = (observable, oldValue, newValue) -> { + this.canvas.setWidth(scene.getWidth()); + this.canvas.setHeight(scene.getHeight()); + }; + scene.widthProperty().addListener(sceneSizeListener); + scene.heightProperty().addListener(sceneSizeListener); - Group root = new Group(); - root.getChildren().add(this.canvas); - Scene scene = new Scene(root); stage.setScene(scene); + stage.setMaximized(true); stage.show(); } @Override public void stop() throws Exception { this.chamber.setRunning(false); + this.chamberThread.join(); + this.renderer.setRunning(false); + this.renderThread.join(); super.stop(); - System.exit(0); } } diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java index ee1d4ab..a9412c7 100644 --- a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java +++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java @@ -1,9 +1,5 @@ package nl.andrewlalis.threadripper.engine; -import javafx.application.Platform; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.paint.Color; import lombok.extern.slf4j.Slf4j; import nl.andrewlalis.threadripper.particle.Particle; @@ -13,28 +9,28 @@ import java.util.concurrent.*; @Slf4j public class ParticleChamber implements Runnable { private static final int DEFAULT_THREAD_POOL = 100; - private static final double DEFAULT_UPDATE_FPS = 60; + private static final double DEFAULT_UPDATES_PER_SECOND = 60; private final Set particles; + private double simulationRate = 1.0; + private boolean allowCollision = true; + private int threadCount; - private ExecutorService executorService; - private CompletionService particleUpdateService; + private final ExecutorService executorService; + private final CompletionService particleUpdateService; private boolean running; private double updateFps; + private double secondsSinceLastUpdate; - private final Canvas canvas; - - public ParticleChamber(Canvas canvas) { + public ParticleChamber() { this.particles = new HashSet<>(); this.threadCount = DEFAULT_THREAD_POOL; this.executorService = Executors.newFixedThreadPool(this.threadCount); this.particleUpdateService = new ExecutorCompletionService<>(this.executorService); - this.updateFps = DEFAULT_UPDATE_FPS; - - this.canvas = canvas; + this.updateFps = DEFAULT_UPDATES_PER_SECOND; } /** @@ -49,6 +45,14 @@ public class ParticleChamber implements Runnable { this.running = running; } + public synchronized void setSimulationRate(double simulationRate) { + this.simulationRate = simulationRate; + } + + public synchronized void setAllowCollision(boolean allowCollision) { + this.allowCollision = allowCollision; + } + @Override public void run() { this.running = true; @@ -61,6 +65,7 @@ public class ParticleChamber implements Runnable { final long elapsedMilliseconds = currentTimeMilliseconds - previousTimeMilliseconds; millisecondsSinceLastUpdate += elapsedMilliseconds; + this.secondsSinceLastUpdate = millisecondsSinceLastUpdate / 1000.0; final double millisecondsPerFrame = 1000.0 / this.updateFps; @@ -68,12 +73,12 @@ public class ParticleChamber implements Runnable { final double secondsSinceLastUpdate = millisecondsSinceLastUpdate / 1000.0; millisecondsSinceLastUpdate = 0L; //log.info("Updating particles after {} seconds elapsed.", secondsSinceLastUpdate); - this.updateParticles(secondsSinceLastUpdate); - this.drawParticles(); + this.updateParticles(secondsSinceLastUpdate * this.simulationRate); } previousTimeMilliseconds = currentTimeMilliseconds; } + this.executorService.shutdown(); log.info("Particle chamber stopped."); } @@ -84,7 +89,7 @@ public class ParticleChamber implements Runnable { private void updateParticles(double deltaTime) { // First submit a new callable task for each particle. for (Particle particle : this.particles) { - this.particleUpdateService.submit(new ParticleUpdater(particle, this.particles)); + this.particleUpdateService.submit(new ParticleUpdater(particle, this.particles, this.allowCollision)); } int updatesReceived = 0; @@ -103,28 +108,41 @@ public class ParticleChamber implements Runnable { } } + Set particlesToRemove = new HashSet<>(this.particles.size()); + Set particlesToAdd = new HashSet<>(this.particles.size()); // Implement the updates for each particle. for (ParticleUpdate update : updates) { update.getFocusParticle().updateVelocity(update.getAcceleration(), deltaTime); update.getFocusParticle().updatePosition(deltaTime); - //log.info("Particle updated: {}", update.getFocusParticle().toString()); + if (!update.getCollidesWith().isEmpty() && !particlesToRemove.contains(update.getFocusParticle())) { + particlesToRemove.addAll(update.getCollidesWith()); + particlesToRemove.add(update.getFocusParticle()); + // Create a new particle as combination. + Particle p = update.getFocusParticle(); + for (Particle collided : update.getCollidesWith()) { + p = p.combine(collided); + } + particlesToAdd.add(p); + } } + + this.particles.removeAll(particlesToRemove); + this.particles.addAll(particlesToAdd); } - /** - * Draws all particles on the canvas. - */ - private void drawParticles() { - Platform.runLater(() -> { - GraphicsContext gc = this.canvas.getGraphicsContext2D(); - gc.setFill(Color.WHITE); - gc.clearRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight()); - gc.setFill(Color.BLACK); - gc.setStroke(Color.BLUE); - for (Particle particle : this.particles) { - Vec2 pos = particle.getPosition(); - gc.fillOval(pos.getX() - 3, pos.getY() - 3, 6, 6); - } - }); + public double getSecondsSinceLastUpdate() { + return this.secondsSinceLastUpdate; + } + + public double getSimulationRate() { + return simulationRate; + } + + public Set getCopyOfParticles() { + Set set = new HashSet<>(this.particles.size()); + for (Particle p : this.particles) { + set.add(p.getCopy()); + } + return set; } } diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java index 7b117ee..b7169e2 100644 --- a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java +++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java @@ -3,6 +3,8 @@ package nl.andrewlalis.threadripper.engine; import lombok.Getter; import nl.andrewlalis.threadripper.particle.Particle; +import java.util.Set; + /** * Describes an update to a particle's motion. */ @@ -19,8 +21,14 @@ public class ParticleUpdate { */ private final Vec2 acceleration; - public ParticleUpdate(Particle focusParticle, Vec2 acceleration) { + /** + * A set of particles that this one has collided with. + */ + private final Set collidesWith; + + public ParticleUpdate(Particle focusParticle, Vec2 acceleration, Set collidesWith) { this.focusParticle = focusParticle; this.acceleration = acceleration; + this.collidesWith = collidesWith; } } diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java index 17b7def..d1698fd 100644 --- a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java +++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java @@ -3,6 +3,7 @@ package nl.andrewlalis.threadripper.engine; import lombok.extern.slf4j.Slf4j; import nl.andrewlalis.threadripper.particle.Particle; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; @@ -14,24 +15,40 @@ import java.util.concurrent.Callable; public class ParticleUpdater implements Callable { private final Particle focusParticle; private final Set particles; + private final boolean allowCollision; - public ParticleUpdater(Particle focusParticle, Set particles) { + public ParticleUpdater( + Particle focusParticle, + Set particles, + boolean allowCollision + ) { this.focusParticle = focusParticle; this.particles = particles; + this.allowCollision = allowCollision; } @Override public ParticleUpdate call() throws Exception { double accelerationX = 0L; double accelerationY = 0L; + Set collidesWith = new HashSet<>(); for (Particle particle : this.particles) { if (!particle.equals(this.focusParticle)) { Vec2 partialAcceleration = this.computeAcceleration(particle); accelerationX += partialAcceleration.getX(); accelerationY += partialAcceleration.getY(); + + // Collision detection: + final double distance = this.focusParticle.getPosition().distance(particle.getPosition()); + if ( + Math.abs(this.focusParticle.getRadius() - particle.getRadius()) <= distance + && distance <= (this.focusParticle.getRadius() + particle.getRadius()) + ) { + collidesWith.add(particle); + } } } - return new ParticleUpdate(this.focusParticle, new Vec2(accelerationX, accelerationY)); + return new ParticleUpdate(this.focusParticle, new Vec2(accelerationX, accelerationY), collidesWith); } /** @@ -51,17 +68,13 @@ public class ParticleUpdater implements Callable { final double gravityNewtons = Constants.G * (this.focusParticle.getMass() * other.getMass()) / Math.pow(radius, 2); final double gravityAcceleration = gravityNewtons / this.focusParticle.getMass(); final Vec2 gravityAccelerationVector = Vec2.fromPolar(gravityAcceleration, angle); -// log.info( -// "Force of gravity between particles {} and {} is {} Newtons.\nCauses {} m/s^2 acceleration on particle {}.", -// this.focusParticle.getId(), -// other.getId(), -// gravityNewtons, -// gravityAcceleration, -// this.focusParticle.getId() -// ); final double emNewtons = Constants.Ke * (this.focusParticle.getCharge() * other.getCharge()) / Math.pow(radius, 2); - final double emAcceleration = emNewtons / this.focusParticle.getMass(); + final boolean isRepulsion = emNewtons < 0.0; + double emAcceleration = emNewtons / this.focusParticle.getMass(); + if (isRepulsion) { + emAcceleration *= -1.0; + } final Vec2 emAccelerationVector = Vec2.fromPolar(emAcceleration, angle); final Vec2 totalAcceleration = gravityAccelerationVector.add(emAccelerationVector); diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java b/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java index d316d31..9fd8d5e 100644 --- a/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java +++ b/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java @@ -50,4 +50,8 @@ public class Vec2 { public String toString() { return String.format("[%f, %f]", this.getX(), this.getY()); } + + public Vec2 getCopy() { + return new Vec2(this.getX(), this.getY()); + } } diff --git a/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java b/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java index 2bc3d6d..b34c501 100644 --- a/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java +++ b/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java @@ -15,7 +15,7 @@ public class Particle { /** * Unique id for this particle. */ - private long id; + private final long id; /** * The particle's position, in meters. @@ -37,15 +37,25 @@ public class Particle { */ private double charge; + /** + * The particle's radius, in meters. + */ + private double radius; + public Particle(Vec2 position) { - this(position, new Vec2(0, 0), 1, 0); + this(position, new Vec2(0, 0), 1, 0, 1); } - public Particle(Vec2 position, Vec2 velocity, double mass, double charge) { + public Particle(Vec2 position, double mass, double charge, double radius) { + this(position, new Vec2(0, 0), mass, charge, radius); + } + + public Particle(Vec2 position, Vec2 velocity, double mass, double charge, double radius) { this.position = position; this.velocity = velocity; this.mass = mass; this.charge = charge; + this.radius = radius; this.id = NEXT_PARTICLE_ID; NEXT_PARTICLE_ID++; @@ -95,4 +105,32 @@ public class Particle { this.getVelocity() ); } + + public Particle getCopy() { + return new Particle( + this.getPosition().getCopy(), + this.getVelocity().getCopy(), + this.getMass(), + this.getCharge(), + this.getRadius() + ); + } + + public Particle combine(Particle other) { + final Vec2 position = new Vec2( + (this.getPosition().getX() + other.getPosition().getX()) / 2.0, + (this.getPosition().getY() + other.getPosition().getY()) / 2.0 + ); + final Vec2 velocity = new Vec2( + (this.getVelocity().getX() + other.getVelocity().getX()) / 2.0, + (this.getVelocity().getY() + other.getVelocity().getY()) / 2.0 + ); + return new Particle( + position, + velocity, + this.getMass() + other.getMass(), + this.getCharge() + other.getCharge(), + this.getRadius() + other.getRadius() + ); + } } diff --git a/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java b/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java index eb2a57e..f3d6669 100644 --- a/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java +++ b/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java @@ -9,6 +9,8 @@ public class ParticleFactory { private final double maxMass; private final double minCharge; private final double maxCharge; + private final double minRadius; + private final double maxRadius; private final Vec2 minPosition; private final Vec2 maxPosition; private final Vec2 minVelocity; @@ -19,6 +21,8 @@ public class ParticleFactory { double maxMass, double minCharge, double maxCharge, + double minRadius, + double maxRadius, Vec2 minPosition, Vec2 maxPosition, Vec2 minVelocity, @@ -28,6 +32,8 @@ public class ParticleFactory { this.maxMass = maxMass; this.minCharge = minCharge; this.maxCharge = maxCharge; + this.minRadius = minRadius; + this.maxRadius = maxRadius; this.minPosition = minPosition; this.maxPosition = maxPosition; this.minVelocity = minVelocity; @@ -45,14 +51,20 @@ public class ParticleFactory { random.nextDouble(this.minVelocity.getY(), this.maxVelocity.getY()) ); final double mass = random.nextDouble(this.minMass, this.maxMass); -// final double charge = random.nextDouble(this.minCharge, this.maxCharge); + final double charge; + if (this.minCharge == 0.0 && this.maxCharge == 0.0) { + charge = 0.0; + } else { + charge = random.nextDouble(this.minCharge, this.maxCharge); + } + final double radius = random.nextDouble(this.minRadius, this.maxRadius); - final double charge = 0; return new Particle( position, velocity, mass, - charge + charge, + radius ); } diff --git a/src/main/java/nl/andrewlalis/threadripper/render/ParticleChamberRenderer.java b/src/main/java/nl/andrewlalis/threadripper/render/ParticleChamberRenderer.java new file mode 100644 index 0000000..4d7a685 --- /dev/null +++ b/src/main/java/nl/andrewlalis/threadripper/render/ParticleChamberRenderer.java @@ -0,0 +1,140 @@ +package nl.andrewlalis.threadripper.render; + +import javafx.application.Platform; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import lombok.extern.slf4j.Slf4j; +import nl.andrewlalis.threadripper.engine.ParticleChamber; +import nl.andrewlalis.threadripper.engine.Vec2; +import nl.andrewlalis.threadripper.particle.Particle; + +import java.util.LinkedList; +import java.util.Set; + +/** + * The renderer is responsible for drawing a particle chamber's contents to + * a canvas. + */ +@Slf4j +public class ParticleChamberRenderer implements Runnable { + private static final double DEFAULT_FPS = 60.0; + private static final int FPS_READING_COUNT = 360; + + private final ParticleChamber chamber; + private final Canvas canvas; + + private double targetFps; + private boolean running; + + private Vec2 offset; + private double scale; + + private LinkedList recentFpsReadings; + + public ParticleChamberRenderer(ParticleChamber chamber, Canvas canvas) { + this.chamber = chamber; + this.canvas = canvas; + + this.targetFps = DEFAULT_FPS; + this.recentFpsReadings = new LinkedList<>(); + + this.offset = new Vec2(0, 0); + this.scale = 1.0; + } + + public synchronized void setRunning(boolean running) { + this.running = running; + } + + public synchronized void setTargetFps(double targetFps) { + if (targetFps > 0) { + this.targetFps = targetFps; + } + } + + public synchronized void setOffset(Vec2 offset) { + this.offset = offset; + } + + public synchronized void setScale(double scale) { + if (scale > 0) { + this.scale = scale; + } + } + + @Override + public void run() { + this.running = true; + long previousTimeMilliseconds = System.currentTimeMillis(); + long millisecondsSinceLastFrame = 0L; + + log.info("Starting particle chamber renderer."); + while (this.running) { + final long currentTimeMilliseconds = System.currentTimeMillis(); + final long elapsedMilliseconds = currentTimeMilliseconds - previousTimeMilliseconds; + + millisecondsSinceLastFrame += elapsedMilliseconds; + + final double millisecondsPerFrame = 1000.0 / this.targetFps; + + if (millisecondsSinceLastFrame > millisecondsPerFrame) { + final double currentFps = 1000.0 / millisecondsSinceLastFrame; + this.updateFpsReading(currentFps); + this.draw(); + millisecondsSinceLastFrame = 0L; + } + + previousTimeMilliseconds = currentTimeMilliseconds; + } + log.info("Particle chamber renderer stopped."); + } + + private void draw() { + final double secondsSinceLastUpdate = this.chamber.getSecondsSinceLastUpdate(); + final Set particles = this.chamber.getCopyOfParticles(); + Platform.runLater(() -> { + GraphicsContext gc = this.canvas.getGraphicsContext2D(); + gc.setFill(Color.WHITE); + gc.clearRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight()); + + + for (Particle particle : particles) { + particle.updatePosition(secondsSinceLastUpdate); + Vec2 pos = particle.getPosition().add(this.offset); + if (particle.getCharge() > 0) { + gc.setFill(Color.BLUE); + } else { + gc.setFill(Color.RED); + } + gc.fillOval( + pos.getX() - particle.getRadius(), + pos.getY() - particle.getRadius(), + particle.getRadius() * 2, + particle.getRadius() * 2 + ); + } + + gc.setFill(Color.BLACK); + gc.fillText(String.format("FPS: %.2f", this.getAverageFps()), 10, 10); + }); + } + + private void updateFpsReading(double recentFpsReading) { + this.recentFpsReadings.addFirst(recentFpsReading); + if (this.recentFpsReadings.size() > FPS_READING_COUNT) { + this.recentFpsReadings.removeLast(); + } + } + + public double getAverageFps() { + if (this.recentFpsReadings.size() == 0) { + return 0.0; + } + double sum = 0.0; + for (double reading : this.recentFpsReadings) { + sum += reading; + } + return sum / this.recentFpsReadings.size(); + } +}