OBJ Loader の基礎

Wavefront のモデルファイル(*.OBJ)を入力します。

前田稔の超初心者のプログラム入門

OBJ モデルファイル

  1. 簡単な OBJ 形式のモデルファイルを作成して、これを入力して描画してみましょう。
    Java3D には Wavefront で作成した OBJ ファイルのローダーと Lightwave で作成した LWS のローダーが用意されています。
    ところが OBJ のローダーは「エラーになったり、モデルが正常に描画されない」ことが良くあります。
    そこで OBJ Loader を自前で開発することにしました。
    立方体のモデルを OBJ 形式で作成したファイル「cube.obj」です。
    OBJ モデルは TEXT 形式なので、エディッタなどでタイプしてプログラムと同じフォルダーに格納して下さい。
    # 立方体の頂点座標を定義
    g cube
    v -5 -5 -5
    v 5 -5 -5
    v -5 5 -5
    v 5 5 -5
    v -5 -5 5
    v 5 -5 5
    v -5 5 5
    v 5 5 5
    f 1 3 4 2
    f 1 5 7 3
    f 2 4 8 6
    f 1 2 6 5
    f 3 7 8 4
    f 5 6 8 7
    
  2. OBJ ファイルは TEXT 形式で、全ての行がキーワードで始まります。
  3. 頂点座標と法線ベクトルを定義した立方体のモデル「cube_normal.obj」です。
    # 立方体の頂点座標と法線ベクトルを定義
    g cube
    v -1 -1 -1
    v 1 -1 -1
    v -1 1 -1
    v 1 1 -1
    v -1 -1 1
    v 1 -1 1
    v -1 1 1
    v 1 1 1
    vn 0 0 -1
    vn -1 0 0
    vn 1 0 0
    vn 0 -1 0
    vn 0 1 0
    vn 0 0 1
    f 1//1 3//1 4//1 2//1
    f 1//2 5//2 7//2 3//2
    f 2//3 4//3 8//3 6//3
    f 1//4 2//4 6//4 5//4
    f 3//5 7//5 8//5 4//5
    f 5//6 6//6 8//6 7//6
    

