DirectX の構成要素

キャンバスとカメラ

Charles Petzold

コード サンプルのダウンロード

Charles Petzold約 15 年前、イギリス人の芸術家デイビッド・ホックニーはルネサンス芸術に関するある説を唱え、大論争を巻き起こしました。ホックニーは、ヤン・ファン・エイク、ディエゴ・ベラスケス、レオナルド・ダ・ヴィンチなどの巨匠が、現実世界の遠近感と陰影をきわめて正確にキャンバスに写し取っていたことに驚嘆しました。彼は、そのような正確さは、カメラ・オブスクラやカメラ・ルシーダなど、レンズと鏡から成る光学機器を利用しなければ実現できなかったはずだと確信するに至りました。3D シーンを平面化するこれらの機器を使えば、画家は現実の光景を手元に引き寄せてキャンバスに再現できます。ホックニーは、『秘密の知識: 巨匠も用いた知られざる技術の解明』(青幻舎、2006 年) という説得力のある見事な書籍で自身の説を発表しました。

もっと最近では、発明家の Tim Jenison (Video Toaster や LightWave 3D を開発した NewTek の設立者) は、光学機器を用いてヨハネス・フェルメールによる 350 年前の絵画『音楽の稽古』を再現することに夢中になりました。彼は、原作を模造した部屋を作り、複製の家具と小物で飾り付け、さらに生身のモデル (実娘も含む) まで雇い、自分で発明したシンプルな光学機器を使ってその光景を描きました。その一部始終は、『Tim’s Vermeer』(Sony Pictures、2013 年) というすばらしいドキュメンタリーに記録されているので、ぜひご覧になってみてください。開いた口がふさがらないこと請け合いです。

3D シーンを 2D 面上に正確に再現するために、光学機器 (現在ではシンプルなカメラ) を使用する必要があるのはなぜでしょう。私たちが現実世界で見えていると考えているものの大半は、比較的まばらな視覚情報から脳内で作り出されたものです。現実世界の光景全体を大きな 1 つのパノラマで見ていると思っていますが、実際にはどの瞬間でも焦点を当てているのはごく一部にすぎません。こうした断片的な視覚情報をつなぎ合わせて、現実世界を模倣した合成絵画を描くのは、事実上不可能です。しかし、それ自体が人間の目の光学を模倣し、かつ上記のような目の限界も取り払っているカメラのようなしくみでキャンバスを補えば、随分と楽になるでしょう。

コンピューター グラフィックスでも、同じようなプロセスを踏みます。2D グラフィックスをレンダリングするときは、キャンバスがあれば十分です。コンピューター グラフィックスではキャンバスが描画サーフェイスになり、最もわかりやすい形態では、ビデオ ディスプレイを構成するピクセルの行と列が直接キャンバスに対応します。

しかし、これが 2D から 3D になると、これにカメラの考え方を加える必要があります。現実世界のカメラと同様、コンピューター グラフィックスのカメラは 3D 空間のオブジェクトを撮影し、それをサーフェイスに平面化してキャンバスに移せるようにします。

カメラの特性

現実のカメラにはいくつかの特性がありますが、これは 3D グラフィックスで必要となる数学的表現に簡単に置き換えることができます。まず、カメラは空間上の特定の位置に設置され、これを 3D 上の点として表すことができます。

また、カメラは特定の方向を向きますが、これは 3D ベクトルで表せます。この 3D ベクトルは、カメラが直接向いている被写体の位置情報を取得し、カメラの位置を減算することで計算できます。

カメラを特定の位置に固定し、特定の方向を向けた場合でも、まだカメラを左右に動かしたり、360 度回転させることも可能です。つまり、カメラから見た "上" を示すもう 1 つのベクトルが必要です。このベクトルは、カメラの向いている方向に対して垂直になります。

空間でカメラをどう位置付けるかを決めたら、カメラの設定を調整します。

現代のほとんどのカメラには、ズーム機能が搭載されています。広範囲を視野に収める広角レンズから、視野を狭めて細部をアップする望遠レンズまで、カメラのレンズを調整できます。両者の違いは、画像を取得する平面と、光が通過する単一の点である焦点との距離に基づきます (図 1 参照)。焦点面と焦点との距離を、焦点距離と呼びます。

