DirectXでの正投影でハマったこと

正射影とも並行投影とも呼ばれるでしょうが、ここでは正投影と呼ばせていただきます。

DirectX を使うサンプルコードは多く透視投影を使っていると思います。 自分もそこから入ったのですが、正投影もやってみるべ、と思ったところ意外と手間取ったので記録します。

# 前提となる構成
透視投影を以下の関数群で実現しているとします。

* [XMMatrixLookToRH()](https://learn.microsoft.com/ja-jp/windows/win32/api/directxmath/nf-directxmath-xmmatrixlooktorh)
    * ビュー行列を生成します。
* [XMMatrixPerspectiveFovRH()](https://learn.microsoft.com/ja-jp/windows/win32/api/directxmath/nf-directxmath-xmmatrixperspectivefovrh)
    * 投影行列を生成します。

各行列、および座標値の関係は以下のようになります。
```math
(ビュー座標) = (モデル座標) × (ビュー行列)
```
```math
(投影座標) = (ビュー座標) × (投影行列)
```
各座標値は (x, y, z, w) の同次座標系とします。
ビュー座標はカメラ座標と呼ばれるかもしれません。$(0, 0, 0)$ がカメラ位置、-Z 方向がカメラの向き、X, Y がスクリーンの横、縦の方向となります。通常モデル座標系に対して向きを変換するだけだと思います。
投影座標はスクリーン座標とでも呼ばれそうな座標値で、x, y は共に $[-1, 1]$ の範囲、Z 値は通常 $[0, 1]$、w 値はビュー座標の-z値相当の値が入るようです。 

なお投影座標は左手系で、Z=0 が手前、 Z=1 が奥の向きです。

# 正投影の実装

投影行列に XMMatrixOrthographicRH() を使えば正投影になります。

この関数の説明は以下のページにあるのですが、分かる人にしか分からないと思うのは私だけでしょうか...

https://learn.microsoft.com/ja-jp/windows/win32/api/directxmath/nf-directxmath-xmmatrixorthographicrh

関数の宣言は以下の通り。正投影での描画範囲は直方体で、投影行列は向きも変えませんから、ViewWidth、ViewHeight はまあ分かるかと思います。 
```C++
XMMATRIX XM_CALLCONV XMMatrixOrthographicRH(
  [in] float ViewWidth,
  [in] float ViewHeight,
  [in] float NearZ,
  [in] float FarZ
) noexcept;
```
問題は NearZ と FarZ。これらの引数について、上記ドキュメントでは以下のような記述があります。

> NearZ と FarZ を同じ値にすることはできません。また、0 より大きくする必要があります。

因みに英語版も同様。
> NearZ and FarZ cannot be the same value and must be greater than 0.

これは多分大嘘です。 XMMatrixPerspectiveFovRH() のドキュメントのコピーの残骸に思われます。

これらの引数は Z 値の変換にのみ影響しますが、右手系を使っているので $Z=-NearZ$ の時に 0 、$Z=-FarZ$ の時に 1 となるような一次変換をしてくれます。 変換する一次式を考えてみれば分かりますが、 NearZ や FarZ が正の値でないといけない理由はありません。 実際 XMMatrixOrthographicRH() の NearZ を負の値にしても意図した通りの行列を生成してくれます。

正投影を使う場合、ビューの回転操作はモデルの描画中心を中心に回転させると使いやすいのではないかと思います。 XMMatrixLookToRH() を使うと原点(カメラ位置)を中心に向きを変えますので、回転はカメラ位置である原点で行いたいということになります。 そうなるとNearZ に負の値を入れたくなります。 で、試した結果やっぱ動くよなと分かった次第。

なお [XMMatrixLookAtRH()](https://learn.microsoft.com/ja-jp/windows/win32/api/directxmath/nf-directxmath-xmmatrixlookatrh)関数を使えば注視点(FocusPosition)を指定することができます。 注視点を中心に向きを変えることがやりやすくなるかもしれません。 そうすると NearZ が正の値ないし 0 まででも問題なくなるでしょう。
が、カメラ位置と注視点が両方あるのは冗長というか、別の問題を抱え込みそうで嫌なのですよね...

# ハマったこと
で、本題です。正投影自体は上記の通りで実装は難しくないのですが、実際描画させてみるとなんか NearZ/FarZ の指定が効いていなさそうなことに気づきました。 

やったことは、上記の通り透視投影で動かしていたコードをざっくり正投影に変えただけでした。ついでに NearZ や FarZ は投資投影で使っていた値(0.01 とかの小さい値と、十分大きい値)を使いました。
この場合カメラ位置を回転中心になるようにしてビュー操作をすると、手前半分描画されないはずなのに、何故か普通に描画されているっぽい。 但しポリゴンの角度によってはカリングされていることもありそうな微妙な挙動。(この辺は不明です。)

原因はラスタライズの設定で `DepthClipEnable` が FALSE になっていたことでした。 参考にしたコードからコピーしたままになってました。

[D3D11_RASTERIZER_DESC 構造体](https://learn.microsoft.com/ja-jp/windows/win32/api/d3d11/ns-d3d11-d3d11_rasterizer_desc) より
> 距離に基づいてクリッピングを有効にします。
ハードウェアは、ラスター化された座標の x と y のクリッピングを常に実行します。 DepthClipEnable を default-TRUE に設定すると、ハードウェアは z 値もクリップします。
(中略)
DepthClipEnable を FALSE に設定すると、ハードウェアは z クリッピング (つまり、前のアルゴリズムの最後の手順) をスキップします。 ただし、ハードウェアでは "0 < w" クリッピングが引き続き実行されます。

透視投影では w の値にカメラ座標系における -Z の値が入るので、カメラ位置より手前の座標値では w が負の値になります。投資投影の時に手前の点をクリップする分には DepthClipEnable が FALSE でもよかったということの様です。(但し FarZ の値は効いていなかった。)
正投影では w の値は常に 1 になるので問題になったと。

この機能、3次元の描画を行う上で基本機能に思えるのでオフにできるということがちょっと驚きでした。 この機能をオフにした方が描画処理が軽くなることは理解できますし、透視投影を実現するうえで `DepthClipEnable=FALSE` にすることに意味がある程度には効果があるってことなんですね。(ドキュメントにも記載がありますが。) 

# 参考リンク

* [DirectX の構成要素 キャンバスとカメラ](https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2014/june/directx-factor-the-canvas-and-the-camera)
    * ビュー行列、投影行列について説明があります。
* [D3DXMatrixOrthoRH](https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthorh)
    * DirectX における XMMatrixOrthographicRH() 相当の関数のドキュメント。こちらには行列の定義が書かれています。