OBJ モデルにマテリアル(色やテクスチャなど)を設定して描画します。

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

OBJ カラーモデル

  1. 立方体の各面に色を設定したモデル cube_color.obj です。
    OBJ モデルは TEXT 形式なので、エディッタなどでタイプして保存して下さい。
    色の設定は cube_color.obj とは別の material.mtl で定義されています。
    # cube object(マテリアルの設定)
    mtllib material.mtl
    
    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
    vn 0 0 -1
    vn -1 0 0
    vn 1 0 0
    vn 0 -1 0
    vn 0 1 0
    vn 0 0 1
    s off
    
    g face1
    usemtl emerald
    f 1//1 3//1 4//1 2//1
    g face2
    usemtl jade
    f 1//2 5//2 7//2 3//2
    g face3
    usemtl obsidian
    f 2//3 4//3 8//3 6//3
    g face4
    usemtl pearl
    f 1//4 2//4 6//4 5//4
    g face5
    usemtl ruby
    f 3//5 7//5 8//5 4//5
    g face6
    usemtl gold
    f 5//6 6//6 8//6 7//6
    
  2. 立方体に色を設定する material.mtl です。
    マテリアルファイルは TEXT 形式なので、エディッタなどでタイプして保存して下さい。
    # Created By Carrara 7.0
    
    #エメラルド
    newmtl emerald
    Ns 600
    d 1
    Ni 0.001
    illum 2
    Ka 0.0215  0.1745   0.0215
    Kd 0.07568 0.61424  0.07568
    Ks 0.633   0.727811 0.633
    
    #ひすい
    newmtl jade
    Ns 100
    d 1
    Ni 0.001
    illum 2
    Ka 0.135    0.2225   0.1575
    Kd 0.54     0.89     0.63
    Ks 0.316228 0.316228 0.316228
    
    #黒曜石
    newmtl obsidian
    Ns 300
    d 1
    Ni 0.001
    illum 2
    Ka 0.05375  0.05     0.06625
    Kd 0.18275  0.17     0.22525
    Ks 0.332741 0.328634 0.346435
    
    #真珠
    newmtl pearl
    Ns 88
    d 1
    Ni 0.001
    illum 2
    Ka 0.25     0.20725  0.20725
    Kd 1.0      0.829    0.829
    Ks 0.296648 0.296648 0.296648
    
    #ルビー
    newmtl ruby
    Ns 60
    d 1
    Ni 0.001
    illum 2
    Ka 0.1745   0.01175   0.01175
    Kd 0.61424  0.04136   0.04136
    Ks 0.727811 0.626959  0.626959
    
    #金  
    newmtl gold
    Ns 400
    d 1
    Ni 0.001
    illum 2
    Ka 0.24725  0.1995    0.0745
    Kd 0.75164  0.60648   0.22648
    Ks 0.628281 0.555802  0.366065
    
  3. OBJ ファイルは TEXT 形式で、全ての行がキーワードで始まります。
  4. マテリアルファイルは TEXT 形式で、全ての行がキーワードで始まります。

OBJ テクスチャモデル

  1. テクスチャを張り付けた立方体のモデル「cube_texture.obj」です。
    mtllib で指定されるファイルはカラーモデルと同様で、この中でテクスチャ画像が指定されます。
    # cube object(テクスチャを張り付ける)
    mtllib texmtl.mtl
    
    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
    vt 0 0
    vt 1 0
    vt 1 1
    vt 0 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
    s off
    
    g face1
    usemtl emerald
    f 1/1/1 3/2/1 4/3/1 2/4/1
    g face2
    usemtl jade
    f 1/1/2 5/2/2 7/3/2 3/4/2
    g face3
    usemtl obsidian
    f 2/1/3 4/2/3 8/3/3 6/4/3
    g face4
    usemtl pearl
    f 1/1/4 2/2/4 6/3/4 5/4/4
    g face5
    usemtl ruby
    f 3/1/5 7/2/5 8/3/5 4/4/5
    g face6
    usemtl gold
    f 5/1/6 6/2/6 8/3/6 7/4/6
    
  2. cube_texture.obj から参照するマテリアルのファイル texmtl.mtl です。
    # Created By Carrara 7.0
    
    #エメラルド
    newmtl emerald
    Ns 600
    d 1
    Ni 0.001
    illum 2
    Ka 0.0215  0.1745   0.0215
    Kd 0.07568 0.61424  0.07568
    Ks 0.633   0.727811 0.633
    map_Kd ayu.jpg
    
    #ひすい
    newmtl jade
    Ns 100
    d 1
    Ni 0.001
    illum 2
    Ka 0.135    0.2225   0.1575
    Kd 0.54     0.89     0.63
    Ks 0.316228 0.316228 0.316228
    map_Kd ayu.jpg
    
    #黒曜石
    newmtl obsidian
    Ns 300
    d 1
    Ni 0.001
    illum 2
    Ka 0.05375  0.05     0.06625
    Kd 0.18275  0.17     0.22525
    Ks 0.332741 0.328634 0.346435
    map_Kd ayu.jpg
    
    #真珠
    newmtl pearl
    Ns 88
    d 1
    Ni 0.001
    illum 2
    Ka 0.25     0.20725  0.20725
    Kd 1.0      0.829    0.829
    Ks 0.296648 0.296648 0.296648
    map_Kd earth.jpg
    
    #ルビー
    newmtl ruby
    Ns 60
    d 1
    Ni 0.001
    illum 2
    Ka 0.1745   0.01175   0.01175
    Kd 0.61424  0.04136   0.04136
    Ks 0.727811 0.626959  0.626959
    map_Kd earth.jpg
    
    #金
    newmtl gold
    Ns 400
    d 1
    Ni 0.001
    illum 2
    Ka 0.24725  0.1995    0.0745
    Kd 0.75164  0.60648   0.22648
    Ks 0.628281 0.555802  0.366065
    map_Kd earth.jpg
    
  3. テクスチャを張り付けた立方体のモデルをテストするときは、画像ファイルもプロジェクトのフォルダーに格納して下さい。
    上記のモデルでは ayu.jpg と earth.jpg の画像が使われています。

