Search code examples
javascriptshaderp5.js

p5.js shader Metaballs issue


I've attempted to port this Shadertoy code into p5.js WebGL, everything works nicely. However instead of passing xs, ys, colorRs, etc, I'd like to pass vec2's and vec3s directly into the uniforms somehow, e.g. uniform vec2 positions[], and uniform vec3 colors[]. I've attempted mapping positions and colors just like done with radii, but nothing has worked so far, just a black screen.

Here's what I've tried in sketch.js: metaballShader.setUniform('positions', balls.map(b => [b.pos.x, b.pos.y]));, and etc for colors.

sketch.js:

let metaballShader;
let balls = [];

function preload() {
  metaballShader = loadShader("metaball.vert", "metaball.frag");
}

function setup() {
  createCanvas(680, 530, WEBGL);
  
  shader(metaballShader);
  
  balls.push(new Ball(random(width), random(height), [1, 0, 0]));
  balls.push(new Ball(random(width), random(height), [0, 1, 0]));
  balls.push(new Ball(random(width), random(height), [0, 0, 1]));
  
  metaballShader.setUniform('width', width);
  metaballShader.setUniform('height', height);
  metaballShader.setUniform('rs', balls.map(b => b.r));
  metaballShader.setUniform('colorRs', balls.map(b => b.color[0]));
  metaballShader.setUniform('colorGs', balls.map(b => b.color[1]));
  metaballShader.setUniform('colorBs', balls.map(b => b.color[2]));
}

function draw() {
  metaballShader.setUniform('xs', balls.map(b => b.pos.x));
  metaballShader.setUniform('ys', balls.map(b => b.pos.y));
  quad(-1, -1, 1, -1, 1, 1, -1, 1);
  for (const ball of balls) {
    ball.update(balls);
  }
}

ball.js:

const BASE_SPEED = 2.5;

class Ball {
  constructor(x, y, color) {
    this.pos = {
      x: x,
      y: y
    }
    
    this.angle = random(0, 2 * Math.PI);
    
    this.vel = {
      x: BASE_SPEED * cos(this.angle),
      y: BASE_SPEED * sin(this.angle)
    }
    
    this.r = random(0.4, 0.9);
    
    this.color = color;
  }
  
  update() {
    this.pos.x += this.vel.x;
    this.pos.y += this.vel.y;
    
    if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
    if (this.pos.y < 0 || this.pos.y > height) this.vel.y *= -1;
  }
}

metaball.vert:

attribute vec3 aPosition;

uniform float width;
uniform float height;

varying highp vec2 vPos;

void main() {
  gl_Position = vec4(aPosition, 1.0);

  vPos = vec2(
    (gl_Position.x + 1.) / 2. * width,
    (gl_Position.y + 1.) / 2. * height
  );
}

metaball.frag:

precision highp float;

#define BALLS 3

uniform float xs[BALLS];
uniform float ys[BALLS];

uniform float rs[BALLS];

uniform float colorRs[BALLS];
uniform float colorGs[BALLS];
uniform float colorBs[BALLS];

varying highp vec2 vPos;

struct Metaball {
  float r;
  vec2 pos;
  vec3 col;
};

vec4 getDist(Metaball ball) {
  float dist = float(ball.r) / length(vPos - ball.pos);

  return vec4(ball.col * dist, dist);
}

Metaball balls[BALLS];

void main() {
  for (int i = 0; i < BALLS; i++) {
    Metaball ball;

    ball.pos = vec2(xs[i], ys[i]);
    ball.r = rs[i];
    ball.col = vec3(colorRs[i], colorGs[i], colorBs[i]);

    balls[i] = ball;
  }

  float total;
  vec3 color;

  for (int i = 0; i < BALLS; i++) {
    vec4 colorValue = getDist(balls[i]);

    total += colorValue.a;
    color += colorValue.rgb;
  }

  float threshold = total > 0.016 ? 1. : 0.;
  color /= total;

  gl_FragColor = vec4(color * threshold, 1.);
}

Any ideas? Thanks!


Solution

  • You need to use flatMap or something equivalent to flatten your arrays. For example if you were specifying the value for a uniform vec2 example[3] you would want to use something like setUniform('example', [1, 2, 3, 4, 5, 6]) as opposed to setUniform('example', [[1, 2], [3, 4], [5, 6]]). Below is a working example.

    let metaballShader;
    let balls = [];
    
    function setup() {
      createCanvas(680, 530, WEBGL);
      
      metaballShader = createShader(
        `
    attribute vec3 aPosition;
    
    uniform float width;
    uniform float height;
    
    varying highp vec2 vPos;
    
    void main() {
      gl_Position = vec4(aPosition, 1.0);
    
      vPos = vec2(
        (gl_Position.x + 1.) / 2. * width,
        (gl_Position.y + 1.) / 2. * height
      );
    }`,
        `
    precision highp float;
    
    #define BALLS 3
    
    uniform highp vec2 positions[BALLS];
    
    uniform float rs[BALLS];
    
    uniform vec3 colors[BALLS];
    
    varying highp vec2 vPos;
    
    struct Metaball {
      float r;
      highp vec2 pos;
      vec3 col;
    };
    
    vec4 getDist(Metaball ball) {
      float dist = float(ball.r) / length(vPos - ball.pos);
    
      return vec4(ball.col * dist, dist);
    }
    
    Metaball balls[BALLS];
    
    void main() {
      for (int i = 0; i < BALLS; i++) {
        Metaball ball;
    
        ball.pos = positions[i];
        ball.r = rs[i];
        ball.col = colors[i];
    
        balls[i] = ball;
      }
    
      float total;
      vec3 color;
    
      for (int i = 0; i < BALLS; i++) {
        vec4 colorValue = getDist(balls[i]);
    
        total += colorValue.a;
        color += colorValue.rgb;
      }
    
      float threshold = total > 0.016 ? 1. : 0.;
      color /= total;
    
      gl_FragColor = vec4(color * threshold, 1.);
    }`
      );
      
      shader(metaballShader);
      
      balls.push(new Ball(random(width), random(height), [1, 0, 0]));
      balls.push(new Ball(random(width), random(height), [0, 1, 0]));
      balls.push(new Ball(random(width), random(height), [0, 0, 1]));
      
      metaballShader.setUniform('width', width);
      metaballShader.setUniform('height', height);
      metaballShader.setUniform('rs', balls.map(b => b.r));
      metaballShader.setUniform('colors', balls.flatMap(b => b.color));
    }
    
    function draw() {
      metaballShader.setUniform('positions', balls.flatMap(b => [b.pos.x, b.pos.y]));
      quad(-1, -1, 1, -1, 1, 1, -1, 1);
      for (const ball of balls) {
        ball.update(balls);
      }
    }
    
    const BASE_SPEED = 2.5;
    
    class Ball {
      constructor(x, y, color) {
        this.pos = {
          x: x,
          y: y
        }
        
        this.angle = random(0, 2 * Math.PI);
        
        this.vel = {
          x: BASE_SPEED * cos(this.angle),
          y: BASE_SPEED * sin(this.angle)
        }
        
        this.r = random(0.4, 0.9);
        
        this.color = color;
      }
      
      update() {
        this.pos.x += this.vel.x;
        this.pos.y += this.vel.y;
        
        if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
        if (this.pos.y < 0 || this.pos.y > height) this.vel.y *= -1;
      }
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>