「点群表示機能を自作してみる」のその1です。
DirectX11 を用いてポリゴンを表示するところまでやってみます。
はじめに
DirectXの使い方を学ぶにあたり下記ページを参考にさせていただきました。 お礼申し上げます。
出来上がり図
GitHub の以下のリビジョンを参照してください。
Revision: a7ba950b049dd09f343f9f22abdb27e5296a28b5
Message:
implemented drawing triangles.
modified to work resizing window.
- とりあえずポリゴンを描画。Depthバッファの動作も確認。
- 右手系を採用。
- ライティングはなし。透過も未対応。
- FovAngleY を90度と極端な値にしていますが、その時菱形の上下端がビューの上下位置に来るように合わせてあります。
視点を変えたらこんな感じ。 ライティングがないので形が分かりにくいですね。
説明
概要
- D3dGraphics クラスは ID3D11DeviceContext などをまとめて持ち、DirectX をラップすることが目的です。 DirectX のメソッド呼出しは基本このクラス内で行う予定。(呼び出し側にバッファなどのインスタンスは返す。)
- OpenGLなんかだと変換行列は ModelMatrix, ViewMatrix, ProjectionMatrix 位に分かれてますが、今回は後者二つのみ用意します。
- 今回は ChildView (特に OnPaint())にほとんどの実装があります。
ViewMatrix には XMMatrixLookAtRH() を使って設定しています。 この行列は視点を原点、視線方向を-Z方向にした座標系を表す直交行列になるようです。 透視投影は ProjectionMatrix との組み合わせで実現するのですね。
D3DShaderContext は見ての通りのクラスです。 事前にインスタンスを用意しておき、描画時に使用します。 同様の機能が既に存在しているような気もしましたが、とりあえず作っちゃいました。 とりあえず自分が使う情報しか保持しません。
class D3DShaderContext
{
private:
D3DInputLayoutPtr m_pIAInputLayout;
D3DVertexShaderPtr m_pVS;
D3DBufferPtr m_pVSConstantBuffer;
D3DGeometryShaderPtr m_pGS;
D3DBufferPtr m_pGSConstantBuffer;
D3DPixelShaderPtr m_pPS;
};
ポリゴンを描画するのに必要な処理
詳細はコードを参照ということで、使用した関数を列挙。
- Shader の作成
- D3DCompileFromFile()
- ID3D11Device::CreateInputLayout()
- ID3D11Device::CreateVertexShader()
- ID3D11Device::CreatePixelShader()
- DepthStencil バッファの作成
- ID3D11Device::CreateDepthStencilState()
- IDXGISwapChain::GetDesc()
- ID3D11Device::CreateTexture2D()
- ID3D11Device::CreateDepthStencilView()
- 出力バッファ の作成
- IDXGISwapChain::GetBuffer()
- ID3D11Device::CreateRenderTargetView()
- その他の初期化・設定
- D3D11CreateDeviceAndSwapChain()
- ID3D11Device::CreateRasterizerState()
- 描画データの作成
- ID3D11Device::CreateBuffer()
- 描画前処理
- ID3D11DeviceContext::ClearRenderTargetView()
- ID3D11DeviceContext::ClearDepthStencilView()
- ID3D11DeviceContext::RSSetViewports()
- ID3D11DeviceContext::RSSetState()
- ID3D11DeviceContext::OMSetDepthStencilState()
- ID3D11DeviceContext::OMSetRenderTargets()
- ID3D11DeviceContext::IASetPrimitiveTopology()
- ID3D11DeviceContext::IASetVertexBuffers()
- ID3D11DeviceContext::IASetIndexBuffer()
- 描画・後処理
- ID3D11DeviceContext::DrawIndexed()
- IDXGISwapChain::Present()
- その他
- IDXGISwapChain::ResizeBuffers()
やらなかったこと
- Sampler の使用
- Texture の使用
- ライティング設定
- これはポリゴン描画においてはやるべきことだと思いますが、今回は目標が点群描画ですので省略しました。 (点群だからライトが要らないということもないのかな。)
Window サイズが変わった時の処理
Windows サイズが変わった場合、サイズ変更を DirectX にも伝える必要があります。D3DGraphics::ResizeBuffers() をその時に呼ぶべき関数としました。(今回のコードでは CChildView::OnSize()から呼ばれる。)
void D3DGraphics::ResizeBuffers(const CSize& newSize)
{
P_C_FUNC_BEGIN("D3DGraphics::ResizeBuffers");
if (m_pSwapChain) {
// release related buffer objects.
// See https://learn.microsoft.com/ja-jp/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-resizebuffers?source=recommendations
// See https://www.sfpgmr.net/blog/entry/dxgi-resizebuffers%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AB%E6%B0%97%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B%E3%81%93%E3%81%A8.html
m_pRenderTargetView.Reset();
m_pDepthStencilView.Reset();
HRESULT hr = m_pSwapChain->ResizeBuffers(
0, 0, 0, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);
if (FAILED(hr)) {
P_THROW_ERROR("ResizeBuffers");
}
m_viewport.Width = float(newSize.cx);
m_viewport.Height = float(newSize.cy);
}
P_C_FUNC_END;
}
-
IDXGISwapChain::ResizeBuffers method() を呼び出せばよいのですが、この関数は作り替えるべきバッファをアプリが保持しているとエラーを返します。 その対象となるバッファが今回のプログラムでは m_pRenderTargetView と m_pDepthStencilView ということになります。
- m_pRenderTargetView は出力先のバックバッファなので描画サイズと同じであるべきバッファ。
- m_pDepthStencilView は Depth バッファ、Stencil バッファでやはり同様。
- ここで破棄した m_pRenderTargetView と m_pDepthStencilView は次回描画時に再作成します。
その他詳細のメモ
DirectX の行列
変換行列は以下の定義がしばしば混乱します。
- row major か column major か。
- 座標値を右からかける(post-multiplication)か左からかける(pre-multiplication)か。
後述しますが、Microsoft のドキュメントでも混乱している個所があるようです。
整理すると以下のようになりました。
型 | row/column major | pre/post multiplication |
---|---|---|
XMMATRIX | row major | pre-multiplication |
XMFLOAT4X4 | row major | (post-multiplication) |
hlsl の matrix | column major | pre-multiplication |
row major か column major かとはメモリイメージがどうなるかの問題です。 C言語の2次元の配列としてみた時に matrix[行][列]となるのが row major。逆が column major。
座標値を右からかけるか左からかけるかは厳密には C++ の型に対する定義ではなく、使う場所ごとに選択することが可能です。 ここでは描画するための変換行列の文脈でどうなるかを確認しました。
- pre/post-multiplication というのは本来は2つの行列の掛け算の順序に関する用語であり、このページでは用語の使い方が正確ではないかもしれません。 ここでは pre/post-multiplication を "This matrix is pre/post-multiplied by a vector." の意味で使っています。 pre/post なのは行列ではなく座標値/ベクトルです。(参考サイト:Matrix(行列)用語まとめ)
- (2023/3/25 追記)XMFLOAT4X4 を post-multiplication と書いていますが、今回のコードではそうなっているというだけです。
XMMATRIX
- このクラスはコードで計算処理を行うためのクラスのようです。
- align 指定がされているのはそのためでしょう。(SIMD命令を使うのに多分必要。)
- XMMatrixLookToRH() や XMMatrixPerspectiveFovRH() が返す行列の型もこの型。
- このクラスは row major、 pre-multiplication であることが上記ドキュメントに明記されている。
マクロで無効化されているので微妙だが、以下のような定義があることからも row major と思われます。
#ifdef _XM_NO_INTRINSICS_
float operator() (size_t Row, size_t Column) const { return m[Row][Column]; }
float& operator() (size_t Row, size_t Column) { return m[Row][Column]; }
#endif
掛け算も以下の感じ。左右が入れ替わっているようなことはなさそう。 (XMMatrixMultiply の実装は省略します。)
inline XMMATRIX& XM_CALLCONV XMMATRIX::operator*=(FXMMATRIX M)
{
*this = XMMatrixMultiply( *this, M );
return *this;
}
XMFLOAT4X4
- このクラスは値を保持するためのクラスということのようです。 (要出典)
-
XMStoreFloat4x4() には「XMFLOAT4X4 は行主行列形式」と書いてありますが、row major ということです。
- operator () の定義を見ても以下のようにあることから間違いないでしょう。
float operator() (size_t Row, size_t Column) const { return m[Row][Column]; }
今回のコードでは下記のようにして XMMATRIX (XMMatrixLookAtRH()の結果)を XMFLOAT4X4 (shaderParam.viewMatrix など)に変換し、そのメモリイメージがシェーダーの matrix になっています。
XMStoreFloat4x4(&shaderParam.viewMatrix, XMMatrixTranspose(
XMMatrixLookAtRH(MakeXmVector(0, 0, 1.5),
MakeXmVector(0, 0, 0), MakeXmVector(0, 1, 0))
));
XMMATRIX を XMFLOAT4X4 に代入する際に XMMatrixTranspose() を使うことから、座標値をかける向きは左右逆となることが分かります。 すなわち XMFLOAT4X4 は post-multiplication として使われることになります。
hlsl の matrix
- vsMain() を見ると以下のような定義になることが分かります。 即ち viewMatrix, projectionMatrix は pre-multiplication です。
(projected coord) = (model coord) * viewMatrix * projectionMatirx;
- row major か column major かの明記は見つけられませんでしたが、post-multiplication である XMFLOAT4X4 と同じバイナリイメージが pre-multiplication として使われているのですから、 column major であると推測できます。
- こちらのページにもそのようなことが書かれています。
参考
XMMATRIX を hlsl に渡す手順
(2023/4/1 追記。)
既に軽く述べていますが XMMATRIX を hlsl に渡す手順は以下の通りです。
- XMMATRIX を転置して XMFLOAT4X4 に代入する。 (上述)
- XMFLOAT4X4 のメモリイメージを ID3DBuffer にコピーする。(UpdateSubresource()を使用。)
XMStoreFloat4x4()のドキュメント には以下のように書かれていました。
XMFLOAT4X4 は行主行列形式です。 列メジャー データを書き出すには、ストア関数を呼び出す前に XMMatrixTranpose を使用して XMMATRIX を入れ替える必要があります。
この部分を以前以下のようにコメントしていました。
後半の「列メジャー~」というコメント(英語版では"To write out column-major data requires the XMMATRIX be transposed via XMMatrixTranpose before calling the store function.")は間違っているように思われます。 (column major と pre/post-multiplication をごっちゃにしている。)
改めて見直してみると、この記述は誤りというよりは言葉足らずかもしれないですね。
- 「列メジャー『である hlsl の matrix に』データを書き出すには、『XMFLOAT4X4 を媒介して』ストア関数を呼び出す前に XMMatrixTranpose を使用~」と考えるとまあ正しいかなと。
- XMMATRIX と XMFLOAT4X4 は pre multiplication か post multiplication かが違うだけなので代入時に転置することは正しいです。ですが別に列メジャーデータを書き出すために転置しているわけではありません。
ProjectionMatrix / XMMatrixPerspectiveFovRH
ProjectionMatrix には XMMatrixPerspectiveFovRH() を使っています。 この行列はいつも覚えられないので以下にメモを残します。
- いつも FovAngleY の意味を覚えられないのですが、上下両方向の視野角ですね。 即ち FovAngleY=90°の場合、視点からスクリーンまでの距離を$L$としたときスクリーンの高さは$2L$になります。 一般にスクリーンの高さは $2L・tan(FovAngleY/2)$ です。
- この行列は X, Y, Z の各方向にスケールするだけで、向きは変えません。 X, Y(Viewの縦横)については -1~1に変換、Z 方向は -nearZ~-farZ を 0~1 に変換するようです。(右手系なので -Z 方向が視線方向。)
- 但しここでは同次座標が用いられており、Output.W ≂ -Input.Z となる変換がされるようです。 (この W 値によりパースが実装されている。) nearZ~farZ が 0~1 に変換されるのは同次座標を3次元座標に戻した後の話です。
- なお Y 方向は GDI と異なり View の上方向が +Y 方向です。
- 勘違いする余地はあまりないかもしれませんが、透視投影は平面に投影しているのであり、円筒スクリーンに投影しているわけではありません。
その他
興味深い記事が関連記事として引っかかったのでリンクします。