先週は3D空間における回転などの変換を行ってみました。今週はそれを応用して,マウスでモデルを操作できるようにしてみます。

そうですね,マウスでドラッグしたら回転や移動をできるようにしてみましょうか。

原理は簡単。マウスでのドラッグ量に応じて,モデルの回転量や移動量を決めればいいのです。

マウスでの操作

それでは,まずは回転からやってみましょう。

完成版はこちらからダウンロードできます ManipulatableCube.java

マウスのドラッグ量を調べるためには,MouseListenerインタフェースとMouseMotionListenerインタフェースを使用します。これは普通のAWT/Swingプログラミングと同じですね。

ドラッグはまずマウス・ボタンが押されたところからはじまります。押された位置をMouseEventオブジェクトから取得して,それをドラッグの開始点にします。

    GLCanvas canvas = new GLCanvas();
    canvas.addGLEventListener(this);
 
    canvas.addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent e) {
            prevMouseX = e.getX();
            prevMouseY = e.getY();
        }
    });

MouseAdapterクラスを使用しているのはMouseListenerインタフェースのmousePressedメソッド以外のメソッドを使用しないからです。

マウスのボタンを押したままマウスが移動するのがドラッグです。マウスの移動を検知しなければならないので,ここはMouseMotionListenerインタフェースを使用します。

canvas.addMouseMotionListener(new MouseMotionAdapter() {
    public void mouseDragged(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();

        Dimension size = e.getComponent().getSize();

        // 回転量の算出
        // 端から端までで,360度回転するようにする
        float thetaY = 360.0f * ((float)(x-prevMouseX)/size.width);
        float thetaX = 360.0f * ((float)(prevMouseY-y)/size.height);

        // 角度の更新
        angleX -= thetaX;
        angleY += thetaY;

        // 現在のマウスの位置を保存
        prevMouseX = x;
        prevMouseY = y;
    }
});

回転の量はGLCanvasの端から端までマウスをドラッグしたときに360度になるようにしました。

angleXとangleYが回転させる値になります。最後に次の差分を計算するときのために,現在のマウスの位置を更新しておきます。

これで,ドラッグ量に応じた回転量が決まりました。後はこの値を使用して回転させるだけです。

先週の解説と同様に回転はdisplayメソッドで行います。

    public void display(GLAutoDrawable drawable) {
        gl.glClear(GL.GL_COLOR_BUFFER_BIT);
 
        gl.glPushMatrix();
 
        // マウスの移動量に応じて回転
        gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f);
        gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f);
 
        // 線画の立方体を描画
        glut.glutWireCube(2.0f);
 
        gl.glPopMatrix();
    }

これで完成といきたいところなのですが,もう少し手を加える必要があります。このままだとdisplayメソッドは1度しかコールされないので,周期的にコールされるように変更しなくてはいけません。

この用途にはcom.sun.opengl.util.Animatorクラスを使用します。

使い方は簡単です。

まず,GLAutoDrawableオブジェクトを引数にして,Animatorオブジェクトを生成します。ここではGLAutoDrawableインタフェースをインプリメントしているGLCanvasオブジェクトを引数にしました。

生成したら,startメソッドをコールするだけです。

アプリケーションを終了するときにはstopメソッドをコールします。

ManipulatableCubeクラスのコンストラクタの変更部分を以下に示しておきます。

    animator = new Animator(canvas);
 
    frame.addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
            animator.stop();
            System.exit(0);
        }
    });
 
    frame.setVisible(true);
    animator.start();

これでdisplayメソッドが定期的にコールされるようになります。

ソースができたので,コンパイルして実行してみましょう。

C:\temp>javac ManipulatableCube.java
 
C:\temp>java ManipulatableCube
ドラッグできる立方体
図1 ドラッグできる立方体

初期状態では,立方体が正面を向いていますが,ドラッグすると回転するはずです。

移動も同じように行うことができます。回転の場合とほとんど変らないので,ここではプログラムは示しません。ManipulatableCube.javaのソースコードをぜひ参照してください。

単にドラッグをしたときに回転,Shiftキーを押しながらドラッグしたときに移動を行うようにしてあります。

今回は実装していませんが,マウスのホイールでz軸方向に移動するというのもいいかもしれません。

このようなインタラクティブ性を加えるだけで,ずいぶんアプリケーションの印象が変わります。3Dの操作をしているという感じが醸し出されるのではないでしょうか。

視点と見える範囲

今週は今まで後回しにしてきた視点と見える範囲について簡単に説明を加えておきます。

今まで3D,3Dといってきましたが,私たちはディスプレイで見ているのですから,最終的には2Dで表示されます。3Dの像をカメラで撮影して,テレビで見ているようなものです。

3Dの像を2D空間に投影する手法には,直投影と透視投影の2種類があります。直投影は遠近法がない世界で,透視投影は遠近法がある世界です。それぞれの手法には長所と短所があります。私たちが見ている世界に近いのは透視投影です。

OpenGLでも両方の手法を使用できます。どちらもGLクラスのメソッドで設定します。直投影はglOrthoメソッド,透視投影はglFrustumメソッドを使用します。

引数で描画する範囲を示します。第1引数から,右,左,下,上,前,後の順になります。例えば,次のように指定した場合,原点を中心にした1辺の長さが2の立方体になります。

    gl.glMatrixMode(GL.GL_PROJECTION);
    gl.glLoadIdentity();
    gl.glFrustum(-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f);

1行目のglMatrixModeメソッドで投影(projection)に関する行列を指定しています。次のglLoadIdentityは恒等行列をロードしています。そして,次のglFrustumで描画する範囲を設定しています。

ここで気を付けなくてはならないのが,前と後ろがどちらも正の値になっていることです。両方の値とも原点からの距離になっているので,正の値になっています。

ちなみにglFrustumメソッドに使われているFrustumという言葉ですが,日本語では錐台と呼ばれます。錐の尖っている部分を切ってしまった形です。

次にカメラの位置を決めましょう。こちらは移動の時に使用したGL#glTranslatefメソッド/glTranslatedを使用します。例えば,カメラを手前に20動かす場合,次のように記述します。

    gl.glMatrixMode(GL.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glTranslatef(0.0f, 0.0f, -20.0f);

先ほどと違って,glMatrixModeメソッドの引数にはGL.GL_MODELVIEWを使用します。これはカメラや3Dで描画する物体に関する行列を示しています。

次に先ほどと同じように恒等行列をロードします。最後にz軸の移動量を表す第3引数を-20として,glTranslatefメソッドをコールします。座標値が負の値になることにご注意ください。

さて,ManipulatableCubeクラスに戻りましょう。ManipulatableCubeクラスではreshapeメソッドにここで説明した処理が記述されています。

  public void reshape(GLAutoDrawable drawable,
                      int x, int y,
                      int width, int height) {
    float ratio = (float)height / (float)width;
         
    gl.glViewport(0, 0, width, height);
 
    gl.glMatrixMode(GL.GL_PROJECTION);
    gl.glLoadIdentity();
    gl.glFrustum(-1.0f, 1.0f, -ratio, ratio,
                 5.0f, 40.0f);
 
    gl.glMatrixMode(GL.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glTranslatef(0.0f, 0.0f, -20.0f);
  }

ratioという変数は表示領域の縦横比を表しています。次のglViewportメソッドで実際に表示する大きさを設定しています。

その後の処理は前述したとおりです。

ところで,いつまでも線画で表示するのも味気ないですね。来週は光と素材に関して説明していきます。これで,線画とはおさらばですよ。

著者紹介 櫻庭祐一

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