The Focal Plane, Focal Point and Focal Length
図 1 焦点面、焦点、および焦点距離

焦点面のサイズと焦点距離によって、焦点から放射状に広がる視野角が決まります。望遠 (より正確には "長焦点") は一般に 35 度未満の視野角を、広角は 65 度を超える視野角を指し、その間が通常の視野角になります。

コンピューター グラフィックスは、現実では不可能なカメラの特性を追加します。3D グラフィックス プログラミングでは、多くの場合、透視投影と平行投影のどちらかを実現するカメラを選択します。透視投影は現実と似ており、焦点からの距離が遠くなるほど広範囲が視野に含まれるため、カメラから離れるにしたがって被写体は小さく映ります。

平行投影はこれとは異なり、カメラからの距離に関わりなく、すべての被写体が実際のサイズとの相対サイズでレンダリングされます。数学的には、2 つの投影法では平行投影の方がシンプルで、技術図面や建築図面に最も適しています。

カメラの変換

3D グラフィックス プログラミングでは、カメラを数学的に構築します。カメラは、3D 空間でオブジェクトを操作するのとよく似た 2 つの行列変換から構成されます。この 2 つのカメラの変換を、View (表示) および Projection (投影) と呼びます。

View 行列は 3D 空間でカメラの位置と方向を効果的に決め、Projection 行列はカメラが何をどのように "見る" かを決めます。これらのカメラの変換は、3D 空間 ("ワールド空間" と呼ぶことも多い) でのオブジェクトの位置を決めるために他の行列変換をすべて使用した後に適用します。他のすべての変換の後に、まず View 変換を、最後に Projection 変換を適用します。

DirectX プログラミングでは、Direct3D を使用するか、Direct2D で 3D の概念を用いるかに関わらず、DirectX Math ライブラリを使ってこれらの行列変換を構築するのが最も簡単な方法です。DirectX Math ライブラリは、XM という文字で始まり、XMVECTOR と XMMATRIX のデータ型を使う DirectX 名前空間の関数のコレクションです。これら 2 つのデータ型は、CPU レジスタのプロキシなので、これらの関数は通常きわめて高速に動作します。

View 行列を計算するため利用できる関数は、次の 4 つです。

  • XMMatrixLookAtRH (EyePosition, FocusPosition, UpDirection)
  • XMMatrixLookAtLH (EyePosition, FocusPosition, UpDirection)
  • XMMatrixLookToRH (EyePosition, EyeDirection, UpDirection)
  • XMMatrixLookToLH (EyePosition, EyeDirection, UpDirection)

関数の引数には、"Eye (目)" という単語が含まれていますが、今回は "カメラ" という単語を用います。

LH と RH は、それぞれ左手と右手を表します。本コラムの例では左手座標系を想定して話を進めます。つまり、左手の人差し指を X 軸の正方向へ向けると、中指は Y 軸の正方向を、親指は Z 軸の正方向を指します。X 軸の正方向が右、Y 軸の正方向が上を向く場合 (3D プログラミングで一般的な方向)、Z 軸の負方向は画面の手前を向きます。

4 つの関数はすべて、XMVECTOR 型の 3 つのオブジェクトを受け取り、XMMATRIX 型のオブジェクトを返します。4 つの関数すべてに共通して、これらの引数のうち 2 つはカメラの位置 (関数テンプレートでは EyePosition) と UpDirection (上方向) を示します。LookAt 関数にはカメラの焦点の位置を示す FocusPosition という引数が、LookTo 関数にはベクトルを表す EyeDirection という引数が含まれます。これは、ある形式を別の形式に変換するだけのシンプルな計算です。

たとえば、点 (0, 0, –100) にカメラを置き、原点 (つまり、 Z 軸の正方向) に向け、カメラ上部を上に向ける場合を考えてみましょう。次のいずれかを呼び出すことができます。

XMMATRIX view =
  XMMatrixLookAtLH(XMVectorSet(0, 0, -100, 0),
                   XMVectorSet(0, 0, 0, 0),
                   XMVectorSet(0, 1, 0, 0));

または

