SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

Java3Dを使わずにパースをかけて立方体を表示する

透視投影変換と背面消去による立体図形の描画

透視投影変換の計算

 透視投影変換には、『OpenGLプログラミングガイド』や『ゲームプログラミングのための3Dグラフィックス数学』などのCGの参考書によく載っている、同軸座標系の行列を使用しました。透視投影変換行列は、以下のようになります。

透視投影変換行列
透視投影変換行列

 ここで、nは視点からビューボリュームのニアクリップ面までの距離、fはファークリップ面までの距離となります。また、t、b、l、rはニアクリップ面と視線との交点からの、上辺、下辺、左辺、右辺の位置となります。

ビューボリューム
ビューボリューム

 実際の計算では、この行列にモデル変換後の各頂点へのベクトル v = [x, y, z, 1]tを右からかけてやります。

v' = P v
   = [x', y', z', -z]t

 ここで、v'の4番目の項は-zとなりますが、同軸座標系なので、x'、y'、z'をこの-zで割り、x" = - x'/z、y" = - y'/z、z" = - z' / zとしておきます。2次元座標系でのx, yを求めるときは、x"、y"をそのまま、ウィンドウ座標系にマッピングします。z"は、各頂点の奥行きにあたり、zバッファ法を使うときなどに、この値を使います。

計算方法の概要

 詳しい解説は、CGの参考書に譲りますが、透視投影変換行列の計算方法が気になる方のために、簡単に計算方法を説明します。

 まず、視点を原点に置き、視線の前方をz軸のマイナス方向とおきます。このとき、3次元空間上の点v=[x, y, z]tと視点を結ぶ直線と、ビューボリュームのニアクリップ面との交点をv'=[x', y', z']tとおきます。

 このとき、

z' = -n
x':x = y':y = z':z

 となるため、

x' = - nx / z
y' = - ny / z

 となります。

ビューボリューム上面図
ビューボリューム上面図

 次に、ニアクリップ面の座標を、-1≦x"≦1、-1≦y"≦1となるように、x'をx"、y'をy"に変換します。こうすることで、透視投影変換後の座標から、ウィンドウ座標系へのマッピングが可能になります。

 すなわち、例えばx座標なら以下のように変換します。

[l, r] → [-1, 1]

 このとき、以下の式が成り立ちます。

x"+1 : 2 = x'-l : r-l

 したがって、

x"+1 = 2(x'-l) / (r-l)
x" = 2(x'-l) / (r-l) - 1
   = 2x'/(r-l) - 2l/(r-l) - 1

 この式に、先ほど求めたx'の式を代入すると、

x" = - 2nx/(r-l)z - 2l/(r-l) - 1
   = - 2nx/(r-l)z + (-2l-r+l)/(r-l)
   = - 2nx/(r-l)z - (r+l)/(r-l)

 よって、

x" = - 2nx/(r-l)z - (r+l)/(r-l)

 同様に、y"についても、

y" = - 2ny/(t-b)z - (t+b)/(t-b)

 次に、z"ですが、x"、y"がともに、定数をzで割った式と定数の和という形になっているため、z"についても、同じ形の式を仮定します。

z" = a / z + b

 -1≦z"≦1となるように、z= -n、z= -fのときの式を求めると、

1 = a/-f + b
-1 = a/-n + b

 以上より、

2 = a(1/n - 1/f) = a(f-n)/fn
a = 2nf/(f-n)

 また、

b = 1 + a/f
  = 1 + 2n/(f-n)
  = (f+n) / (f-n)

 したがって、

z" = 2nf / (f-n)z + (f+n)/(f-n)

 以上より、以下の3式が導かれます。

