アニメーション
これまでに扱ったプログラムの基本のまとめとして、HandyGraphicを使ったアニメーション表示をやってみよう。
その前に1つだけ新しい内容を。
定数(教科書4.2節)
プログラム中に数字を直接書くことがあるが、どのような意味の数字なのか分かりにくい場合があったり、同じ意味の数字が何ヶ所か出てくるような場合がある。例えば次のプログラムを見てみよう:
/***** circles.c 円をたくさん描く M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> int main() { int x; int y; HgOpen(400, 400); for (x = 50; x < 400; x += 50) { for (y = 50; y < 400; y += 50) { HgCircle(x, y, 50); } } HgGetChar(); exit(0); }
ダウンロードはこちら circles.c
このプログラムは400x400のウィンドウを開き、半径50の円を格子状に半径だけずらしながら描く。
上のプログラムを見ると、400と書かれている箇所はウィンドウの大きさ(正方形なので幅と高さは同じ)、50と書かれている箇所は円の半径であることがわかる。
では、このプログラムを修正して半径を80に変えてみよう。すると、50と書いてあった箇所を全部80に書き換えなければならなくなる。この程度のプログラムなら注意して作業すれば修正ミスしないかもしれないが、大きなプログラムになるとミスするかもしれない。さらに、半径を意図したのではない50が他にもあったら、修正するべきか修正してはいけないかを都度判断する必要が出てくる。
この問題の対処法の一つは変数を使うことである。上のプログラムは次のように書ける:
/***** circles.c 円をたくさん描く M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> int main() { int x; int y; int radius = 50; int windowSize = 400; HgOpen(windowSize, windowSize); for (x = radius; x < windowSize; x += radius) { for (y = radius; y < windowSize; y += radius) { HgCircle(x, y, radius); } } HgGetChar(); exit(0); }
数字を書くより手間が増えているが、プログラムの意図は分かりやすくなるし、値の変更も楽になる。例えば円の半径を80に変えるなら、radius = 80;とするだけで良い。
これで良いような気もするが、この方法には欠点がある。一つは変数の値をプログラムのどこかで書き換えてしまわないように注意する必要があること。もう一つは、変数として使えるメモリを使ってしまうことである。前者の欠点は、変数の名前を特殊なものにしておく(定数には大文字ばかりの名前を付けることが多い)ことで防ぐことができる。後者の欠点は、メモリが潤沢にある環境ならば気にしなくてもよいが、常に潤沢にあるとは限らない。
C言語の場合、プリプロセッサ命令のマクロ定義 #define を使うことができる。#define の書式は次のようになる:
#define 文字列1 文字列2
プログラム中の文字列2を文字列1と定義する
(コンパイルする前に、文字列1を文字列2に置換する)
プリプロセッサ命令なので;が不要なことに注意。また、プログラムの冒頭に書かなければならない(通常、#includeの次に書く)。
#defineを使って書き換えると次のようになる:
/***** circles.c 円をたくさん描く M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; int y; HgOpen(WINDOWSIZE, WINDOWSIZE); for (x = RADIUS; x < WINDOWSIZE; x += RADIUS) { for (y = RADIUS; y < WINDOWSIZE; y += RADIUS) { HgCircle(x, y, RADIUS); } } HgGetChar(); exit(0); }
#define は定数を定義する以外の使い道もあるが、ややこしくなるので現時点では定数の定義だけにとどめておく。
アニメーションの原理
アニメーションの基本原理はパラパラマンガである。つまり、少しずつ変化する静止画を連続的に切り替えて表示することで、人間には動いているように見える。動いているように見せるポイントとしては、できるだけ高速に切り替えることと、前後の静止画で変化をできるだけ少なくすること、である。
HandyGraphicは描画速度がかなり遅いようなので、大量の絵を描くとアニメーションにならない。この件に関しては荻原先生に問い合わせ中。
例えば次のプログラムのように、中心の座標を右にずらしながら円を描いてみる:
/***** move.c 円を動かしてみる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 HgOpen(WINDOWSIZE, WINDOWSIZE); for (x = 0; x < WINDOWSIZE; x++) { HgCircle(x, WINDOWSIZE / 2, RADIUS); } HgGetChar(); exit(0); }
ダウンロードはこちら move.c
円を描く過程は見れるかもしれないが、最終的に真っ黒な帯のようになってしまう。上から円をどんどん描くだけなので、前に描いたのが残ってしまっているからである。
そこで、毎回ウィンドウの表示内容を消す関数を追加する。
/***** move.c 円を動かしてみる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 HgOpen(WINDOWSIZE, WINDOWSIZE); for (x = 0; x < WINDOWSIZE; x++) { HgClear(); // ウィンドウを消去する HgCircle(x, WINDOWSIZE / 2, RADIUS); } HgGetChar(); exit(0); }
これを実行させると、かすかに円が見えるだけで動いているようには見えない(実行するマシンの性能にもよる)。これは表示が速すぎて、人間の目に円が見える(知覚する)前に画面を消してしまっているから。「高速に切り替える」というのは、前の絵が見ている(残像ができる)状態で、途切れることなく次の絵を切り替えて表示する、ということである。
そこで、1回の表示に「溜め」を入れるために、msleep関数を使う(HandyGraphicの説明書参照)。アニメーションがなめらかに動く基準としては、1秒間に20枚以上とされているので、0.05秒だけ「溜め」を入れてみる。
/***** move.c 円を動かしてみる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 HgOpen(WINDOWSIZE, WINDOWSIZE); for (x = 0; x < WINDOWSIZE; x++) { HgClear(); // ウィンドウを消去する HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); // 0.05秒間実行を止める } HgGetChar(); exit(0); }
動くのが遅すぎるので、もうちょっと早く動くようにしてみよう。方法としては2つある。1つは「溜め」の時間を短くする方法。なめらかに動かすにはこの方法が良いが、短くしすぎると人間の目には見えなくなってしまう問題に戻ってしまう。もう一つは、円の移動速度を上げる方法。このプログラムでは、1回描くたびにxの値を1だけ増やしているが、ここの増分を5に大きくしてみる:
普通のディスプレイの画面更新速度は60Hzが多いので、表示時間(「溜め」の長さ)を短くしすぎても無意味である。
/***** move.c 円を動かしてみる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 HgOpen(WINDOWSIZE, WINDOWSIZE); for (x = 0; x < WINDOWSIZE; x += 5) { // 移動速度を5に増やしてみる HgClear(); // ウィンドウを消去する HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); // 0.05秒間実行を止める } HgGetChar(); exit(0); }
「前後の静止画で変化をできるだけ少なくすること」からすると変化が大きいため少々ちらついて見えてしまうが、致し方ないところである。
プログラムでアニメーションを描くポイントは少しずつ変化する絵を連続して描くことにあるが、そのためには描く図形を変数を使って表現し、その変数の値を変化させる。そのためには物理や数学で使うような式で表すことを考えればよい。この例の場合、円が等速直線運動をしているので円の位置xは次の式で表せる:
x = x0 + vt, x0は初期位置、vは速度、tは時刻
tが0.05[秒]、vが100[ピクセル/秒]とすると、vt = 5となる。繰り返しごとにx座標を5増やすというのは、意味的にはこのvtの分だけ位置を変化させているということになる。ウィンドウ消去と円を描くのにかかる時間を無視すると、この円は100[ピクセル/秒]の速度で等速直線運動をしている。
簡単なアニメーションの練習問題
練習1. 拡大する円
400x400の大きさのウィンドウに、中心が(200, 200)で、半径が0から300まで、5刻みで円が大きくなるアニメーションを表示するプログラムzoom.cを作成せよ。1回の描画ごとのmsleepの時間は0.05秒でよい。
実行例はこちら zoom
【重要】実行例はダウンロードしただけでは許可が無いので実行できない。ターミナルでダウンロードしたファイルのパスに移動し、次のコマンドを実行すること(他の練習および課題の実行例も同様)。
$ cd ~/Download $ chmod 755 zoom
$ ./zoom
(ブラウザからクリックすると~/Downloadディレクトリに保存される。それ以外のディレクトリに置いておきたい場合はmvコマンドを使って移動させておくこと。)
練習2. 斜めに移動する円
400x400の大きさのウィンドウに、半径50の円の中心が左下(0, 0)から右上(400, 400)に斜めに移動するアニメーションを表示するプログラムxymove.cを作成せよ。移動速度はx方向およびy方向に1回あたり5ピクセル、msleepの時間は0.05秒でよい(これ以外の値でも構わない)。
実行例はこちら xymove
条件との組み合わせ
例題:400x400の大きさのウィンドウに、半径50の円の中心が(0, 200)から右向きに速度5ピクセル/フレームで移動し、ウィンドウの右端まで達したら左向きに速度5ピクセル/フレームで移動し、(0, 200)まで戻ってくるアニメーションを表示するプログラムreflect.cを作成せよ。
右に移動してウィンドウの右端まで達するところまでは最初のプログラム例と同じである。ということは、右端に達した後に、戻ってくるアニメーションのためのコードを追加すればよい:
/***** reflect.c 円が右端まで行って戻ってくる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 HgOpen(WINDOWSIZE, WINDOWSIZE); // 右向きに移動 for (x = 0; x < WINDOWSIZE; x += 5) { HgClear(); HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); } // 左向きに移動 for (x = WINDOWSIZE; x >= 0; x -= 5) { HgClear(); HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); } HgGetChar(); exit(0); }
開始座標と終了座標は注意して決める必要がある。この例ではウィンドウの幅は移動速度でちょうど割りきれるので右端から開始するのでよいが、割りきれない場合は微妙にずれて見えることもある。表示が十分速くて小さな誤差が気にならない程度なら問題無いが。
指示された内容を実現するだけならこれでもよいが、応用性に欠ける。右向きと左向きで処理内容がまったく一緒なのでまとめる方法を考えてみよう。等速直線運動の式
x = x0 + vt
で考えると、x軸は右向きが正であるから、移動速度vが正ならば右向きに移動、負ならば左向きに移動となる。そこで、速度を変数にして、右端に達したら速度の値を変えるようにしてみる:
/***** reflect.c 円が右端まで行って戻ってくる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 int v; // x方向の速度[ピクセル/フレーム] HgOpen(WINDOWSIZE, WINDOWSIZE); // 右向きに移動 v = 5; for (x = 0; x < WINDOWSIZE; x += v) { HgClear(); HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); } // 左向きに移動 v = -5; for (x = WINDOWSIZE; x >= 0; x += v) { HgClear(); HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); } HgGetChar(); exit(0); }
さらに、右向きと左向きの繰り返しをまとめてしまうことを考える。vの値が変わるタイミングは円が右端に達したときなので、これを条件文で書いてみる。また、繰り返しの終了条件としては、円が戻ってきて左に行きすぎたら、としてみる:
/***** reflect.c 円が右端まで行って戻ってくる M.Minakuchi *****/ #include <stdio.h> #include <stdlib.h> #include <handy.h> #define WINDOWSIZE 400 #define RADIUS 50 int main() { int x; // 中心のx座標 int v; // x方向の速度[ピクセル/フレーム] HgOpen(WINDOWSIZE, WINDOWSIZE); v = 5; for (x = 0; x >= 0; x += v) { HgClear(); HgCircle(x, WINDOWSIZE / 2, RADIUS); msleep(0.05); if (x >= WINDOWSIZE) { v = -5; } } HgGetChar(); exit(0); }
(6/18訂正:25行目、x = -5;となっていましたが正しくはv = -5;です。)
このプログラムでは、基本的な移動の計算は1つにまとめ(x += v)、移動方向を変数で表し、移動方向の変化を条件で判定している。この方法の方がより物理的な表現に即している。
速度を変数としたアニメーションの練習問題
練習3. 跳ね返り続ける円
reflect.cをさらに修正して、左端まで達したら右向きに跳ね返るようにしてみよう。また、繰り返しの終了条件を設定せず、無限ループで繰り返すようにしてみよう。
実行例はこちら reflect 。停止させるにはコマンドを実行したターミナルでControl+C。
練習4. 等加速度運動
400x400の大きさのウィンドウに、半径50の円の中心が(200, 0)(200, 400)から下向きに、初期速度0で、1回の描画あたりに速度が2ピクセルずつ増えるように移動するアニメーションを表示するプログラムaccelerate.cを作成せよ。msleepの時間は0.05秒でよい。また、画面外まで移動するように、中心のy座標は-100くらいまで繰り返すようにするとよい。
(6/25 最初の座標が間違っていたので訂正しました)
実行例はこちら accelerate 。描く際の速度の値をターミナルに表示している。
高度なアニメーション
この授業では簡単なアニメーションの説明だけにとどめておくが、基本的な考え方、つまり、ある表示時刻において、描きたい図形がどのように計算されるかを式で表し、変数の値を表示時刻から計算するという方法は複雑なアニメーションでも同様である。図形の位置や大きさ以外にも色などの表示属性を変化させることもできるし、回転の計算方法を使えば図形を回すこともできる。3次元CGになるとさらにカメラや光源の位置から面がどのように見えるかを計算することになる。いずれにしても、物理的な現象を数式で表現し、それをプログラムで計算させる、ということになるので、数学や物理も勉強しておかなければならない。
課題
400x400の大きさのウインドウに、半径50の円の中心が初期位置(0, 0)、初期速度が(5, 3)で、ウィンドウの端に当たると跳ね返るように動くプログラムbound.cを作成せよ。跳ね返りは無限ループでよい。また、msleepの時間は0.05秒でよい。
実行例はこちら bound 。停止させるにはコマンドを実行したターミナルでControl+C。
補足(7/2) 「初期速度(5, 3)」というのはx軸方向(横方向)の速度成分が5、y軸方向(縦方向)の速度成分が3、という意味です。最初に端に当たるまでは1回繰り返すごとに、x座標が5、y座標が3ずつ増えます。端に当たったら反対向き、すなわち-5や-3に変わります。x軸方向とy軸方向をそれぞれ別に扱えばよいです。
ヒント:練習3のプログラムに、y方向の移動を追加する。
提出期限:7月7日(月) 13:15