Wavefront で作成した OBJ Loader のα版です。

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

OBJ Loader

  1. obj_loader class を独立して、別ファイル(obj_loader.java)にしました。
    メインプログラムと同じフォルダーに置いて、コンパイルして下さい。
    //★ obj_loader  Alpha 版 2010/09/23    前田 稔
    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.*;
    import javax.imageio.ImageIO;
    import java.awt.image.BufferedImage;
    import com.sun.j3d.utils.image.TextureLoader;
    
    // ★ obj_loader Object Class
    public class obj_loader extends LoaderBase
    {
        public static final int RESIZE = 1;
        SceneBase       Base;          // SceneBase
        int             FLAG;          // Loader Flag
        TransformGroup  Trans;         // 中央移動
        TransformGroup  Size;          // サイズ調整
        // OBJ 入力関係
        URL             PATH= null;    // ファイルの PATH
        BufferedReader  BR;            // 入力 Reader
        String          BUF;           // 1行分の入力バッファ
        StringTokenizer token;         // トークン Object
        // material 関係
        BufferedReader  mtlBR;         // 入力 Reader
        String          mtlBUF;        // 1行分の入力バッファ
        String          mtlID;         // usemtl ID
        String          mtlTEX;        // Texture name
    
        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 の並び
    
        // Constructor
        public obj_loader()
        {   FLAG= 0;
        }
        public obj_loader(int flag)
        {   FLAG= flag;
        }
    
        @Override
        public Scene load(String fname)
        {   Base = new SceneBase();
            try
            {   File file = new File(fname);
                java.net.URI  uri = file.toURI();
                PATH = uri.toURL();
                BR = new BufferedReader(new FileReader(file));
            }
            catch(IOException e)
            {   System.out.println("File Open Error" + fname);
                return Base;
            }
            Create();
            return Base;
        }
    
        @Override
        public Scene load(URL aURL)
        {   Base = new SceneBase();
            if (PATH == null)   PATH= aURL;
            try
            {   InputStream istream = aURL.openStream();
                InputStreamReader ireader = new InputStreamReader(istream);
                BR = new BufferedReader(ireader);
            }
            catch(IOException e)
            {   System.out.println("URL Open Error" + aURL);
                return Base;
            }
            Create();
            return Base;
        }
    
        @Override
        public Scene load(Reader reader)
        {   Base = new SceneBase();
            BR = new BufferedReader(reader);
            Create();
            return Base;
        }
    
        // Scene を生成
        public void Create()
        {   Base.setSceneGroup(new BranchGroup());
    
            // 移動の初期化
            Trans = new TransformGroup();
            Transform3D t3d = new Transform3D();
            t3d.setTranslation(new Vector3d(0.0, 0.0, 0.0));
            Trans.setTransform(t3d);
            // サイズの初期化
            Size = new TransformGroup();
            Transform3D scl = new Transform3D();
            scl.setScale(1.0);
            Size.setTransform(scl);
    
            Size.addChild(Trans);
            Base.getSceneGroup().addChild(Size);
            set_obj();          //※ OBJ data の登録
            Close();
            if ((FLAG&1) == 1)  // RESIZE フラグ ON
            {   Resize();  }
        }
    
        // OBJ File を入力して Base を生成
        public void set_obj()
        {   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);
                    }
                }
                else  if ("mtllib".equals(str)) // mtllib material.mtl
                {   str = token.nextToken();
                    open_mtl(str);
                }
                else  if ("usemtl".equals(str)) // usemtl emerald
                {   str = token.nextToken();    // 次の material ID
                    creatscene();               // ポリゴンモデルの生成
                    idx.clear();                // Index 配列のクリア
                    xvp.clear();
                    xvt.clear();
                    xvn.clear();
                    mtlID = str;
                }
            }
            creatscene();   // ポリゴンモデルの生成
        }
    
        // Scene を生成
        public void creatscene()
        {   int i,cnt;
    
            if (xvp.size()<1)   return;
    
            // 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);
    
            // Texture の設定
            if (xvt.size()>0)
            {   ginfo.setTextureCoordinateParams(1,2);
                ginfo.setTextureCoordinates(0,texture);
                ginfo.setTextureCoordinateIndices(0,texidx);
            }
    
            // 法線ベクトルの設定
            if (xvn.size()>0)
            {   ginfo.setNormals(normal);
                ginfo.setNormalIndices(normidx);
            }
            else
            {    NormalGenerator gen = new NormalGenerator();
                 gen.generateNormals(ginfo);
            }
    
            // Material を設定して SceneBase(Trans) に追加
            Shape3D shape = new Shape3D(ginfo.getGeometryArray());
            shape.setAppearance(createAppearance());
            Trans.addChild(shape);
        }
    
        // Material の設定
        private Appearance createAppearance()
        {   Appearance ap = new Appearance();
            Material mat = new Material();
    
            if (mtlBR!=null)
            {   if (search())                       // mtl ID を検索
                {   set_mat(mat);                   // mtl ID のマテリアルを設定
                    if (xvt.size()>0 && mtlTEX!=null)
                    {
                        BufferedImage bimage = loadImage(mtlTEX);
                        TextureLoader texload = new TextureLoader(bimage);
                        Texture2D texture2d = (Texture2D)texload.getTexture();
                        ap.setTexture(texture2d);
                    }
                }
            }
            ap.setMaterial(mat);
            return ap;
        }
    
        // material File Open
        private void open_mtl(String str)
        {
            try
            {   InputStream is = new URL(PATH, str).openStream();
                InputStreamReader myin = new InputStreamReader(is);
                mtlBR = new BufferedReader(myin);
                mtlBR.mark(2000);
            }
            catch(IOException e)
            {   System.out.println("material Open Error= " + str);  }
        }
    
        // search mtl ID
        private boolean search()
        {   String  str;
            try
            {   mtlBR.reset();
                while((mtlBUF=mtlBR.readLine()) != null)
                {   token = new StringTokenizer(mtlBUF, " ,\t", false);
                    if (token.hasMoreTokens()==false)   continue;
                    str = token.nextToken();
                    if ("newmtl".equals(str)==false)    continue;
                    str = token.nextToken();
                    if (mtlID.equals(str))  return true;
                }
            }
            catch(IOException e)
            {   System.out.println("material Read Error");  }
            return false;
        }
    
        // マテリアルを設定
        private void set_mat(Material mat)
        {   String  str;
            mtlTEX = null;
            try
            {   while(true)
                {   mtlBUF= mtlBR.readLine();
                    if (mtlBUF==null)  return;
                    token = new StringTokenizer(mtlBUF, " ,\t", false);
                    if (token.hasMoreTokens()==false)   continue;
                    str = token.nextToken();
                    if ("newmtl".equals(str))   return;
                    else  if ("Ka".equals(str))       // Ambient color
                    {   mat.setAmbientColor(new Color3f(MVal(), MVal(), MVal()));
                    }
                    else  if ("Kd".equals(str))       // Diffuse color
                    {   mat.setDiffuseColor(new Color3f(MVal(), MVal(), MVal()));
                    }
                    else  if ("Ks".equals(str))       // Specular
                    {   mat.setSpecularColor(new Color3f(MVal(), MVal(), MVal()));
                    }
                    else  if ("Ns".equals(str))       // Shininess (clamped to 1.0 - 128.0)
                    {   mat.setShininess(MVal());
                    }
                    else  if ("map_Kd".equals(str))   // map_Kd File
                    {   mtlTEX = token.nextToken();
                    }
                }
            }
            catch(IOException e)
            {   System.out.println("material Read Error");  }
        }
    
        // Model の Resize
        private void Resize()
        {   float   xmn,xmx,ymn,ymx,xrate,yrate;
            if (vp.size()==0)   return;
            xmn=xmx= vp.get(0).x;
            ymn=ymx= vp.get(0).y;
            for(int i=1; i<vp.size(); i++)
            {   if (xmn>vp.get(i).x)    xmn= vp.get(i).x;
                if (xmx<vp.get(i).x)    xmx= vp.get(i).x;
                if (ymn>vp.get(i).y)    ymn= vp.get(i).y;
                if (ymx<vp.get(i).y)    ymx= vp.get(i).y;
            }
    
            // 移動
            xrate= 0.0f - (xmx+xmn)/2.0f;
            yrate= 0.0f - (ymx+ymn)/2.0f;
            Transform3D t3d = new Transform3D();
            t3d.setTranslation(new Vector3d(xrate, yrate, 0.0));
            Trans.setTransform(t3d);
            // 縮小
            xrate= 1.0f/(xmx-xmn);
            yrate= 1.0f/(ymx-ymn);
            Transform3D scl = new Transform3D();
            if (xrate<yrate)    scl.setScale(xrate);
            else                scl.setScale(yrate);
            Size.setTransform(scl);
        }
    
        // token(mtlBUF)から次の値を取得
        private float MVal()
        {   if (token.hasMoreTokens()==false)   return 0.0f;
            return Float.parseFloat(token.nextToken());
        }
    
        // Line Read(BUF に入力)
        private boolean LineRead()
        {
            try
            {   BUF = BR.readLine();  }
            catch(IOException e)
            {   System.out.println(e);  }
            if (BUF==null)  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");  }
        }
    
        // BufferedImage の入力
        public BufferedImage loadImage(String fileName)
        {   BufferedImage img= null;
            try
            {   URL u = new URL(PATH, fileName);
                img = ImageIO.read(u);
            }
            catch (IOException e)
            {   System.out.println("Image Open Error=" + fileName);  }
            return img;
        }
    }
    
  2. 前回の OBJ モデルにマテリアルを設定 から修正した箇所を重点的に説明します。
    Java Applet でも使えるようにクラス名を小文字で統一しました。
    私がサーバーにアップロードするときに、全てのファイル名を小文字に変換しているためです。
    また Java Applet から呼び出せるように、全ての load() メソッドをサポートしました。
    これで任意のフォルダーに格納されているモデルを直接指定することが出来ます。
        @Override
        public Scene load(String fname)
        {   Base = new SceneBase();
            try
            {   File file = new File(fname);
                java.net.URI  uri = file.toURI();
                PATH = uri.toURL();
                BR = new BufferedReader(new FileReader(file));
            }
            catch(IOException e)
            {   System.out.println("File Open Error" + fname);
                return Base;
            }
            Create();
            return Base;
        }
    
        @Override
        public Scene load(URL aURL)
        {   Base = new SceneBase();
            if (PATH == null)   PATH= aURL;
            try
            {   InputStream istream = aURL.openStream();
                InputStreamReader ireader = new InputStreamReader(istream);
                BR = new BufferedReader(ireader);
            }
            catch(IOException e)
            {   System.out.println("URL Open Error" + aURL);
                return Base;
            }
            Create();
            return Base;
        }
    
        @Override
        public Scene load(Reader reader)
        {   Base = new SceneBase();
            BR = new BufferedReader(reader);
            Create();
            return Base;
        }
        
  3. モデルのサイズが大きすぎたり、小さすぎたりするとプログラムは正しくても描画されないことがあります。
    そこでモデルを標準のサイズに縮小・拡大して、画面中央に配置する RESIZE パラメータをサポートしました。
            // モデルの入力
            obj_loader f = new obj_loader(obj_loader.RESIZE);
            Scene s = null;
            s = f.load(Model_name);
                ・・・
    
        public static final int RESIZE = 1;
        SceneBase       Base;          // SceneBase
        int             FLAG;          // Loader Flag
        TransformGroup  Trans;         // 中央移動
        TransformGroup  Size;          // サイズ調整
                ・・・
    
        // Constructor
        public obj_loader()
        {   FLAG= 0;
        }
        public obj_loader(int flag)
        {   FLAG= flag;
        }
        
  4. Create() メソッドで Trans と Size に初期値を設定して下さい。
            // 移動の初期化
            Trans = new TransformGroup();
            Transform3D t3d = new Transform3D();
            t3d.setTranslation(new Vector3d(0.0, 0.0, 0.0));
            Trans.setTransform(t3d);
            // サイズの初期化
            Size = new TransformGroup();
            Transform3D scl = new Transform3D();
            scl.setScale(1.0);
            Size.setTransform(scl);
    
            Size.addChild(Trans);
            Base.getSceneGroup().addChild(Size);
        
  5. Resize() メソッドでモデルの大きさを調べて、移動と拡大・縮小を設定します。
        // Model の Resize
        private void Resize()
        {   float   xmn,xmx,ymn,ymx,xrate,yrate;
            if (vp.size()==0)   return;
            xmn=xmx= vp.get(0).x;
            ymn=ymx= vp.get(0).y;
            for(int i=1; i<vp.size(); i++)
            {   if (xmn>vp.get(i).x)    xmn= vp.get(i).x;
                if (xmx<vp.get(i).x)    xmx= vp.get(i).x;
                if (ymn>vp.get(i).y)    ymn= vp.get(i).y;
                if (ymx<vp.get(i).y)    ymx= vp.get(i).y;
            }
    
            // 移動
            xrate= 0.0f - (xmx+xmn)/2.0f;
            yrate= 0.0f - (ymx+ymn)/2.0f;
            Transform3D t3d = new Transform3D();
            t3d.setTranslation(new Vector3d(xrate, yrate, 0.0));
            Trans.setTransform(t3d);
            // 縮小
            xrate= 1.0f/(xmx-xmn);
            yrate= 1.0f/(ymx-ymn);
            Transform3D scl = new Transform3D();
            if (xrate<yrate)    scl.setScale(xrate);
            else                scl.setScale(yrate);
            Size.setTransform(scl);
        }
        
  6. Shape3D は Trans に追加します。
            // Material を設定して SceneBase(Trans) に追加
            Shape3D shape = new Shape3D(ginfo.getGeometryArray());
            shape.setAppearance(createAppearance());
            Trans.addChild(shape);
        

