SHOEISHA iD

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

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

特集記事

3Dモデルを表示するJavaアプレットの作成

Javaによる3DCGの基礎


  • このエントリーをはてなブックマークに追加

3Dモデルをマウスで回転させられるJavaアプレットの作成方法を紹介します。Java3Dなどの便利なAPIは使用せず、ゼロからアプレットの作成を行います。少しずつステップを重ねて3DCGの基礎を学習することを目的としています。

  • このエントリーをはてなブックマークに追加

はじめに

 ホームページ上に立体的な形状を表示して、それをマウスでグルグル動かせたら楽しいですね。

 この記事では、3D形状を扱う楽しさを実現するJavaアプレットの作成方法を紹介します。

 ブラウザ上で3Dモデルを表示しようと考えた場合、X3DCult3DYAPPAShockwave3DXVLなどなど、素晴らしいアプリケーションが既にたくさんありますが、今回は3Dモデルを表示するためのアプレットをゼロから自作することを行います。JavaにはJava3Dという3D用の便利なAPIがありますが、この力も借りません(!)。実際のコードを元に3DCGの基礎を解説しますので、Javaアプレット作成の学習と共に3Dの基礎的な内容の学習にも役立てていただけたら幸いです。

 全体的には次のような5つのステップで3Dモデルを表示する方法の説明を行います。

  • Step1 3Dモデルを定義してとりあえず描画してみる
  • Step2 ワイヤフレームモデルをグルグル回してみる
  • Step3 面のあるモデルを表示する
  • Step4 面の明るさを設定する
  • Step5 ファイルからモデルのデータを読み込む

 最終的には、図1のような3Dモデルをグルグル動かせるようになります。

図1:この記事で紹介する3Dモデル表示アプレット
図1:この記事で紹介する3Dモデル表示アプレット

 コンピュータで3Dモデルを表示するプログラムを作成するときに最も重要な点は、見える部分をどのように表示するか、つまり「見えない部分をどのように隠すか」ということです。見えない部分を隠すことを「陰面消去」と呼び、Z-バッファ法レイトレーシング法スキャンライン法などの様々な方法が存在します。今回は「奥のものから順番に描画する」という最も単純で実装も簡単なアルゴリズムの「優先順位法(ペインターズアルゴリズム)」を紹介します。

対象読者

 3DCGの基礎を学びたい人が対象です。

必要な環境

 J2SE Development Kit(JDK) 1.4.xが必要です。

【ステップ1】3Dモデルを定義してとりあえず描画してみる

 まず始めにアプレットに表示する3Dモデルの形を決めましょう。最初ですから、なるべく簡単なモデルがよいでしょう。と言っても単なる立方体では面白くないので、まずは図2のような形を考えてみます。

図2:アプレットで表示する3Dモデルの形
図2:アプレットで表示する3Dモデルの形
座標系について
 図2ではx軸が右、y軸が上、そしてz軸が手前を向いていることに注意しましょう。数学の教科書などに登場する座標系と異なり、コンピューターグラフィックスの世界では、画面の右方向がx軸、画面の上方向がy軸、そして手前または奥の方向がz軸になることが一般的です。図2のように、z軸が手前方向になっている座標系を「右手座標系」と言います。OpenGLでは、この右手座標系が使用されています(z軸が奥に向かうものを「左手座標系」と言います)。

 さて、この3Dモデルを表示するにはどのような情報があればよいでしょうか。

 まず各頂点の座標値が必要です。頂点の座標に関する情報を次のようにdouble型の2次元配列で定義してみます。

