[Go to “teaching” in Morimoto Lab]
ボールをバウンドさせよう
ー物理、for文、配列ー
前回、「ボールをバウンドさせよう」では、基礎的な内容でボールの跳ね返りを実現しました。
前回の内容に加えて、今回は簡易物理シミュレーション、for文や配列を使ったボールの増殖です。
ボール同士の衝突はチャレンジできる人だけチャレンジしてみましょう!
繰り返しの処理をするための命令がforです。
とても重要かつ特徴的です。
コンソールというProcessingのエディター(エディタの下部の黒い画面)に字を書きます。
size(300,300);
for(int i=0 ; i < 5; i++)//(2)
{
println(i);//(1)
}//(3)
(1) printlnでコンソールに0~4の数字を出力するプログラム。(printと違って改行する)
(2) forの丸括弧内はセミコロン(① ; ② ; ③)で3つに分かれる。
①にはforループのはじまりを示すデータの定義と初期値、
②にはforループの終わりを示すデータの範囲、
③にはforループ中のデータの変化の仕方、を記述する。
(3) 中括弧{}内には、一回のループの間にする処理を記述、
これが何度も繰り返し呼び出されるので、
0,1,2,3,4と出力される。
下記は円を一定間隔で描画するプログラム。
変数iに適当な定数を乗算して、x座標にすると一定間隔で描画されます。
size(300,300);
for(int i=0 ; i < 5; i++)
{
ellipse(i*200+100,100,100,100);
//楕円の中心のx座標、y座標、横幅、縦幅
}
for文の中にfor文をいれる。
一つのfor文で一列分描画し、
もう一つのfor文でそれを縦方向に増やす。
for(int j=0 ; j < 5; j++)
{
for(int i=0 ; i < 5; i++){
ellipse(i*200+100,j*200+100,100,100);
}
}
ifは「もし〇〇だったら△△する」を実現する命令です。
以下のウェブを見てexamplesの内容をやってみよう。
if : https://processing.org/reference/if.html
ついでにforとifを組み合わせてcontinueを使う方法を知っておきましょう。
リンク先のページcontinueの例では、
「forでiが0, 10, 20, … 90となっていくときに、iが70のときだけcontinue以下の処理をスキップする」
というものになっています↓(iが70のときだけ線が描かれていません)
continueより上(前)のforの括弧内の処理は実行されるということを見るために、ifより前にprintでiの値を書き出してみると以下のようになります。
for (int i = 0; i < 100; i += 10) {
print(i+", ");//この行だけ追加
if (i == 70) {
continue;
}
line(i, 0, i, height);
}
コンソールに書き出される内容は
→0, 10, 20, 30, 40, 50, 60, 70, 80, 90,
ifより前の処理はiが70のときでも処理されています。
elseはifと一緒に用い、「もし〇〇だったら△△する、それ以外の時は××する」を実現する命令です。
以下のウェブを見てexamplesの内容をやってみよう。
else : https://processing.org/reference/else.html
比較演算子はifと一緒によく用いられ、「もし〇より△が大きかったら」を実現する命令です。
a<b .. aはbより小さい
a>b .. aはbより大きい
a<=b .. aはb以下の a>=b .. aはb以上の
a!=b .. aはbでない a==b .. aはbと同じ
比較演算子はifと一緒によく用いられ、「もし〇より△が大きかったら」などを実現する命令です。
a+b .. 足し算
a-b .. 引き算
a*b .. 掛け算
a/b .. 割り算
a%b .. 割り算の余り
a=b .. 代入。aをbの値にする
a++ .. a=a+1と同じ
a– .. a=a-1と同じ
1. 以下のような結果を2重でないfor文で作りなさい。
2. ボールを10個以上並べ、3の倍数番目のときだけ違う色で表示。
3の倍数を見つけるのには%を使います。
色の指定にはfill()やstroke()などを使います。
3. 2重forループで円を画面上に敷き詰め、一定の倍数などの条件のときだけ、色を変える。
沢山のボールをアニメーションさせる時、それぞれの直径や速度が必要です。
ボールが10個あれば、10個の直径、10個の速度…。
配列を知らなければ、速度だけの場合でも、定義は以下のようにたくさんになります。
int vel0;
int vel1;
int vel2;
int vel3;
int vel4;
int vel5;
int vel6;
int vel7;
…(以下略)
配列なら以下のように、一度に10個のデータを作れます。
int [] vel = new int [10];
使うときは以下のように一つずつ使いますが、
vel[0]=0;
vel[1]=0;
以下のように一度のforで全ての配列要素にアクセスできます。
for(int i=0 ; i < 10; i++)
{
vel[i]=0; //代入の例
int a = vel[i] + 10; //計算の例
}
配列はデータをいれる棚↓みたいなもので、
たくさんのデータを一気に扱うことができます。
[0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
!注意!
10個の配列を作り、11個目のデータにアクセスすると、エラーになる。
配列は0番目からデータができるので、10個作ったらvel[0]~vel[9]が作られる
(つまりvel[10]はない)
以下は、配列を使ったたくさんのボールを描画する例↓
float[] bx=new float[10]; //☜定義・生成 define & creation
float[] by=new float[10];
float bs=100;
void setup(){
size(1000,1000);
for(int i=0 ; i < 10; i++){//初期化 initialize
bx[i] = i*100;
by[i] = 200;
}
}
void draw(){
background(255);
for(int i=0 ; i < 10; i++){//描画 draw
ellipse(bx[i], by[i], bs, bs);
}
}
ここではボールの位置を配列にしています。
半径は100で全て同じです。
位置と半径はsetupとdrawのどちらでも使いたいのでグローバル変数として定義しています。
同じ挙動をsetupやdrawを使わなくてもできますが、あとからボールを動かすことを考えて、ここではsetupでボールの位置を決めて、drawで描画する例を載せています。
半径の異なる10個のボールを、配列を使って作ってください。
※ 半径を変える方法はいくつかありますが、乱数を使う方法もあります。
乱数の使用方法は、以下のウェブに書いてあります。
random() : https://processing.org/reference/random_.html
一旦、配列の内容から離れて、前回の続き、ボール一つのアニメーションに重力などを考慮しましょう。
重力を考慮するというのは、重力加速度を考慮します。
これは等加速度(運動)なので、毎秒、速度が一定ずつ増えます(本当は9.8m/s2)。
以下はサンプルプログラムです。
draw内の最初のifの部分は右壁との衝突したときの処理で、ここだけ模範解答が書かれています。
その他の左壁、床、天井は不完全なので、模範解答の右壁の部分を参考に、めり込まないようにしてください。
float bx=200;
float by=100;
float vx = 10;
float vy = 0;
float ballSize = 100;
float gravity = 0.98f;//重力
void setup() {
size(500, 600);
}
void draw() {
background(255);
ellipse(bx, by, ballSize, ballSize);
vy = vy + gravity;//重力の考慮
bx = bx+vx;//速度の更新
by = by+vy;
if (bx+ballSize/2 >= width) {//右壁の球の半径分を考慮した跳ね返り
vx = -vx; //速度の反転
bx=width-1-ballSize/2; //壁の内側まで強制移動→壁に沈むのを防ぐ
}
if (bx < 0) {
vx = -vx;
}
//画面のy座標は逆なので
//↓が床とのあたり判定
if (by >= height) {
vy = -vy;
}
if (by < 0) {
vy = -vy;
}
}
”速度が一定ずつ増えます”
drawは1秒間に30回呼ばれるので、1/30秒に一回呼ばれるので、実際は9.8/30だし、そもそも単位はピクセルじゃなくてメートルなので、厳密に実世界に合わせるのはここではやめます。
でも、毎回drawが呼び出されるたびに、速度を一定ずつ加速していくと、重力のような動きが実現されます。どうして重力を考慮するとボールが床に沈むのか?
1.床に当たったと判定
2.床より内側に戻るように速度ベクトルが反転
3.重力が加わる
4.ボール位置を更新するが、速度が変化した分、戻れない
1に戻り、連続して床の下と判定される
2になり、速度が反転し、画面の外に向かう速度になり、更に沈む、の繰り返し…
まずは1つのボールで以下の1~3までやってみてください。
次に4~6ではボールを増やしていきます。
※半径は全て同じでOKです。
1. 上下左右全ての壁(床・天井を含む)に対し、ボールの半径を考慮した跳ね返りを実装する。
2. 上下左右全ての壁に対し、跳ね返るときに内側に強制移動する。
3. 空気抵抗、壁と床の摩擦、跳ね返り係数を考慮しなさい。
ヒント:
※ 空気抵抗は、常に空気と接することで速度が遅くなる。
※ 壁・床の摩擦は、それらと接するときに速度が遅くなる。
※ 跳ね返り係数は、跳ね返りが起こるときに、速度が遅くなる。
※ 重力は常に下向きですが、摩擦や空気抵抗は方向関係なく、速度が遅くなるので、足し算ではなく乗算を使う。
float air_drag = 0.99f;//空気抵抗
float wall_drag = 0.99f;//壁・床の摩擦
float reflection_coe = 0.9f;//はねかえり係数
4. 2個のボールのアニメーションを、配列を使って作りなさい。
ボール同士の衝突は、まずはなくてよいです。
できたら、3個以上に増やしてみましょう。
int numなどの変数を用意して、球の数を与えれるようにすると便利です。
5. 2個のボールが互いに衝突した時に跳ね返るようにしましょう。
まず球同志の衝突判定をします。
マウスとのあたり判定と違うのは、点と丸でなく、丸と丸になること。
判定の距離が変わってきます。
めり込みが気になる場合は、壁同様、ボール同士でも強制移動しましょう。
必ずしも配列やforを使わなくてもいいですよ。
【めり込み解消のヒント】
必須ではないですが、複数問い合わせがあったので考え方を記載します。
以下のめり込んだ2つのボールを見てみましょう。 ボールの中心位置をb1,b2、 半径r、 b1-b2の距離dとすると、
めり込んだ距離m=2*r-dです(下図の黄線の長さ)。めり込んだ分、b1を動かす場合、b2からb1に向かうベクトルはb1-b2、
これを黄線の大きさにするため単位ベクトル化すると(b1-b2)/d、 これにmを掛けると黄色矢印のベクトルが求められます。
強制移動分のベクトルがわかったので、これをb1に足せば、めり込みを解消できます。 ※この方法は一例です。
このあたりでボールの反射が正しくないことが気になった人は、GameByBall1の最後のページに正しい反射についての解説があります。最後まで読むと意外と簡単です。
6. 配列で作った3つ以上のボール同士の跳ね返りを実装しましょう。
全てのボールのペアの組み合わせで、衝突判定していきます。
そのために、全てのボールを一つずつforで見ていきつつ、その一つ一つに対して、for文で他の全部のボールとの組み合わせを作ります(2重for文を使います)。
※ 2重for文の例はこちらのExamplesの4つ目にあります。
この例の場合は、i, jがx, y座標を表していますが、
この課題では、iもjも同じボールの配列を見て行き、全通りのペア↓で衝突判定します。
※ 2重for文の中で、自分同士の衝突判定をするのはおかしいので、スキップするには、continueを
使いましょう。
continue : https://processing.org/reference/continue.html
※ めり込みが気になる場合は、壁同様、ボール同士でも強制移動する。
コンストレイント法(制約法)とペナルティ法
めり込まない位置までボールを移動する方法はコンストレイント法といいます。
考え方は簡単ですが、ボール同志の衝突などを考えたとき、たくさんのボールがあると計算が複雑になります。
ペナルティ法は、このような衝突が複雑に起こる場合でも、対処が容易で、めり込みの深さに応じて力を加える方法です。
物理的な正確さは保証されませんが、実装方法として現実的でおすすめです。
▼ペナルティ法にチャレンジ(してもいい)
ペナルティ法による実装結果例、ぷるぷるしている。
ペナルティ法による実装の参考URL:
http://30min-processing.hatenablog.com/entry/2017/01/01/000000
ペナルティ法含め、その他ボールシミュレーションの物理的実装方法についての解説の参考URL :https://qiita.com/NatsukiLab/items/476e00fea40b86ece31f
(興味がある人向け)もう少し詳しく:衝突判定 collision detection
CGにおける物理ベースアニメーションでは、物体間の衝突判定が不可欠である。
衝突判定は、球や直方体などの単純な形状(プリミティブ)が用いられる。
球による判定は最も簡単だが、直方体や凸多面体で形状を近似するものがある
→AABB、OOBB、凸包(形状の種類)。
より物体の形に近いものは正確な衝突判定になる一方で、計算コストが高いので工夫が必要になる。例えば、複雑な形状などの場合、
大まかな形状で衝突判定で衝突の可能性があるものを探し(ブロードフェーズ)、それらにだけより正確に衝突判定を行う(ナローフェーズ)方法などがある。
それぞれのフェーズでも更に様々な方法がある。単純な衝突判定では、あるフレーム(または時間)一瞬の状況のみから衝突を判定する。
しかしそれでは、物体の速度が大きいときなどに、壁をすり抜けることを回避できない。
近年よく使われているCCD (continuous collision detection)は、連続的な状況から衝突を判定する。
スイープに基づくCCDでは、前後のフレームの物体の間を埋めて、その繋がった物体を用いて衝突判定する(参考書籍の図5.49右図がわかりやすい)。
他にspeculative CCDでは、前後フレームの物体の両方を覆うような直方体(AABB)用いるなど。しかしそれぞれ長所と短所がある(参考サイト:Unityマニュアル。日本語ページあり)。
衝突判定は奥が深い…。