プログラムの作成

  1. OBJ ファイルを入力して、マテリアルを設定してモデルを描画します。
    //★ OBJ Model をロードする(Texture の設定)    前田 稔
    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;
    
    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.08);
            objScale.setTransform(t3d);
            objRoot.addChild(objScale);
    
            // Mouse 操作の設定
            TransformGroup trans = new TransformGroup();
            SetMouse(objRoot, trans);
            objScale.addChild(trans);
    
            // モデルの入力
            obj_load f = new obj_load();
            Scene s = null;
            //s = f.load("cube_color.obj");
            s = f.load("cube_texture.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
        // OBJ 入力関係
        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 の並び
    
        @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" + fname);  }
    
            Base = new SceneBase();
            Base.setSceneGroup(new BranchGroup());
            set_obj();          //※ OBJ data の登録
            Close();
            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 を入力して 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 に追加
            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();
    
            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)
        {
            File    mfile = new File(str);
            try
            {   mtlBR = new BufferedReader(new FileReader(mfile));
                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");  }
        }
    
        // 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)
            {   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");  }
        }
    
        // BufferedImage の入力
        public static BufferedImage loadImage(String fileName)
        {   InputStream is = null;
            try
            {   is = new FileInputStream(fileName);
                BufferedImage img = ImageIO.read(is);
                return img;
            }
            catch (IOException e)
            {   throw new RuntimeException(e);  }
            finally
            {   if (is != null)
                    try { is.close(); }
                    catch (IOException e) {}
            }
        }
    }
    
  2. 「OBJ Loader の基礎」の比べて material 関係の処理が増えています。
    mtlBR は material ファイルを入力する BufferedReader で mtlBUF が一行分の入力バッファです。
    mtlID は usemtl で参照されたプロパティの名前です。
    mtlTEX は map_Kd で指定されたテクスチャ画像です。
        // material 関係
        BufferedReader  mtlBR;     // 入力 Reader
        String          mtlBUF;    // 1行分の入力バッファ
        String          mtlID;     // usemtl ID
        String          mtlTEX;    // Texture name
        
  3. 「OBJ Loader の基礎」と比べて最も大きな違いは、usemtl でマテリアルの設定(ポリゴンの色やテクスチャ)が変わるので、 それまでに定義されたポリゴンをその都度生成することです。
    set_obj(); メソッドでシーンを登録する全ての処理を行います。
        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" + fname);  }
    
            Base = new SceneBase();
            Base.setSceneGroup(new BranchGroup());
            set_obj();          //※ OBJ data の登録
            Close();
            return Base;
        }
        
  4. シーンを登録する set_obj(); メソッドです。
    "mtllib" でマテリアルファイルをオープンします。
    "usemtl" でそれまでに定義されたポリゴンを生成します。
        // OBJ File を入力して Base を生成
        public void set_obj()
        {
                    ・・・
                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;
                }
        
  5. GeometryInfo() にテクスチャを設定します。
    setTextureCoordinateParams(1,2); の 1,2 は、二次元のテクスチャ座標を一枚使う設定です。
    どうもこの辺はバグがまだ残っているようで、将来仕様が変化しそうな雰囲気もあり、そのまま書いて下さい。
            // Texture の設定
            if (xvt.size()>0)
            {   ginfo.setTextureCoordinateParams(1,2);
                ginfo.setTextureCoordinates(0,texture);
                ginfo.setTextureCoordinateIndices(0,texidx);
            }
        
  6. Material の設定では mtlID; で指定されたプロパティを検索して、マテリアルやテクスチャを設定します。
    テクスチャが使われているときは loadImage(mtlTEX); で画像を入力して ap.setTexture(texture2d); で設定します。
        // 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;
        }
        
  7. ネットの動画サイトから OBJ モデル(mikuA.obj)をダウンロードすると、ページ先頭の画像を描画することが出来ます。
    obj_loader を開発していて気付いたのですが、ダウンロードしたモデルには意外とエラーが多いのですね。
    当初私は「モデルはツールを使って作成するのでエラーは無い」と思っていたのですが、明らかなタイプミスや mtl のエラーが目につきました。
    エラーが無くて、かっこ良く (^_^;) 描画出来るモデルは mikuA.obj ぐらいしか見つかりませんでした。
    素敵なモデルをお持ちの方は、ぜひ転送して下さい。
    「Loader の作成」は多少は腕に覚えのある方を対象にしています。
    プログラムの説明は、これまで説明した各ページを参照して下さい。

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