SHOEISHA iD

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

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

特集記事

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

Javaによる3DCGの基礎


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

【ステップ5】ファイルからモデルのデータを読み込む

 前回までは、表示するモデルをコードの中に配列として書き込んでいましたが、この方法だと、表示するモデルを変更する場合には毎回コンパイルし直す必要があるため大変です。

 そこで、予め別ファイルにモデルデータを保存しておき、それをアプレットから読み込むように変更してみます。

 ここではモデルデータが次のようなテキスト形式で保存されたファイルを読み込むこととします。

v -1.0 0.0 0.0
v 0.0 1.0 0.0
v 0.0 0.0 -1.0
v 1.0 0.0 0.0
v 0.0 0.0 1.0
v 0.0 -1.0 0.0
f 2 5 3
f 2 1 5
f 2 3 1
f 4 3 5
f 1 6 5
f 5 6 4
f 4 6 3
f 3 6 1

 上記のテキストファイルでは、vで始まる行に頂点の座標値がx, y, z座標の順に記述され、fで始まる行に、面を構成する頂点の識別番号(識別番号に0は無く、最初の頂点が1になります。)が記述されています。

 このような形式を「wavefront objフォーマット」といい、多くの一般的な3DCGソフトウェアからこの形式で3D形状のデータを書き出すことができます(ソフトウェアによってはobj形式であっても、もう少し複雑な形式でファイルにデータが書き出されることがあります)。

 今回は上記のようなファイルを読み込むための機能を追加します。

 objファイルのファイル名は、アプレットタグを使用してHTMLファイルで指定するものとします。次のようにfilenameという名前のパラメータで指定します。ここで指定するobjファイルはアプレットクラスと同じ場所に配置します。

<applet code="Hello3D_Step5" width=300 height=300>
<param name="filename" value="model.obj">
</applet>

 アプレットのコードでは、フィールドの先頭に次のようなファイル名を保持する変数を定義します。

String dataFileName;  // モデル情報を保存したファイル名

 アプレットタグで指定されたファイル名を取得するには、initメソッドに次のようなコードを追加します。

// 引数からモデルファイル名を取得する
dataFileName = getParameter("filename");

 続いて、指定されたファイルを読み込むようにsetModelDataメソッドを次のように変更します。

 java.ioパッケージに含まれるStreamTokenizerクラスを使うと、objファイルような空白文字で区切られた文字や数字(トークン)を順番に読み込むことが簡単に実現できます。

