0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

メタボールBlobの描き方

Last updated at Posted at 2025-02-27

はじめに

和名「メタボール」英語名「Blob」で呼ばれる、CGでよく登場するやつです。

image.png

環境

Processing 4

ソースコード

chatGPTに書いてもらったそのままです。
アニメーションしてくれます。

Processing
int numMetaballs = 5; // メタボールの数
Metaball[] metaballs;

void setup() {
  size(600, 600);
  metaballs = new Metaball[numMetaballs];
  
  for (int i = 0; i < numMetaballs; i++) {
    metaballs[i] = new Metaball(random(width), random(height), random(20, 60));
  }
}

void draw() {
  background(0);
  loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      float sum = 0;
      
      for (Metaball m : metaballs) {
        float dx = x - m.x;
        float dy = y - m.y;
        float d = sqrt(dx * dx + dy * dy);
        sum += m.r * m.r / (d * d + 1); // ボールの影響を計算
      }
      
      // しきい値を設定
      if (sum > 1.0) {
        pixels[y * width + x] = color(255);
      } else {
        pixels[y * width + x] = color(0);
      }
    }
  }
  
  updatePixels();

  for (Metaball m : metaballs) {
    m.update();
  }
}

class Metaball {
  float x, y, r;
  float vx, vy;
  
  Metaball(float x, float y, float r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = random(-2, 2);
    this.vy = random(-2, 2);
  }
  
  void update() {
    x += vx;
    y += vy;
    
    if (x < 0 || x > width) vx *= -1;
    if (y < 0 || y > height) vy *= -1;
  }
}

プログラムの説明
1.Metaball クラスを作成し、ボールの位置や速度を管理
2.draw() 内で各ピクセルの影響値を計算し、しきい値を超えたら白く描画
3.update() でボールを動かし、端で反射するように調整

解説

メタボールの生成時に移動速度vx,vyを乱数で決めている。
メタボールの半径rは20~60の範囲で、乱数で決めている。
メタボールの移動はupdate()で行っており、画面外では、速度成分を反転させている。
すべてのピクセルにおいて、メタボールの計算式の値の合計sumを計算し、1より大きければ白、それ以外は黒で塗っている。

ここで計算式とは、塗りたいピクセルとメタボールとの距離をd、メタボールの半径をrとしたとき、以下の式で表されるものを使っている。

\frac{r^2}{d^2+1}

どんな式でもいいと思うが、この式の場合、
・0除算の心配がない
・d=∞で0になる
・計算量が少ない
という、特徴がある。

この式について、x方向に絞って、エクセルで計算する。
F2にメタボールの中心座標
G2にメタボールの半径
A列が各ピクセルのx座標
B列が差dx
C列が距離d
D列がsum

image.png

これをグラフ化する。
以下順に、r=20,40,60のときのグラフ。
中心100からr離れた点で、sum=1となっていることに注目。

image.png
image.png
image.png

しきい値と色の調整で見せ方が変わる

   // しきい値を設定
  if (sum > 2.0) {
    pixels[y * width + x] = color(0);
  } else if(sum > 1.0){
    pixels[y * width + x] = color(255);
  } else {
    pixels[y * width + x] = color(0);
  }

image.png

滑らかな表示

chatGPTが、滑らかにできると提案してきたので、してもらいました。

改良点
✅ map(sum, 0, 3, 0, 255) で sum の値を 0~255 のグレースケールに変換
✅ constrain(intensity, 0, 255) でピクセルの明るさを制限
✅ 白黒の滑らかなメタボールの流れるアニメーション を実現
image.png

Processing
// しきい値処理の代わりにmap()を使って滑らかなグラデーションを作成
float intensity = map(sum, 0, 3, 0, 255); // 値をグレースケールにマッピング
intensity = constrain(intensity, 0, 255); // 色の範囲を制限
pixels[y * width + x] = color(intensity);

メタリックにもAIで挑戦しましたが、無理でした。
image.png

image.png

image.png

image.png

image.png

image.png

image.png

3番目のソースコードです
int numMetaballs = 20;
Metaball[] metaballs;
PVector lightDir = new PVector(-0.5, -0.5, 1); // 光源の方向(左上から照射)

void setup() {
  size(600, 600, P2D);
  metaballs = new Metaball[numMetaballs];

  for (int i = 0; i < numMetaballs; i++) {
    metaballs[i] = new Metaball(random(width), random(height), random(30, 80));
  }

  lightDir.normalize(); // 光の方向を正規化
}

void draw() {
  background(0);
  loadPixels();

  float[][] field = new float[width][height];

  // 各ピクセルの sum を事前計算
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      float sum = 0;
      
      for (Metaball m : metaballs) {
        float dx = x - m.x;
        float dy = y - m.y;
        float d = sqrt(dx * dx + dy * dy);
        sum += m.r * m.r / (d * d + 1);
      }

      field[x][y] = sum; // フィールドに保存
    }
  }

  // 画素ごとに法線を計算しライティングを適用
  for (int y = 1; y < height - 1; y++) {
    for (int x = 1; x < width - 1; x++) {
      float sx = field[x + 1][y] - field[x - 1][y]; // X方向の勾配
      float sy = field[x][y + 1] - field[x][y - 1]; // Y方向の勾配
      PVector normal = new PVector(-sx, -sy, 1); // 法線を形成
      normal.normalize();

      // 拡散光 (Diffuse)
      float diffuse = max(0, normal.dot(lightDir));

      // 鏡面反射 (Specular)
      PVector reflectDir = PVector.sub(lightDir, normal.mult(2 * normal.dot(lightDir)));
      float specular = pow(max(0, reflectDir.z), 10) * 255; // シャープな反射光

      // メタリックベースカラー (暗めのコントラスト)
      float metallicBase = map(field[x][y], 0, 3, 30, 220);
      metallicBase = constrain(metallicBase, 30, 220);

      // ライティングを適用
      float r = metallicBase * diffuse + specular;
      float g = metallicBase * diffuse + specular;
      float b = metallicBase * diffuse + specular;

      // 反射光の強調 (青みを足して水銀風に)
      if (specular > 200) {
        r += 50;
        g += 50;
        b += 100;
      }

      pixels[y * width + x] = color(constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
    }
  }

  updatePixels();

  for (Metaball m : metaballs) {
    m.update();
  }
}

class Metaball {
  float x, y, r;
  float vx, vy;

  Metaball(float x, float y, float r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vx = random(-2, 2);
    this.vy = random(-2, 2);
  }

  void update() {
    x += vx;
    y += vy;

    if (x < 0 || x > width) vx *= -1;
    if (y < 0 || y > height) vy *= -1;
  }
}

メタボール曲線と円の比較

上記式でメタボールを定義した場合の、円との比較
r=100のとき、メタボール距離別に
200
223(100√5)
250
283(200√2)
300
316(100√10)
346(200√3)

metaball_vs_arc_200.png
metaball_vs_arc_223.png
metaball_vs_arc_250.png
metaball_vs_arc_283.png
metaball_vs_arc_300.png
metaball_vs_arc_316.png
metaball_vs_arc_346.png

metaball_vs_arc
int numMetaballs = 2; // メタボールの数
Metaball[] metaballs;

void setup() {
  size(700, 500);
  metaballs = new Metaball[numMetaballs];

  metaballs[0] = new Metaball(200, 200, 100);
  metaballs[1] = new Metaball(200+200, 200, 100);//①
  //metaballs[1] = new Metaball(200+100*sqrt(5), 200, 100);//②
  //metaballs[1] = new Metaball(200+250, 200, 100);//③
  //metaballs[1] = new Metaball(200+200*sqrt(2), 200, 100);//④
  //metaballs[1] = new Metaball(200+300, 200, 100);//⑤
  //metaballs[1] = new Metaball(200+100*sqrt(10), 200, 100);//⑥
  //metaballs[1] = new Metaball(200+200*sqrt(3), 200, 100);//⑦

  background(0);
  loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      float sum = 0;

      for (Metaball m : metaballs) {
        float dx = x - m.x;
        float dy = y - m.y;
        float d = sqrt(dx * dx + dy * dy);
        sum += m.r * m.r / (d * d + 1); // ボールの影響を計算
      }

      // しきい値を設定
      if (sum > 1.0) {
        pixels[y * width + x] = color(255);
      } else {
        pixels[y * width + x] = color(0);
      }
    }
  }
  updatePixels();

  //grid
  stroke(0, 255, 0);
  for (int y=100; y<height; y+=100) {
    line(0, y, width, y);
  }
  for (int x=100; x<width; x+=100) {
    line(x, 0, x, height);
  }

  stroke(255, 0, 0);
  translate(200,200);
  noFill();
  circle(0, 0, 200);//①
  //circle(100, -200, 200);//
  circle(200, 0,200);//②
  //circle(100*sqrt(5), 0,200);//②
  //circle(250, 0, 200);//②
  //circle(200*sqrt(2), 0, 200);//②
  //circle(300, 0, 200);//②
  //circle(100*sqrt(10), 0, 200);//②
  //circle(200*sqrt(3), 0, 200);//②
  circle(100, sqrt(200*200-100*100), 200);//③
  //circle(50*sqrt(5), sqrt(200*200-pow(50*sqrt(5),2)), 200);//③
  //circle(125, sqrt(200*200-125*125), 200);//③
  //circle(100*sqrt(2), 100*sqrt(2), 200);//③
  //circle(150, sqrt(200*200-150*150), 200);//③
  //circle(50*sqrt(10), sqrt(200*200-50*50*10), 200);//③
  //circle(100*sqrt(3), 100, 200);//③

  //save("metaball_vs_arc_200.png");//200
  //save("metaball_vs_arc_223.png");//223.60679
  //save("metaball_vs_arc_250.png");//250
  //save("metaball_vs_arc_283.png");//282.8
  //save("metaball_vs_arc_300.png");//300
  //save("metaball_vs_arc_316.png");//316.227766//sqrt10
  //save("metaball_vs_arc_346.png");//346.410161514
}


class Metaball {
  float x, y, r;
  float vx, vy;

  Metaball(float x, float y, float r) {
    this.x = x;
    this.y = y;
    this.r = r;
  }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?