Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

p5.js particles behavior under the influence of a 2D vector field showing poor response

I got half-way through what I wanted in the representation of physics vector fields in 2D with p5js here. The other half is to get random particles to dynamically follow the forces of the vector field, and I am having a lot of problems with it. I have tried multiple things to take into account the wrap-around of the particles, as well as the fact that I am translating the origin of the plot to the center of the canvas. However, the particles seem minimally affected by the individual vectors in the field, and ultimately march along the x axis with slight bumpiness.

enter image description here

The fact that I am completely new at JS doesn't help splice all these elements from several presentations available online, and I would appreciate any advise as to what may be going wrong, and where I should focus on.

Here is what I have so far: a file sketch.js corresponding to my own answer quoted above:

scl = 35;
var cols,rows;
var fr;
var particles = [];
var flowfield;

function setup() {
  createCanvas(windowWidth, windowHeight);
  cols = floor(width/scl);
  rows = floor(height/scl);
  fr = createP("");
   
  flowfield = new Array(cols * rows);

  for (var i = 0; i < 1000; i++) {
    particles[i] = new Particle();
  }
  background(51);
}



function draw() {
  translate(height/2, height/2);  //moves the origin to bottom left
  scale(1, -1);  //flips the y values so y increases "up"
  background(255);
  loadPixels();
  for (var y = -rows; y < rows; y++) {
    for (var x = - cols; x < cols; x++) {
      var index = x + y * cols;
      //var v = createVector(sin(x)+cos(y),sin(x)*cos(y));
      var v = createVector(y,-x);
      flowfield[index] = v;
      fill('blue');
      stroke('blue');
      push();
      translate(x*scl,y*scl);
      rotate(v.heading());
      line(0,0,0.5*scl,0);
      let arrowSize = 7;
      translate(0.5*scl - arrowSize, 0);
      triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
      pop();
    }
  }
    for (var i = 0; i < particles.length; i++) {
    particles[i].follow(flowfield);
    particles[i].update();
    particles[i].edges();
    particles[i].show();
  }
}

and a second file called particle.js:

class Particle {
  constructor() {
    this.pos = createVector(random(-width,width), 
                            random(-height,height));
    this.vel = createVector(0, 0);
    this.acc = createVector(0, 0);
    this.maxspeed = 4;
    this.prevPos = this.pos.copy();
    this.size = 8;
  }

  update() {
    this.vel.add(this.acc);
    this.vel.limit(this.maxspeed);
    this.pos.add(this.vel);
    this.acc.mult(0);
  }

  follow(vectors) {
    var x = floor(this.pos.x / scl);
    var y = floor(this.pos.y / scl);
    var index = x + y * cols;
    var force = vectors[index];
    this.applyForce(force);
  }

  applyForce(force) {
    this.acc.add(force);
  }

  show() {
    noStroke();
    fill('rgba(100,0,255,.5)');
    circle(-(this.pos.x+width/2), -(this.pos.y-height/2), this.size);
    this.updatePrev();
  }

  updatePrev() {
    this.prevPos.x = this.pos.x;
    this.prevPos.y = this.pos.y;
  }

  edges() {
    if (this.pos.x > width) {
      this.pos.x = -width;
      this.updatePrev();
    }
    if (this.pos.x < -width) {
      this.pos.x = width;
      this.updatePrev();
    }
    if (this.pos.y > height) {
      this.pos.y = -height;
      this.updatePrev();
    }
    if (this.pos.y == -height) {
      this.pos.y = height;
      this.updatePrev();
    }

  }

}

The beginning of the simulation with the updated code on this edit is not bad:

enter image description here

but soon enough all particles align with the last row along the x axis. So I guess I need some help understanding flow fields or scaling down the effect of the vectors at the bottom.


Ethan Hermsey did solve this plotting problem for me perfectly. At this point, and undoubtfully due to either some glitch in the code, or some miscommunication, the code in the accepted answer happens to actually result in a different output to that desired in asking the question, and the code that Ethan himself solved for me. So just for reference, this is the effect intended:

enter image description here

Generated as follows:

const scl = 35;
var cols, rows;
var particles = [];
var flowfield;

function setup() {

    createCanvas(750, 750);
    cols = ceil( width / scl );
    rows = ceil( height / scl );


    flowfield = new Array( cols * rows );

    for (var i = 0; i < 1000; i ++ ) {
        particles[i] = new Particle();
    }
}

function draw() {

    translate(height / 2, height / 2); //moves the origin to center
    scale( 1, - 1 ); //flips the y values so y increases "up"
    background( 255 );

    for ( var y = 0; y < rows; y ++ ) { 
        for ( var x = 0; x < cols; x ++ ) { 
      
      var index = x + y * cols;

      let vX = x * 2 - cols;
      let vY = y * 2 - rows;
                
     
      var v = createVector( vY, -vX );
      v.normalize();
          
      flowfield[index] = v;
      
      // The following push() / pull() affects only the arrows     
      push();
      fill( 'red' );
      stroke( 'red' );
      translate(x*scl-width/2,y*scl-height/2);
      rotate(v.heading());
      line(0,0,0.5*scl,0);
      let arrowSize = 7;
      translate(0.5*scl - arrowSize, 0);
      triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
      pop();
// The preceding push() / pull() affects only the arrows     
    }// Closes inner loop
  }// Closes outer loop to create vectors and index.
  
//This next loop actually creates the desired particles:
    for (var i = 0; i < particles.length; i++) {
    particles[i].follow(flowfield);
    particles[i].update();
    particles[i].edges();
    particles[i].show();
  }
} // End of the function draw

class Particle {

    constructor() {

        // changed startpostion. Since the origin is in the center of the canvas,
        // the x goes from -width/2 to width/2
        // the y goes from -height/2 to height/2
        // i also changed this in this.edges().

        this.pos = createVector( random( - width / 2, width / 2 ),
            random( - height / 2, height / 2 ) );
        this.vel = createVector( 0, 0 );
        this.acc = createVector( 0, 0 );
        this.maxspeed = 4;
        this.steerStrength = 15;
        this.prevPos = this.pos.copy();
        this.size = 8;

    }

    update() {

        this.vel.add( this.acc );
        this.vel.limit( this.maxspeed );
        this.pos.add( this.vel );
        this.acc.mult( 0 );

    }

    follow( vectors ) {

        var x = floor( map( this.pos.x, - width / 2, width / 2, 0, cols - 1, true ) );
        var y = floor( map( this.pos.y, - height / 2, height / 2, 0, rows - 1, true ) );
        var index = ( y * cols ) + x;

        var force = vectors[ index ].copy();
        force.mult( this.steerStrength );
        this.applyForce( force );

    }

    applyForce( force ) {

        this.acc.add( force );

    }

    show() {

        noStroke();
        fill( 'rgba(100,0,255,.5)' );

        // you can just draw on the position.
        circle( this.pos.x, this.pos.y, this.size );

        this.updatePrev();

    }

    updatePrev() {

        this.prevPos.x = this.pos.x;
        this.prevPos.y = this.pos.y;

    }

    edges() {

        //clamp between -width/2 and width/2. -height/2 and height/2
        if ( this.pos.x > width / 2 ) {

            this.pos.x = - width / 2;
            this.updatePrev();

        }
        if ( this.pos.x < - width / 2 ) {

            this.pos.x = width / 2;
            this.updatePrev();

        }
        if ( this.pos.y > height / 2 ) {

            this.pos.y = - height / 2;
            this.updatePrev();

        }
        if ( this.pos.y < - height / 2 ) {

            this.pos.y = height / 2;
            this.updatePrev();

        }

    }

}
like image 232
Antoni Parellada Avatar asked Oct 31 '25 02:10

Antoni Parellada


1 Answers

we've had contact before via reddit. I'd like to post my answer here.

Like Paul said in the other answer, the 2 different coordinate systems are confusing and caused the main problems.

  • When you're generating the flowfield, looping from [-cols, cols], while the field is only [0, cols] big. That meant 3/4 of the vectors where placed in the array on invalid positions outside the array ( or, where not placed at all and the grid is filled with just one quadrant of the formula ).

  • In Particle.follow() the index wasn't calculated correctly, so it would try to access the flowfield array on a invalid position that does not exist, giving an out of bounds exception.

If it's necessary to keep the 2 coordinate systems, you have to keep remapping the x and y values in the particle class, but also within the field generation loop, to get the right results.

