はじめに
ホームページ上に立体的な形状を表示して、それをマウスでグルグル動かせたら楽しいですね。
この記事では、3D形状を扱う楽しさを実現するJavaアプレットの作成方法を紹介します。
ブラウザ上で3Dモデルを表示しようと考えた場合、X3DやCult3D、YAPPA、Shockwave3D、XVLなどなど、素晴らしいアプリケーションが既にたくさんありますが、今回は3Dモデルを表示するためのアプレットをゼロから自作することを行います。JavaにはJava3Dという3D用の便利なAPIがありますが、この力も借りません(!)。実際のコードを元に3DCGの基礎を解説しますので、Javaアプレット作成の学習と共に3Dの基礎的な内容の学習にも役立てていただけたら幸いです。
全体的には次のような5つのステップで3Dモデルを表示する方法の説明を行います。
- Step1 3Dモデルを定義してとりあえず描画してみる
- Step2 ワイヤフレームモデルをグルグル回してみる
- Step3 面のあるモデルを表示する
- Step4 面の明るさを設定する
- Step5 ファイルからモデルのデータを読み込む
最終的には、図1のような3Dモデルをグルグル動かせるようになります。
コンピュータで3Dモデルを表示するプログラムを作成するときに最も重要な点は、見える部分をどのように表示するか、つまり「見えない部分をどのように隠すか」ということです。見えない部分を隠すことを「陰面消去」と呼び、Z-バッファ法、レイトレーシング法、スキャンライン法などの様々な方法が存在します。今回は「奥のものから順番に描画する」という最も単純で実装も簡単なアルゴリズムの「優先順位法(ペインターズアルゴリズム)」を紹介します。
対象読者
3DCGの基礎を学びたい人が対象です。
必要な環境
J2SE Development Kit(JDK) 1.4.xが必要です。
【ステップ1】3Dモデルを定義してとりあえず描画してみる
まず始めにアプレットに表示する3Dモデルの形を決めましょう。最初ですから、なるべく簡単なモデルがよいでしょう。と言っても単なる立方体では面白くないので、まずは図2のような形を考えてみます。
さて、この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; // 面(三角形)列を保持する
続いて、このArrayList
にVertex
クラスと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モデルの頂点と面を表すオブジェクトをvertices
とfaces
という名前のArrayList
に格納することができます。後は、これに基づいて3Dモデルを描画することになります。
今回は、描画する図はz座標軸の正の方向から原点へ向かう方向に向かってモデルを見た場合の形だとします(図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.x
、center.y
の値を追加しています。次にアプレットの座標系とモデルの座標系のy軸の向きが逆なので(アプレットのy軸は下向き)、頂点のy座標値にマイナスの符号をつけています。また、3Dモデルの大きさは1辺の大きさが2の立方体に収まる程度なので、アプレットの大きさを基準にしたscale
値を掛け合わせることで、アプレットの描画領域に適した大きさにしています。以上の手順で、頂点の座標値からアプレットに描画する時の座標を求めることができます。
ここまでの文章で、3Dモデルを表示するための概要を説明しました。それでは、今までの説明を総合したアプレット「Hello3D_Step1」のコードを見てみましょう。
このコードで、z軸方向から眺めた3Dモデルのワイヤフレームが表示されます。
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」実際にコンパイルして実行してみると、次のような結果になります。
確かに目的の形をz軸方向から眺めた形になっていますね。でも別の角度から見ることができないので、実際にどのような立体形状をしているのか把握することができません。
次のステップでは、このモデルをマウスでグルグル回転できるようにしてみます。