RIGHT:寄稿:東條遼平 噴水のエフェクトを作ってみる * 簡単な物理法則とぼかし [#ac1358b6] 実際の噴水とは随分違いますが、それなりに動いていると思います。 色の選び方や、残像を作ってぼかしているあたりが少々面倒ではありますが、 炎と同じく水滴の動きの理論自体は大変簡単です。 #ref(img1.png,nolink) #ref(img2.png,nolink) 高校物理で習う上記の式を使います。これで初速度と噴出角度、経過時間がわかれば、 水滴の座標を知ることができ、実装することができそうです。 ただし、初速度、経過時間はそれぞれ単位がメートル/秒と秒ですので、 それを画面上でどう置き換えるかにもよるのですが、 得られた値を座標にし、経過時間をループ回数などとすれば、 重力加速度gが9.8では恐らく残念な結果になると思います。 市販のゲームですらジャンプした直後は重力と逆向きに加速しているぐらいですから、 自分の納得のいく見え方になるように柔軟に値を変更する事も大切です。 それでは噴水において水滴のそれぞれの動きを考えてみます。 物理法則は上記の式で良いのですが、初速度や噴出角度が同じでは面白くありません。 初速度に関しては最大の初速度を決めておき、その範囲内でランダムにそれぞれの 初速度を決め、角度についても一定の範囲内でランダムに決める事にします。 また、水滴の座標に点をプロットするだけでは余りに御粗末です。 そのため残像を付けたり、炎のときのようにぼかしてみました。 これに色を付けることでライトアップされたような噴水ができあがります。 それではソースコードを見ていきます。今回メインのFountainクラスに加えて それぞれの水滴を扱うDropofwaterクラス、色情報を扱うPalletcolorクラスを 作成しました。 public double getX(){ return -velocity*Math.cos(Math.toRadians((double)degree))*time; } public double getY(){ return -velocity*Math.sin(Math.toRadians((double)degree)) *time + 0.8*time*time/2; } これはDropofwaterクラスのメソッドの一部です。 それぞれの座標を得るために上記の式を計算しているだけです。 注意点としてはcosやsinの関数は引数としてラジアンを取るためにtoRadiansを使用して 度数をラジアンに変更しています。また、画面上の左上が原点となり、右方向にX軸 した方向にY軸をとりますので符号に気を付けてください。 public void setisonscreen(boolean isonscreen){ this.isonscreen = isonscreen; } public boolean isOnscreen(){ return isonscreen; } これらのメソッドは、水滴が画面より外にでた事を調べるために使用します。 水滴を無限に作成することはできないので、画面から外れたものから順に 発射し直す必要があります。isOnscreenの返り値の真偽によって判断します。 public void timeInc(){ time++; } 経過時間もそれぞれのインスタンス毎に管理すべき値です。 メソッドを作成しておきます。 public Palletcolor(int colornum, int gradationnum){ this.colornum = colornum; this.gradationnum = gradationnum; nowcolor = (int)(colornum*Math.random()); color = new Color[colornum][]; for(int i=0; i<colornum; i++){ color[i] = new Color[gradationnum]; for(int j=0; j<gradationnum; j++){ color[i][j] = Color.getHSBColor((float)i/colornum, 1-(float)j/gradationnum, (float)j/gradationnum); } } } これはPalletcolorクラスのコンストラクタです。今回も色の決定は大変重要です。 RGBではなくHSBのパラメータで色を決定しています。 colornumは扱う色相の種類の数、gradationnumはそれぞれの色相につき どれだけのグラデーションを持つかという変数です。nowcolorは現在の 噴水の色で最初はランダムに設定します。 それではいよいよメインのクラスです。 int image[]; //RGB情報をもった配列 int pallet[]; //対応するピクセルが使っているパレットのインデックスを格納 int position[][]; //水滴の位置 imageの配列の情報を用いて画面のイメージを作成しています。 palletはPalletcolorクラスで作成したグラデーションの番号を保持しています。 そのためこの値が変化しなくても色相が変化すれば色はかわります。 positionは各水滴の位置を保持しています。多次元配列となっており 過去数回分の水滴の位置を保持しておきます。 if(System.currentTimeMillis() - timerstart > 5000){ palletcolor.setnextColor(); nowmaxvelocity = (int)(maxvelocity * Math.random()); timerstart = System.currentTimeMillis(); } run内での処理です。毎回同じ色や初速度では面白くないので 指定した時間が経過する度色相を次のものにし、各々の水滴が取り得る 最大の初速度をランダムでnowmaxvelocityに保持しています。 //残像の作成 for(int i=0; i<AFTERIMAGENUM; i++){ for(int j=0; j<width*height; j++){ position[AFTERIMAGENUM-i][j] = (int)(position[AFTERIMAGENUM-1-i][j]*4/5.0); } } //水滴の現在位置のクリア for(int i=0; i<width*height; i++) position[0][i] = 0; 時間的に新しいものを1つずつ古い方に送り、次の位置を保持するために現在位置を クリアします。古い方に送る際に4/5倍しているのは残像ははっきり見えない方がいいという 考えです。 //画面の上部と左右の端は値が変化しないためクリアする for(int i=0; i<width; i++) pallet[i] = 0; for(int i=0; i<height; i++) pallet[i*width] = 0; for(int i=0; i<height; i++) pallet[i*width + width-1] = 0; 画面の端は値が無くならないので、うまくぴったり端の座標を取った水滴は消えることがありません。 それでは都合が悪いのでクリアしています。下部は水を張っているのでクリアしてはいけません。 //前回描画された情報の上に新しい水滴を残像から順に描画 for(int i=0; i<AFTERIMAGENUM+1; i++){ for(int j=0; j<width*(height-1); j++){ if(position[AFTERIMAGENUM-i][j]!=0) pallet[j] = position[AFTERIMAGENUM-i][j]; } } 古い残像から順に書き込んでいます。if文で分岐させているのでわかると思いますが、 値が無い部分は書き込んではいけません。というのはpalletには画面のそれぞれの ピクセルに対応させてカラーパレットの番号を保持しています。 また毎回周りのピクセルの値と平均を取りながらぼかしているため palletの値はクリアされず、前の状態を保ちながら新しい値を追加しています。 そのため値がない部分も書き込んでしまうとせっかくぼけさせたデータが消えてしまいます。 //ぼやけさせる for(int i=1; i<height-1; i++){ for(int j=1; j<width-1; j++){ pallet[i*width + j] = (pallet[i*width + j] + pallet[(i-1)*width + j] + pallet[i*width + j-1] + pallet[i*width + j+1] + pallet[(i+1)*width + j])/5; } } 値を平均化しています。といってもここで値を混ぜているわけではなく、 Palletcolorクラスにてパレット番号の小さいもの程明度や彩度を 低くしています。 - &ref(Fountain.java); - &ref(Dropofwater.java); - &ref(Palletcolor.java);