XMMATRIX view =
  XMMatrixLookToLH(XMVectorSet(0, 0, -100, 0),
                   XMVectorSet(0, 0, 1, 0),
                   XMVectorSet(0, 1, 0, 0));

どちらの場合も、関数は次の View 行列を作成します。

この View 行列は、単に 3D シーン全体を Z 軸の正方向に 100 単位移動します。多くの View 行列ではさまざまな種類の回転も使用しますが、スケーリングは行いません。3D シーンに View 変換を適用すると、カメラは原点に位置しており (カメラ上部は Y の正方向を向く)、Z の正方向 (左手座標系の場合) または Z の負方向 (右手座標系の場合) を向くと考えられます。この向きに設定することで、Projection 変換が格段にシンプルになります。

Projection (投影) の規則

以前のコラムで Direct2D の範囲内で 3D プログラミングの世界に足を踏み入れましたが、そのときは単に Z 座標を無視することで 3D 空間から 2D のビデオ ディスプレイにオブジェクトを変換しました。

今回は、標準カメラの Projection 行列にカプセル化される規則を使用して、もう少し専門的な方法で 3D から 2D へ変換します。3D から 2D への標準変換は、実際には 3D 座標を正規化した 3D 座標に変換してから、2D 座標に変換するという 2 段階で行います。プログラムで指定する Projection 変換は、最初の変換を制御します。Direct3D プログラミングでは、通常 2 つ目の変換はレンダリング システムによって自動的に実行されます。Direct2D を使って 3D グラフィックスを表示するプログラムでは、この 2 つ目の変換をプログラム自体で実行しなければなりません。

Projection 変換の目的の 1 つは、シーン内のすべての 3D 座標を正規化することです。この正規化は、最終的なレンダリングにどのオブジェクトを表示し、どれを除外するかを定義します。この正規化を行うと、最終的にレンダリングされるシーンには、左に -1、右に 1 の範囲の X 座標、下に –1、上に 1 の範囲の Y 座標、そして 0 (カメラに最も近い) から 1 (カメラから最も遠い) の範囲の Z 座標が含まれるようになります。Z 座標は、どのオブジェクトに視界が遮られ、他のオブジェクトが見えなくなるかを判断するためにも使用します。

この空間外のものをすべて破棄した後に、正規化した X 座標と Y 座標を表示面の幅と高さにマッピングし、Z 座標を無視します。

Z 座標を正規化する場合、Projection 行列を計算する関数に、Z 軸に沿ったカメラからの距離を示す NearZ と FarZ という浮動小数点型の引数が常に必要です。これら 2 つの距離を、それぞれ 0 と 1 という正規化した Z 座標に変換します。

これは、3D 空間にはカメラに近すぎるために表示されない領域とカメラから遠すぎるために表示されない領域があることを意味します。これは直感的には理解しにくいかもしれません。しかし、このようなやり方で奥行きを制限する必要があるのには、実際的な理由があります。たとえば、カメラの背後にあるものはすべて除外する必要があり、被写体がカメラに近すぎる場合は、他のものすべてが遮蔽されます。また、Z 座標が無限に伸びていると想定すると、どのオブジェクトが他のオブジェクトの上に重なるかを判断する際に浮動小数点数の解を求めるの大きな負担がかかります。

カメラの View 行列がカメラにとって可能な移動と回転を行うため、Projection 行列の基盤となるカメラは必ず原点に配置され、Z 軸に沿って方向が定められることになります。ここでは左手座標系を使用するので、カメラは Z 軸の正方向を向きます。左手座標系の方が Projection 変換を処理するのがややシンプルになります。これは、NearZ と FarZ が Z 軸の負の座標点ではなく正の座標点に相当するためです。

DirectX Math ライブラリでは Projection 行列を計算する関数が 10 個定義されています。そのうち 4 個は平行投影、6 個は透視投影用で、左手座標系用と右手座標系用が半々です。

XMMatrixOrthographicRH 関数と XMMatrixOrthographicLH 関数では、NearZ と FarZ と共に ViewWidth と ViewHeight を指定します。図 2 に示すのは、Y 軸上の正の位置から 3D 座標系を見下ろした平面図です。この図は、左手座標系の場合に原点から見える直方体が上記の引数によってどのように定義されるかを示しています。

