2017年6月4日日曜日

Processingでboid 〜初歩的な群衆シミュレーション〜

実は私,マニアとまでは行かないまでも,ディズニーは結構好きです.東京ディズニーランドなんかは,子供の頃は毎年長期休みのたびに家族で行っていましたし,大人になってからも何度か一人で行った程度には好きです.
ディズニーのアニメーション作品も,子供の頃から結構よく見ていました.親が結構ディズニー好きだったので,普段ビデオソフトは絶対買わずに,テレビ放送時に録画してすませるのに,ディズニーのアニメーションだけはビデオを買ってきて見せられたものでした.見終わった後で母親に感想を聞かれ,その時に母親が望む内容の感想を答えられないと(どのシーンを見てこのように思った.この物語の教訓を受けてこれから○○して生きていこうと思った.というような感想)急に不機嫌になり,育て方を間違えた的なことを言われるので,私は家族でディズニーのビデオを見るのがとても嫌でした.その代わり,家で留守番している時に一人で見るのは好きでした.
私の地元では,子供が見るアニメーション映画といえばほとんどディズニーくらいでした.後はドラえもんやクレヨンしんちゃんといった,テレビアニメーションの劇場版くらいで,スタジオジブリなどは,もののけ姫の頃までほとんど流行っていなかったと記憶しています.なので子供は皆当然のように公開されるディズニー映画を見ていました.
その頃公開されていた作品群は,ディズニーの長編アニメーション史で言えば,「ディズニー・ルネッサンス」と言われる時代の作品群になります.「白雪姫」から始まり,「ふしぎの国のアリス」や「ピーター・パン」等のヒット作を連発していたディズニー社は,創業者ウォルト・ディズニーがディズニーランドの建設・運営事業に熱中し始めた頃から徐々に陰りが出始めます.更に,ウォルト・ディズニーの死後,実写ハリウッド映画の台頭に押されディズニー社の人気は風前の灯火となりました.その状況を打破したのが1989年公開の「リトル・マーメイド」という作品で,これ以後の1990年代,爆発的なヒット作を飛ばし続け,現在に続く不動の人気を築いていきました.この時期が「ディズニー・ルネッサンス」で,私や私の幼馴染達がディズニー映画に親しんでいた時期です.
この時期の作品の特徴は,段階的にピクサー社の3DCGによる表現が取り入れられたことです.「リトル・マーメイド」で初めてピクサー社のプログラムを取り入れた(この時使ったのは3DCGではなく,セル画の彩色支援ソフトウェア)ディズニーは,次々とコンピュータによる表現を作品に取り入れるようになります.


©︎Disney

有名なのが「美女と野獣」の舞踏会のシーン.ダンスホールの背景を3DCGで描き,ダイナミックに視点移動することで,これまでのアニメーションでは出すのが難しかった奥行きのある映像表現を行い,世界中のアニメータを驚かせました.また「アラジン」では,飛ぶだけでなく,感情を持って動く魔法の絨毯の造形の全てが3DCGによって行われました.




©︎Disney

このように,ディズニー社を不動の地位にまで押し上げた作品群において,3DCGが大きな役割を果たしてきたと言えるでしょう.そして,この時期の3DCGを取り入れた表現において最も実験的だったと思われるのが1996年「ノートルダムの鐘」におけるラスト部分のこのシーン.

©︎Disney

この作品はご存知ヴィクトル・ユゴーの小説を原作とした作品ですが,小説とは違ってハッピーエンドとなっており,(少しネタバレ)悪者は死んで,奇形児の孤児として差別や偏見に晒されてきた主人公はついに町の人々に受け入れられます.町に平和が訪れたことを喜びながら人々がパリの街を行進し,やがて視点がだんだん遠のいていき,街全体が映ったところで物語が終了します.(このシーンだけ見たい方は,ぶっちゃけYouTubeにありますのでご覧ください.

この壮大なシーンは,群衆の動きを計算モデルによってシミュレーションし,自動生成することによって作られました.大勢の人々の動きを一人一人描き分け,一つのシーンとして纏め上げる,手書きであれば膨大な作業量が必要とされますが,それをコンピュータによる自動生成で行ってしまう,これまでの作品の奥行きやダイナミズムの表現とは違った角度のCGの活用法に,人々は改めて驚いたのでした.

群衆の動きをシミュレーションするアルゴリズムは,この作品の10年ほど前にCraig Reynoldsによって提唱されたBoidというアルゴリズムが有名です.Boidは人ではなく,鳥の群れ(っぽい動き)をシミュレーションするアルゴリズムですが,非常に自然な動きをするため,改良されて多くのアニメーションで使われています.言わば群衆シミュレーションの古典とも言えるアルゴリズム.現在では,各言語でクラスとしてそのまま使えるコードがあったりして,わざわざ実装する気にもならないようなアルゴリズムですが,改めて実際どうなっているのか復習したいと思います.

言語はProcessing.再利用しやすい実装を目指して色々と活用したいわけではなく ,どういう処理手順で実現されているか知りたいので,draw()関数にダダダダダッとアルゴリズムを書いていきます.

まずは初期化です.個体をランダムな位置に,ランダムな初速度で配置します.色も適当につけておきます.


int len=50; //個体数
int Width=960,Height=540; //解像度
float[] x=new float[len]; //個体のx座標
float[] y=new float[len]; //個体のy座標
float[] r=new float[len]; //個体の半径
float[] dx=new float[len]; //個体のx方向の移動量
float[] dy=new float[len]; //個体のy方向の移動量
// 色
float[] red=new float[len];
float[] green=new float[len];
float[] blue=new float[len];
void setup(){
    int k;
    for(k=0;k<len;k++){
        r[k]=10;
        x[k]=random(r[k]/2,Width-r[k]/2);
        y[k]=random(r[k]/2,Height-r[k]/2);
        red[k]=random(0,255);
        green[k]=random(0,255);
        blue[k]=random(0,255);
        dx[k]=random(-2,2);
        dy[k]=random(-2,2);
    }
    size(960,540);
    background(0,0,0);
}




 

いよいよdraw()関数の中にboidを書いていきます.boidは,各個体がランダム(ランダムな位置や速さ)に動いている時に,以下の条件を加えると実現できます.

① それぞれの個体が,全体の分布の中心に向かって進む
② 互いに近づきすぎたら反対へ動く
③ 他の個体の速度に合わせる

現在の各個体の位置や速度から,①〜③についての移動速度を別々に算出し,最後にそれを合計して,その個体の移動速度とします.

 float vx1=0,vy1=0,vx2=0,vy2=0,vx3=0,vy3=0; //それぞれの条件の速度
 float ax=0,ay=0; //位置の平均値
 float avx=0;avy=0; //速度の平均値

まずは①.自分以外の位置の平均を求め,自分自身が平均に向かう方向の速度を求めます.
        //① それぞれの個体が,全体の分布の中心に向かって進む
        //自分以外の個体の位置の平均を求める
        for(n=0;n<len;n++){
            if(k!=n){
                ax+=x[n];
                ay+=y[n];
            }
        }
        ax/=len-1;
        ay/=len-1;
        //自分を平均値に近づける方向を速度とする(適当に重みをつけて)
        vx1=(ax-x[k])/400;
        vy1=(ay-y[k])/400;
            }
        }
ここで最後の/400の400は定数です.この値を変えると,動きの印象が変わってきます.

次は②.自分以外の個体との距離を求め,近づいていたら,反対方向の速度を加えます.

Post to 
LiveJournal
        //② 互いに近づきすぎたら反対へ動く
        for(n=0;n<len;n++){
            if(k!=n){
                float dist=sqrt((x[k]-x[n])*(x[k]-x[n])+(y[k]-y[n])*(y[k]-y[n])); //自分とそれ以外の距離
                //距離が近かったら反対方向の速度成分を加える
                if(dist<30){
                    vx2-=(x[n]-x[k])/25;
                    vy2-=(y[n]-y[k])/25;
                }
            }
        }
<30 pre="" vx2-="(x[n]-x[k])/25;" vy2-="(y[n]-y[k])/25;"> ここでも速度を定数で割り算しています.ネットで見かけるサンプルでは,割り算していないものが多いのですが,割り算しないと,見えない壁にぶつかって跳ね返ったような動きが目立つのですが,割り算するとより自然な動きになります.

最後に③.今度は速度の平均を求め,自分自身の速度との差に応じて,速度を算出します.


        //③ 他の個体の速度に合わせる
        //自分以外の個体の現在の速度の平均を求める
        for(n=0;n<len;n++){
            if(k!=n){
                avx+=dx[n];
                avy+=dy[n];
            }
        }
        avx/=len-1;
        avy/=len-1;
        //自分の速度との差を速度成分に加える
        vx3=(avx-dx[k])/2;
        vy3=(avy-dy[k])/2;


これで①〜③の計算ができました.これらの値を現在の速度に加えていきます.この時,①〜③の値それぞれに異なった重みをつけることで,動きをより自然にします.この値も,変化させると印象が変わります.