頂点の座標値の定義
// 頂点データ
final double[][] VERTEX_DATA = { 
    {-1, 0, 0}, {0, 1, 0}, {0, 0, -1},
    {1, 0, 0}, {0, 0, 1}, {0, -1, 0}};

 内部の配列に含まれる3つの値が各頂点のx,y,z座標値を表します。この例では、0番目の頂点(図中のP0)の座標が(-1, 0, 0)で、1番目の頂点(図中のP1)の座標が(0, 1, 0)、という具合に頂点の座標値を順番に定義しています。対象とする3Dモデルには頂点が6つあるため、このような頂点の座標値を表す配列が6つ並んでいます。

 続いて「3Dモデルの各面が、どの頂点から構成されるか」という情報を次のようなint型の2次元配列で定義します。

頂点の座標値の定義
// 面データ
final int[][] FACE_DATA = { 
    {1, 4, 2}, {1, 0, 4}, {1, 2, 0}, {3, 2, 4},
    {0, 5, 4}, {4, 5, 3}, {3, 5, 2}, {2, 5, 0}};

 内部に含まれる3つの値が、1つの面(三角形)の輪郭を構成する頂点のインデックス(識別番号)を表します。つまり、{1, 4, 2}で定義される最初の面は、1番目の頂点(P1)、4番目の頂点(P4)、2番目の頂点(P2)によって構成され、次の面は1番目の頂点(P1)、0番目の頂点(P0)、4番目の頂点(P4)から構成される、という具合に面を構成する頂点のインデックスを定義しています。対象とする3Dモデルには面が8つあるため、このような面を構成する頂点を表す配列が8つ並んでいます。

 以上のように「頂点の座標値情報」と「面を構成する頂点のインデックス情報」があれば、これを元に3Dモデルを表示できるようになります。

 それでは、頂点を表すクラスと面を表すクラスを作成してみましょう。それぞれVertex(頂点)とFace(面)という名前のクラスにします。

 Vertexクラスは3次元空間上の点を表すので、次のようにメンバ変数として(x, y, z)の座標値を持つように定義します。

頂点クラスの定義
// 頂点クラス
class Vertex {
    public double x, y, z; // モデルの頂点座標

