アニメーション

これまでに扱ったプログラムの基本のまとめとして、HandyGraphicを使ったアニメーション表示をやってみよう。

その前に1つだけ新しい内容を。

定数(教科書p.193〜196)

プログラム中に数字を直接書くことがあるが、どのような意味の数字なのか分かりにくい場合があったり、同じ意味の数字が何ヶ所か出てくるような場合がある。例えば次のプログラムを見てみよう:

/*****
    circles.c
    円をたくさん描く
    M.Minakuchi
*****/
#include <stdio.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();
    HgClose();
    return 0;
}

ダウンロードはこちら circles.c

このプログラムは400x400のウィンドウを開き、半径50の円を格子状に半径だけずらしながら描く。

上のプログラムを見ると、400と書かれている箇所はウィンドウの大きさ(正方形なので幅と高さは同じ)、50と書かれている箇所は円の半径であることがわかる。

では、このプログラムを修正して半径を80に変えてみよう。すると、50と書いてあった箇所を全部80に書き換えなければならなくなる。この程度のプログラムなら注意して作業すれば修正ミスしないかもしれないが、大きなプログラムになるとミスするかもしれない。さらに、半径を意図したのではない50が他にもあったら、修正するべきか修正してはいけないかを都度判断する必要が出てくる。

この問題の対処法の一つは変数を使うことである。上のプログラムは次のように書ける:

/*****
    circles.c
    円をたくさん描く
    M.Minakuchi
*****/
#include <stdio.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();
    HgClose();
    return 0;
}

数字を書くより手間が増えているが、プログラムの意図は分かりやすくなるし、値の変更も楽になる。例えば円の半径を80に変えるなら、radius = 80;とするだけで良い。

これで良いような気もするが、この方法には欠点がある。欠点の一つは、変数の値をプログラムのどこかで書き換えてしまわないように注意する必要があること。もう一つは、変数として使えるメモリを使ってしまうことである。前者の欠点は、変数の名前を特殊なものにしておく(定数には大文字ばかりの名前を付けることが多い)ことで防ぐことができる。後者の欠点は、メモリが潤沢にある環境ならば気にしなくてもよいが、常に潤沢にあるとは限らない。

C言語の場合、定数=実行時に変更する必要の無い値に名前を付けて扱うために、プリプロセッサ命令のマクロ定義 #define を使うことができる。#define の書式は次のようになる:

#define 文字列1 文字列2
プログラム中の文字列2を文字列1と定義する
(コンパイルする前に、文字列1を文字列2に置換する)

プリプロセッサ命令なので行末に;が不要なことに注意。また、プログラムの冒頭に書かなければならない(通常、#includeの次に書く)。

#defineを使って書き換えると次のようになる:

/*****
    circles.c
    円をたくさん描く
    M.Minakuchi
*****/
#include <stdio.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();
    HgClose();
    return 0;
}

#define は定数を定義する以外の使い道もあるが、ややこしくなるので現時点では定数の定義だけにとどめておく。

アニメーションの原理

アニメーションの基本原理はパラパラマンガである。つまり、少しずつ変化する静止画を連続的に切り替えて表示することで、人間には動いているように見える。動いているように見せるポイントとしては、できるだけ高速に切り替えることと、前後の静止画で変化をできるだけ少なくすること、である。

HandyGraphicでなめらかにアニメーションさせるには少々コツが必要である。少し難しいので、多少ちらついて表示されたり、表示が遅いかもしれないが、まずは単純なプログラムの書き方を説明する。

例えば次のプログラムのように、中心の座標を右にずらしながら円を描いてみる:

/*****
    move.c
    円を動かしてみる
    M.Minakuchi
*****/
#include <stdio.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();
    HgClose();
    return 0;
}

ダウンロードはこちら move.c

円を描く過程は見れるかもしれないが、最終的に真っ黒な帯のようになってしまう。上から円をどんどん描くだけなので、前に描いたのが残ってしまっているからである。

そこで、毎回ウィンドウの表示内容を消す関数を追加する。

/*****
    move.c
    円を動かしてみる
    M.Minakuchi
*****/
#include <stdio.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();
    HgClose();
    return 0;
}

これを実行させると、かすかに円が見えるだけで動いているようには見えない(実行するマシンの性能にもよる)。これは表示が速すぎて、人間の目に円が見える(知覚する)前に画面を消してしまっているから。「高速に切り替える」というのは、前の絵が見ている(残像ができる)状態で、途切れることなく次の絵を切り替えて表示する、ということである。

そこで、1回の表示に「溜め」を入れるために、HgSleep関数を使う(HandyGraphicの説明書参照)。アニメーションがなめらかに動く基準としては、1秒間に20枚以上とされているので、0.05秒だけ「溜め」を入れてみる。溜を入れる位置に注意。円を描き終わってから、一旦止めるようにする。

/*****
    move.c
    円を動かしてみる
    M.Minakuchi
*****/
#include <stdio.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);
        HgSleep(0.05);  // 0.05秒間実行を止める
    }    
    
    HgGetChar();
    HgClose();
    return 0;
}

動くのが遅すぎるので、もうちょっと早く動くようにしてみよう。方法としては2つある。1つは「溜め」の時間を短くする方法。なめらかに動かすにはこの方法が良いが、短くしすぎると人間の目には見えなくなってしまう問題に戻ってしまう。もう一つは、円の移動速度を上げる方法。このプログラムでは、1回描くたびにxの値を1だけ増やしているが、ここの増分を5に大きくしてみる:

普通のディスプレイの画面更新速度は60Hz=1秒間に60回が多いので、表示時間(「溜め」の長さ)を短くしすぎても無意味である。

/*****
    move.c
    円を動かしてみる
    M.Minakuchi
*****/
#include <stdio.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);
        HgSleep(0.05);  // 0.05秒間実行を止める
    }    
    
    HgGetChar();
    HgClose();
    return 0;
}

「前後の静止画で変化をできるだけ少なくすること」からすると変化が大きいため少々ちらついて見えてしまうが、致し方ないところである。

プログラムでアニメーションを描くポイントは少しずつ変化する絵を連続して描くことにあるが、そのためには描く図形を変数を使って表現し、その変数の値を変化させる。そのためには物理や数学で使うような式で表すことを考えればよい。この例の場合、円が等速直線運動をしているので円の位置xは次の式で表せる:

x = x0 + vt, x0は初期位置、vは速度、tは時刻

tが0.05[秒]、vが100[ピクセル/秒]とすると、vt = 5となる。繰り返しごとにx座標を5増やすというのは、意味的にはこのvtの分だけ位置を変化させているということになる。ウィンドウ消去と円を描くのにかかる時間を無視すると、この円は100[ピクセル/秒]の速度で等速直線運動をしている。

確認課題その1. 簡単なアニメーション

以下、出来たら教員に確認を受けること。原則的に易しい順となっているが、順序どおりに取り組む必要はない(その2も含めて)。また、授業時間内にすべてができることを求めるものではない。Do your best!

確認課題1. 拡大する円

400x400の大きさのウィンドウに、中心が(200, 200)で、半径が0から300まで、5刻みで円が大きくなるアニメーションを表示するプログラムzoom.cを作成せよ。1回の描画ごとの待ち時間(HgSleep関数に与える時間)は0.05秒でよい。

実行例はこちら

確認課題2. 斜めに移動する円

400x400の大きさのウィンドウに、半径50の円の中心が左下(0, 0)から右上(400, 400)に斜めに移動するアニメーションを表示するプログラムxymove.cを作成せよ。移動速度はx方向およびy方向に1回あたり5ピクセル、1回の描画ごとの待ち時間(HgSleep関数に与える時間)は0.05秒でよい(これ以外の値でも構わない)。

実行例はこちら

ヒント:中心のx座標とy座標を同時に変化させながら繰り返す。for文でも書けるが、while文を使う方が分かりやすいだろう。

条件との組み合わせ

例題:400x400の大きさのウィンドウに、半径50の円の中心が(0, 200)から右向きに速度5ピクセル/フレームで移動し、ウィンドウの右端まで達したら左向きに速度5ピクセル/フレームで移動し、(0, 200)まで戻ってくるアニメーションを表示するプログラムreflect.cを作成せよ。

右に移動してウィンドウの右端まで達するところまでは最初のプログラム例と同じである。ということは、右端に達した後に、戻ってくるアニメーションのためのコードを追加すればよい:

/*****
    reflect.c
    円が右端まで行って戻ってくる
    M.Minakuchi
*****/
#include <stdio.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);
        HgSleep(0.05);
    }

    // 左向きに移動
    for (x = WINDOWSIZE; x >= 0; x -= 5) {
        HgClear();
        HgCircle(x, WINDOWSIZE / 2, RADIUS);
        HgSleep(0.05);
    }    
    
    HgGetChar();
    HgClose();
    return 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);
        HgSleep(0.05);
    }

    // 左向きに移動
    v = -5;
    for (x = WINDOWSIZE; x >= 0; x += v) {
        HgClear();
        HgCircle(x, WINDOWSIZE / 2, RADIUS);
        HgSleep(0.05);
    }    
    
    HgGetChar();
    exit(0);
}

さらに、右向きと左向きの繰り返しをまとめてしまうことを考える。vの値が変わるタイミングは円が右端に達したときなので、これを条件文で書いてみる。また、繰り返しの終了条件としては、円が戻ってきて左に行きすぎたら、としてみる:

/*****
    reflect.c
    円が右端まで行って戻ってくる
    M.Minakuchi
*****/
#include <stdio.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);
        HgSleep(0.05);
        if (x >= WINDOWSIZE) {
            v = -5;
        }
    }

    HgGetChar();
    HgClose();
    return 0;
}

このプログラムでは、基本的な移動の計算は1つにまとめ(x += v)、移動方向を変数で表し、移動方向の変化を条件で判定している。この方法の方がより物理的な表現に即している。

確認課題その2. 速度を変数としたアニメーション

確認課題3. 跳ね返り続ける円

reflect.cをさらに修正して、左端まで達したら右向きに跳ね返るようにしてみよう(無限ループで繰り返す)。

実行例はこちら プログラムを停止させるには左上のウィンドウを閉じるボタン(赤い丸)をクリックする。

確認課題4. 等加速度運動 [advanced]

400x400の大きさのウィンドウに、半径50の円の中心が(200, 400)から下向きに、初期速度0で、1回の描画あたりに速度の絶対値が2ピクセルずつ増えるように移動するアニメーションを表示するプログラムaccelerate.cを作成せよ。HgSleepの時間は0.05秒でよい。また、画面外まで移動するように、中心のy座標は-100くらいまで繰り返すようにするとよい。なお、下向きに移動するので速度の値は負であることに注意。

実行例はこちら 描く際の速度の値をターミナルに表示している。

アニメーションまとめ

アニメーションの基本を整理すると次のような構造になる。

for (;;) {
    // 画面を消す(HgClear)
    // 絵を描く
    // 少し止める(HgSleep)
    // 絵を描くための変数の値を変える
}

この例では無限ループとしているが、必要に応じて終了条件を書いてもよい。変数の値を変える箇所では、条件に応じて値の変え方を切り替えることで複雑な動きも作ることができる。

この授業では簡単なアニメーションの説明だけにとどめておくが、基本的な考え方、つまり、ある表示時刻において、描きたい図形がどのように計算されるかを式で表し、変数の値を表示時刻から計算するという方法は複雑なアニメーションでも同様である。図形の位置や大きさ以外にも色などの表示属性を変化させることもできるし、回転の計算方法を使えば図形を回すこともできる。3次元CGになるとさらにカメラや光源の位置から面がどのように見えるかを計算することになる。いずれにしても、物理的な現象を数式で表現し、それをプログラムで計算させる、ということになるので、数学や物理も勉強しておかなければならない。

また、HandyGraphicで滑らかにアニメーション表示する方法については別のページにまとめておくので、興味があったら参照して欲しい。

本日の提出課題

提出課題1. 跳ね回る円

400x400の大きさのウインドウに、半径50の円の中心が初期位置(0, 0)、初期速度が(5, 3)で、中心がウィンドウの端に当たると跳ね返るように動くプログラムbound.cを作成せよ。跳ね返りは無限ループでよい。また、HgSleepの時間は0.05秒でよい。

腕に覚えのある人は、中心ではなく円の縁がウィンドウの端に当たったら跳ね返るようにしてみよう。その場合、初期位置は(50, 50)としてよい。

提出期限:次の土曜日の24:00まで

実行例はこちら

ヒント:確認課題3のプログラムに、y方向の移動を追加する。「初期速度(5, 3)」というのはx軸方向(横方向)の速度成分が5、y軸方向(縦方向)の速度成分が3、という意味。最初に端に当たるまでは1回繰り返すごとに、x座標が5、y座標が3ずつ増える。端に当たったら反対向き、すなわち-5や-3に変わる。x軸方向とy軸方向をそれぞれ別に扱えばよい。

提出課題? 最終課題準備

最終課題の準備をしよう。まずどんなものを作るか構想を練ろう。