// モデルデータの設定
public void setModelData() {
    vertices = new ArrayList(); // 頂点列を初期化
    faces = new ArrayList();    // 面列を初期化
    InputStream is;
    double modelSize = 0;
    
    try  {
        // ファイルのストリームとトークナイザを作成
        is = new URL( getDocumentBase(), dataFileName )
            .openStream();
        Reader r = new BufferedReader(new InputStreamReader(is));
        StreamTokenizer st = new StreamTokenizer(r);
    
        // トークンごとの読み込み
        int token;
        while((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
            if(token == StreamTokenizer.TT_WORD) {
                if(st.sval.equals("v")) { // 頂点情報の取得
                    st.nextToken(); double x = st.nval; // x座標
                    st.nextToken(); double y = st.nval; // y座標
                    st.nextToken(); double z = st.nval; // z座標

                     // モデルサイズを更新    
                    modelSize = Math.max(modelSize, x);
                    modelSize = Math.max(modelSize, y);
                    modelSize = Math.max(modelSize, z);
                    
                    // 頂点列に新しい頂点を追加
                    vertices.add(new Vertex(x, y, z));
                }
                else if(st.sval.equals("f")) { // 面情報の取得
                    // 頂点インデックスの取得
                    st.nextToken(); int v0 = (int)st.nval;
                    st.nextToken(); int v1 = (int)st.nval;
                    st.nextToken(); int v2 = (int)st.nval;
                    
                    // 面列に新しい面を追加
                   faces.add(new Face( (Vertex)vertices.get(v0 - 1),
                                    (Vertex)vertices.get(v1 - 1), 
                                    (Vertex)vertices.get(v2 - 1)));
                }
            }
        }
    }
    catch(Exception e) {
        getAppletContext().showStatus("Data Load Error");
    }
    
    // モデルの大きさが1になるように正規化
    for(int i = 0; i < vertices.size(); i++) {
        Vertex v = (Vertex)vertices.get(i);
        v.x /= modelSize;
        v.y /= modelSize;
        v.z /= modelSize;
    }
}

 これにより、図14のように任意のモデルを読み込んで表示できるようになります。

図14:Hello3D_Step5.javaの実行結果
図14:Hello3D_Step5.javaの実行結果

 なお、最終的なHello3D_Step5.javaのコードは次のようになります。

Hello3D_Step5.java
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import java.net.*;

/*
    <applet code="Hello3D_Step5" width=300 height=300>
    <param name="filename" value="model.obj">
    </applet>
*/

public class Hello3D_Step5 extends Applet 
 implements MouseMotionListener, MouseListener {
    String dataFileName;  // モデル情報を保存したファイル名
    ArrayList vertices;   // 頂点列を保持する
    ArrayList faces;      // 面(三角形)列を保持する
    Point center;         // アプレットの中心座標
    Point mousePosition;  // マウス位置
    double scale;         // モデル描画時のスケール
    double phi;           // x軸周りの回転角
    double theta;         // y軸周りの回転角
    Image bufferImage;    // ダブルバッファリング用のイメージ
    Dimension appletSize; // アプレットサイズ
    
    public void init() {
        // 引数からモデルファイル名を取得する
        dataFileName = getParameter("filename");

        // イベントリスナの登録
        addMouseMotionListener(this);
        addMouseListener(this);

        // アプレットサイズの取得
        appletSize = getSize();
        
        // アプレットの中心座標の取得
        center =
            new Point(appletSize.width / 2, appletSize.height / 2);

        // マウス位置の初期化
        mousePosition = new Point(0, 0);

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

        // 回転角の初期化
        phi = 0.0;
        theta = 0.0;
        
        // モデルデータの設定
        setModelData();
         
        // 頂点のスクリーン座標の設定
        setScreenPosition();
    }

    public void paint(Graphics g) {
        // ダブルバッファリング用のイメージを作成
        if(bufferImage == null) {
            bufferImage =
                createImage(appletSize.width, appletSize.height); 
        }
        
        // バッファにモデルを描画
        drawModel(bufferImage.getGraphics());
        
        // バッファイメージをアプレットに描画
        g.drawImage(bufferImage, 0, 0, this);
    }

    //描画更新時に背景の塗りつぶし処理を行わないためのオーバーライド
    public void update(Graphics g) {
        paint(g);
    }

    public void mouseMoved(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) { }
    public void mouseEntered(MouseEvent e) { }
    public void mouseExited(MouseEvent e) { }
    public void mouseReleased(MouseEvent e) { }

    public void mouseDragged(MouseEvent e) {
        if ((e.getModifiers() & e.SHIFT_MASK) == e.SHIFT_MASK ) {
            // スケールの更新
            scale += (e.getX() - mousePosition.x);
        } else {
            // 回転角の更新
            theta += (e.getX() - mousePosition.x) * 0.01;
            phi   += (e.getY() - mousePosition.y) * 0.01;
        
            // x軸周りの回転角に上限を設定
            phi = Math.min(phi, Math.PI/2);
            phi = Math.max(phi, -Math.PI/2);
        } 
        // マウス位置の更新
        mousePosition.setLocation(e.getX(), e.getY());
        
        // 頂点のスクリーン座標の更新
        setScreenPosition();
        
        // 描画更新
        repaint();
    }
    
    public void mousePressed(MouseEvent e) {
        // マウス位置の更新
        mousePosition.setLocation(e.getX(), e.getY());
    }
    
    // モデルデータの設定
    public void setModelData() {
        vertices = new ArrayList(); // 頂点列を初期化
        faces = new ArrayList();    // 面列を初期化
        InputStream is;

        Vertex minV = new Vertex(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
        Vertex maxV = new Vertex(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);
        
        try  {
            // ファイルのストリームとトークナイザを作成
            is = new URL( getDocumentBase(), dataFileName )
                .openStream();
            Reader r =
                new BufferedReader(new InputStreamReader(is));
            StreamTokenizer st = new StreamTokenizer(r);
        
            // トークンごとの読み込み
            int token;
            while((token=st.nextToken()) != StreamTokenizer.TT_EOF)
            {
                if(token == StreamTokenizer.TT_WORD) {
                    if(st.sval.equals("v")) { // 頂点情報の取得
                        st.nextToken(); double x = st.nval; //x座標
                        st.nextToken(); double y = st.nval; //y座標
                        st.nextToken(); double z = st.nval; //z座標

                         // モデルサイズを更新    
                         minV.x = Math.min(minV.x, x);
                         minV.y = Math.min(minV.y, y);
                         minV.z = Math.min(minV.z, z);
                         maxV.x = Math.max(maxV.x, x);
                         maxV.y = Math.max(maxV.y, y);
                         maxV.z = Math.max(maxV.z, z); 
                      
                        // 頂点列に新しい頂点を追加
                        vertices.add(new Vertex(x, y, z));
                    }
                    else if(st.sval.equals("f")) { // 面情報の取得
                      // 頂点インデックスの取得
                      st.nextToken(); int v0 = (int)st.nval;
                      st.nextToken(); int v1 = (int)st.nval;
                      st.nextToken(); int v2 = (int)st.nval;
                        
                      // 面列に新しい面を追加
                      faces.add(
                          new Face( (Vertex)vertices.get(v0 - 1), 
                                    (Vertex)vertices.get(v1 - 1), 
                                    (Vertex)vertices.get(v2 - 1)));
                    }
                }
            }
        }
        catch(Exception e) {
            getAppletContext().showStatus("Data Load Error");
        }
        
        double modelSize = Math.max(maxV.x - minV.x, maxV.y - minV.y);
        modelSize = Math.max(modelSize, maxV.z - minV.z);
        
        // モデルの大きさが原点を中心とする1辺が2の立方体に収まるようにする
        for(int i = 0; i < vertices.size(); i++) {
            Vertex v = (Vertex)vertices.get(i);
            v.x = (v.x - (minV.x + maxV.x) / 2) / modelSize * 2;
            v.y = (v.y - (minV.y + maxV.y) / 2) / modelSize * 2;
            v.z = (v.z - (minV.z + maxV.z) / 2) / modelSize * 2;
        }
    }

    // 頂点のスクリーン座標を更新する
    private void setScreenPosition() {
        for(int i = 0; i < vertices.size(); i++) {
            Vertex v = (Vertex)vertices.get(i);

            // 回転後の座標値の算出
            v.rx =   v.x * Math.cos(theta) 
                   + v.z * Math.sin(theta);
            v.ry =   v.x * Math.sin(phi) * Math.sin(theta) 
                   + v.y * Math.cos(phi) 
                   - v.z * Math.sin(phi) * Math.cos(theta);
            v.rz = - v.x * Math.cos(phi) * Math.sin(theta) 
                   + v.y * Math.sin(phi) 
                   + v.z * Math.cos(phi) * Math.cos(theta);
            
            // スクリーン座標の算出
            v.screenX = (int)(center.x + scale * v.rx );
            v.screenY = (int)(center.y - scale * v.ry );
        }
        
        for(int i = 0; i < faces.size(); i++) {
            Face face = (Face)faces.get(i);
            
            // 面の奥行き座標を更新
            face.z = 0.0;
            for(int j = 0; j < 3; j++) {
                face.z += face.v[j].rz;
            }
            
            // 2辺のベクトルを計算
            double v1_v0_x = face.v[1].rx - face.v[0].rx;
            double v1_v0_y = face.v[1].ry - face.v[0].ry;
            double v1_v0_z = face.v[1].rz - face.v[0].rz;
            double v2_v0_x = face.v[2].rx - face.v[0].rx;
            double v2_v0_y = face.v[2].ry - face.v[0].ry;
            double v2_v0_z = face.v[2].rz - face.v[0].rz;
            
            // 法線ベクトルを外積から求める
            face.nx = v1_v0_y * v2_v0_z - v1_v0_z * v2_v0_y;
            face.ny = v1_v0_z * v2_v0_x - v1_v0_x * v2_v0_z;
            face.nz = v1_v0_x * v2_v0_y - v1_v0_y * v2_v0_x;
            
            // 法線ベクトルの正規化
            double l = Math.sqrt(face.nx * face.nx
                + face.ny * face.ny + face.nz * face.nz );
            face.nx /= l;
            face.ny /= l;
            face.nz /= l;
        }
        
        // 面を奥行き座標で並び替える
        Collections.sort(faces, new FaceDepthComparator());
    }
    
    // モデルの描画
    private void drawModel(Graphics g) {
        // 白色で全体をクリア
        g.setColor(Color.white);
        g.fillRect(0, 0, appletSize.width, appletSize.height);

        // 三角形描画のための座標値を格納する配列
           int[] px = new int[3];
           int[] py = new int[3];
           
        // 各面の描画
        for(int i = 0; i < faces.size(); i++) {
            Face face = (Face)faces.get(i);

            // 面の法線が裏を向いている場合は描画をスキップ
            if(face.nz < 0) { 
                continue; 
            }
            
            // 面の輪郭の座標を設定
            for(int j = 0; j < 3; j++) {
                px[j] = face.v[j].screenX;
                py[j] = face.v[j].screenY;
            }
            
            // 描画色の指定
            g.setColor(Color
              .getHSBColor((float)0.4,(float)0.5,(float)face.nz));
            // 面の塗りつぶし
            g.fillPolygon(px, py, 3);
        
            // 面の輪郭線の描画
            g.setColor(Color.black);
            g.drawPolygon(px, py, 3);
        }
    }
}

// 面を奥行き順にソートするための Comparator
class FaceDepthComparator implements Comparator {
    public int compare(Object f1, Object f2) {
        return ((Face)f1).z > ((Face)f2).z ? 1 : -1;
    }
}

// 面クラス
class Face {
    public Vertex[] v = new Vertex[3]; // 面を構成する3つの頂点
    public double z;                   // 奥行き
    public double nx, ny, nz;          // 法線

    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 double rx, ry, rz;    // 回転させた後の座標
    public int screenX, screenY; // スクリーン上の座標

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

おわりに

 以上、5つのステップで3Dモデルを表示するためのアプレットを作成する方法を見てきました。

 実際に3Dモデルを表示するためのアプレットをゼロから自作することはあまりないと思いますが、今回説明したような3DCGの基本が理解できていると、いろいろ応用できると思います。

 しかし、はじめに記したように今回紹介した3D表示方法は面単位で描画を行う非常に原始的な方法なので、面と面が交差しているような形状や、図15のような奥行き順が三つ巴の関係にあるようなものは正しく描画することができません。

図15:三つ巴の関係にある3つの三角形
図15:三つ巴の関係にある3つの三角形

 また、透過オブジェクト、光の反射などを表現するには、レイトレーシングなど別のアルゴリズムを用いる必要があります。「どのように3Dモデルを画面に表示するか」という課題はCGの世界では「レンダリング」というカテゴリで多くの方法が研究されています。今回紹介した方法は、その中の極めて初歩的なものです。しかし、どのようなレンダリング手法を用いる場合でも、今回の記事で紹介した「モデルの座標値をスクリーン座標に投影する」ということは必ず必要になってくる基本的な事柄ですので、今後のCGの学習に役立てていただければ幸いです。

修正履歴

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

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

もっと読む

この記事の著者

三谷 純(ミタニ ジュン)

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

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

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

この記事をシェア

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

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング