4

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();

        }

    }

}
JAP
  • 405
  • 2
  • 11

2 Answers2

2

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>
Ethan Hermsey
  • 930
  • 4
  • 11
  • Ethan, thank you very much for your answer. You were the person who cracked this problem for me, so I know it is just an oversight somewhere in your current version of the code, but as is in your answer, it doesn't do what it is intended to do. After your very helpful advice, here is where we left it on our prior conversations: https://editor.p5js.org/Mathcurious/sketches/0LdSgriz5 – JAP Jan 11 '22 at 00:08
  • 1
    Well, it's literally the same code as yours. The only difference is that you turned the steerstrength up to 15 ;) – Ethan Hermsey Jan 11 '22 at 12:38
1

I found the mixing of coordinate systems very confusing in your code. I think it is better to have the particles and the flow field vectors both exist in the same coordinate system, here is an example:

// The number of pixels between rows and columns
const scl = 35;
let cols, rows;
let fr;
let particles = [];
let flowfield;

let minX, maxX, minY, maxY;

function setup() {
  createCanvas(windowWidth, windowHeight);
  cols = floor(width / scl);
  rows = floor(height / scl);
  
  minX = -cols / 2;
  maxX = cols / 2;
  minY = -rows / 2;
  maxY = rows / 2;
  
  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 center
  scale(1, -1); // flips the y values so y increases "up"
  background(255);
  for (let r = 0; r < rows; r++) {
    let y = map(r, 0, rows - 1, minY, maxY);
    for (let c = 0; c < cols; c++) {
      let x = map(c, 0, cols - 1, minX, maxX);
      let index = c + r * cols;
      
      // Notice I'm normalizing these vectors so that they don't get larger further from the center, I'm also making there magnitude quite small so that you don't get excessive acceleration
      let v = createVector(y, -x).normalize().mult(0.0001);
      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();
  }
}

class Particle {
  constructor() {
    // Use the same coordinate system for Particle position as is used for flow field vectors
    this.pos = createVector(random(minX, maxX), random(minY, maxY));
    this.vel = createVector(0, 0);
    this.acc = createVector(0, 0);
    // keep in mind that his is in the scale of the width / 35 coordinate system
    this.maxspeed = 0.05;
    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) {
    // Map from X/Y space into rows and columns (constrain to the bounds of the flow field)
    var c = round(map(this.pos.x, minX, maxX, 0, cols - 1, true));
    var r = round(map(this.pos.y, minY, maxY, 0, rows - 1, true));
    var index = c + r * cols;
    var force = vectors[index];
    // I think you could simplify the code a bit by cutting out the acc vector and just updating velocity directly here.
    this.applyForce(force);
  }

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

  show() {
    noStroke();
    fill("rgba(100,0,255,.5)");
    circle(this.pos.x * scl, this.pos.y * scl, this.size);
    this.updatePrev();
  }

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

  edges() {
    /*
    // the toroidal mapping (wrapping from the right edge to the left edge) results
    // in some pretty chaotic behavior
    if (this.pos.x > maxX) {
      this.pos.x = minX;
      this.updatePrev();
    }
    if (this.pos.x < minX) {
      this.pos.x = maxX;
      this.updatePrev();
    }
    if (this.pos.y > maxY) {
      this.pos.y = minY;
      this.updatePrev();
    }
    if (this.pos.y < minY) {
      this.pos.y = maxY;
      this.updatePrev();
    } */
    
    // Let's try bouncing instead
    // Nope, this is pretty chaotic as well
    if (this.pos.x > maxX) {
      this.vel.x *= -1;
      this.pos.x = maxX;
    }
    if (this.pos.x < minX) {
      this.vel.x *= -1;
      this.pos.x = minX;
    }
    if (this.pos.y > maxY) {
      this.vel.y *= -1;
      this.pos.y = maxY;
    }
    if (this.pos.y < minY) {
      this.vel.y *= -1;
      this.pos.y = minY;
    }
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

The behavior may not be what you were hoping for, but I think it is basically "correct."

Paul Wheeler
  • 18,988
  • 3
  • 28
  • 41
  • Thank you, Paul. I played your code, and if you wait long enough the particles seem to bounce off the edges. Aside from this, there is tons I can learn from the code. At this point I have the code exactly as I envisioned it thanks to the help on another platform from one of the users. If he doesn't post the answer here, I will do it myself with appropriate credit to him. This is the [solution I was after](https://editor.p5js.org/Mathcurious/sketches/0LdSgriz5). – JAP Jan 09 '22 at 23:13