また,速度が想像以上に早かった場合,速度の正規化を行います.

     //計算結果を各個体の速度に加える(重み付き)
        dx[k]+=vx1*r1+vx2*r2+vx3*r3;
        dy[k]+=vy1*r1+vy2*r2+vy3*r3;
        
        //速度の絶対値が4以上だったら正規化
        float velocity=sqrt(dx[k]*dx[k]+dy[k]*dy[k]);
        if(velocity>4){
            dx[k]=(dx[k]/velocity)*4;
            dy[k]=(dy[k]/velocity)*4;
        }
        //自分の速度との差を速度成分に加える
        vx3=(avx-dx[k])/2;
        vy3=(avy-dy[k])/2;

以下がソースの全体です.

int len=50; //個体数
int Width=960,Height=540; //解像度
float[] x=new float[len]; //個体のx座標
float[] y=new float[len]; //個体のy座標
float[] r=new float[len]; //個体の半径
float[] dx=new float[len]; //個体のx方向の移動量
float[] dy=new float[len]; //個体のy方向の移動量
// 色
float[] red=new float[len];
float[] green=new float[len];
float[] blue=new float[len];
void setup()
{
    int k;
    for(k=0;k<len;k++){
        r[k]=10;
        x[k]=random(r[k]/2,Width-r[k]/2);
        y[k]=random(r[k]/2,Height-r[k]/2);
        red[k]=random(0,255);
        green[k]=random(0,255);
        blue[k]=random(0,255);
        dx[k]=random(-2,2);
        dy[k]=random(-2,2);
    }
    size(960,540);
    background(0,0,0);
}

void draw()
{
    int k;
    int n;
    //速度を足し合わせる重み係数
    float r1 = 1;
    float r2 = 0.8;
    float r3 = 0.1;
    background(0,0,0);
    for(k=0;k<len;k++){
        float vx1=0,vy1=0,vx2=0,vy2=0,vx3=0,vy3=0; //それぞれの条件の速度
        float ax=0,ay=0; //位置の平均値
        float avx=0;avy=0; //速度の平均値
        noStroke();
        fill(red[k],green[k],blue[k]);
        ellipse(x[k],y[k],r[k],r[k]);
        
        //① それぞれの個体が,全体の分布の中心に向かって進む
        //自分以外の個体の位置の平均を求める
        for(n=0;n<len;n++){
            if(k!=n){
                ax+=x[n];
                ay+=y[n];
            }
        }
        ax/=len-1;
        ay/=len-1;
        //自分を平均値に近づける方向を速度とする(適当に重みをつけて)
        vx1=(ax-x[k])/400;
        vy1=(ay-y[k])/400;
        
        //② 互いに近づきすぎたら反対へ動く
        for(n=0;n<len;n++){
            if(k!=n){
                float dist=sqrt((x[k]-x[n])*(x[k]-x[n])+(y[k]-y[n])*(y[k]-y[n])); //自分とそれ以外の距離
                //距離が近かったら反対方向の速度成分を加える
                if(dist<30){
                    vx2-=(x[n]-x[k])/25;
                    vy2-=(y[n]-y[k])/25;
                }
            }
        }
        
        //③ 他の個体の速度に合わせる
        //自分以外の個体の現在の速度の平均を求める
        for(n=0;n<len;n++){
            if(k!=n){
                avx+=dx[n];
                avy+=dy[n];
            }
        }
        avx/=len-1;
        avy/=len-1;
        //自分の速度との差を速度成分に加える
        vx3=(avx-dx[k])/2;
        vy3=(avy-dy[k])/2;
        
        //計算結果を各個体の速度に加える(重み付き)
        dx[k]+=vx1*r1+vx2*r2+vx3*r3;
        dy[k]+=vy1*r1+vy2*r2+vy3*r3;
        
        //速度の絶対値が4以上だったら正規化
        float velocity=sqrt(dx[k]*dx[k]+dy[k]*dy[k]);
        if(velocity>4){
            dx[k]=(dx[k]/velocity)*4;
            dy[k]=(dy[k]/velocity)*4;
        }
        
        //求めた速度で移動
        x[k]+=dx[k];
        y[k]+=dy[k];
        
        //端まで行ったら跳ね返る
        if(x[k]-r[k]<=0){
            x[k]=r[k];
            dx[k]*=-1;
        }
        if(y[k]-r[k]<=0){
            y[k]=r[k];
            dy[k]*=-1;
        }
        if(x[k]+r[k]>=Width){
            x[k]=Width-r[k];
            dx[k]*=-1;
        }
        if(y[k]+r[k]>=Height){
            y[k]=Height-r[k];
            dy[k]*=-1;
        }
    }
}

こうして作ったboidの動きがこちらです.
単純な割に,色々変えると面白い動きになるので,遊びがいがありそうです.






0 件のコメント:

コメントを投稿