OffscreenRendering - HexagramNM/NM_WindowCaptureVirtualCamera GitHub Wiki
WinRTのWindows.Graphics.Captureでキャプチャしたウィンドウの画像をテクスチャとして取得しているので、これを共有テクスチャに書き込みます。DirectXでは基本的にポリゴンを画面(スクリーン)に描画しますが、今回は画面に描画せず、テクスチャに描画します。このことから、今回の手法は オフスクリーンレンダリング と呼ばれます。
このオフスクリーンレンダリングではDirectXのデバイス、デバイスコンテキストを使用します。詳細はこちらをご覧ください。以降のコードでは、デバイスは_dxDevice
、デバイスコンテキストは_dxDeviceContext
の変数に入っているものとします。
コードの詳細(変数の型など)についてはNM_WindowCapture/NM_WindowCapture.h
やNM_WindowCapture/NM_WindowCapture.cpp
をご覧ください。
オフスクリーンレンダリングに必要なものをまとめると以下の通りになります。
Note
厳密にはテクスチャのサンプリング設定を決めるサンプラーも必要ですが、NM_WindowCaptureではデフォルトのものを使っています。
それぞれの関係を図示すると以下のような感じになります。(イメージで描いているので、少々厳密性に欠けるところはありますが)
WinRTのWindows.Graphics.Captureで取得してきたキャプチャしたウィンドウのテクスチャです。取得方法についてはWinRTのWindowsGraphicsCaptureAPIをご覧ください。ID3D11Texture2D
のポインタとして取得できるはずです。
DirectXの共有テクスチャにあるように、あらかじめ共有テクスチャを作成しておく必要があります。
描画結果をどこに記録するかを設定するもので、今回は共有テクスチャに描画するよう設定します。このレンダーターゲットビューをDirectXのデバイスコンテキストに設定し、描画先の設定が反映されるようにします。
以下のレンダーターゲットビューの作成は共有テクスチャ作成後に一度行う必要があります。
DXGI_FORMAT _dxgiFormat = DXGI_FORMAT_B8G8R8A8_UNORM;
CD3D11_RENDER_TARGET_VIEW_DESC renderTargetViewDesc(D3D11_RTV_DIMENSION_TEXTURE2D, _dxgiFormat);
// レンダーターゲットビューの作成
// _sharedCaptureWindowTexture: 描画先の共有テクスチャ
// _renderTargetViewForSharedCaptureWindow: レンダーターゲットビュー
_dxDevice->CreateRenderTargetView(_sharedCaptureWindowTexture.get(),
&renderTargetViewDesc, _renderTargetViewForSharedCaptureWindow.put());
デバイスコンテキストへの設定はオフスクリーンレンダリングを行う度に必要です。共有テクスチャは他のプロセスでも使われ、占有できないためです。最後にデバイスコンテキストにあるレンダーターゲットビューの設定も解除する必要があります。
// デバイスコンテキストにレンダーターゲットビューを設定
ID3D11RenderTargetView* tempRenderTargetViewPtr = _renderTargetViewForSharedCaptureWindow.get();
_dxDeviceContext->OMSetRenderTargets(1, &tempRenderTargetViewPtr, nullptr);
// ここでオフスクリーンレンダリング処理(後述)
// デバイスコンテキストにあるレンダーターゲットビューの設定解除
_dxDeviceContext->OMSetRenderTargets(0, nullptr, nullptr);
描画で使用するテクスチャを登録するものです。登録することで、後述のシェーダからテクスチャの色情報にアクセスできるようになります。
キャプチャしたウィンドウのテクスチャは毎フレーム異なるため、このシェーダリソースビューの作成とデバイスコンテキストへの設定はオフスクリーンレンダリングを行う度に必要です。
// シェーダリソースビューの作成
// currentTexture: キャプチャしたウィンドウのテクスチャ
DXGI_FORMAT _dxgiFormat = DXGI_FORMAT_B8G8R8A8_UNORM;
CD3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc(D3D11_SRV_DIMENSION_TEXTURE2D, _dxgiFormat);
_dxDevice->CreateShaderResourceView(currentTexture.get(), &shaderResourceViewDesc, _shaderResourceView.put());
// デバイスコンテキストにシェーダリソースビューを設定
ID3D11ShaderResourceView* tempShaderResourceViewPtr[] = { _shaderResourceView.get() };
_dxDeviceContext->PSSetShaderResources(0, 1, tempShaderResourceViewPtr);
DirectXの世界での座標をスクリーン上の座標に変換するもので、この設定で描画先の画像サイズを指定できます。
ビューポートの作成やデバイスコンテキストへの設定は最初に一度行う必要があります。後から共有テクスチャのサイズを変更する場合は再作成、再設定が必要です。
D3D11_VIEWPORT vp = { 0.0f, 0.0f, (float)VCAM_VIDEO_WIDTH, (float)VCAM_VIDEO_HEIGHT, 0.0f, 1.0f };
_dxDeviceContext->RSSetViewports(1, &vp);
DirectXでテクスチャを貼り付けたポリゴンを描画するためのコードです。シェーダコードはNM_WindowCapture/SpriteShader.hlsl
をご覧ください。(座標変換も行わない、単純なシェーダコードではありますが)
以下のコードで、シェーダコードの文字列からシェーダをコンパイルし、デバイスコンテキストにそのシェーダを使用するように設定できます。このコンパイルや設定は最初に一度行う必要があります。シェーダを変更したい場合は再度コンパイルや設定が必要になります。
// hlslOffscreenRenderingCode: シェーダコードの文字列
size_t hlslSize = std::strlen(hlslOffscreenRenderingCode);
// 頂点シェーダのコンパイル
com_ptr<ID3DBlob> compiledVS;
D3DCompile(hlslOffscreenRenderingCode, hlslSize, nullptr, nullptr, nullptr,
"VS", "vs_5_0", 0, 0, compiledVS.put(), nullptr);
// ピクセルシェーダのコンパイル
com_ptr<ID3DBlob> compiledPS;
D3DCompile(hlslOffscreenRenderingCode, hlslSize, nullptr, nullptr, nullptr,
"PS", "ps_5_0", 0, 0, compiledPS.put(), nullptr);
// デバイスコンテキストに頂点シェーダを設定
_dxDevice->CreateVertexShader(compiledVS->GetBufferPointer(),
compiledVS->GetBufferSize(), nullptr, _spriteVS.put());
_dxDeviceContext->VSSetShader(_spriteVS.get(), 0, 0);
// デバイスコンテキストにピクセルシェーダを設定
_dxDevice->CreatePixelShader(compiledPS->GetBufferPointer(),
compiledPS->GetBufferSize(), nullptr, _spritePS.put());
_dxDeviceContext->PSSetShader(_spritePS.get(), 0, 0);
シェーダコードの文字列は、以下の記事を参考にinclude文を用いて直接埋め込んでいます。また、SpriteShader.hlsl
はビルド時にコンパイルしないよう、Visual Studioでコードのプロパティを開き、「項目の種類」を「ビルドに含めない」に変更する必要があります。
#define HLSL_EXTERNAL_INCLUDE(...) #__VA_ARGS__
const char* hlslOffscreenRenderingCode =
#include "SpriteShader.hlsl"
;
ポリゴンの頂点を格納する頂点バッファと頂点シェーダを紐づけるためのものです。
入力レイアウトの作成や設定は最初に一度行う必要があります。シェーダを変更する場合は再度、作成や設定が必要です。
// "POSITION"や"TEXUV"はSpriteShader.hlslのVS関数がとる、引数のラベル
// TEXUVのところで指定している12は、POSITIONが3つのfloat(4バイト)であり、12バイト分のオフセットが必要であることに由来
D3D11_INPUT_ELEMENT_DESC layout[2] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXUV", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
// 入力レイアウトを作成し、デバイスコンテキストに設定
// 各頂点に対し、位置座標とテクスチャ座標の2要素があるので、CreateInputLayoutの要素数にあたる引数を2にする。
_dxDevice->CreateInputLayout(layout, 2, compiledVS->GetBufferPointer(),
compiledVS->GetBufferSize(), _spriteInputLayout.put());
_dxDeviceContext->IASetInputLayout(_spriteInputLayout.get());
ポリゴンの各頂点のデータを格納するバッファです。今回は長方形のポリゴンを描画すれば良いので、4頂点分のデータが必要で、各頂点の位置やテクスチャのUV座標を入れておきます。
頂点バッファの作成やデバイスコンテキストへの設定は最初に一度行います。
struct VertexType
{
::DirectX::XMFLOAT3 Pos;
::DirectX::XMFLOAT2 Tex;
};
// 頂点バッファのフラグ設定
// _polygonVertexに入る頂点データは左上、右上、左下、右下の順に格納します。
D3D11_BUFFER_DESC _vbDesc;
VertexType _polygonVertex[4];
_vbDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
_vbDesc.ByteWidth = sizeof(VertexType) * 4;
_vbDesc.MiscFlags = 0;
_vbDesc.StructureByteStride = 0;
_vbDesc.Usage = D3D11_USAGE_DEFAULT;
_vbDesc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA initData = {
_polygonVertex, sizeof(_polygonVertex), 0
};
// 頂点バッファ (_vertexBuffer) の作成
_dxDevice->CreateBuffer(&_vbDesc, &initData, _vertexBuffer.put());
// デバイスコンテキストに頂点バッファを設定し、頂点データからどのようにポリゴンを作るかを設定。
UINT stride = sizeof(VertexType);
UINT offset = 0;
ID3D11Buffer* tempBufferPtr = _vertexBuffer.get();
_dxDeviceContext->IASetVertexBuffers(0, 1, &tempBufferPtr, &stride, &offset);
_dxDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
ウィンドウの大きさ、トリミングの設定に応じて、頂点の位置やUV座標は変わります。これはオフスクリーンレンダリングの度にデバイスコンテキストのUpdateSubresource
を呼び出すことで対応できます。この処理で頂点バッファの中のデータが更新されます。
// あらかじめ、_polygonVertexに変更後の頂点データを入れておきます。
_dxDeviceContext->UpdateSubresource(_vertexBuffer.get(), 0, nullptr, _polygonVertex, 0, 0);
オフスクリーンレンダリングを行う度に、以下の処理を呼び出すことで、共有テクスチャに描画結果を書き込むことができます。共有テクスチャは処理中に他のプロセスにアクセスされないよう、mutexを取得し、処理が終わったら解放することに注意してください。
// 共有テクスチャのmutexを取得
// _sharedCaptureWindowTexture: 描画先の共有テクスチャ
com_ptr<IDXGIKeyedMutex> mutex;
_sharedCaptureWindowTexture.as(mutex);
mutex->AcquireSync(MUTEX_KEY, INFINITE);
// デバイスコンテキストにレンダーターゲットビューを設定
ID3D11RenderTargetView* tempRenderTargetViewPtr = _renderTargetViewForSharedCaptureWindow.get();
_dxDeviceContext->OMSetRenderTargets(1, &tempRenderTargetViewPtr, nullptr);
// 背景色で塗りつぶし
float color[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
_dxDeviceContext->ClearRenderTargetView(_renderTargetViewForSharedCaptureWindow.get(), color);
// 中略(ポリゴン頂点位置やUV座標の計算し、_polygonVertexに結果を格納)
// 頂点バッファの更新
_dxDeviceContext->UpdateSubresource(_vertexBuffer.get(), 0, nullptr, _polygonVertex, 0, 0);
// 描画命令
_dxDeviceContext->Draw(4, 0);
_dxDeviceContext->Flush();
// デバイスコンテキストにあるレンダーターゲットビューの設定解除
_dxDeviceContext->OMSetRenderTargets(0, nullptr, nullptr);
// 他プロセスが共有テクスチャにアクセスできるよう、mutexを解放
mutex->ReleaseSync(MUTEX_KEY);