A Top View of an Orthographic Transform
図 2 平行投影変換の平面図

多くの場合、ViewWidth と ViewHeight の比はレンダリングに使用するディスプレイの縦横比と同じです。Projection 変換は、–ViewWidth / 2 から ViewWidth / 2 のすべてを –1 から 1 の範囲にスケーリングし、後でこれらの正規化座標をレンダリングのために表示面のピクセル幅の半分にスケーリングします。ViewHeight も計算は同じです。

次に示すのは、ViewWidth と ViewHeight を 40 と 20 に、NearZ と FarZ を 50 と 100 に設定した XMMatrixOrthographicLH 関数の呼び出しです。ここでは、図に 10 単位ごとに目盛りを付けることを想定しています。

XMMATRIX orthographic =
  XMMatrixOrthographicLH(40, 20, 50, 100);

結果の行列は、次のようになります。

変換式は、次のようになります。

x 値の –20 と 20 はそれぞれ –1 と 1 に、y 値の –10 と 10 はそれぞれ –1 と 1 に変換されることがわかります。Z 値 50 は 0 に、Z 値 100 は 1 に変換されます。

OffCenter という単語を関数名に含む他の 2 つの Orthographic 関数は、幅と高さではなく、上下左右の座標を指定します。

XMMatrixPerspectiveRH 関数と XMMatrixPerspectiveLH 関数の引数は、XMMatrixOrthograhicRH 関数や XMMatrixOrthograhicLH 関数と同じですが、定義するのは 4 辺の視錐台 (上部を切り落としたピラミッドのような形状) です (図 3 参照)。

A Top View of a Perspective Transform
図 3 透視変換の平面図

変換関数の ViewWidth 引数と ViewHeight 引数は、NearZ の視錐台の幅と高さを制御しますが、FarZ の幅と高さは FarZ と NearZ の比に比例して増加します。この図は、カメラに近い x 座標と y 座標と同じ空間に、カメラから離れた広範囲の x 座標と y 座標が (結果的により小さく) マッピングされるしくみも示しています。

以下は、XMMatrixOrthographicLH 関数に使用したのと同じ引数を使った XMMatrixPerspectiveLH 関数呼び出しです。

XMMATRIX perspective =
  XMMatrixPerspectiveLH(40, 20, 50, 100);

この呼び出しでは、以下の行列が作成されます。

行列の 4 列目から、この変換が非アフィン変換であることが分かります。アフィン変換の場合は、m14、m24、および m34 の値は 0、m44 は 1 になりますが、ここでは m34 が 1 で m44 が 0 です。

3D プログラミング環境ではこのようにして遠近感を生み出します。それでは、変換乗算を詳しく見ていきます。

行列の乗算は、次の変換式になります。

w´ 値に注目します。4 月号のコラムの実装で説明したように、3D 変換で w 座標を使用すると表面的には移動変換が可能になりますが、同次 (射影) 座標の演算も必要になります。アフィン変換は、常に w が 1 の 4D 空間における 3D サブセットで実行しますが、この非アフィン変換では座標を 3D 空間の外部へ移動しています。そのため座標をすべて w´ で除算して 3D 空間に戻す必要があります。この必要な調整を組み込んだ変換式は、次のようになります。

z が NearZ (50) と等しい場合は、変換式は平行投影と同じです。

たとえば、-20 から 20 の x 値は、-1 から 1 の x´ 値に変換されます。

それ以外の z 値については変換式が異なり、z が FarZ (100) と等しい場合は次のようになります。

このカメラからの距離では、-40 から 40 の x 値は -1 から 1 の x´ 値に変換されます。FarF 点の x 値と y 値は NearZ 点よりも広範囲になりますが、視野に変化はありません。

平行投影の関数と同様に、他の 2 つの関数の関数名には OffCenter という単語が含まれ、幅と高さではなく、上下左右の座標を設定できます。

XMMatrixPerspectiveFovRH 関数と XMMatrixPerspectiveFovLH 関数により、幅と高さではなく視野 (FOV) 角を指定できます。この視野は、X 軸と Y 軸のどちらで指定するかによって異なると考えられます。Y 軸に沿って指定し、幅と高さの比も指定する必要があります。

