Javaソース付き 3Dプログラミング入門   (2004.08.10〜 S.Kitade)


このページは割と個人的なメモです。運良くこれが誰かの役に立てば幸いです。

・・・作成途中です (2004.08.29up)・・・





ここではコンピューターによる3D表示について考えます。
言い換えると、コンピューターのメモリー内に用意した3Dの物体のデータを、同じく用意したカメラで捕らえたと想定して画面に透視図として表示する処理を考えます。

そこで必要になるのが、空間にある3D物体をカメラを中心とした相対的な位置に変換する処理です。またカメラから見た位置が求まったら、カメラから遠い位置の点をカメラの視野の中心(=画面中心)に寄せてやる処理も必要です。
前者は回転と平行移動で、後者はカメラからの距離で割り算して解決します。後者の処理は透視変換と呼ばれます。


ではまず、回転処理について考えます。
話を簡単にするため、まずXY平面のみで考えます。

回転は一般に
x' = x*cosθ - y*sinθ
y' = x*sinθ + y*cosθ
というように求められます。

  XY平面上の点Aを角度θだけ回転させた図

どうしてかというと、三角関数の加法定理
sin(α+β) = sinα * cosβ + cosα * sinβ
cos(α+β) = cosα * cosβ - sinα * sinβ
で、
cosα を x
sinα を y
β を θ
に置き換えてみればわかります。
x' = cos(α+θ) = x*cosθ - y*sinθ
y' = sin(α+θ) = y*cosθ + x*sinθ = x*sinθ + y*cosθ
となりますので。

その他一般的には回転はベクトルの考え方で説明できます。このページの真ん中あたりにあった説明はわかりやすかったです。
改めて説明すると、元々のベクトルA(x, y)はX軸の単位ベクトル(1, 0)をu、Y軸の単位ベクトル(0, 1)をvとすると
ベクトルA = x * u + y * v
となります。(xやyはスカラー量です)
ここで角度θだけ回転した座標系を考えます。同じく回転したベクトルAは ベクトルA’= x * u' + y * v'
となり、θだけ回転した単位ベクトルの成分は
u' = (cosθ, sinθ)
v' = (cos(θ+π/2), sin(θ+π/2)) = (-sinθ, cosθ)
なので、
ベクトルA’= x * u' + y * v' = (x*cosθ, x*sinθ) + (-y*sinθ, y*cosθ) = (x*cosθ - y*sinθ, x*sinθ + y*cosθ)
となって加法定理での説明と一致します。

  座標系の回転


以上説明してきたXY平面上の原点を中心とする回転を、行列とベクトルの演算の形で書き直してみます。
x' = x*cosθ - y*sinθ
y' = x*sinθ + y*cosθ
z' = z
ですから、





x'
y'
z'





=




cosθ
sinθ
0
-sinθ
cosθ
0
0
0
1










x
y
z





となりますね。
真ん中の3x3の行列が、XY平面の原点を中心とする、つまりZ軸まわりの角度θの回転変換の行列です。

多項式ではなく、行列とベクトルの演算で表現することにより一般化でき応用がしやすくなり、またハードウェアの力を借りて演算を高速に行うことも容易になります。

他の軸を中心とする回転の行列を考える前に、前提として以下の座標系で考えることに決めておきます。左手座標系というやつです。尚、3DCGの世界でYは一般的に上向きです。
 

Z軸まわりの回転を考えた時と同じ向きで考えるように注意しながら、同様に他の軸まわりの回転変換の行列を導くと

X軸まわりの回転変換行列





1
0
0
0
cosθ
sinθ
0
-sinθ
cosθ






Y軸まわりの回転変換行列





cosθ
0
-sinθ
0
1
0
sinθ
0
cosθ






となります。


では次に、ゲーム世界内の基準となる座標系(ワールド座標系)にある物体の座標を、カメラを中心とする座標系(カメラ座標系)での座標に変換する具体的な手順を考えます。

     

図のベクトルVcをカメラの向きを表すベクトルとします。このベクトルVcはカメラの位置からカメラの注視点を結ぶベクトル、あるいはそれを正規化した(=長さ1の)ベクトルです。

変換の手順としては、まず、
(1)ワールド座標系での座標を-θ2だけY軸中心に回転
その後、
(2)X軸を中心としてθ1だけ回転
させます。

ここで、軸まわりの回転の向きを図にしておきます。
  各軸まわりの回転の向き
最初のXY平面での回転変換のθの向きをすべてに適用したため、この図の矢印の向きが角度の正の向きになります。

sin(-θ2) = -sin(θ2)、cos(-θ2) = cos(θ2) であることに注意して、上記の2段階の回転変換を行列とベクトルの積で表すと、





x'
y'
z'





=




1
0
0
0
cosθ1
sinθ1
0
-sinθ1
cosθ1










cosθ2
0
sinθ2
0
1
0
-sinθ2
0
cosθ2










