はじめに
和名「メタボール」英語名「Blob」で呼ばれる、CGでよく登場するやつです。
環境
Processing 4
ソースコード
chatGPTに書いてもらったそのままです。
アニメーションしてくれます。
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
これをグラフ化する。
以下順に、r=20,40,60のときのグラフ。
中心100からr離れた点で、sum=1となっていることに注目。
しきい値と色の調整で見せ方が変わる
// しきい値を設定
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);
}
滑らかな表示
chatGPTが、滑らかにできると提案してきたので、してもらいました。
改良点
✅ map(sum, 0, 3, 0, 255) で sum の値を 0~255 のグレースケールに変換
✅ constrain(intensity, 0, 255) でピクセルの明るさを制限
✅ 白黒の滑らかなメタボールの流れるアニメーション を実現
// しきい値処理の代わりにmap()を使って滑らかなグラデーションを作成
float intensity = map(sum, 0, 3, 0, 255); // 値をグレースケールにマッピング
intensity = constrain(intensity, 0, 255); // 色の範囲を制限
pixels[y * width + x] = color(intensity);
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)
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;
}
}