前の例と一貫性のある Perspective (遠近感) 行列を作成するには、y 引数を NearZ の高さの半分、x 引数を NearZ とした atan2 関数を用い、結果を 2 倍して視野を算出します。

float angleY = 2 * atan2(10.0f, 50.0f);

次に示すように、2 つ目の引数は幅と高さの比で、この例では 2 です。

XMMATRIX perspective =
  XMMatrixPerspectiveFovLH(angleY, 2, 50, 100);

この呼び出しにより、先ほど XMMatrixPerspectiveLH 関数で作成したのと同じ Perspective 行列が形成されます。

リング状のテキスト

2 月号のコラムの実装で、リング状の 3D テキストを作成し、アニメーションで回転させる方法を説明しました。ただし、このときの演習では Direct2D のジオメトリを使用し、かなり低いパフォーマンスしか得られませんでした。3 月号のコラムでは、テキストにテセレーションを実行して三角形の集合にする方法を説明しました。この方法を使うと、ジオメトリを使う場合より著しくパフォーマンスが向上すると考えられました。5 月号のコラムでは、三角形を使って 3D オブジェクトを作成してレンダリングする方法を説明しました。

今回は、これまでに学習した手法を統合します。今回のコラムのダウンロード可能なソース コードには、TessellatedText3D という Windows 8 Direct2D プロジェクトを含めています。このプログラムでは、XMFLOAT3 オブジェクトを使って 3D の三角形を次のように定義しています。

struct Triangle3D
{
  DirectX::XMFLOAT3 point1;
  DirectX::XMFLOAT3 point2;
  DirectX::XMFLOAT3 point3;
};

TessellatedText3DRenderer クラスのコンストラクターはフォント ファイルを読み込み、フォント フェイスを作成し、ここでは 100 に設定した任意のフォント サイズを使って GetGlyphRunOutline メソッドから ID2D1PathGeometry オブジェクトを生成します。次に、このジオメトリをカスタムのテセレーション シンクで Tessellate メソッドを使って三角形の集合に変換します。指定した特定のフォント、フォント サイズ、およびグリフのインデックスを使って、Tessellate メソッドは 1,741 個の三角形を生成します。

次に、テキストをリング状にまとめることで 2D 三角形を 3D 三角形に変換します。ここでは 100 に指定した任意のフォント サイズに基づき、この円の半径は約 200 (m_sourceRadius に格納) になり、円の中心は 3D の原点に位置します。

TessellatedText3DRenderer クラスの Update メソッドでは、XMMatrixRotationX 関数と XMMatrixRotationY 関数が X 軸と Y 軸を中心にテキストを回転させる 2 つの変換を提供します。これら 2 つの関数は、rotateMatrix と tiltMatrix という XMMATRIX オブジェクトにそれぞれ格納されます。

次に、Update メソッドは図 4 に示したコードに続きます。このコードは、View 行列と Projection 行列を計算します。View 行列は円の半径に基づき Z 軸の負の方向上にカメラの位置を設定します。そのため、カメラはリング状のテキストから外側に半径分離れた点に位置し、円の中心を向きます。

図 4 TessellatedText3D の View 行列と Projection 行列

void TessellatedText3DRenderer::Update(DX::StepTimer const& timer)
{
  ...
  // Calculate camera view matrix
  XMVECTOR eyePosition = XMVectorSet(0, 0, -2 * m_sourceRadius, 0);
  XMVECTOR focusPosition = XMVectorSet(0, 0, 0, 0);
  XMVECTOR upDirection = XMVectorSet(0, 1, 0, 0);
  XMMATRIX viewMatrix = XMMatrixLookAtLH(eyePosition,
                                         focusPosition,
                                         upDirection);
  // Calculate camera projection matrix
  float width = 1.5f * m_sourceRadius;
  float nearZ = 1 * m_sourceRadius;
  float farZ = nearZ + 2 * m_sourceRadius;
  XMMATRIX projMatrix = XMMatrixPerspectiveLH(width,
                                              width,
                                              nearZ,
                                              farZ);
  // Calculate composite matrix
  XMMATRIX matrix = rotateMatrix * tiltMatrix *
                    viewMatrix * projMatrix;
  // Apply composite transform to all 3D triangles
  XMVector3TransformCoordStream(
    (XMFLOAT3 *) m_dstTriangles3D.data(),
    sizeof(XMFLOAT3),
    (XMFLOAT3 *) m_srcTriangles3D.data(),
    sizeof(XMFLOAT3),
    3 * m_srcTriangles3D.size(),
    matrix);
  ...
}

