p5.js 二维矢量场影响下的粒子行为表现出较差的响应

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

在使用 p5js here 表示 2D 物理矢量场时,我已经完成了一半。另一半是让随机粒子动态地跟随矢量场的力,我遇到了很多问题。我已经尝试了多种方法来考虑粒子的环绕,以及我正在将绘图的原点转换到 canvas 的中心这一事实。然而,粒子似乎受场中单个矢量的影响最小,并最终沿着 x 轴行进,并带有轻微的颠簸。

事实上,我在 JS 方面是全新的,这无助于拼接在线提供的几个演示文稿中的所有这些元素,我将不胜感激任何关于可能出了什么问题以及我应该关注的地方的建议。

这是我目前所拥有的:一个文件 sketch.js 对应于我上面引用的自己的答案:

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

和第二个名为 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();
    }

  }

}

本次编辑更新代码的模拟开头还不错:

但很快所有粒子都与 x 轴上的最后一行对齐。所以我想我需要一些帮助来理解 flow fields 或缩小底部矢量的影响。


Ethan Hermsey 完美地为我解决了这个绘图问题。在这一点上,毫无疑问,由于代码中的一些小故障,或者一些沟通不畅,接受的答案中的代码恰好导致了与提出问题所需的输出不同的输出,以及 Ethan 自己为我解决的代码.所以仅供参考,这是预期的效果:

生成如下:

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

        }

    }

}

我发现您的代码中坐标系的混合非常混乱。我觉得粒子和流场矢量最好在同一个坐标系下,举个例子:

// 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>

该行为可能不是您所希望的,但我认为它基本上是“正确的”。

我们之前通过 reddit 有过联系。我想post在这里回答。

就像保罗在另一个答案中所说的那样,这两个不同的坐标系令人困惑并导致了主要问题。

  • 当您生成流场时,从 [-cols, cols] 循环,而场只有 [0, cols] 大。这意味着 3/4 的向量放置在数组中数组外的无效位置(或者,根本没有放置并且网格仅填充公式的一个象限)。

  • 在Particle.follow()中索引计算不正确,所以它会尝试在不存在的无效位置访问流场数组,给出越界异常.

如果需要保留 2 个坐标系,您必须不断重新映射粒子中的 x 和 y 值 class,但也在场生成循环中,以获得正确的结果。

我喜欢 Paul 对矢量进行归一化并使用 map() 将粒子坐标重新映射到流场坐标的方式,我也这样做了。

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>