x
y
z





         =




cosθ2
-sinθ1*sinθ2
cosθ1*sinθ2
0
cosθ1
sinθ1
-sinθ2
-sinθ1*cosθ2
cosθ1*cosθ2










x
y
z





のようになります。上の図からわかる通り、
sinθ1 = dy/a、cosθ1 = b/a
sinθ2 = dx/b、cosθ2 = dz/b
なので、行列の成分を求めることができます。
尚、この行列はカメラの位置や向きが変わった際には再計算する必要があります。



ではいよいよ、カメラ座標系内の図形を仮想スクリーンへ投影する処理について考えます。

  カメラ座標系と仮想スクリーン

この図のように、空間中の三角形を仮想スクリーンにカメラ位置から見える絵になるように投影しなくてはいけません。そのためには、カメラ座標系での座標x', y'を奥行きz'で割って、カメラから遠い図形をスクリーン中心に寄せてやります。
画面に表示する座標を(Sx, Sy)とすると、
Sx = x'*ScreenDepth/z'
Sy = y'*ScreenDepth/z'
となります。ScreenDepthは視点(カメラ座標系原点)から仮想スクリーンまでの距離です。また通常、スクリーンより手前の物体は表示しないように処理します。


これで3D表示の準備がひとまずできましたので、デモプログラムとソースで確認してみます。
以下は複数の三角形で構成されたモデルをワイヤーフレーム表示するアプレットです。

デモ:
 3D表示アプレット(ワイヤーフレーム)
ソース:
 GeomDefs.java:基本的な定義
 Vector3D.java:ベクトル
 Triangle3D.java:三角形
 Model3D.java:3Dモデル
 Camera3D.java:カメラ
 modelview01.java:サンプルアプレット本体





さて、ワイヤーフレームでは素っ気ないですので、三角形の面を塗りつぶしてみましょうか。
しかし、ただ単に塗りつぶすと面の境界がわからなくなってしまいます。ということで、光源を設定して、三角形がその光源の向きを向いていたら明るく、反対方向を向いていたら暗く表示することにします。

  面の明るさ

面の明るさは、三角形の面が光源方向に向いていれば明るくなります。これを判定するために面の法線ベクトルを求め、光源向きのベクトルとの内積を計算します。

内積は、2つのベクトルをV1 = (V1x, V1y, V1z)、V2 = (V2x, V2y, V2z)とすると
内積 = V1x*V2x + V1y*V2y + V1z*V2z
であり、また、2つのベクトルのなす角をθとすると
内積 = |V1|*|V2|*cosθ
と表されます(理由はこのへんとか)。

V1、V2共に単位ベクトルであれば内積=cosθとなりますので、この大きさ(-1〜1)が面の法線ベクトルと光源向きのベクトルのなす角の差異の小ささ、つまり明るさを表します。
このように明るさ判定では大きさが問題になるので、計算の前に両方のベクトルを単位ベクトルにしておく(長さ1に正規化しておく)必要があります。

三角形の面の法線ベクトルは、1頂点を起点とする2辺をベクトルと見なした場合の外積で求めることができます。

内積、外積については、このページあたりを見て頂ければと思います。
内積はスカラー量で向きはありませんが、外積はベクトルです。1つ目のベクトルの向きから2つ目のベクトルの向きへの回転を右ネジに加えた場合の、ネジの進行方向がその向きになります。


ついでに、カメラに対して裏側を見せている三角形は描画しない処理も加えておきます。
これはカメラベクトルと三角形の法線ベクトルの内積によって判定できます。
前述の通り内積にはcosθが反映されていますので、内積が0以上の場合はcosθがプラスの範囲のθ、つまり2つのベクトルのなす角が-90度〜90度だったことを意味します。内積が負の値であればカメラと面の法線ベクトルは互いに向き合った状態と判断できます。
このカメラと向き合った状態の三角形のみ描画してやればよいわけです。

  向きの判定


では、ポリゴンの表裏とライトを考慮した3D表示のデモを見てみましょう。

デモ:
 3D表示アプレット(テクスチャなしポリゴン、平行光源)
ソース:
 GeomDefs.java:基本的な定義
 Vector3D.java:ベクトル
 Triangle3D.java:三角形
 Model3D.java:3Dモデル
 Camera3D.java:カメラ
 modelview02.java:サンプルアプレット本体





これまでのデモでは問題になっていませんが、各三角形の描画の順番をうまくしないと奥のものが手前に描かれてしまうような不具合が出てしまいます。
対策として、奥行き値(カメラ座標系でのZ値)が大きい三角形から描画します。具体的には描画の際に毎回、描画要素のリストを作成して、それに三角形を追加する際にこれまでに追加された要素の奥行き値と比較して妥当な場所に要素を挿入します。
PS2のようにハードウェアでZバッファを用意できるものはそれに頼ればよいですが、そうでない環境では三角形毎にZ値を比較して並び替え(ソート)しながら描画することになると思います。

