Technote 1125
Building a 3D application that calls RAVE
3D
アプリケーションの設計
高いパフォーマンスを発揮する3Dアプリケーションを設計するには、多くの作業が必要です。広いworld-spacesと大量のポリゴンを用いるのはもはや例外でなく常識となっています。成功するアプリケーションには以下の様な、いくつかの異なった種類の処理が必要となります。
- 可視/不可視の判断(Visibility determination) --
指定された現在のカメラの位置から、どのポリゴンが見えるかの判断。オブジェクトのviewing
frustumから外れた部分や他のポリゴンに隠れた部分は、効率的にクリッピングされるべきです。
- ディテールのレベル --
遠くにあるオブジェクトは、近くのものより少ないポリゴンで構成されるべきです。
- 隠面除去(Hidden Surface Removal) --
ポリコンの描写は、その深度順(depth
ordering)が保証されなければなりません。
- ラスタ化(Rasterization) --
いかに最終的に得られたポリゴンのリストを高速に描写するか、またピクセルの再描画を最小とするか
ハードウェア・アウセラレーションは3Dアプリケーション作成のルールを変えました。ラスタ化は通常ハードウェアによって実行され、他の処理とは非同期に行われます。ハードウェアによるz-bufferingは、隠面除去処理をソフトウェアによる処理より高速に行うために使われます。デベロッパは3Dアプリケーションの設計時に、可視/不可視の判断とディテールのレベル調整に焦点を絞ることにより、3Dアプリケーションのアドバンテージを享受することができます。以下はハードウェア・ベースのアプリケーションを構築する場合のサジェスチョンです。
3Dアクセラレータは非常に効率の良いpixel
pipelineですが、ポリゴン・スキャンライン・個々のピクセル毎に決まるオーバーヘッドがあります。多くの場合、ある種の
filtering
modeでの描画はオーバーヘッドを引き起こします。例えば、trilinear
filteringはpoint
samplingの8倍のtexelの引き出し処理を必要とします。PCIバスによるポリゴン・データの転送によるオーバーヘッドに加えて、初期のアクセラレータではソフトウェアによる3角ポリゴンの準備処理が膨大になり、ポリゴンあたりの準備時間が何サイクルも増えてしまいます。
小さなポリゴンのレンダリング時間の総計は、ソフトウェアのみによるレンダリングに必要な時間より多くなります。少なくとも1社のデベロッパが、彼等のアプリケーションによってレンダリングされたポリゴンの多くは4ピクセルかそれより小さなものだと報告しています。この場合、低レベルのディテールのモデルを作れば、ハードウェアに送られるポリンゴンの数を減らし、イメージの品質に関する対価を小さくします。
可視/不可視の判断(Visibility determination)
大きなワールドを作成する時に、ポリゴンのかなりの部分はカメラ位置からは見えないでしょう。いくつかは視野(viewing
frustum)から完全にはずれています。残りの見えない部分は、他のポリゴンに隠れています。アプリケーションは単純にどんどんデータをハードウェアに送り込むのではなく、効率良くポリゴンが選択される様データを管理し、十分早く描画されるよう除外しましょう。さらに、各ポリゴンをそれぞれに描画することは、テクスチャのロードを引き起こします。テクスチャの廃棄は顕著なパフォーマンスの低下を招きます(この詳細は後述します)。かつてデベロッパが使ってきた手法が2つあります。詳細は触れませんが、どちらも参考文献のなかで扱われています。
Quake法はBSPツリーを使って座標情報を管理します。BSPツリーの一つの利点は、視野に入るかどうかの一度の判断でそれ以下のサブツリーを除外でき、多くのポリゴンを効率良く除外できる事です。さらに、Quakeはツリーの各葉の可視ポリゴンのルーズな集合に対して事前の計算を行います。フレームをレンダリングする時に、Quakeでは現在の葉のPVSに含まれるポリゴンしか考慮しませんので、調査しなくてはならないポリゴンの数をドラスティックに減らすことができます。
Portals法は、効率良くポリゴンを除去するもう一つの方法です。一つの出入り口しかない閉ざされた不透明な部屋の中からは、出入り口の向こうがわにあるポリゴンしか見えないでしょう。出入り口は、次の部屋への玄関(portal)となります。Portalを使ってレンダリングするには、部屋の中の全てのポリゴンを描画し、現在の視野(viewing
frustum)から見えるportalを判断し、リカーシブにportalによって狭められた視野を使って次の部屋を描画していきます。
Clipping
いくつかのハードウェアはレンダリングされた全てのピクセルに自動的にクリッピングを施します。これはOpenGLの"scissor
clip"として知られています。膨大な三角がテストすべき幾千のピクセルを生じさせ、ハードウェアによって破棄されます。よりよい解法は、ソフトウェアによってalgorithmicallyにポリゴンをクリップする事です。RAVEはエンジンがクリッピングを行わないと判断するので、アプリケーションでRAVEに渡す前に全てのポリゴンに対して事前のクリッピングを施す必要があります。
Z-Buffering
ハードウェアによるZ-Bufferingは、隠面除去に非常に役に立ちますが、一般的に実際にレンダリングするバッファより多くのメモリを必要とします。2Mbyteの2D/3Dカードでは、640x480のフロント・バック・z-bufferを使うと、テキスチャには1/4Mbyte以下のVRAMしか残りません。2Mbyteのカードで実行するアプリケーションでは、テクスチャに十分なVRAMを与えるために、ソフトウェアによるHSRアルゴリズムの導入を考慮する必要があるでしょう。
一部のハードウェアは、z-buffering処理をより効率的に行います。シーンの一部のポリゴンがすでに(BSPツリーの様に)ソートされている場合、そのポリゴンの順序をハードウェアに送れば、パフォーマンスを向上させることができます。例えば、そのハードウェアで"z
write"の方が"z test"より早い場合、"z
test"をオフにしてソートしたポリゴンを奥から前に順番に描画します。逆に"z
test"の方が"z
write"より早い場合は、testをオンにして前から奥に描画します。RAVEはこの様な方法を見つけるためのAPIを提供しませんので、様々なハードウェアであなたのアプリケーションをテストすることが、このような特性を発見する手助けとなるでしょう。
RAVE
が期待するものは
RAVE はハードウェアに被せた非常に薄い層です。RAVE
ルーチンの呼び出しはたいていの場合 RAVE
ドライバを直接呼び出すのと変わりません。RAVE
自体にはオーバーヘッドはありません。
デフォルトでは、アプリケーションが
QARenderStart
を呼び出してフレームを開始すると、エンジンは RAVE
コンテキストで指定された色でバックバッファを初期化し、Z
バッファを距離 1.0
に設定します。続いてアプリケーションは、エンジンにポリゴンを送出します。ポリゴンはあらかじめ表示可能領域でクリップされていなければなりません。アプリケーションが
QARenderEnd
を呼び出すと、実行待ちのコマンドはすべてハードウェアに吐き出され、ハードウェアは非同期にフレームをフロントバッファにコピーします。RAVE
の初期のバージョン (および初期のエンジンの多く) は
QARenderEnd
の中で同期を取り、ハードウェアの処理の終了を待ちます。
RAVE
エンジンがどんな値を期待しているのかを知ると便利です。このため、テクスチャの頂点の構造体
(TQAVTexture)
に保存される典型的な値を見てみましょう。グーローシェーディング
(gouraud-shading) されたポリゴンもほぼ同様です。
x と y は画面の座標を表す浮動小数点値です。640x480
コンテキストでは、それぞれ値の範囲は、0 <= x <
640、0 <= y < 480
になります。下と右の境界点は含まれないことに注意してください。RAVE
のデバッグバージョンはバウンディングボックスの外側への描画を検知します。
z の値は 0 から 1 です。昨今の 3D ハードウェアがサポートする
Z バッファはほとんど 16 ビットです。したがって、Z
バッファの値域は最大限、使いたいはずです。ヒザー平面 (hither
plane) は 0 の位置に置かなければなりません。Z バッファは最初
1.0 に初期化されるので、ヨン平面 (yon plane) をきっちり 1.0
の位置に置くと、その平面上またはその近くのピクセルは描画されません。そのため、視点錐台内のすべての点を表示するにために十分な、最も近い位置にヨン平面を移動させるとよいでしょう。エンジンには、この値を計算するための
2 つのタグ変数 (kQATag_ZMinScale および
kQATag_ZMinOffset) が用意されています。
invW の値は 1/w
です。w は遠近法 (perspective viewing)
で使用される補正因子 (correction factor) です。RAVE では通常
invW は 1/z
に単純化できます。しかし、ヒザー平面を z=0
に置くとそうはできません。ヒザー平面が 0
より少し大きい位置にあれば、Z
バッファの値域をわずかに失うだけで 1/z
が使用できるため、何も問題はありません。でなければ、Z 値を 0
と 1 の間に調整する前に invW
を計算しなければなりません。
遠近法補正済みのテクスチャマッピングを行うためには、テクスチャ座標
U および V に invW
を掛ける必要があります。(u,v) 座標は
(0,0)
がテクスチャマップの左下角になります。
この他、特殊なテクスチャリングモードを必要とするアプリケーションはカラー値を設定しなければなりません。アップルの
3D アクセラレータは常にこのようなモードを実行するので
(禁止はできません)、アップルのハードウェアに描画する場合は必ずモジュレーション値とハイライト値を設定してください。
テクスチャモジュレーションはライティング効果を出すのに便利です。転写効果
(decal effects) はフォギングをシミュレートでき、Z
座標に基づいてアルファ値を選び、テクスチャとライティング値をブレンドします。ハードウェアによるフォギングが一般的になっています。RAVE
の将来のバージョンでは直接サポートする予定です。
既存の 3D アプリケーションを RAVE
を呼び出すように変更するにはこれまでの情報で十分でしょう。すでにこうした変更を行ったゲームデベロッパの多くは、グーローシェーディングを施す三角形については一時間もあれば十分でした。テクスチャを施す三角形の場合は、テクスチャ管理が伴うため、もっと時間がかかります。
RAVE への変換作業を速める 1 つの方法は、モニタをもう 1 台と
3D
アクセラレータビデオカードを追加することです。はじめはソフトウェアによるレンダリング部分はそのままにしておき、RAVE
モニタに対しても同じ呼び出しを行うようにします。こうすると、アプリケーションのソフトウェアのレンダリングとハードウェアのレンダリングを比較できるというメリットも生まれます。
最終的には、ソフトウェアラスタライザへの呼び出しと RAVE
への呼び出しを切り替えられるようにします。RAVE はかなり薄い
API です。ですから、手持ちのソフトウェアラスタライザを RAVE
エンジンとして作り直して、アプリケーションの起動時に RAVE
エンジンとして登録する、という対応も可能です。ハードウェアがない場合はあなたの作成したエンジンでアップルのソフトウェアエンジンを置き換えるわけです。アップルのソフトウェアエンジンは高品質の描画を行いますが、ゲームで性能が出るようにはオプティマイズされていません。
3D
アプリケーションの構築
このセクションでは、3D
アプリケーションを書いたことのない方のために、必要となるいくつかの概念と数式を紹介します。このテーマについてはたくさんの本が書かれています。このうちいくつかは、RAVE
のドキュメントの中と、この TECHNOTE の最後の参考文献で紹介されています。
どんなものでも描画はカメラと表示平面 (viewing plane)
を基準に行われます。カメラは 3D ワールド内である位置
(position) と向き (orientation)
を持っています。表示平面は表示領域 (viewed area)
内の画面の位置を示します。クリッピングに使う錐台(viewing
frustum)は表示平面の周りに形成されます。描画対象のすべてのオブジェクトがこの錐台にしたがってクリップされます。
これから座標系と若干の定義を行い、典型的なアプリケーションがたどるレンダリングのパイプラインを紹介します。ここでは、陰面消去
(HSR = hidden surface removal) やオブジェクトの間引き
(object culling)
など、複雑な形式は取り上げません。それを説明すると 1
冊の本になってしまうからです。
我々のアプリケーションでは左手を基準にした座標系を用います。x
は左から右へ、y は下から上へ、z
は画面の奥に向かって大きくなります。
ローカル座標
ローカル座標は、モデルの変換 (transform)
前の座標です。モデルは原点を中心に Z
軸の向きに配置します。
オブジェクトはそれぞれ固有の位置と向きを持ちます。それを元にローカル座標からワールド座標に変換します。
ワールド座標
ワールド座標は、すべてのオブジェクトを配置する標準の座標系です。これを画面に描画するには、観察点、カメラの向き、表示平面までの距離を決めます。表示矩形
(viewing rectangle)
は向きを表すベクトルに対して常に垂直であると考えます。こうすると、アプリケーションの設計が単純になります。
カメラ座標
カメラの位置と向きがわかると、カメラの視線を Z
軸に揃え、Y
軸の上昇方向をカメラの上方向にするよう、観察点を原点に移動し、座標系を回転させる変換マトリクス
(transformation matrix)
を計算することができます。この座標系が得られると、簡単に Z
軸についての遠近法補正 (z-perspective correction)
を行うことができます。
後で、さらにカメラの変換マトリクスを修正し、クリッピングをオプティマイズします。これは画面の変換マトリクスには影響を与えますが、最終結果には影響しません。
画面座標系
実際に RAVE エンジンに渡される座標です。x と y
はピクセル値で、z は 0 から 1 の間です。
3D
レンダリングパイプライン
描画するオブジェクトはすべて我々のレンダリングパイプラインを通します。ここにパイプライン全体を示します。後に数式を見ながら個々の部分を見ていきます。ここでは述べない部分
(ライティング、バックフェイシング) は、参考文献の中の何冊かの本の中で触れられています。ここではすべてのポリゴンが凸形
(convex)
であると仮定します。凸形でないポリゴンは複数の凸形のポリゴンを並べて作成できるからです。
-
オブジェクトが錐台の内部に完全に含まれるかどうか、完全に錐台の外部にあるのか、部分的に錐台の内部と重なるのか、決定します。錐台の外部のオブジェクトは除外します。
- オブジェクトの変換マトリクスを計算し、オブジェクトをカメラ座標に変換します。
- 光源 (ライティング) を決定します (この TECHNOTE
では触れません)。
- 裏側がこちらを向いている (バックフェーシング)
ポリゴンを除外します (触れません)。
- オブジェクト内の残るポリゴンについて、それぞれ次の処理を行います。
- オブジェクトが錐台と交わる場合は錐台についてクリップします。
- ポリゴンを画面座標に投影します。
- ポリゴンを描画します。
点の投影
マトリクスの計算はすべて省き、カメラ空間から画面空間へ点を投影
(projection)
するところからはじめます。マトリクスの計算は大半の 3D
の本で取り上げられています。
カメラ空間では、我々のカメラは原点に置かれ、Z
軸を見下ろす位置にあります。表示平面は Z
軸と垂直に一定の距離 (Zview) を挟んで置かれます。ある点
(x0, y0, z0)
を表示平面に投影します。それには、原点からその点まで投影された光線と表示平面との交点を計算します。
この計算は x 座標と y 座標については同じなので、ここでは
x 座標の計算だけを示します。
2 つの同じような三角形があります。一方は
(0,0,0)〜(0,0,Z0)〜(X0,0,Z0)、もう一方は
(0,0,0)〜(0,0,Zview)〜(Xview,0,Zview)
です。両三角形は任意の 2
辺の割合が同じでなければなりません。
Xview/Zview = X0/Z0
Xview についての投影の計算式は次のようになります。
Xview = X0 * zview / z0
Yview = Y0 * zview / z0
表示距離の選択
表示平面を配置する正確な距離を選択しなければなりません。コンテキストの表示矩形の中心に
Z
軸が来るよう配置します。人間の視野に与える奥行きを選択します。三角形
(0,0,0)〜(0,0,Zview)〜(XView, 0,ZView)
があるとして、表示領域の半分として角度を計算し、それを 2
倍します。
ここでは、XView をコンテキストの幅の半分 (ピクセルで)
とします。
tan (angle) = Xview/Zview;
angle = tan-1 (Xview/Zview);
したがって最終的な表示角度 (viewing angle) はこの 2
倍、つまり 2tan-1 (Xview/Zview) です。同様に縦の視野は
2tan-1 (Yview/Zview) です。
別の方法として、特定の視野から平面と原点の距離を決定する方法もあります。適切な水平表示角度はおよそ
110 度です。
Zview = Xview/tan(horizontal_angle/2)
または
Zview = Yview/tan(vertical_angle/2)
アプリケーションが複数のコンテキストサイズで動作する必要がある場合は、すべてのモデルデータを単一のコンテキストサイズに結び付けてしまうのはナンセンスです。この場合、より小さいワールドでモデルを構築して、最終結果を適切な画面解像度に合わせて拡大します。32x24
の表示ウィンドウは、最終の x 値と y 値を整数倍で
512x384、640x480、800x600、832x624、1024x768
に拡大可能です。すべてのクリッピングやモデルの計算は小さい値で行うことができます。
錐台に合わせたクリッピング
視点錐台の外部に外れたポリゴンはすばやく間引きしなければなりません。錐台の側面の
4
つのクリッピング平面は表示矩形のサイズと表示平面の距離から計算できます。
我々の投影式 (projection formula) には z
による除算が含まれるので、z<=0
となる点が投影されないようにしなければなりません。これはヒザー平面
(z=hither)
を指定することで行います。同様に、ヨンクリッピング平面
(z=yon)
を指定して、カメラから非常に遠い距離にあるオブジェクトも除外します。
6
つのクリッピング平面を定義する前に、そもそも点、線、凸形のポリゴンを
3D
平面でクリップする方法を述べなければなりません。通常任意の平面は次の方程式で表します。
Ax + By + Cz - D = 0
(A,B,C) が法線ベクトル (plane's normal)
となります。(A,B,C) が単位ベクトルだと、D
は原点から平面までの距離になります。
任意の 3D 平面で点をクリップする方法
法線ベクトル (N) と平面上の点 P0 があるとします。
法線ベクトル: N = Ai + Bj + Ck
点: P0 = (X0,Y0,Z0)
その平面上の第 2 の任意の点 P を選び、P0 から P へ線分 L
を引きます。
P = (X,Y,Z)
L = (X-X0)i + (Y-Y0)j + (Z-Z0)k
N は平面上のどの線とも垂直であることがわかっています。2
つのベクトルのドット積 (dot product = ・) は 0
になります。
L・N = 0
A(X-X0) + B(Y-Y0) + C(Z-Z0) = 0
AX + BY + CZ = AX0 + BY0 + CZ0
P・N = P0・N
平面上の任意の点と法線ベクトルとのドット積はすべて同じで、距離
D になります。
ある点について、その点と法線ベクトルとのドット積を求めると、その法線ベクトルに沿った距離が得られます。その距離が
D
より大きいと、その点は平面の内部にあり、クリップされません。逆に小さいと、その点は平面の外側にあり、クリップされます。1
つの点のクリッピングには、1 回のドット積と 1
回の比較が必要です。
任意の 3D 平面で線をクリップする方法
線分のクリッピングも同程度に簡単です。線分の両端の点の距離を計算します。両方とも平面の内部にあれば、線分全体が平面の内部にあることになり、クリッピングされません。両方の点が平面の外側にある場合は、線分全体がクリップされます。点が平面の内と外にある場合は、線分とクリッピング平面との交点を求め、平面の外側にある部分をクリップします。
クリッピング平面上の点のドット積は D
であることがわかっています。P1 (X1,Y1,Z1) から
P2 (X2,Y2,Z2) へ至る線分のパラメータ方程式
(parametric equation) は次のとおりです。ここで、t が 0 から
1 に変化するにつれて点 P は P1 から P2 へと変化します。
P = P1 + t(P2-P1)
P・N = D となる t
の値は次のようにして求めることができます。
- P・N = D
- (P1 + t(P2-P1)・N = D
- P1・N + t(P2・N - P1・N) = D
-
- D1 = P1・N
- D2 = P2・N
-
- D1 + t(D2-D1) = D
-
- (D-D1)
- t = -------
- (D2-D1)
D1 と D2
はすでにクリップする線分について求めたドット積です。D
は平面についてすでに求められています。こうして求めた T
を上記のパラメータ方程式に代入して P
値を求めてください。
任意の 3D
平面で凸形のポリゴンをクリップする方法
凸形のポリゴンを平面でクリップするには、各辺について順に平面に対してクリッピングの検査をします。この結果新しい凸形のポリゴンが得られます。この処理で頂点が増えることも減ることもあります
(例外はポリゴンが完全に除外される場合です)。
簡単なアルゴリズムとして、ポリゴンの各頂点を順に調べる方法があります。それぞれ
2
つの頂点について、最初の点が平面内に収まるときは、それを出力します。2
つの点が平面をはさんで別の側にある場合は交点を出力します。
クリッピングに使う錐台
平面に対してポリゴンをクリッピングする方法がわかりました。クリッピングに用いる錐台の各面の方程式を計算することができます。以前のとおり、視点は原点にあり、表示矩形は一定の距離、Zview
にあります。
- Xview = view_width/2
- Yview = view_height/2
- Zview = view_plane_distance
ポリゴンを平面でクリップする方法がわかったところで、クリッピングに用いる錐台の各面の方程式を計算します。視点は原点に、表示矩形は
(0,0,distance)
を中心にした位置にあります。ヒザー平面とヨン平面の距離も必要です。
ここで 6
つのクリッピング平面を計算します。ヒザー平面とヨン平面が一番簡単です。いずれも
Z 軸に垂直だからです。
ヒザー平面とヨン平面を計算するのが最も簡単です。
- ヒザー平面: 法線ベクトル = (0,0,1)、距離 =
hither
- ヨン平面: 法線ベクトル = (0,0,-1)、距離 = -yon
残りの 4
つの平面はすべて原点を通ります。したがって常に距離は 0
です。水平のクリッピング平面は、原点から (X0,0,Z0) と
(-X0,0,Z0)
へ至る線分と垂直な法線ベクトルを選んで求めます。
- X 平面: 法線ベクトル = (-Z0,0,X0)、距離 = 0
- X 平面: 法線ベクトル = (Z0, 0,X0)、距離 = 0
Y 座標も同様の方法で求めます。
意味のある距離であるためには、これらの法線ベクトルはすべて長さ
1.0 に正規化しなければなりません。
ヒザー値とヨン値を選ぶ
ヒザー値はカメラの前に現れる最も近いオブジェクトの距離より小さくなくてはなりません。基本的には、地形
(terrain)
やオブジェクトがこの距離より手前に来ないよう衝突検知
(collision detection)
をしなくてはなりません。そうなっていないとすれば、間違って、観察者がオブジェクトの内部にいるか、地面の内部を透かして見ていることになります。これは
3D ゲームでよくあるバグです。
ヨン平面によって予期せぬ効果が生まれることがあります。画面の端に見えている地形を中央に寄せる
(Z 平面を横切る)
と消えてしまうとか、あなたがオブジェクトの方を向くと消えてしまうなどです。ヨン値を大きくし、他の効果
(フォギング)
を加えると、オブジェクトはよりスムーズに現れるようになり、ヨン平面を横切る際にオブジェクトが突然パッと現れるのを避けることができます。
クリッピングのオプティマイズ
クリッピングはすべてのポリゴンに対して行う操作なので、可能な限り高速でなければなりません。理想的には、オブジェクトごとに行う計算を最小にして、可能な限り早い時点で除外できるものは除外します。このセクションでは、特にオブジェクトのクリッピングを取り上げます。地面など他のものは、BSP
ツリーなど他のアルゴリズムによる計算のほうが効率よくクリップできるかもしれません。
ヒザー平面のオプティマイズ
画面の端の近くでは幾分近くでクリップしてもかまわない場合は、ヒザー平面によるクリッピングを省き、4
つの x および y クリッピング平面の焦点を原点から
(0,9,hither) へ移すことができます。
錐台に対する境界となる球面のチェック
複数ポリゴンから構成される任意のオブジェクトは、オブジェクト全体を包む境界となる球面
(bounding sphere)
を求め、オブジェクト全体で間引き処理を行います。この球面の半径
(radius) は原点から最も遠い頂点までの距離から求めます。
オブジェクトの位置と我々の平面とのドット積を求めると、球面の中心がわかります。これを
Dcenter と呼びます。
もし (Dcenter + radius < Dplane)
が成り立てば、球面全体が平面の外側にあるので、これ以上計算は行わずオブジェクト全体を除外できます。
もし (Dcenter - radius > Dplane)
だと、球面全体がこのクリッピング平面の内部にあります。オブジェクト内のポリゴンはどれも平面と交わることはないため、このオブジェクトのフラグをセットして、以降のパイプラインで、処理量の多いポリゴンレベルのクリッピングを無駄に行わないようにします。
これには錐台を変換してワールド座標に戻すのが一番よい方法でしょう。そうすれば、変換マトリクスを計算せずにオブジェクトの位置を直接操作できます。逆変換はカメラが移動した場合にしか計算する必要はありません。
この計算にはたかだか 6
回のドット積しか必要ありません。これだけで、パイプラインで扱うオブジェクトの個数が減り、必要なポリゴンのクリッピングの回数も大幅に減ります。しかしオブジェクトのほとんどが錐台と交わる場合は、このテクニックは性能を落とします。
クリッピング用錐台を正規化する
各ポリゴンのクリッピングにおいて、各頂点を各クリップ平面でクリッピングするために
1
回のドット積が必要です。例えば、実際にはクリッピングが起こらない場合でも、5
つの頂点を 6 つの平面でクリップすると、30
回のドット積、つまり 90 回の乗算と 60
回の加算が必要です。明らかに、どんな速いマシンであっても、この回数を減らすことが得策でしょう。
水平および垂直のクリッピング平面が 45
度の角度になっている場合、ドット積の計算は、1 回の加算だけ
(乗算はなし)
で済みます。平面の方程式は次のようになります。
- X 平面: 法線ベクトル = (-1,0,1)、距離 = 0
- Y 平面: 法線ベクトル = (1, 0,1)、距離 = 0
(法線ベクトルは単位ベクトルに正規化されます。)
点 P と最初の法線ベクトルとのドット積を求める場合、距離は
-Xp + Zp
です。比較は加算なしで行うことができます。もし (Xp
> Zp)
が成り立てば、点はクリッピングされます。線分がクリッピングされることがわかれば、数回の加算と
1 回の除算で行うことができます。
既存のカメラマトリクスにさらに、X および Y 平面を 45
度に配置するスケーリングマトリクスを適用することができます。同時に、Z
座標変換を施して Z 値を 0 から 1
の間にすることができます。
(Sx,Sy,Sz) = (Z0/Zyon * X0, z0/Zyon*Y0,
1/Zyon).
点が画面空間に投影される場合、逆変換を施して、X および Y
座標を正しい値にスケーリングすることができます。
(Sx,Sy,Sz) = (x0/z0, y0/z0, 1)
この変換は結果を RAVE
座標に投影する数式の中で行うことができます。スケール後のカメラ空間内の
(Xc, Yc, zc) については、最終の投影方程式 (projection
equation) は次のとおりです。
- InvW = 1/Zc;
- Xp = Xc * X0 * invW;
- Yp = Yc * Y0 * invW;
- Zp = z
- U/W = u * invW;
- V/W = v * invW;
z の現在のスケール値ではヨン平面が z=1.0
に位置します。除算でそれよりやや大きい値を使えば、すべての z
値が使用可能な Z バッファ空間内に収まります。
テクスチャ管理
RAVE
は最低限のテクスチャ管理しか行いません。アプリケーションは、フラグ
(kQATexture_Lock)
を指定することで、エンジンに対して、そのテクスチャをオンボード
VRAM
に永久保存することを指示することができます。これは常時使用するテクスチャにのみ指定してください。スワップできないテクスチャの個数が多いと他のテクスチャを保存するためのメモリが足りなくなるからです。
どんな場合でも、エンジンはどのようなテクスチャであれ通常のメモリに保持しておき、描画で必要になった時だけ
VRAM
にコピーする、という方法を選ぶことができます。こうすれば、エンジンは、ある時点で
VRAM
に納まらないからといってテクスチャの作成を拒否せずに済みます。アプリケーションは必要なテクスチャを確実に
RAVE
エンジンにロードするようにしなければなりません。この部分が、3D
アプリケーションを RAVE
を呼び出すように変更するために必要なコードの一番多い箇所です。
まず、理想的な状況におけるテクスチャ管理からはじめます。ついでそれでは足りない部分を補います。それから、テクスチャ管理の改善のため、アプリケーションとエンジンがしたがうべき事柄を提示します。
RAVE エンジンの「理想的」なテクスチャ管理
アプリケーションはたくさんのテクスチャを作成します
(QATextureNew)。必要なテクスチャの集合はシステムで利用可能な
VRAM
の総量を越えます。アプリケーションは、あるテクスチャの設定
(QASetPtr)、そのテクスチャを使うポリゴン群の送出を繰り返して、すべてのポリゴンを描画します。1
個のシーンの描画で同じテクスチャを複数回設定してもかまいません。使用済みのテクスチャは削除します
(QATextureDelete)。すでに送出済みの三角形は、それが使用するテクスチャが削除されても正しく描画されます。
アニメーションの各フレームごとに新しいテクスチャが生成されます。エンジンが効率よくテクスチャを作成できるよう、アニメーションテクスチャや、テクスチャデータをライトマップと組み合わせたテクスチャが作成できます。
理想的なエンジンであれば、クラッシュしたり、破棄してしまったテクスチャを間違えて使ったりせずに、シーンを正確に描画できます。やたら遅くなったりせずに、エンジンはテクスチャの作成と削除を効率よく行います。
現実との対応
上で述べたような理想的なエンジンに近いものを作成することはできますが、エンジンは通常テクスチャのロードとアンロードを適切にスケジュールできる情報を持っていません。
エンジンは VRAM
に納まるより多くのテクスチャ集合の作成を許すものとします。これらのテクスチャを
1 つ 1
つ切り替えていくと、エンジンは特定のポリゴンの描画を後回しにするか、それとも必要なテクスチャを
VRAM
にコピーするか、いずれかの方法をとります。ある場合は、同じテクスチャを使うポリゴンがもっとバッファリングされることを期待して、いっさい描画を行わない、ということになります。別の場合は、エンジンはすぐに描画を行いますが、テクスチャを
VRAM にコピーするのに長い時間がかかります。
多くのエンジンは VRAM
に納まらないテクスチャの割り当ては禁止します。アプリケーションはこの状況をすぐに知ることができますが
(QATextureNew が失敗する
)、結果として特定時点においてより少数のテクスチャ集合しか使用できません。例えば、2
メガバイトの 3D カードで、640x480、16
ビットのフロントバッファ、バックバッファ、Z
バッファを持つものは、250K のテクスチャ VRAM
しかありません。小さなコンテキストを割り当てたり、Z
バッファを使用しなければ、VRAM
が多く使えるようになりますが、その結果利用可能なオプションが減ります。
これが意味するのは、アプリケーションは
QATextureNew
が失敗する場合に備えて、不要なテクスチャを削除して、必要なテクスチャをロードするメモリ空間を作る必要がある、ということです。しかしながら、エンジンの中には、そのテクスチャを使用するポリゴンがすべてラスタライズ済みかどうかをチェックしないで削除してしまうものもあります。この部分の
RAM
は通常再使用されるため、間違えてゴミが描画されてしまいます。QASync
を呼び出してそれまでの描画を完結させることはできますが、そうすると、非同期ハードウェアが持つ多くの利点を損なうことになります。ハードウェアや
RAVE
エンジンが非同期の描画を多く行えば行うほど、QASync
の呼び出しは大きな速度低下をもたらします。
アップル 3D アクセラレータでは、QARenderEnd
が呼ばれた時に、すべてのテクスチャが VRAM
になければなりません。レンダリングループ内でテクスチャの削除はできません。このような場合、アプリケーションにはあまり多くの選択肢はなく、エンジンが正しいことをしてくれることを期待するしかありません。
エンジンのテクスチャ管理の進め方
このセクションでは、エンジンがテクスチャ管理を実装するひとつの方法について述べます。この方法では、より多くのテクスチャを安全に
VRAM
に置くことができ、不自然なラスタライズを起こさずに、安全にテクスチャを削除することができます。
エンジンの設計者は VRAM
に納まるより多くのテクスチャを許可するようにすべきです。QATextureNew
内で大量の計算を行う場合は特にそうです。例えば、エンジンが複数レベルのミップマップを作成したり、ソフトウェアでテクスチャを圧縮する場合は、テクスチャのロードの度にこうした計算が繰り返されるため、性能が落ちてしまいます。
テクスチャはアプリケーションのメモリ、エンジンのメモリ、VRAM
に置くことができるものとします。エンジンのメモリはおそらくシステムヒープに割り当てられるでしょうが、必要ならアプリケーションヒープにあってもかまいません。VRAM
上のテクスチャはあくまでコピーです。テクスチャを失うことなくいつでも削除可能です。最後に、テクスチャの割り当てや解放の際はできるだけエンジンが同期を取るのを防いでください。エンジンはまた、送出済みのポリゴンが正しく描画されたことを確認してから安全にテクスチャを削除しなければなりません。
次に設計について。まず、すべてのテクスチャについて参照回数をカウントするようにします。割り当てたばかりのテクスチャは参照カウントを
1 にします。ポリゴンを送出するたびに、参照カウントを 1
だけ増やします。描画が済むたびに 1
減らします。最後に、アプリケーションが
QATextureDelete を呼び出したら、参照カウントを
1 減らします。参照カウントがまだ正の値なら、テクスチャを
VRAM
にロックして未然に破棄されないようにします。アプリケーションがテクスチャを解放した際、参照カウントが
0 になったら、VRAM
からテクスチャを削除し、エンジン内のコピーも削除します。
同時に、削除のためにキューイングした全テクスチャの個数もグローバル変数に保持しておきます。このカウントは、QATextureDelete
内で増やし、レンダラーがテクスチャを削除したら減らします。
描画コマンドであるテクスチャが参照されたら、それが VRAM
にロード済みかどうかチェックしてください。VRAM
になければ、そのテクスチャが入るだけの空きスペースを探します。収容可能なスペースがなければ、同程度のサイズの別のテクスチャを探し、それを
VRAM
から削除します。仮想メモリがオンの場合は、テクスチャをロックして、割り込み時でもコピーできるようにしておきます。
エンジンの中には VRAM
とエンジンメモリを概念的に区別しないものもあります。このようなエンジンは通常、実際にテクスチャの作成が指示された時点で
(QATextureNew). VRAM
にテクスチャを作成し、そこにコピーします。この実現方法を示します。QATextureNew
がテクスチャを割り当てるだけの十分なメモリがない場合、削除のためにキューイングされたテクスチャがないかチェックします。テクスチャがあれば、そのカウントがゼロになるまでブロックし、テクスチャの割り当てに成功するか、キュー上のすべてのテクスチャが削除されるまで再試行します。
最後に、アプリケーションから利用可能なテクスチャメモリの量の問い合せを受けたら、すべてのテクスチャが削除されるまで同期を取り、適切なメモリ量を返します。
この動作を表す仮想コードを示します。
"QA"
で始まる関数は普通のアプリケーション関数です。
"eng"
で始まる関数はエンジンの内部関数です。以下ではすべてのルーチンが定義されているわけではありません。
"int" で始まる関数は、割り込み時または MP
タスクとして走行可能なエンジン関数です。
VRAM_ENGINE は、別にエンジン VRAM
を使うのではなく、割り当て時にすべてのテクスチャを VRAM
にコピーするエンジンに該当します。
QATextureNew
{
#if VRAM_ENGINE
engReserveMemory();
#endif
テクスチャオブジェクトを割り当てる
参照カウント (refCount) を 1 に設定する
#if VRAM_ENGINE
intLoadTexture();
#elseif
if (テクスチャのロックフラグがオン)
intLoadTexture();
#endif VRAM_ENGINE
}
QATextureDetach
{
#if !VRAM_ENGINE
engReserveMemory();
テクスチャをエンジンメモリにコピーする
#endif
}
QATextureDelete
{
テクスチャの参照カウントを 1 減らす
if (refCount == 0)
{
engDeleteFromEngineMemory()
engDeleteFromVRAM()
}
else
{
gNumberOfTexturesToDelete++;
テクスチャのロックフラグを設定する
intLoadTexture();
}
}
engReserveMemory
{
while ( (gNumberOfTexturesToDelete > 0) かつ
(テクスチャをコピーするだけのエンジンメモリがない)
{
MPYield();
YieldToAnyThread();
}
}
QADrawTriTexture
{
assert (削除キューにテクスチャがない)
テクスチャの参照カウントを 1 増やす
描画コマンドをキューイングする
}
intDrawTriTexture
{
#if !VRAM_ENGINE
if (テクスチャが VRAM にない)
intLoadTexture();
#endif
三角形を描画する
テクスチャの参照カウント (refCount) を 1 減らす
if (テクスチャの refCount がゼロに等しい)
{
engDeleteFromEngineMemory()
engDeleteFromVRAM()
gNumberOfTexturesToDelete--;
}
}
intLoadTexture
{
#if !VRAM_ENGINE
if (テクスチャを入れる VRAM がない)
空きができるまでアンロックされたテクスチャを削除する
#endif
テクスチャを VRAM にコピーする
}
|
VRAM からテクスチャを削除する場合、しばらく使用されていない
(LRU キャッシング)
同じサイズのテクスチャを選んで削除します。
ポリゴンは、それ自体の順序ではなく、VRAM
にテクスチャをロードする回数を最小にするような順序でキューイングできれば理想的です。しかし問題は、同じ
Z
値を共有する複数のポリゴンの扱いです。正しいポリゴンの順序で描画したときとは違うピクセル値が書き込まれてしまいます。
VRAM
エンジンは、新しいテクスチャが割り当てられるとブロックする可能性があります。他のエンジンはほとんどブロックする必要はありませんが
(QATextureDetach 内では)、テクスチャを VRAM
にロードするのにより多くの時間がかかります。アプリケーションが頻繁にテクスチャを変更する場合は特にそうです。これから、テクスチャの割り当てによる負荷を最小にするようアプリケーションが行うべき事柄を述べます。
アプリケーションのテクスチャ管理の進め方
ここでは、VRAM
に入るより多くのテクスチャがあることを仮定します。今日ではこれが普通のケースだからです。ただ、特定のゲームレベルで、テクスチャがすべて
VRAM
に納まり、テクスチャ集合の切り替えが不要な場合は、すばらしい性能が出ます。
QATextureNew
を呼び出すと、多くのエンジンは自動的にテクスチャをミップマップします。しかしこうすると、テクスチャの作成時間が増大するので、可能なかぎりテクスチャは事前にミップマップしておくようにしてください。あらかじめミップマップされたテクスチャは
RAM をおよそ 33%
多く使用しますが、たくさんのテクスチャ集合を管理する際に多くの利点が生じます。
完全なサイズのテクスチャが VRAM
に納まらない場合でも、テクスチャをダウンサンプルすることで
VRAM に入れられる場合があります。完全サイズのテクスチャで
QATextureNew
を呼び出すのではなく、次に小さなミップマップで呼び出します。もっと複雑な例では、そのテクスチャを使って描画されるポリゴンのリストを調べ、最も近い
Z
座標に基づいてミップマップレベルを選択します。特定のテクスチャで描かれるポリゴンが小さく、離れている場合は、描画結果には無視できる程度の影響しか与えずに、より小さなミップマップを使用することができます。128x128
ミップマップを 32x32 ミップマップで置き換えると、VRAM
の使用量はおよそ 95% 少なくなります。VRAM
が少ない場合は大きな効果があります。より小さなミップマップが作成済みの場合、より大きなテクスチャを作成するのは後にしたほうがよいでしょう。
一度にたくさんのテクスチャを作成するのではなく、作業をアニメーションの複数フレームに散らすようにしてください。こうすることで、多数のテクスチャのローディングによるフレームレートの落ちを減らし、表示がスムーズになります。例えば、多数のテクスチャを要する領域に移る場合、複数フレームに渡って、それらのテクスチャをあらかじめ取得しておき、実際に描画で必要になる時点ではすでに全部がメモリ上にあるようにします。テクスチャの小さめのバージョンを先にロードしておき、大きなテクスチャをゆっくり持ってきます。最後に、メモリ上に適切なテクスチャがない場合、最小のミップマップレベルからカラーを取り出し、かわりにそれで三角形にグーローシェーディングをかけるという方法もあります。
重要な性能向上策のひとつに、現在のテクスチャを切り替える回数を減らすことがあります。テクスチャをセットすると、多くのエンジンはそのテクスチャを
VRAM
にロードするなどの処理を行います。ソフトウェアエンジンでは何もしなくても、ハードウェアアクセラレータの側でテクセルキャッシュを用いて性能向上を図っているかもしれません。テクスチャを切り替えると、キャッシュがフラッシュされてしまいます。単一のテクスチャで多数のポリゴンを描画すると性能が向上します。
考え方を言うと、これは、アプリケーションのデータを適切に編成することで実現できます。例えば、1
個のテクスチャをモデル全体に被せるような場合、そのテクスチャに基づくオブジェクトは一度に描画するようにします。こうすれば、1
個のモデルにつきロードするテクスチャの数が 1
個に減ります。
テクスチャの割り当ての前準備
より徹底した解決方法として、QARenderStart
を呼び出す前に、すべてのポリゴン情報を計算し保存しておく方法があります。この計算はハードウェアが直前のフレームを描画する間に行うことが可能なので、非同期ハードウェアではメリットがあります。この情報に基づき、ポリゴン情報をソートして、このシーンの描画で実際に必要なテクスチャ集合を見つけることができます。ただし、この方法では、計算済みのポリゴンデータをメモリ上に保持しなければならないというオーバーヘッドが生じます。
利用可能なシーン情報から必要な全テクスチャを決定することが可能な場合もあります。この場合、必要なテクスチャを全部ロードした後、再びリストをたどり、レンダラーに送出します。
いずれの方法を用いても、結果として現在のテクスチャを設定するための呼び出し回数が最小になり、VRAM
へのテクスチャのロードとアンロードを繰り返すことで生じるスラッシングを大幅に減らすことができます。必要なテクスチャのリストをたどり、エンジンにまだロードしないものがあれば、テクスチャを作成します。この後、ロード済みのテクスチャを使うポリゴンをすべて描画します。さらに別のテクスチャをロードするには、QASync
を呼び出し、いくつかテクスチャを削除し、描画すべき新しいテクスチャ群をロードします。テクスチャを削除する時点でエンジンが自動的に同期を取ることがわかっている場合は
QASync
を呼び出す必要はなく、そのほうが性能が向上します。いずれの場合でも、描画すべきテクスチャがなくなるまで、テクスチャの作成と削除を繰り返します。
通常アルファブレンドテクスチャを使うポリゴンの描画は、不透明な三角形の描画がすべて済んでから行います。フレーム全体の描画が終了する前にアルファブレンドポリゴンで使うテクスチャを削除してはいけません。
以下にレンダリングループを表す仮想コードの断片を示します。
RenderTextures()
は、ロード済みのテクスチャのリストをたどり、それらのテクスチャに対応するすべての三角形を描画します。
LoadTextures()
は、テクスチャのリストをたどり、まだ描画されていない三角形に対応したテクスチャがあればそれをロードします。テクスチャのロードに失敗したら、不要なテクスチャをいくつか削除して再試行します。そのシーンの描画にすべてのテクスチャが必要な場合、ルーチンは終了します。LoadTextures()
は、エンジンにロードできなかったテクスチャがあると false
を返します。テクスチャを削除できないカードに対応するため、二回続けて新しいテクスチャが作成できなかったら、true
を返さなくてはなりません。
レンダリングループは次のようになります。
QASync();
done = LoadTextures();
QARenderStart(...);
RenderTextures();
while (!done)
{
// Loadtextures() で削除するため、ここまでのレンダリングを終了させる
QASync();
done = LoadTextures();
RenderTextures();
}
QARenderEnd(...);
|
エンジン自身が同期を取る場合は QASync
への呼び出しはすべて削除できます。
RAVE
のパフォーマンスチューニング
パフォーマンスチューニングはこの TECHNOTE
の最大の焦点でした。理由は RAVE
を使用して書かれるアプリケーションはほとんどゲームだからです。このセクションでは
RAVE
のパフォーマンスを上げるのに役立ついくつかの事柄を述べます。
1) QARenderStart を呼び出すと、RAVE エンジンは
Z バッファの位置をすべて z=1.0
にリセットし、全ピクセルを背景色に設定します。コンテキストに初期化通知ルーチンが登録されていると、背景色は設定されず、初期化メソッドが呼び出されます。アプリケーションが必ず毎フレームのバッファ全体を描画する場合は、何もしない関数を初期化手続きとして登録することで、数サイクル節約できます。
2)
どんなアプリケーションでも全く変化しない静的なデータを持っているはずです。エンジンが描画キャッシュをサポートする場合、静的データを全部キャッシュに描き、これで各フレームを初期化します。キャッシュは追加の
VRAM を使うかもしれませんが、エンジンは大量のイメージと Z
バッファ情報を高速に初期化できます。キャッシュの使用は高速な
Z
テスト機能をサポートするハードウェアでは良い選択ですが、VRAM
が足りない場合は不適切です。キャッシュの描画で使用したテクスチャはメインのグラフィックスループではロードされないため、このメモリは回復できます。
3)
アプリケーションのポリゴンがすでに奥からの深さにしたがってソート済みの場合は、Z
バッファなしのコンテキストを作成してください。これによりテクスチャ用に追加の
VRAM が使用できるようになり、不要な Z
バッファ情報の書き込みに費やされる時間が節約されます。
ポリゴンの一部がソート済みの場合、Z ライトまたは Z
テストのいずれかがより効率的かどうかを調べてください。Z
ライトのほうが効率が良ければ、kQATag_ZFunction
タグを kQAZFunction_True
に変えて、ソート済みのポリゴンをすべて奥から手前に向かって描画してください。その後通常の設定に戻し、残りのポリゴンを描画してください。Z
テストのほうが速ければ、kQATag_ZFunction
タグはそのままにして、まずソート済みのポリゴンを手前から奥へ描画してださい。
4) RAVE
イメージ上に後で合成が必要な場合、それをすべてビットマップに描画して、QADrawBitMap
でコピーしてみてください。最終的な合成はハードウェアで行われるため、標準グラフィックスルーチンでビットマップを描画することが可能になります。
5)
テクスチャの切り替えを最小にするようデータを編成してください。全フレームで使用されるテクスチャはロックフラグを設定してください。データはストリップやファンを用いて単一のテクスチャで描画して、ハードウェアアクセラレータとの間で必要となるバンド幅を減らしてください。ストリップが使えない場合は、三角形の集まりを一度に全部
kQAVertexMode_Tri
フラグを使って送出してください。
6)
非同期ハードウェアを活用できるようエンジンを設計してください。QARenderEnd
を呼び出したら、すぐにアニメーションの次のフレームの計算を開始してください。RAVE
の現行バージョンは QARenderEnd
の中で同期を取りますが、将来の RAVE バージョンと多くの RAVE
エンジンでは、ハードウェアによるレンダリングと 3D
アプリケーションの処理をオーバーラップさせることができるようになるでしょう。
RAVE は現在マルチプロセサ API
をサポートしませんが、アプリケーションでこれを用いることはできます。すべての
RAVE
への呼び出しはメインスレッドから行う必要がありますが、ジオメトリの計算は
MP
タスク内で実行できます。シングルプロセサシステムであっても、メインタスクが
QASync
内でブロックしている間、エンジンには実行時間が与えられます。
|
参考文献
RAVE のドキュメントには 3D
アプリケーションの構築に関するすばらしい文献が紹介されています。ここでは、特に
3D ゲームの設計に関する書籍を紹介します。
Zen of Graphics Programming, Michael Abrash,
ISBN: 1883577896
Black Art of Game Programming, Andre Lamothe, ISBN:
1571690042
Black Art of Macintosh Game Programming, Kevin
Tieskoetter, ISBN: 157169059
さらに、「Game Developer
Magazine」には、3D
アクセラレータ、エンジンの設計、テクスチャ管理に関するすばらしい記事が掲載されています。
|
|