Skip to content

A simple particle life simulator that simulates symmetric or asymmetric attraction and repulsion forces between different particle types inside a toroidal rectangle.

Notifications You must be signed in to change notification settings

FrameXX/particle-life

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Particle life

A simple particle life simulator that simulates symmetric or asymmetric attraction and repulsion forces between different particle types inside a toroidal rectangle.

User interface

  1. Button to close or open option panel
  2. Matrix to customize particle type relations
    • Scroll up or left click for higher values
    • Scroll down or right click for lower values
    • Middle click to reset to 0
  3. Button to apply all options and respawn all particles
  4. Button to apply all options (some options changes may remove or spawn particles anyway)
  5. Button to pause the simulation
    • You can use a keyboard shortcut Ctrl+P
  6. Button to reset camera position to default
  7. Button to toggle window to fullscreen
    • You can use a keyboard shortcut F11

Launching

  1. Install dependencies by running mvn dependency:resolve in the root directory of the project (where this file is located).

  2. Compile and launch the program by running mvn javafx:run or mvn clean javafx:run in case you want to recompile from scratch.

Technical documentation

Dependencies

  • GUI with JavaFX (21)
  • Logging with SLF4J (2.0.17) and Logback (1.5.18)
  • Testing with TestNG (7.11.0)

For more details visit pom.xml.

Launching with logging

You can launch the program with logging by passing an argument --loglevel with one of these values:

  • OFF
  • ERROR
  • WARN
  • INFO
  • DEBUG
  • TRACE

For example by using --loglevel=WARN you would set the logging level to WARN and all message with level WARN and above (as in the list) would get logged.

Rendering a frame

This app does not use JavaFX canvas. The rendering consists of 4 stages:

  1. RGBA values are put into an IntBuffer.
// Allocate 100 pixels
IntBuffer intBuffer = IntBuffer.allocate(100);
// Put black into the first pixel
intBuffer.put(0, 0xFF000000);
  1. The values of the IntBuffer are used inside a PixelBuffer.
// Create buffer of size 10x10 = 100
PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(
        10,
        10,
        intBuffer,
        PixelFormat.getIntArgbPreInstance()
);
  1. After the PixelBuffer is prepared a WritableImage is updated with its values.
WritableImage writableImage = new WritableImage(pixelBuffer);
// The lambda passed to the updateBuffer method returning null only means that all pixels from the buffer will be copied (not just a specific segment).
pixelBuffer.updateBuffer(buffer -> null);
  1. An ImageView is updated with the WritableImage.
ImageView imageView = new ImageView();
imageView.setImage(writableImage);

So the basic cycle is: IntBuffer -> PixelBuffer -> WritableImage -> ImageView.

Rendering a particle

Here's a simplified process:

  1. Calculate the particle position relative to the camera position. This is sometimes called displacement. It tells us where the particles is in the camera view area.

  1. Check whether few important positions on the particle are in the camera view area. If none of the positions are in the camera view area the particle rendering is skipped.

  1. Go over the center position of each pixel in a box around the particle and draw it if it's inside the camera view area and is within a distance from the particle center.

The rendering process could be more optimized however I didn't ever felt a need to optimize it as it was never the bottleneck.

Updating particles physical properties

Each particle has 3 physical properties:

  • Position
  • Velocity
    • First derivative of position
    • Change of position in time
  • (Immediate) Acceleration
    • Second derivative of position
    • Change of velocity in time

Here's a simplified process of a simulation tick:

  1. Go over each pair of particles in the simulation and calculate how particle A attracts or repels particle B and vice versa and add these values to their immediate acceleration. The distance of the particles in the pair matters. The calculation of pairs that are too far apart to interact with each other is mostly automatically skipped, because of space partitioning technique to make the simulation more efficient.

The blue particles repel the red particle while the white particles attract it. The cumulated (total) force is the red arrow.

  1. The calculation of particles immediate acceleration is naturally computationally exponentially demanding and before the computation finishes timeDeltaSeconds amount of time elapses. This time is used to transfer that acceleration we calculated to a change of velocity for each particle. The immediate aaceleration is reset to zero for the next tick after the change in velocity is calculated.
// Type declarations for better readability
Vector2Double velocity;
Vector2Double acceleration;
double timeDeltaSeconds;

velocity += acceleration.scaled(timeDeltaSeconds);
  1. Transfer the velocity to change of position.
// Type declarations for better readability
Vector2Double position;
Vector2Double velocity;
double timeDeltaSeconds;

position += velocity.scaled(timeDeltaSeconds);
  1. Correct the particle position to stay withing the simulation space by making the space toroidal. Wrap the space horizontally and vertically (to make all ends of the world connect to the opposite end) which makes for a donut shape in 3D! It's as simple as using a remainder of division by the space with and height.
// Type declarations for better readability
Vector2Double position;
Vector2Double spaceSize;

position = new Vector2Int(
        Math.abs(position.x() % spaceSize.x()),
        Math.abs(position.y() % spaceSize.y())
);

  1. Apply velocity half life (friction) over time for every particle. Velocity half life defines how many seconds it takes for a particle to naturally lose half of its velocity.
// Type declarations for better readability
Vector2Double velocity;
double timeDeltaSeconds;
double velocityHalfLife;

final double velocityHalfLifesElapsed = timeDeltaSeconds / velocityHalfLife;
velocity.scale(1 / Math.pow(2, velocityHalfLifesElapsed);

The actual calculation of 1 tick looks more like this:

When calculating properties that use timeDeltaSeconds the timeDeltaSeconds represents the amount of seconds it took to calculate the whole previous tick.

About

A simple particle life simulator that simulates symmetric or asymmetric attraction and repulsion forces between different particle types inside a toroidal rectangle.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published