Z値の比較の際には、前回の追加位置から調べるように工夫してみました。3Dゲームの場合、疎な空間に多数のポリゴンを含むモデルが散在するような状況が多いと思いますし、またIDの連続したポリゴンは近い位置にあるはずですから、前回の追加位置周辺から調べるのが効率的だと思います。

ついでにシーンマネージャなるものを用意して、モデルやカメラや光源の管理もできるようにしてみました。

デモ:
 3D表示アプレット(Zソート、テクスチャなしポリゴン、平行光源)
ソース:
 GeomDefs.java:基本的な定義
 SceneManager3D.java:3Dシーンマネージャ
 Vector3D.java:ベクトル
 Primitive3D.java:描画要素基本
 Triangle3D.java:三角形
 Model3D.java:3Dモデル
 Camera3D.java:カメラ
 modelview03.java:サンプルアプレット本体





以下はテクスチャを貼ったデモです。そのうち解説したいです。

 3D表示アプレット(テクスチャ付きポリゴン、平行光源)





一般に、多くの3Dゲームのキャラクターは関節を持った構造をしています。これまでは単体のモデルを扱ってきましたが、関節を持ったモデルについて考えてみます。
関節は、親モデルに子モデルが付属するというような親子関係で表現します。子モデルは親モデルに対して位置のずれと向きのずれを持っているのが普通です。末端の子モデルまで正しく描画するには、まずこのずれを1つずつ戻してワールド座標系での子モデルの座標を計算する必要があります。

  子モデルのローカル座標をワールド座標系での座標に変換する

図のようなモデル1の子モデルとしてモデル2が付属する状況を考えます。

ここで回転行列と書いてあるものは、親の座標系に対する各軸の回転を与える行列です。中身は各軸の回転行列の積です。回転量が変われば再計算する必要があります。

まず、モデル2のローカル座標系のベクトルPを、モデル1座標系からモデル2座標系への回転変換でモデル1座標系での向きに直して、次にワールド座標系からモデル1座標系への回転変換でワールド座標系での向きに直します。
つまり、P"=M1*M2*P でベクトルP"がワールド座標系でのPになります。
位置のずれに関しては、それぞれのずれを親モデルの回転分だけ回転させた量を足しこみます。上の図ではオフセット1+オフセット2のM1による回転変換分がずれの総和です。

以上の説明では向きとモデル位置(モデル中心位置)のずれを別々に計算していますが、回転行列と平行移動を合わせて4次の行列にすることにより、一度に変換することもできます。R0x等が回転行列の要素、Tx等が平行移動分(オフセット)です。






x'
y'
z'
1






=





R0x
R0y
R0z
0
R1x
R1y
R1z
0
R2x
R2y
R2z
0
Tx
Ty
Tz
1












x
y
z
1






※以下のサンプルでは別々に計算しています。また以下のサンプルでは子モデルは親モデルからの積算された回転行列と中心位置を保持しています。


ではデモを見てみます。一番下と真ん中のモデルを違う速さで回転させています。ソースではModel3Dクラスが主な更新です。
各モデルのローカル座標系での3軸の回転を指定できるようにしてあります。

デモ:
 3D表示アプレット(多関節、Zソート、テクスチャなしポリゴン、平行光源)
ソース:
 GeomDefs.java:基本的な定義
 SceneManager3D.java:3Dシーンマネージャ
 Vector3D.java:ベクトル
 Primitive3D.java:描画要素基本
 Triangle3D.java:三角形
 Model3D.java:3Dモデル
 Camera3D.java:カメラ
 modelview05.java:サンプルアプレット本体





モーションを作ってみました。モーションの更新によって各子モデルの相対角度(関節の回転角)や、大きさの比率(スケール)を変更できるようにしてあります。これまでのサンプルで関節の回転については説明済みなので数学的に新しいことはありませんが、各軸方向に拡大縮小できるようにModel3DクラスにScaleデータを付加しました。
その他大きな変更点としては、モーション再生機能を持ったMotionModel3DクラスをModel3Dクラスの派生クラスとして新たに用意しました。このあたりは実装の問題ですので、目的に合ったプログラムを作成すればよいと思います。以下は1つの例です。

デモ:
 3D表示アプレット(多関節、モーション付き、Zソート、テクスチャなしポリゴン、平行光源)
ソース:
 GeomDefs.java:基本的な定義
 Vector3D.java:ベクトル
 Primitive3D.java:描画要素基本
 Triangle3D.java:三角形
 Model3D.java:3Dモデル
 MotionModel3D.java:モーション再生機能付きモデル
 Motion3D.java:3Dモーション
 Camera3D.java:カメラ
 SceneManager3D.java:3Dシーンマネージャ
 modelview06.java:サンプルアプレット本体


・・・続く







ファミコン基礎科学研究所

Mail: kitade@za2.so-net.ne.jp