メタボールについて
この日の作品は、画像処理によるメタボール表現を行うものでした:
#つぶやきProcessing
— Koji Saito (@KojiSaito) May 17, 2020
def setup():size(500,500);noFill();strokeWeight(2);blendMode(ADD)
def C(x,y,r):
for t in range(r):stroke(t);circle(x,y,r-t)
N=lambda t:noise(sin(t),frameCount*.01)*500
def draw():
clear()
for i in range(10):C(N(i),N(i*.3),i*15+5)
filter(THRESHOLD,.2) pic.twitter.com/V67eanbtEs
メタボールとは…と説明をしだすと、ちょっと複雑になりますが、 ひとつのメタボールの存在は空間上に影響を与え、 その影響は関数 \( f_i \) にて測定することができるーとします。
ここで \( f_i \) の添字 i は i 番目のメタボールという意味として理解して下さい。
で、この関数の引数は空間中のある位置 p とします。 つまり、\( f_i(p) \) で、空間中のある点 p の、 メタボール i による影響が測定できる、と考えます。
空間中におけるメタボールの影響は加算的であると考えます。 つまり、N 個のメタボールが存在するとき、 点 p におけるメタボールが与える影響の大きさは \( \sum_{i=1}^N f_i(p) \) にて測定できる、とします。
これをここでは関数 f と表すことにします。
メタボールによる形状は、この関数 f を用いて、 \( f(p)=c \) を満足する点 p の集合、と定義されます (c はとある定数を表すとします)。
厳密に言えば、このように定義された形状は 一般的には表面のみを表しますので、 形状内部までを表すのであれば、 \( f(p) \le c \) を満たす p の集合、と表されます。
素直な実装方法
1 つのメタボールが空間に与える影響は、 空間における電荷などを模したものを使うのが一般的です (メタボール=等電位面のアナロジー)。
しかし、ここでは単に半径 \( r_i \) のメタボールの中心位置 \( p_i \) とし、 メタボールの中心では、\( f_i \) は \( r_i \) を返し、 円周上では 0 となる関数を考えてみます。
$$ \left\{ \begin{array}{l} f_i(p_i)=r_i \newline f_i(p)=0 \hspace{1ex} ( \mbox{ただし $p$ は 円周上の点とする} ) \end{array} \right. $$
メタボールの中心と円周上以外では、中心からの距離が半径より小さい場合は r-d を、半径より大きな場合は 0 とすることにしましょう (変数 d は、空間中の任意の点 p とメタボールの中心点との距離を表しています)。
少々天下り的ですが、これをまとめると、
$$ f_i(p)=\max \left( r- \left| p-p_i \right|, 0 \right) $$
と記述できます。
メタボールは任意の次元にて定義可能です。 ここでは 2 次元空間の場合について考えてみると、 \( f_i \) は、
$$ f_i(x,y)=\max \left( r- \sqrt{ \left(x-x_i\right)^2+\left(y-y_i\right)^2 }, 0 \right) $$
と書き表されます。
N 個のメタボールの場合は、当然
$$ f(x,y)=\sum_{i=i}^N f_i(x,y)=\max \left( r- \sqrt{ \left(x-x_i\right)^2+\left(y-y_i\right)^2 }, 0 \right) $$
となります。
メタボールの内部を例えば白色で塗りつぶす場合、 全てのピクセルの (x,y) に対して上記の関数 f を計算し、 例えばその値がある値 c より大きかったら、そのピクセルを白色で塗る、 という処理となります。
プログラムで書くと次のようになります:
def setup():
size(500,500)
N=10
P=[ [random(500),random(500),random(100)] for i in range(N) ]
def f(x,y):
result=0
for i in range(N):
xi=P[i][0]
yi=P[i][1]
ri=P[i][2]
result+=max(ri-dist(x,y,xi,yi),0)
return result
def draw():
clear()
stroke(255)
c=5
for y in range(500):
for x in range(500):
if f(x,y)>c: point(x,y)
画層処理を用いた方法
素直な実装では、全てのピクセルに対して、f の値を計算する必要がありました。 しかし、関数 f の定義をよく見ると、それぞれのピクセルに対し、 関数 \( f_i \) の値を累積していっても f の値を得ることができそうです。
何を言っているのかというと、 あるピクセルの位置 (x,y) を固定し、\( f_i \) を計算するのではなく、 \( f_i \) の方、添字 i の方を固定し、 全ての (x,y) について累積的に値を計算していっても問題ない、 ということです。
実際のところ、\( f_i \) が 0 となるピクセルについては、 計算しなくても関数 f の値は変わりません。
今、\( f_i \) の定義により、\( f_i \) が 0 でない領域は、 点 ( \( x_i, y_i \) ) を中心とする半径 \( r_i \) の円の内部でした。
であれば、加色混合モードで、色を変化させながら同心円として 円領域を描画すれば、全てのピクセルに f の値が蓄積されそうです。
もちろん、ピクセルに格納されるのは任意の浮動小数点値ではなく、 Processing の場合は R,G,B 各チャネル 8 ビットの整数のようです。
なので、関数 f の値としてピクセルに格納される値の精度は 低下してしまいますが、それでもやってみる価値はありそうです (というか、実際にそれで冒頭に紹介した作品は成立しているので、 ある程度であれば問題なくメタボールの表現は可能です)。
実際のコードは以下のようになります:
def setup():
size(500,500)
noFill()
strokeWeight(2) # 注1
blendMode(ADD) # 加色混合モードを指定
def C(x,y,r):
for t in range(r):
stroke(t) # 色として fi の値を指定
circle(x,y,r-t)
N=lambda t:noise(sin(t),frameCount*.01)*500
def draw():
clear()
for i in range(10):
C(N(i),N(i*.3),i*15+5)
filter(THRESHOLD,.2)
このプログラムでは、関数 C により、 点 (x,y) を中心とする半径 r の円を、 \( f_i \) の値を用いて塗りつぶします。
関数 C で描かれる円領域は、 円周の集合体として描かれますので、 半径を 1 づつ変化させる状況では、 隙間ができてしまう可能性があります。
そのため、注 1 のところで指定しているように、 strokeWeight(2) として、円周の幅を少し太くしています。
draw 関数にある for ループを抜けた時には、 全てのピクセルに関数 f の値が格納されています。
その値を参考に、ある値以上のピクセルを可視化するため、 filter(THRESHOLD,0.2) を実行しています。
実際には、255*0.2=51 ですので、 この作品では f(x,y)>51 以上の領域を描き出していることとなります。