同じく円の半径に基づく引数を使い、Projection 行列を計算し、すべての行列を掛け合わせます。XMVector3TransformCoordStream は、並列処理を使ってこの変換を XMFLOAT3 オブジェクトの配列 (実際には、Triangle3D オブジェクトの配列) に適用し、自動的に w*'* で除算します。

図 4 に示した範囲を超えますが、次に Update メソッドはビデオ ディスプレイの半分の幅に基づく倍率を使用し、z 座標を無視することで、これらの変換済み 3D 座標を 2D に変換します。また、Update メソッドは各三角形の 3D 頂点を使ってクロス積を計算します。これは面に対して垂直なベクトルである面法線になります。次に、面法線の z 座標に基づき 2D の三角形を 2 つの集合に分割します。z 座標が負の場合は三角形は見る人の方向を向き、正の場合は反対側を向きます。最後に Update メソッドは、2D 三角形の 2 つの集合に基づき ID2D1Mesh オブジェクトを 2 個作成します。

次に、Render メソッドは背面の三角形のメッシュを灰色のブラシで、前面のメッシュを青のブラシで表示します。この結果を図 5 に示します。

The TessellatedText3D Display
図 5 TessellatedText3D の表示

ご覧のとおり、見る人に近い正面を向いた三角形の集合は、見る人から遠い反対側を向いた三角形の集合よりもずっと大きく表示されます。このプログラムには、ジオメトリのレンダリングで直面したパフォーマンスの問題が生じません。

光を使って効果的な影を生み出せるか

5 月号のコラムの実装で、3D オブジェクトの表示に光という要素を導入しました。5 月号のプログラムでは、光は特定の方向から当てられると考え、光がそれぞれの面に当たる角度に基づき、立体図形の面にさまざまな影を算出しました。

このプログラムでも同じ手法を採用できるでしょうか。

理論的には可能です。しかし、私の経験では、いくつか重大なパフォーマンスの問題が発生します。テキストの影を実装するプログラムは、各三角形を異なる色でレンダリングする必要があります。そのため、Update メソッドで 2 つの ID2D1Mesh オブジェクトを作成するのではなく、それぞれの Update の呼び出しで再作成した 1,741 個の異なる ID2D1Mesh オブジェクトと 1,741 個の対応する ID2D1SolidColorBrush オブジェクトが必要になります。これにより、アニメーションの速度は毎秒約 1 フレームに低下します。

さらに、表示も決して満足のいくものではありません。各三角形には、光源に対する角度に基づいて別の単色が与えられますが、異なる色の間の境界が見えてしまいます。3D オブジェクトのレンダリングに使用する三角形は 3 つの頂点の間にグラデーションを施してもっと見栄え良く着色する必要がありますが、ID2D1Brush から派生するインターフェイスではそうしたグラデーションをサポートできません。

どうやら、Direct2D をさらに深く掘り下げ、Direct3D プログラマが使用するのと同じシェーディング ツールを手に入れる必要があるようです。

Charles Petzold は MSDN マガジンの記事を長期にわたって担当しており、Windows 8 向けのアプリケーション開発についての書籍『Programming Windows, 6th Edition』(Microsoft Press、2013 年) の著者でもあります。彼の Web サイトは charlespetzold.com (英語) です。

この記事のレビューに協力してくれた技術スタッフの Doug Erickson に心より感謝いたします。
Doug Erickson は、Microsoft の OSG 開発者ドキュメンテーション チームのための主任プログラミング作成者です。DirectX グラフィックスのコードとコンテンツを作成および開発しているとき以外は、Charles Petzold などの記事を読んでいます。彼は、余暇でもこんな調子なのです。後は、単車に乗るのも好きです。