メインプログラムの作成

  1. obj_loader Class を使ったメインプログラムの例です。
    OBJ_View.java の名前で obj_loader.java と同じフォルダーに格納して下さい。
    OBJ_View はコマンドラインで指定された OBJ モデルを描画します。
    //★ OBJ Model Viewer    前田 稔
    //   java OBJ_View cube.obj
    import java.awt.*;
    import javax.swing.*;
    import javax.media.j3d.*;
    import javax.vecmath.*;
    import com.sun.j3d.utils.universe.*;
    import com.sun.j3d.loaders.*;
    import com.sun.j3d.utils.behaviors.mouse.*;
    
    public class OBJ_View extends JFrame
    {   static String  Model_name;
    
        // main Method
        public static void main(String[] args)
        {   if (args.length<1)
            {   System.out.println("java  OBJ_View  OBJModel");
                return;
            }
            Model_name= args[0];
            java.awt.EventQueue.invokeLater(new Runnable()
            {   public void run()
                {   new OBJ_View().setVisible(true);  }
            });
        }
    
        // Constructor
        public OBJ_View()
        {   // 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);
    
            // Mouse 操作の設定
            TransformGroup trans = new TransformGroup();
            SetMouse(objRoot, trans);
            objRoot.addChild(trans);
    
            // モデルの入力
            obj_loader f = new obj_loader(obj_loader.RESIZE);
            Scene s = null;
            s = f.load(Model_name);
    
            trans.addChild(s.getSceneGroup());
    
            // 背景色の設定
            Color3f bgColor = new Color3f(0.05f, 0.2f, 0.05f);
            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);
       }
    }
    
  2. OBJ_View はコマンドラインから次のように起動して下さい。
        java OBJ_View cube_texture.obj
        java OBJ_View "C:\\TMP\\j3d-examples\\src\\resources\\geometry\\galleon.obj"
        java OBJ_View "C:\\TMP\\j3d-examples\\src\\resources\\geometry\\beethoven.obj"
        java OBJ_View "C:\\TMP\\mikuAB_obj\\mikuA.obj"
        
  3. ネットの動画サイトから OBJ モデル(mikuA.obj)をダウンロードすると、ページ先頭の画像を描画することが出来ます。
    ダウンロードしたモデルには意外とエラーが多く、明らかなタイプミスや mtl のエラーが目につきました。
    エラーが無くて、かっこ良く描画出来るモデルは mikuA.obj ぐらいしか見つかりませんでした。 (^_^;)

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