    public Vertex(double x,double y,double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

 今回は3Dモデルを構成する面が全て三角形であることにして、Faceクラスには面の輪郭を構成する3つのVertexを次のように配列で持たせることとします。

面クラスの定義
// 面クラス
class Face {
    public Vertex[] v = new Vertex[3]; // 面を構成する3つの頂点

    public Face(Vertex v0,Vertex v1,Vertex v2) {
        v[0] = v0;
        v[1] = v1;
        v[2] = v2;
    }
}

 さて、3Dモデルの形を表現するのに必要なデータ(頂点座標の配列VERTEX_DATAと頂点インデックスの配列FACE_DATA)とクラス(VertexクラスとFaceクラス)を準備できたので、VertexクラスとFaceクラスそれぞれのオブジェクトを生成しましょう(後から、このオブジェクトを元に3Dモデルの表示を行うことになります)。

 頂点と面は複数ずつ管理する必要があるため、それぞれをvertices, facesという名前のArrayListに格納することにします。アプレットコードのフィールドで、次のようにArrayListの定義を行います。

ArrayList vertices;   // 頂点列を保持する
ArrayList faces;      // 面(三角形)列を保持する

 続いて、このArrayListVertexクラスとFaceクラスのオブジェクトを格納するためのsetModelDataという名前のメソッドを次のように作成します。

// モデルデータの設定
public void setModelData() {
    vertices = new ArrayList(); // 頂点列を初期化
    faces = new ArrayList();    // 面列を初期化

    // 頂点の作成
    for(int i = 0; i < VERTEX_DATA.length; i++) {
        vertices.add(new Vertex(VERTEX_DATA[i][0], 
                                VERTEX_DATA[i][1], 
                                VERTEX_DATA[i][2]));
    }

    // 面の作成
    for(int i = 0; i < FACE_DATA.length; i++) {
        faces.add(new Face( (Vertex)vertices.get(FACE_DATA[i][0]), 
                          (Vertex)vertices.get(FACE_DATA[i][1]), 
                          (Vertex)vertices.get(FACE_DATA[i][2])));
    }
}

 上記のコードでは、VERTEX_DATAに格納されたデータを元にVertexクラスのオブジェクトを生成し、それをverticesに追加しています。

 続いて、FACE_DATAに格納された頂点のインデックス情報からVertexオブジェクトをverticesから取得し、Faceクラスのオブジェクトを生成して、それをfacesに追加しています。

 このようにして、3Dモデルの頂点と面を表すオブジェクトをverticesfacesという名前のArrayListに格納することができます。後は、これに基づいて3Dモデルを描画することになります。

 今回は、描画する図はz座標軸の正の方向から原点へ向かう方向に向かってモデルを見た場合の形だとします(図3)。また、簡単のために面の輪郭だけを描画することにします(その結果として、モデルの稜線だけが表示されます。このような表示方法を「ワイヤフレーム表示」と言います)。

図3:視点とモデルの位置の関係
図3:視点とモデルの位置の関係

 それでは、実際に3Dモデルを描画するメソッドdrawModelを作成してみましょう。

 次のコードでは、各面の輪郭線を3本の線分で描画しています。

// モデルの描画
private void drawModel(Graphics g) {
    // 描画色を黒に設定
    g.setColor(Color.black);

    // 各面の描画
    for(int i = 0; i < faces.size(); i++) {
        Face face = (Face)faces.get(i);

        // 面の輪郭線の描画
        for(int j = 0; j < 3; j++) {
          int x0 = (int)(center.x + face.v[j].x * scale);
          int y0 = (int)(center.y - face.v[j].y * scale);
          int x1 = (int)(center.x + face.v[(j + 1) % 3].x * scale);
          int y1 = (int)(center.y - face.v[(j + 1) % 3].y * scale);

          g.drawLine(x0, y0, x1, y1);
        }
    }
}

 facesに格納されたそれぞれのFaceオブジェクトに対して輪郭線を描画するには、頂点の座標値をアプレット上に描画するための座標値に変換する必要があります。

 このように3次元空間上の座標値から描画面の座標値を求めることを「投影」と言います。今回は、簡単にするために近くのものも遠くのものも同じ大きさで描画する「平行投影」を行います(遠くのものほど小さく描画する方法を「透視投影」と言います)。視線方向をz軸に平行に定めたので、頂点のz座標値は無視してx座標値とy座標値だけから描画用の座標値を求めることができます。

 この投影は次の計算で行っています。

int x0 = (int)(center.x + face.v[j].x * scale);
int y0 = (int)(center.y - face.v[j].y * scale);

 まず、アプレットの中心が原点となるように、アプレットの中心座標を表すcenter.xcenter.yの値を追加しています。次にアプレットの座標系とモデルの座標系のy軸の向きが逆なので(アプレットのy軸は下向き)、頂点のy座標値にマイナスの符号をつけています。また、3Dモデルの大きさは1辺の大きさが2の立方体に収まる程度なので、アプレットの大きさを基準にしたscale値を掛け合わせることで、アプレットの描画領域に適した大きさにしています。以上の手順で、頂点の座標値からアプレットに描画する時の座標を求めることができます。

 ここまでの文章で、3Dモデルを表示するための概要を説明しました。それでは、今までの説明を総合したアプレット「Hello3D_Step1」のコードを見てみましょう。

 このコードで、z軸方向から眺めた3Dモデルのワイヤフレームが表示されます。

「Hello3D_Step1.java」
import java.applet.Applet;
import java.awt.*;
import java.util.*;

/*
    <applet code="Hello3D_Step1" width=300 height=300>
    </applet>
*/

public class Hello3D_Step1 extends Applet {
    // 頂点データ
    final double[][] VERTEX_DATA = { 
        {-1, 0, 0}, {0, 1, 0}, {0, 0, -1},
        {1, 0, 0}, {0, 0, 1}, {0, -1, 0}};
    // 面データ
    final int[][] FACE_DATA = { 
        {1, 4, 2}, {1, 0, 4}, {1, 2, 0}, {3, 2, 4},
        {0, 5, 4}, {4, 5, 3}, {3, 5, 2}, {2, 5, 0}};
    ArrayList vertices;   // 頂点列を保持する
    ArrayList faces;      // 面(三角形)列を保持する
    Point center;         // アプレットの中心座標
    double scale;         // モデル描画時のスケール
    Dimension appletSize; // アプレットサイズ
    
    public void init() {
        // アプレットサイズの取得
        appletSize = getSize();
        
        // アプレットの中心座標の取得
        center =
            new Point(appletSize.width / 2, appletSize.height / 2);

        // 描画スケールの設定
        scale = appletSize.width * 0.8 / 2; 

        // モデルデータの設定
        setModelData();
    }

    public void paint(Graphics g) {
        drawModel(g);
    }

    // モデルデータの設定
    public void setModelData() {
        vertices = new ArrayList(); // 頂点列を初期化
        faces = new ArrayList();    // 面列を初期化

        // 頂点の作成
        for(int i = 0; i < VERTEX_DATA.length; i++) {
            vertices.add(new Vertex(VERTEX_DATA[i][0], 
                                    VERTEX_DATA[i][1], 
                                    VERTEX_DATA[i][2]));
        }

        // 面の作成
        for(int i = 0; i < FACE_DATA.length; i++) {
          faces.add(new Face( (Vertex)vertices.get(FACE_DATA[i][0]),
                            (Vertex)vertices.get(FACE_DATA[i][1]), 
                            (Vertex)vertices.get(FACE_DATA[i][2])));
        }
    }

    // モデルの描画
    private void drawModel(Graphics g) {
        // 描画色を黒に設定
        g.setColor(Color.black);

        // 各面の描画
        for(int i = 0; i < faces.size(); i++) {
            Face face = (Face)faces.get(i);

            // 面の輪郭線の描画
            for(int j = 0; j < 3; j++) {
                int x0 = (int)(center.x + face.v[j].x * scale);
                int y0 = (int)(center.y - face.v[j].y * scale);
                int x1 =
                    (int)(center.x + face.v[(j + 1) % 3].x * scale);
                int y1 =
                    (int)(center.y - face.v[(j + 1) % 3].y * scale);

                g.drawLine(x0, y0, x1, y1);
            }
        }
    }
}

// 面クラス
class Face {
    public Vertex[] v = new Vertex[3]; // 面を構成する3つの頂点

    public Face(Vertex v0,Vertex v1,Vertex v2) {
        v[0] = v0;
        v[1] = v1;
        v[2] = v2;
    }
}

// 頂点クラス
class Vertex {
    public double x, y, z; // モデルの頂点座標

    public Vertex(double x,double y,double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

 上記の「Hello3D_Step1.java」実際にコンパイルして実行してみると、次のような結果になります。

図4:Hello3D_Step1.javaの実行結果
図4:Hello3D_Step1.javaの実行結果

 確かに目的の形をz軸方向から眺めた形になっていますね。でも別の角度から見ることができないので、実際にどのような立体形状をしているのか把握することができません。

 次のステップでは、このモデルをマウスでグルグル回転できるようにしてみます。

会員登録無料すると、続きをお読みいただけます

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

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

メールバックナンバー

次のページ
【ステップ2】ワイヤフレームモデルをグルグル回してみる

修正履歴

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

  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

三谷 純(ミタニ ジュン)

Javaとの出会いは1996年にJDK1.0が登場した時までさかのぼります。それ以降、アプレットやスタンドアロンのアプリケーション、JSPを用いたサーバサイドのサービスや携帯電話で動くJavaアプリの開発など、広い範囲でJavaに関するプログラミングを行っています。拙著『独りで習うJava』は初めてJava...

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

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

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/38 2006/10/10 14:53

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング