メタボールBlobの描き方

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/0a84bfec-baf1-472d-8649-03c032ed527b.png)

# 環境
Processing 4

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

```java: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としたとき、以下の式で表されるものを使っている。
```math
\frac{r^2}{d^2+1}
```
どんな式でもいいと思うが、この式の場合、
・０除算の心配がない
・d=∞で０になる
・計算量が少ない
という、特徴がある。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/ec550387-922b-438f-a930-94d53714ae68.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/2d8e22e9-b002-422c-84a8-b3bf38615531.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/31ffaa51-60df-4f98-9f9c-ff3cc6fbfac5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/6ed856d4-551c-4eee-babd-ced898308d79.png)

# しきい値と色の調整で見せ方が変わる
```java
   // しきい値を設定
  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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/5f610f03-56cd-4ae5-9a50-fc92851979ae.png)

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

改良点
✅ map(sum, 0, 3, 0, 255) で sum の値を 0～255 のグレースケールに変換
✅ constrain(intensity, 0, 255) でピクセルの明るさを制限
✅ 白黒の滑らかなメタボールの流れるアニメーション を実現
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/30ac5239-24b5-4548-b503-b52f83d9e447.png)
```java:Processing
// しきい値処理の代わりにmap()を使って滑らかなグラデーションを作成
float intensity = map(sum, 0, 3, 0, 255); // 値をグレースケールにマッピング
intensity = constrain(intensity, 0, 255); // 色の範囲を制限
pixels[y * width + x] = color(intensity);
```
メタリックにもAIで挑戦しましたが、無理でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/edf2cd80-bae9-4bf5-9498-1317bf8e9fad.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/76e1d29a-9aaa-4fb7-b56c-a9dd32da4fd6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/4c0587db-c373-47ff-9bbe-7fabe03e998f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/1550f4ee-ddee-4e2c-a25f-e11883857d0a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/65ba1a7e-d81f-4d4d-aaca-3e90a3bd2406.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/a56a5f63-b31b-46af-b484-971c8bd5c123.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/e10fb804-7652-4d45-ae7b-a21ff608102d.png)

```java:３番目のソースコードです
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/8fbf075b-c207-4225-b7f6-f1e47175fd1a.png)
![metaball_vs_arc_223.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/df04f77b-2ba3-4c1d-bb02-89f019b85817.png)
![metaball_vs_arc_250.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/60fb2a8c-c427-4690-ba86-61d627e0e498.png)
![metaball_vs_arc_283.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/c556f2cd-f791-4924-b3d7-edf8f71783ba.png)
![metaball_vs_arc_300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/a4cb961f-2409-40fa-adfd-f9b47fa2a8f6.png)
![metaball_vs_arc_316.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/9d050cdd-36c6-4f5e-b4db-789f8fe87230.png)
![metaball_vs_arc_346.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/5718870d-f5e2-4d20-93b2-82968e91166d.png)

```java: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;
  }
}
```