I like how Paul normalized the vectors, and used map() to remap the particle's coordinates to flowfield coordinates, I did that too.

const arrowSize = 7;
const inc = 0.1;
const scl = 35;
var cols, rows;
var fr;
var particles = [];
var flowfield;

function setup() {

    createCanvas( 500, 500 );
    cols = ceil( width / scl );
    rows = ceil( height / scl );


    flowfield = new Array( cols * rows );


    for ( var i = 0; i < 100; i ++ ) {

        particles[ i ] = new Particle();

    }
    background( 51 );

}



function draw() {

    translate( height / 2, height / 2 ); //moves the origin to center
    scale( 1, - 1 ); //flips the y values so y increases "up"
    background( 255 );
    fill( 'blue' );
    stroke( 'blue' );
    loadPixels();


    for ( var y = 0; y < rows; y ++ ) { // now loops from 0 to rows

        for ( var x = 0; x < cols; x ++ ) { //now loops from 0 to cols

            var index = ( y * cols ) + x;

            // because the formula assumes negative to positive values.
            // remap from range [0, cols] to [-cols/2, cols/2].
            // let vX = x * 2 - cols;
            // let vY = y * 2 - rows;
            
            // But more elegant would be to map to [-1, 1] 
            // ( with the exact same result )
            let vX = ( x / cols ) * 2 - 1;
            let vY = ( y / rows ) * 2 - 1;

            // normalize the vectors. It's common to multiply the vector in the
            // particle class later.
            var v = createVector( vY, - vX );
            v.normalize();
            flowfield[ index ] = v;

            push();

            translate( x * scl - width / 2, y * scl - height / 2 );
            rotate( v.heading() );
            line( 0, 0, 0.5 * scl, 0 );
            translate( 0.5 * scl - arrowSize, 0 );
            triangle( 0, arrowSize / 2, 0, - arrowSize / 2, arrowSize, 0 );

            pop();

        }

    }


    for ( var i = 0; i < particles.length; i ++ ) {

        particles[ i ].follow( flowfield );
        particles[ i ].update();
        particles[ i ].edges();
        particles[ i ].show();

    }

}


class Particle {

    constructor() {

        //changed startposition to be within screen space.
        this.pos = createVector(
              random( - width / 2, width / 2 ),
          random( - height / 2, height / 2 )
        );
        this.vel = createVector( 0, 0 );
        this.acc = createVector( 0, 0 );
        this.maxspeed = 4;
        this.steerStrength = 15;
        this.prevPos = this.pos.copy();
        this.size = 8;

    }

    update() {

        this.vel.add( this.acc );
        this.vel.limit( this.maxspeed );
        this.pos.add( this.vel );
        this.acc.mult( 0 );

    }

    follow( vectors ) {

        var x = floor( map( this.pos.x, - width / 2, width / 2, 0, cols - 1, true ) );
        var y = floor( map( this.pos.y, - height / 2, height / 2, 0, rows - 1, true ) );
        var index = ( y * cols ) + x;

        //find and modify the steering strength.
        var force = vectors[ index ].copy();
        force.mult( this.steerStrength );
        this.applyForce( force );

    }

    applyForce( force ) {

        this.acc.add( force );

    }

    show() {

        noStroke();
        fill( 'rgba(100,0,255,.5)' );
        circle( this.pos.x, this.pos.y, this.size );
        this.updatePrev();

    }

    updatePrev() {

        this.prevPos.x = this.pos.x;
        this.prevPos.y = this.pos.y;

    }

    edges() {

        //clamp between -width/2 and width/2. -height/2 and height/2
        if ( this.pos.x > width / 2 ) {

            this.pos.x = - width / 2;
            this.updatePrev();

        }
        if ( this.pos.x < - width / 2 ) {

            this.pos.x = width / 2;
            this.updatePrev();

        }
        if ( this.pos.y > height / 2 ) {

            this.pos.y = - height / 2;
            this.updatePrev();

        }
        if ( this.pos.y < - height / 2 ) {

            this.pos.y = height / 2;
            this.updatePrev();

        }

    }

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
like image 157
Ethan Hermsey Avatar answered Nov 01 '25 18:11

Ethan Hermsey



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!