プログラムの作成

  1. OBJ ファイルを入力して、頂点座標と法線ベクトルを処理してモデルを描画します。
    //★ OBJ Model をロードする(頂点座標と法線ベクトル)    前田 稔
    import java.awt.*;
    import javax.swing.*;
    import javax.media.j3d.*;
    import javax.vecmath.*;
    import com.sun.j3d.utils.universe.*;
    import com.sun.j3d.utils.geometry.*;
    import java.io.*;
    import java.net.URL;
    import com.sun.j3d.loaders.*;
    import com.sun.j3d.utils.behaviors.mouse.*;
    import java.util.*;
    
    public class OBJ_Model extends JFrame
    {
        // main Method
        public static void main(String[] args)
        {   java.awt.EventQueue.invokeLater(new Runnable()
            {   public void run()
                {   new OBJ_Model().setVisible(true);  }
            });
        }
    
        // Constructor
        public OBJ_Model()
        {   // JFrame の初期化
            super("OBJ Model");
            setSize(512,512);
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            // Java3D 関係の設定
            GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
            Canvas3D canvas = new Canvas3D(config);
            add(canvas);
    
            // SimpleUniverseを生成
            SimpleUniverse universe = new SimpleUniverse(canvas);
            universe.getViewingPlatform().setNominalViewingTransform();
    
            // Scene を生成
            universe.addBranchGraph(CreateScene());
        }
    
        // Scene の生成
        public BranchGroup CreateScene()
        {   BranchGroup objRoot = new BranchGroup();
    
            // Light の設定
            BoundingSphere bounds = new BoundingSphere(new Point3d(),100.0);
            DirectionalLight dlight =
              new DirectionalLight(true, new Color3f(1.0f,1.0f,1.0f), new Vector3f(0.3f,-0.3f,-0.3f));
            dlight.setInfluencingBounds(bounds);
            objRoot.addChild(dlight);
            AmbientLight alight = new AmbientLight();
            alight.setInfluencingBounds(bounds);
            objRoot.addChild(alight);
    
            // 縮小
            TransformGroup objScale = new TransformGroup();
            Transform3D t3d = new Transform3D();
            t3d.setScale(0.03);
            objScale.setTransform(t3d);
            objRoot.addChild(objScale);
    
            // Mouse 操作の設定
            TransformGroup trans = new TransformGroup();
            SetMouse(objRoot, trans);
            //objRoot.addChild(trans);
            objScale.addChild(trans);
    
            // モデルの入力
            obj_load f = new obj_load();
            Scene s = null;
            s = f.load("cube.obj");
            //s = f.load("cube_normal.obj");
    
            trans.addChild(s.getSceneGroup());
    
            // 背景色の設定
            Color3f bgColor = new Color3f(0.05f, 0.05f, 0.5f);
            Background bgNode = new Background(bgColor);
            bgNode.setApplicationBounds(bounds);
            objRoot.addChild(bgNode);
            return objRoot;
        }
    
        // Mouse 操作の設定
        public void SetMouse(BranchGroup objRoot, TransformGroup trans)
        {
            // Model の修正を許可
            trans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
            trans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
    
            // 回転を設定
            BoundingSphere bounds = new BoundingSphere(new Point3d(), 100.0);
            MouseRotate rotator = new MouseRotate(trans);
            rotator.setSchedulingBounds(bounds);
            objRoot.addChild(rotator);
    
            // 移動を設定
            MouseTranslate translator = new MouseTranslate(trans);
            translator.setSchedulingBounds(bounds);
            objRoot.addChild(translator);
    
            // ズームを設定
            MouseZoom zoomer = new MouseZoom(trans);
            zoomer.setSchedulingBounds(bounds);
            objRoot.addChild(zoomer);
       }
    }
    
    // ★ Loader Object Class
    class obj_load extends LoaderBase
    {   SceneBase       Base;      // SceneBase
        BufferedReader  BR;        // 入力 Reader
        String          BUF;       // 1行分の入力バッファ
        StringTokenizer token;     // トークン Object
    
        ArrayList<Point3f>    vp = new ArrayList<Point3f>();    // 頂点座標
        ArrayList<TexCoord2f> vt = new ArrayList<TexCoord2f>(); // テクスチャ座標
        ArrayList<Vector3f>   vn = new ArrayList<Vector3f>();   // 法線ベクトル
    
        ArrayList<Integer>    idx = new ArrayList<Integer>();   // Index の区切り
        ArrayList<Integer>    xvp = new ArrayList<Integer>();   // 頂点 Index の並び
        ArrayList<Integer>    xvt = new ArrayList<Integer>();   // テクスチャ Index の並び
        ArrayList<Integer>    xvn = new ArrayList<Integer>();   // 法線ベクトル Index の並び
    
        @Override
        public Scene load(String fname)
        {
            File    file = new File(fname);
            try
            {   BR = new BufferedReader(new FileReader(file));  }
            catch(IOException e)
            {   System.out.println("File Open Error");  }
    
            set_list();
            creatscene();
            return Base;
        }
    
        @Override
        public Scene load(URL aURL)
        {   Base = new SceneBase();
            return Base;
        }
    
        @Override
        public Scene load(Reader reader)
        {   Base = new SceneBase();
            return Base;
        }
    
        // OBJ File を入力して ArrayList に格納
        public void set_list()
        {   int     i,num;
            String  str;
            int[]   val= new int[3];
    
            while(NextRead())
            {   str = token.nextToken();
                if ("v".equals(str))            // 頂点座標
                {   vp.add(new Point3f(FVal(), FVal(), FVal()));
                }
                else  if ("vt".equals(str))     // テクスチャ座標
                {   vt.add(new TexCoord2f(FVal(), FVal()));
                }
                else  if ("vn".equals(str))     // 法線ベクトル
                {   vn.add(new Vector3f(FVal(), FVal(), FVal()));
                }
                else  if ("f".equals(str))      // Index
                {   num= token.countTokens();
                    idx.add(num);
                    for(i=0; i<num; i++)
                    {   str= token.nextToken(); // 1//2, -1/-1/-1
                        setv(str, val);
                        if (val[0]>0)       xvp.add(val[0]);
                        else  if (val[0]<0) xvp.add(vp.size()+val[0]+1);
                        if (val[1]>0)       xvt.add(val[1]);
                        else  if (val[1]<0) xvt.add(vt.size()+val[1]+1);
                        if (val[2]>0)       xvn.add(val[2]);
                        else  if (val[2]<0) xvn.add(vn.size()+val[2]+1);
                    }
                }
            }
            Close();
        }
    
        // Scene を生成
        public void creatscene()
        {   int i,cnt;
            Base = new SceneBase();
            Base.setSceneGroup(new BranchGroup());
    
            // ArrayList を配列に変換
            Point3f[] vertices = (Point3f[])vp.toArray(new Point3f[0]);
            TexCoord2f[] texture = (TexCoord2f[])vt.toArray(new TexCoord2f[0]);
            Vector3f[] normal = (Vector3f[])vn.toArray(new Vector3f[0]);
    
            // Index 情報を配列に変換
            int[] stripVertexCounts= new int[idx.size()];
            for(i=0; i<idx.size(); i++)  stripVertexCounts[i] = idx.get(i);
            int[] indices = new int[xvp.size()];
            for(i=0; i<xvp.size(); i++)  indices[i] = xvp.get(i)-1;
            int[] texidx = new int[xvt.size()];
            for(i=0; i<xvt.size(); i++)  texidx[i] = xvt.get(i)-1;
            int[] normidx = new int[xvn.size()];
            for(i=0; i<xvn.size(); i++)  normidx[i] = xvn.get(i)-1;
    
            // モデルを生成
            GeometryInfo ginfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
            ginfo.setCoordinates(vertices);
            ginfo.setCoordinateIndices(indices);
            ginfo.setStripCounts(stripVertexCounts);
    
            // 法線ベクトルの設定
            if (xvn.size()>0)
            {   ginfo.setNormals(normal);
                ginfo.setNormalIndices(normidx);
            }
            else
            {    NormalGenerator gen = new NormalGenerator();
                 gen.generateNormals(ginfo);
            }
    
            // Material を設定して SceneBase に追加
            Shape3D shape = new Shape3D(ginfo.getGeometryArray());
            shape.setAppearance(createAppearance());
            Base.getSceneGroup().addChild(shape);
        }
    
        // Material の設定
        private Appearance createAppearance()
        {   Appearance ap = new Appearance();
            Material mat = new Material();
            // デフォルト値
            mat.setLightingEnable(true);            // 照明が有効
            mat.setAmbientColor(0.2f, 0.2f, 0.2f);  // 環境光による色は   濃いグレー
            mat.setEmissiveColor(0.0f, 0.0f, 0.0f); // 発光による色は     黒(発光しない)
            mat.setDiffuseColor(1.0f, 1.0f, 1.0f);  // 拡散反射による色は 白
            mat.setSpecularColor(1.0f, 1.0f, 1.0f); // 鏡面反射による色は 白
            mat.setShininess(64.0f);                // 輝度は             64(中間値)
            mat.setDiffuseColor(new Color3f(0.8f, 0.8f, 0.0f));
            ap.setMaterial(mat);
            return ap;
        }
    
        // Line Read(BUF に入力)
        private boolean LineRead()
        {
            try
            {   BUF = BR.readLine();  }
            catch(IOException e)
            {   System.out.println(e);  }
            if (BUF == null)
            {   System.out.println("End of file");
                return false;
            }
            return true;
        }
    
        // Next Read Line(#をスキップ, Token を設定)
        private boolean NextRead()
        {
            while(LineRead())
            {   if (BUF.length()>0 && BUF.charAt(0)!='#')
                {   token = new StringTokenizer(BUF, " ,;\t", false);
                    return true;
                }
            }
            return false;
        }
    
        // Next FVal(BUF から次の値を取得)
        private float FVal()
        {   if (token.hasMoreTokens()==false)
                if (NextRead()==false)  return -1.0f;
            return Float.parseFloat(token.nextToken());
        }
    
        // '/' で区切られた値を切り出す("1//2")
        private void setv(String str, int[] v)
        {   int p,q,i;
    
            for(i=0; i<3; i++)  v[i]= 0;
            p= 0;
            for(i=0; i<3; i++)
            {   for(q=p; q<str.length() && str.charAt(q)!='/'; q++);
                if (q>p)    v[i]= Integer.parseInt(str.substring(p,q));
                p= q+1;
                if (p>=str.length())    return;
            }
        }
    
        // BufferedReader Close
        public void Close()
        {   try
            {   BR.close();  }
            catch(IOException e)
            {   System.out.println("File Close Error");  }
        }
    }
    
  2. BR は OBJ ファイルを入力する BufferedReader で BUF が一行分の入力バッファです。
    StringTokenizer でトークンを切り出します。
    頂点座標, テクスチャ座標, 法線ベクトルの数が前もって解らないので ArrayList を使っています。
    また各インデックスの数も解らないので柔軟に対応できるように ArrayList を使います。
    class obj_load extends LoaderBase
    {   SceneBase       Base;      // SceneBase
        BufferedReader  BR;        // 入力 Reader
        String          BUF;       // 1行分の入力バッファ
        StringTokenizer token;     // トークン Object
    
        ArrayList<Point3f>    vp = new ArrayList<Point3f>();    // 頂点座標
        ArrayList<TexCoord2f> vt = new ArrayList<TexCoord2f>(); // テクスチャ座標
        ArrayList<Vector3f>   vn = new ArrayList<Vector3f>();   // 法線ベクトル
    
        ArrayList<Integer>    idx = new ArrayList<Integer>();   // Index の区切り
        ArrayList<Integer>    xvp = new ArrayList<Integer>();   // 頂点 Index の並び
        ArrayList<Integer>    xvt = new ArrayList<Integer>();   // テクスチャ Index の並び
        ArrayList<Integer>    xvn = new ArrayList<Integer>();   // 法線ベクトル Index の並び
        
  3. s = f.load("cube.obj"); でモデルを入力して Scene を設定します。
    load メソッドでは、モデルファイルの名前が渡されるものとして load(String fname) だけ処理しています。
    set_list(); で OBJ ファイルを解析して、creatscene(); でモデルを生成します。
            // モデルの入力
            obj_load f = new obj_load();
            Scene s = null;
            s = f.load("cube.obj");
                ・・・
        @Override
        public Scene load(String fname)
        {
            File    file = new File(fname);
            try
            {   BR = new BufferedReader(new FileReader(file));  }
            catch(IOException e)
            {   System.out.println("File Open Error");  }
    
            set_list();
            creatscene();
            return Base;
        }
        
  4. 頂点座標と法線ベクトルを処理するだけで、結構多くのモデルの描画が可能です。
    Java3D のサンプル(j3d-examples-1_5_2-src.zip)に格納されている OBJ モデルも試してみて下さい。
    hand1.obj は三角ポリゴンだけで構成されている簡単なモデルです。
    galleon.obj は帆船の代表的なサンプルですが、描画することが出来ます。
    minimart.obj は f で相対参照が使われている唯一のモデルです。
    このモデルは Java3D でサポートされている ObjectFile() ではエラーが発生して描画できません。 (^_^;)
            s = f.load("cube_normal.obj");
            s = f.load("C:\\TMP\\j3d-examples\\src\\resources\\geometry\\hand1.obj");
            s = f.load("C:\\TMP\\j3d-examples\\src\\resources\\geometry\\p51_mustang.obj");
            s = f.load("C:\\TMP\\j3d-examples\\src\\resources\\geometry\\galleon.obj");
            s = f.load("C:\\TMP\\j3d-examples\\src\\resources\\geometry\\beethoven.obj");
            s = f.load("C:\\TMP\\j3d-examples\\src\\resources\\geometry\\minimart.obj");
        
  5. OBJ File を入力して ArrayList を設定する set_list() メソッドです。
    "v". "vt", "vn", "f" の行だけ処理しています。
  6. シーンを生成する creatscene() メソッドです。
    ArrayList を通常の配列に変換します。
    これで Scene を生成する で説明した、頂点座標と法線ベクトルとポリゴンを構成する Index の配列が整います。
    GeometryInfo() でモデルを生成して、法線ベクトルが定義されていない時は自動的に設定します。
  7. 「Loader の作成」は多少は腕に覚えのある方を対象にしています。
    プログラムの説明は、これまで説明した各ページを参照して下さい。

超初心者のプログラム入門(Java2)