- x"z = (2n / (r-l))x + ((r+l) / (r-l))z
- y"z = (2n / (t-b))y + ((t+b) / (t-b))z
- z"z = (-(f+n) / (f-n))z - 2nf/(f-n)

 ところで、v"=[x", y", z"]tは、同時座標系の4次元座標[-x"z, -y"z, -z"z, -z]tをw座標値-zで割ったものになります。

 したがって、透視投影変換行列Pは、[x, y, z, 1]tを[-x"z, -y"z, -z"z, -z]tに変換する行列となり、前出の式のようになります。

 透視投影変換の概要は以上のとおりです。

変換処理の実装

 透視投影変換処理のソースコードを示します。まず、透視投影変換行列を求めるJavaの行列クラスのメソッドは、以下のとおりです。

透視投影変換行列
public void makePerspective(double l, double r,
                            double b, double t,
                            double n, double f)
{

    mat[0][0] = 2.0 * n / (r - l);
    mat[0][1] = 0.0;
    mat[0][2] = (r + l) / (r - l);
    mat[0][3] = 0.0;

    mat[1][0] = 0.0;
    mat[1][1] = 2.0 * n / (t - b);
    mat[1][2] = (t + b) / (t - b);
    mat[1][3] = 0.0;

    mat[2][0] = 0.0;
    mat[2][1] = 0.0;
    mat[2][2] = -(f + n) / (f - n);
    mat[2][3] = -2.0 * f * n / (f - n);

    mat[3][0] = 0.0;
    mat[3][1] = 0.0;
    mat[3][2] = -1.0;
    mat[3][3] = 0.0;
}

 また、面の描画メソッド内での透視投影変換の処理は、以下のとおりです。

透視投影変換の処理
// 受け取ったモデル変換行列、透視投影変換行列を利用して面を描画する
public void persDraw(Vector2d winCenter, Vector2d winScale, 
                     Matrix4x4d persM, Matrix4x4d modelM,
                     Matrix4x4d rotM, Vector3d eyePos,
                     Vector3d lightDir, Graphics g)
{
    Vector3d Mv[] = new Vector3d[vertex.length];
    Vector3d PMv[] = new Vector3d[vertex.length];
    for (int i = 0; i < vertex.length; i++) {
        // M:モデル変換行列、v:頂点、Mv:モデル変換後頂点
        Mv[i] = modelM.multiplyVecRight(vertex[i]);
        // P:透視投影変換行列、PMv:透視投影変換後頂点
        PMv[i] = persM.multiplyVecRight(Mv[i]);
        // Pが同軸座標のためX、YをZで割る
        PMv[i].divideEqual(-Mv[i].getZ());    
    }

    ……中略……

    // v2d:透視投影後の頂点(2次元座標系)
    Vector2d v2d[] = new Vector2d[vertex.length];
    for (int i = 0; i < vertex.length; i++) {
        v2d[i] = new Vector2d(PMv[i].getX(), PMv[i].getY());
    }

    int x[] = new int[vertex.length];
    int y[] = new int[vertex.length];
    // 透視投影後の頂点のウィンドウ座標へのマッピング
    for (int i = 0; i < vertex.length; i++) {
        x[i] = (int)(winCenter.getX() 
                     + v2d[i].getX() * winScale.getX());
        y[i] = (int)(winCenter.getY() 
                     - v2d[i].getY() * winScale.getY())
    }

    // ポリゴンの描画
    g.setColor(new Color(red, green, blue));
    g.fillPolygon(x, y, vertex.length);
}

回転・移動・ズームのオペレーション

 マウス操作でのオペレーションは、マウス左ボタンを押しながらのドラッグで回転、右ボタンを押しながらのドラッグで上下左右の移動、左ボタン、右ボタン同時に押しながらのドラッグで前後へのズームという風に割り当てました。マウスドラッグから各オペレーションへの変換ですが、まず、描画処理は別スレッドとして走らせ、そこで各変換のパラメータからモデル変換行列を計算し、それを使用して変換後の図形を描画します。マウスドラッグのハンドラでは、マウスドラッグが始まると、マウス位置を保持して前回位置との差分を取り、それを各変換のパラメータの計算に使用します。

 以下に、マウスドラッグのハンドラのソースを示します。

マウスハンドラ
public void mouseDragged(MouseEvent e)
{
    if (e.getModifiers() == InputEvent.BUTTON1_MASK) {
        int x = e.getX();
        int y = e.getY();
        axis.setValue(y - prevY, x - prevX, 0.0);
        double length = axis.normalize();
        angle = 0.5 * length;
        prevX = x;
        prevY = y;
    } else if (e.getModifiers() == InputEvent.BUTTON3_MASK) {
        Dimension size = getSize();
        int width = size.width;
        int height = size.height;
        double w = 2.0 * Math.tan((fov / 2.0) * Math.PI / 180.0) * n;
        double h = w * (double)height / (double)width;
        int x = e.getX();
        int y = e.getY();
        double r = 1.0 / (double)width;
        double dx = r * (x - prevX) * w * - transDistance / n;
        double dy = r * (prevY - y) * h * - transDistance / n;
        Vector3d t = new Vector3d(dx, dy, 0.0);
        transVec.addEqual(t);
        prevX = x;
        prevY = y;
    } else if (e.getModifiers() == 
        (InputEvent.BUTTON1_MASK | InputEvent.BUTTON3_MASK)) {
        int x = e.getX();
        int y = e.getY();
        transDistance += (y - prevY);
        prevX = x;
        prevY = y;
    }
}

最後に

 今回は、立方体という単純な立体図形の描画にとどめたため、あまり面白味がなかったかもしれません。陰面消去の処理にzバッファ法などを利用して、複数の立体図形や、凹んだ部分がある立体図形も描画できるように改良すると、より面白いのではないかと思います。次回はぜひ、zバッファ法などで、しっかりとした陰面消去を行ってみたいと思います。

参考資料

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

伊藤拡(イトウヒロム)

元CGプログラマ現在業務系システムのソフトウェア開発技術者著書『Javaアプレット 3Dゲーム開発とレンダリング』(共著、工学社)『MFCによるOpenGL 3Dプログラミング』(工学社)細々とホームページ公開中 

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/234 2005/12/16 10:35

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー