今週はモデルを動かしてみましょう。

一般的にアニメーションと呼ばれるものです。とはいうもののあまり難しく考えることはありません。

アニメーションとはどういうことなのでしょう。アニメーションの基本は時間的に3Dのモデルが変化していくことにあります。

時間とともにモデルが移動したり,角度が変化したり,形状が変形したりすることはすべてアニメーションと呼ぶことができます。

今までのサンプルはマウスのドラッグによって回転や移動を行っていました。これもアニメーションと呼ぶことができるのです。

はねるボール

アニメーションが時間的に変化を行うことであることはわかりました。かといって,ムチャクチャに動かしたりしてもダメです。そこには,ある程度法則性がないと自然に動いているようには見えません。

今までのサンプルではマウスのドラッグ量を元に回転や移動の量を定めていました。同じように何らかの法則性を与えて,モデルを変化させていくことが必要です。

よく使用されるのが物理法則です。

ボールを放り投げると,放物軌道になります。また,ボールを地面につくと,反発係数によってスピードが減衰していきます。このように,物理的な法則をベースにモデルを変化させると,あたかも自然界にあるもののように動かすことができます。

ここではボールをはねさせることをやってみましょう。

サンプルはこちらからダウンロードできます: BouncingBall.java

ボールの軌跡は放物軌道になるので,数式は2次式になります。つまり次のようになります。

  y = a×t2 + b×t + c

意味はわかりますよね。これをプログラムで表してみましょう。

  // ボールの描画
  private void drawBall() {
    // ボールの y 座標値
    float y;
		
    if (initTime == 0) {
      // 初期時間が設定されてなければ,
      // 現在時間にセットする
      initTime = System.currentTimeMillis();
      y = 0;
    } else {
      long t = System.currentTimeMillis() - initTime;
             
      // 軌跡は2次式で決まる
      y = -0.000005f * t * t + 0.01f * t - 2.0f;
             
      // 一番下までボールがついたら,時間をリセット
      if (y <= -2.0f) {
          initTime = System.currentTimeMillis();
      }
    }
 
    gl.glPushMatrix();

    // ボールの高さに応じて位置を移動
    gl.glTranslatef(0.0f, y, 0.0f);
 
    // ボールの描画
    glut.glutSolidSphere(0.2f, SLICES, STACKS);
 
    gl.glPopMatrix();
  }

上述した式のaが-0.000005,bが0.01,cが-2になっています。

aの値が負の値なのは投げ上げていることを示しています。bが初期スピード,cが初期位置を表しています。

ここでパラメータになるのは時間です。時間はSystemクラスのcurrentTimeMillisメソッドを使用して取得しています。とはいうものの,この時間は1970年1月1日午前0時からのミリ秒を示しているので,とても大きな数になっています。

そこで,計算に使用しやすいように,一番はじめに実行したときの時間を保持しておいて,それとの差分で計算を行っています。

放り上げたボールが落ちてきたら,再び時間をリセットするようにしました。バウンドしても速度が変化しないので,反発係数は1に相当します。

このdrawBallメソッドをdisplayメソッドからコールするようにします。

  public void display(GLAutoDrawable drawable) {
    gl.glClear(GL.GL_COLOR_BUFFER_BIT
               | GL.GL_DEPTH_BUFFER_BIT);
 
    gl.glPushMatrix();
 
    // マウスの移動量に応じて移動
    gl.glTranslatef(distanceX, distanceY, 0.0f);
 
    // マウスの移動量に応じて回転
    gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f);
    gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f);
 
    // ボールの描画 
    drawBall();
 
    gl.glPopMatrix();
  }

重要なことを忘れていました。

このままだと,displayメソッドは1度しかコールされません。これを,繰り返し描画を行うように変更する必要がありました。

繰り返し描画を行わせるにはAnimatorクラスを使用します。あれっ,このクラスはどこかで見たような...

そうです。先々週マウスのドラッグによって回転や移動を行わせるようにしたとき,Animatorクラスを使用していたのです。マウスのドラッグによって描画を変化させるというのも,立派なアニメーションだったわけですね。

もう1度おさらいしましょう。AnimatorクラスはGLAutoDrawableオブジェクトを引数にして,オブジェクトを生成します。通常はGLAutoDrawableインタフェースをインプリメントしているGLCanvasオブジェクトを引数にします。オブジェクトを生成したら,startメソッドをコールすればアニメーションが開始します。

アニメーションを停止するときにはstopメソッドをコールします。

    animator = new Animator(canvas);
 
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        animator.stop();
        System.exit(0);
      }
    });
  
    frame.setVisible(true);
    animator.start();
跳ねるボール
図1 跳ねるボール

さて,これで実行してみましょう。

うまくいけば,ボールが弾んでいるように見えるはずです。図1は複数の時間におけるそれぞれのボールの位置を同一図に表示したものです。はねた直後のボールはスピードが速く,だんだんとゆっくりになっている様子がおわかりになるはずです。

定数をいろいろ変えてみると,スピードや高さなどが変化します。ぜひ,実際にやってみて,はねる様子を見てみてください。

ロボットを動かす

次は先週作成したロボットを動かしてみることにしましょう。

ロボットの動かすといっても,いろいろな動きがありますね。ここでは単純に歩かせてみましょう。

サンプルはここからダウンロードできます: WalkingRobot.java

動かす方法はボールの場合と変わりません。時間的に少しずつ位置を変化させる方法でアニメーションさせます。

歩くという動作は,手足を振る動作と,体が前に前進する動作を組み合わせたものと考えられます。

はじめにまず手足を振らせてみましょう。

厳密にいえば,手足は振り子のように動きます。しかし,ここではそれほど物理的に厳密である必要はありません。なんとなく手足を振って移動していれば,歩いているように見えるはずです。

手足を振らせるということは,手足の付け根を軸に回転させるということです。ここで,手足の付け根を軸にするのがキーです。

順を追ってやってみましょう。

先週,立方体を拡大・縮小してロボットの腕を作りました(図2,3)。図にはわかりやすいようにx,y,z軸を描画してあります。

先週はそのまま胴体の適切な位置に移動させてしまいました。しかし,このままだと腕を回転させるときの軸が腕の真ん中になってしまいます。

そこで,原点の位置まで腕を下にさげます(図4)。その後,回転を行います(図5)。こうすれば,腕の付け根から手を振っているように見えます。

このように回転をさせてから,胴体の適切な位置に移動させます(図6)。他の手足も同じように行えば,手足を振るロボットの完成です(図7)。

立方体を描画 拡大・縮小
図2 立方体を描画 図3 拡大・縮小して腕にする
腕を原点の位置まで下げる 回転
図4 腕を原点の位置まで下げる 図5 回転
胴体の適切な位置に移動 手足を振るロボットの完成
図6 胴体の適切な位置に移動 図7 手足を振るロボットの完成

これをコードで表してみましょう。新しいことは何もなく,今まで使ってきたメソッドだけで実現できます。

まず,図2から図4までのところをまとめたメソッドdrawPartから見ていきます。

  private void drawPart(float width, float height, float depth) {
    gl.glPushMatrix();
  
    // 回転の軸を手足の付け根の部分にするために,
    // 移動させておく
    gl.glTranslatef(0.0f, -height/2.0f, 0.0f);
    gl.glScalef(width, height, depth);
    glut.glutSolidCube(UNIT);
  
    gl.glPopMatrix();
  }

回転や移動などの座標変換は,コードで最後に記述されたものが先に実行されるので,拡大・縮小を行った後に,原点のところまで移動させています。

このdrawPartメソッドはdrawLimbメソッドからコールされます。drawLimbメソッドでは腕もしくは足を回転させてから,胴体の適切な位置に移動させています。

  private void drawLimb(float x, float y, float angle,
                        float width, float height, float depth) {
    gl.glPushMatrix();
  
    // 回転させてから移動
    // 回転の軸は手足の付け根
    gl.glTranslatef(x, y, 0.0f);
    gl.glRotatef(angle, 1.0f, 0.0f, 0.0f);
    drawPart(width, height, depth);
  
    gl.glPopMatrix();
  }

ロボットの本体を描画しているのがdrawRobotメソッドです。drawRobotメソッドの前半の胴体と頭を描画する部分は先週示したものと何ら変わっていません。そこで,後半の手足を描画する部分を示しておきます。

手足を振る角度は変数limbAngleで示しています。また,往復のどちらを行っているかを示しているのがboolean型のlimbModeです。

    // 左手
    drawLimb(ARM_X, ARM_Y, limbAngle,
             ARM_WIDTH, ARM_HEIGHT, ARM_DEPTH);
 
    // 右手
    drawLimb(-ARM_X, ARM_Y, -limbAngle,
             ARM_WIDTH, ARM_HEIGHT, ARM_DEPTH);
 
    // 左足
    drawLimb(LEG_X, LEG_Y, -limbAngle,
             LEG_WIDTH, LEG_HEIGHT, LEG_DEPTH);
 
    // 右足
    drawLimb(-LEG_X, LEG_Y, limbAngle,
             LEG_WIDTH, LEG_HEIGHT, LEG_DEPTH);
 
    // 角度の更新
    // ±MAX_ANGLE までいったら,戻るようにする
    if (limbMode) {
      limbAngle -= 1.0f;
      if (limbAngle <= -MAX_ANGLE) {
        limbMode = false;
      }
    } else {
      limbAngle += 1.0f;
      if (limbAngle >= MAX_ANGLE) {
        limbMode = true;
      }
    }

limbAngleはdrawRobotメソッドがコールされるたびに,1度づつ変化します。MAX_ANGLEは30にしてあるので,前後に30度ずつ手足を振ることになります。

これで,手足を振りつづけるロボットを描画できました。あとはロボットの位置を更新するだけです。

ここでは,単純に円軌道を歩くようにしました。円軌道はy軸を中心に半径1にしました。

円軌道ですから,x座標はsinθ,z座標はcosθで表すことができます。Mathクラスのsin/cosメソッドは引数がラジアンなので,度からラジアンに変換する必要があります。

drawRobotメソッドをコールする前に,回転をさせているのは歩く方向にロボットの正面を向かせるためです。これをしておかないと横歩きになってしまいますよ。

    private void drawWalkingRobot() {
        gl.glPushMatrix();
  
        // ラジアンに変換
        double theta = robotAngle/180.0 * Math.PI;
 
        // 位置を計算
        float robotX = (float)Math.sin(theta);
        float robotZ = (float)Math.cos(theta);
 
        // 回転させてから,移動
        // 回転軸は y 軸
        gl.glTranslatef(robotX, 0.0f, robotZ);
        gl.glRotatef(robotAngle + 90.0f, 0.0f, 1.0f, 0.0f);
        drawRobot();
  
        // 角度の更新
        robotAngle += 0.5f;
        if (robotAngle >= 360.0f) {
            robotAngle = 0.0f;
        }
 
        gl.glPopMatrix();
    }
歩くロボット
図8 歩くロボット

円軌道の角度はdrawWalkingRobotメソッドをコールするたびに0.5度ずつ増加させています。

drawWalkingRobotメソッドはdisplayメソッドからコールされます。displayメソッドが周期的にコールされる保証はありませんが,この程度のアニメーションであれば特にぎこちなさはありません。

周期性にこだわるのであれば,角度の更新は別途タイマーを作成して行うようにしましょう。

さて,コードが書けたところで,実行してみましょう。

手足を動かすという動作と,位置の移動は全く別々に行っていますが,組み合わせてみるとちゃんと歩いて見えるのが不思議なところです。

今回は単純なアニメーションを実装しました。これ以外にも形状を変化させるモーフィングなど,様々なアニメーションの技法があります。

著者紹介 櫻庭祐一

横河電機の研究部門に勤務。同氏のJavaプログラマ向け情報ページ「Java in the Box」はあまりに有名