🕹️

DirectX11でリアルタイムキューブマップテクスチャ作成(Mipmap付き)&DDSファイル保存

2023/12/15に公開

この記事は、神戸電子専門学校 ゲーム技研部 Advent Calendar 2023の15日目の記事です。
https://qiita.com/advent-calendar/2023/kdgamegiken

はじめに

DirectX11とC++でプログラムしていきます。また今回はキューブマップを作成することがメインとなりますので、描画する時に必要なシェーダーなどの説明は省略させていただきます。

使用ライブラリ

DirectXでの開発を楽にするために、下記のライブラリも使用しています。
Visual StudioのNuGetでもインストールできます。

・Microsoft DirectXTK
https://github.com/microsoft/DirectXTK

・Microsoft DirectXTex
https://github.com/microsoft/DirectXTex

キューブマップとは

例えば3Dゲームでつるつるの鉄球を出したとしましょう。つるつるの鉄球はのどんな見た目をしているでしょうか?おそらく鉄球に周りの風景が映り込んでいるように見えるでしょう。しかし3DCGの世界では、周りの風景は勝手に映り込んでくれないのです。これをできるだけ簡単に表現するにはどうすればよいか。そこでキューブマップの出番です。

キューブマップとは、ゲームの風景を上下左右前後の合計6枚の映像を撮影(描画)し、1つのテクスチャとしてまとめたものです。
テクスチャなので、もちろんファイルにも保存できます。ただし6枚の画像をまとめた特殊なテクスチャなのでpngなど一般的な形式では保存できず、DDSという特殊な形式を使用します。

ゲームの世界を撮影するには

キューブマップをリアルタイムに生成するには、ゲーム風景を撮影したい座標から6回描画を実行する必要があります。
たとえば、座標p(0, 2, 0)の位置から

  1. 右向き( 1, 0, 0)の方向に、視野角90°で全物体を描画。
  2. 左向き(-1, 0, 0)の方向に、視野角90°で全物体を描画。
  3. 上向き( 0, 1, 0)の方向に、視野角90°で全物体を描画。
  4. 下向き( 0, -1, 0)の方向に、視野角90°で全物体を描画。
  5. 前向き( 0, 0, 1)の方向に、視野角90°で全物体を描画。
  6. 後向き( 0, 0, -1)の方向に、視野角90°で全物体を描画。
    ってな感じです。


全方向(6方向)撮影しキューブマップテクスチャを作成する
※画像はイメージです。実際にこの箱は見えません※

キューブマップ生成クラスを作成

それでは、撮影を簡単に行えるようにするクラスを作っていきましょう。

CubeMapGenerator.h
#pragma once

//====================================================
// キューブマップ生成クラス
//====================================================
class CubeMapGenerator
{
public:

	// 生成されたキューブマップ取得
	ID3D11ShaderResourceView* const* GetCubeMap() const { return &m_srvCubeMap; }

	// 指定サイズのキューブマップを生成し、風景を描画する。
	// size		:キューブマップテクスチャのサイズ。
	// position	:撮影の中心となるワールド座標。
	// drawProc	:各方向で実行したい描画処理を渡す。
	void Generate(int size, const DirectX::SimpleMath::Vector3& position, std::function<void()> drawProc);
	
	// 解放
	void Release();

	// キューブマップをDDSファイルへ保存する
	void SaveToFile();

	~CubeMapGenerator()
	{
		Release();
	}

private:

	// キューブマップ(読み取り用)
	ID3D11ShaderResourceView* m_srvCubeMap = nullptr;
	// キューブマップ(書き込み用)
	ID3D11RenderTargetView* m_rtvCubeMap = nullptr;

	// 描画時に使用するZバッファ
	ID3D11DepthStencilView* m_dsv = nullptr;

};

Generate関数で、実際に撮影描画を行いキューブマップを作成します。
その後、SaveToFile関数を実行することでDDSファイルへ保存もできるようにします。
描画でキューブマップを使用したい時は、GetCubeMap関数でキューブマップを取得しシェーダーへ渡しましょう。

撮影描画を行う関数「Generate

まず指定座標から上下左右前後を撮影して、6枚の画像を1つのテクスチャに収める関数です。今回の記事のメインとなる部分ですね。
※エラーチェックなどは省略しています

CubeMapGenerator.cpp
#include "CubeMapGenerator.h"

// 安全にReleaseするための関数
template<class T>
void SafeRelease(T*& p)
{
	if (p) { p->Release(); p = nullptr; }
}

void CubeMapGenerator::Generate(int size, const DirectX::SimpleMath::Vector3& position,
std::function<void()> drawProc)
{
	//--------------------------------------------------
	// 1.CubeMapテクスチャ作成(初回のみ)
	//--------------------------------------------------
	if(m_srvCubeMap == nullptr && m_rtvCubeMap == nullptr)
	{
		// テクスチャリソース作成
		D3D11_TEXTURE2D_DESC desc = {};

		desc.Width = (UINT)size;
		desc.Height = (UINT)size;
		desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR対応にするためfloat型
		desc.ArraySize = 6;// 6枚の画像を持ったテクスチャとして作成する
		desc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE | // キューブマップ
				 D3D11_RESOURCE_MISC_GENERATE_MIPS;	// Mipmap生成
		desc.Usage = D3D11_USAGE_DEFAULT;
		desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | 
				 D3D11_BIND_RENDER_TARGET;
		desc.CPUAccessFlags = 0;
		desc.MipLevels = 0;	// 完全なミップマップチェーン
		desc.SampleDesc.Count = 1;
		desc.SampleDesc.Quality = 0;

		ID3D11Texture2D* resource = nullptr;
		d3dDevice->CreateTexture2D(&desc, nullptr, &resource);
		// 正確なMipLevels取得のため、情報再取得
		resource->GetDesc(&desc);

		// RTV作成
		D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {};
		rtvDesc.Format = desc.Format;			// Format
		rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; // テクスチャ配列
		rtvDesc.Texture2DArray.ArraySize = desc.ArraySize; // 要素数
		rtvDesc.Texture2DArray.FirstArraySlice = 0;
		rtvDesc.Texture2DArray.MipSlice = 0;
		d3dDevice->CreateRenderTargetView(resource, &rtvDesc, &m_rtvCubeMap);

		// SRV作成
		D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
		srvDesc.Format = desc.Format;
		srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
		srvDesc.TextureCube.MostDetailedMip = 0;
		srvDesc.TextureCube.MipLevels = desc.MipLevels;
		d3dDevice->CreateShaderResourceView(resource, &srvDesc, &m_srvCubeMap);

		resource->Release();
	}

	//--------------------------------------------------
	// Zバッファを作成(初回のみ)
	//--------------------------------------------------
	if(m_dsv == nullptr)
	{
		D3D11_TEXTURE2D_DESC desc = {};

		desc.Width = (UINT)size;
		desc.Height = (UINT)size;
		desc.Format = DXGI_FORMAT_R24G8_TYPELESS;
		desc.ArraySize = 1;
		desc.MiscFlags = 0;

		desc.Usage = D3D11_USAGE_DEFAULT;
		desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | 
				 D3D11_BIND_DEPTH_STENCIL;
		desc.CPUAccessFlags = 0;
		desc.MipLevels = 1;
		desc.SampleDesc.Count = 1;
		desc.SampleDesc.Quality = 0;

		ID3D11Texture2D* resource = nullptr;
		d3dDevice->CreateTexture2D(&desc, nullptr, &resource);


		D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc = {};
		dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
		dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
		dsvDesc.Texture2D.MipSlice = 0;
		d3dDevice->CreateDepthStencilView(resource, &dsvDesc, &m_dsv);

		resource->Release();
	}

	// テクスチャリソースを取得
	ID3D11Resource* cubeMapResource = nullptr;
	m_rtvCubeMap->GetResource(&cubeMapResource);

	//--------------------------------------------------
	// 2.上下左右前後の計6回描画し、CubeMapを作成する
	//--------------------------------------------------
	// RenderTarget、ZBuffer、Viewportの変更するため、
	// 現在のRenderTargetとZBufferとViewportを取得しておく
	ID3D11RenderTargetView* saveRTV[8] = {};
	ID3D11DepthStencilView* saveDSV = nullptr;
	d3dDeviceContext->OMGetRenderTargets(8, saveRTV, &saveDSV);
	D3D11_VIEWPORT saveVP;
	UINT numVPs = 1;
	d3dDeviceContext->RSGetViewports(&numVPs, &saveVP);

	// Viewportを変更
	D3D11_VIEWPORT vp = { 0, 0, (float)size, (float)size, 0, 1 };
	d3dDeviceContext->RSSetViewports(1, &vp);

	// キューブマップの各面(6枚)を描画
	for (int i = 0; i < 6; i++)
	{
		// キューブマップの各画像(上下左右前後)用のビューを作成
		ID3D11RenderTargetView* rtv = nullptr;
		D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {};
		rtvDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;	// Format
		rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
		rtvDesc.Texture2DArray.ArraySize = 1;
		rtvDesc.Texture2DArray.FirstArraySlice = i;
		rtvDesc.Texture2DArray.MipSlice = 0;
		// レンダーターゲットビュー作成
		d3dDevice->CreateRenderTargetView(cubeMapResource, &rtvDesc, &rtv);

		// RT、Zクリア
		d3dDeviceContext->ClearRenderTargetView(rtv, Math::Color(0, 0, 0, 1));
		d3dDeviceContext->ClearDepthStencilView(m_dsv, D3D11_CLEAR_DEPTH, 1, 0);

		// RTとZを変更する
		d3dDeviceContext->OMSetRenderTargets(1, &rtv, m_dsv);

		// カメラ設定
		DirectX::SimpleMath::Matrix mView;
		switch (i)
		{
		// 右面(X)
		case D3D11_TEXTURECUBE_FACE_POSITIVE_X:
			mView = DirectX::XMMatrixLookAtLH(position, position + DirectX::SimpleMath::Vector3(1, 0, 0), DirectX::SimpleMath::Vector3(0, 1, 0));
			break;
		// 左面(-X)
		case D3D11_TEXTURECUBE_FACE_NEGATIVE_X:
			mView = DirectX::XMMatrixLookAtLH(position, position + DirectX::SimpleMath::Vector3(-1, 0, 0), DirectX::SimpleMath::Vector3(0, 1, 0));
			break;
		// 上面(Y)
		case D3D11_TEXTURECUBE_FACE_POSITIVE_Y:
			mView = DirectX::XMMatrixLookAtLH(position, position + DirectX::SimpleMath::Vector3(0, 1, 0), DirectX::SimpleMath::Vector3(0, 0, -1));
			break;
		// 下面(-Y)
		case D3D11_TEXTURECUBE_FACE_NEGATIVE_Y:
			mView = DirectX::XMMatrixLookAtLH(position, position + DirectX::SimpleMath::Vector3(0, -1, 0), DirectX::SimpleMath::Vector3(0, 0, 1));
			break;
		// 後面(Z)
		case D3D11_TEXTURECUBE_FACE_POSITIVE_Z:
			mView = DirectX::XMMatrixLookAtLH(position, position + DirectX::SimpleMath::Vector3(0, 0, 1), DirectX::SimpleMath::Vector3(0, 1, 0));
			break;
		// 前面(-Z)
		case D3D11_TEXTURECUBE_FACE_NEGATIVE_Z:
			mView = DirectX::XMMatrixLookAtLH(position, position + DirectX::SimpleMath::Vector3(0, 0, -1), DirectX::SimpleMath::Vector3(0, 1, 0));
			break;
		}

		// カメラ座標を定数バッファにセット
		SHADER->m_cb7_Camera.Work().CamPos = mView.Invert().Translation();
		// ビュー行列を定数バッファにセット
		SHADER->m_cb7_Camera.Work().mView = mView;
		// 射影行列を定数バッファにセット
		SHADER->m_cb7_Camera.Work().mProj = DirectX::XMMatrixPerspectiveFovLH(DirectX::XMConvertToRadians(90), 1.0f, 0.01f, 2000);
		// 書き込み
		SHADER->m_cb7_Camera.Write();

		//-----------------
		// 描画実行
		drawProc();
		//-----------------

		// ビュー解放
		rtv->Release();
	}

	// RTとZを元に戻す
	d3dDeviceContext->OMSetRenderTargets(8, saveRTV, saveDSV);
	for (auto&& rtv : saveRTV)
	{
		SafeRelease(rtv);
	}
	SafeRelease(saveDSV);

	// Viewportを元に戻す
	d3dDeviceContext->RSSetViewports(numVPs, &saveVP);

	// 取得したリソースの参照カウンターを減らす
	cubeMapResource->Release();

	// ミップマップの生成
	d3dDeviceContext->GenerateMips(m_srvCubeMap);
}

出来るだけ素のDirectXの型を使用するようにしたため、プログラムが長くなりました…
一部、謎のデータがあるので紹介します。
・d3dDevice … ID3D11Device
・d3dDeviceContext … ID3D11DeviceContext
・SHADER … シェーダー関係のものをまとめたもの
このあたりは皆さんの環境に合わせてくださいね。

.ではキューブマップテクスチャを作成しています。通常のテクスチャと異なった設定になっていますね。

.では実際にfor文で6回繰り返し、上下左右前後の映像を描画しています(描画先はキューブマップの各画像へ)。「あれ?何も描画してないじゃん?」と思いますが、drawProc()が描画を行っている関数です。これは引数で持ってきている関数オブジェクトです。

ゲームの世界を描画する内容は、作るゲームによって異なります。なのでここに直接背景やらキャラクターやらの描画処理を書くわけにはいきませんよね。
そこで、実際の描画処理自体は別のどこかで関数にまとめておいて、その関数自体を持って来てココで実行しているわけです。

この辺りは後ほど説明します。

ミップマップの活用方法

最後のd3dDeviceContext->GenerateMips(m_srvCubeMap);で、ミップマップを生成しています。
ミップマップを簡単に説明すると、元画像の縮小版の画像をいっぱい作ることです。
例えば512 x 512で作った場合だと、256 x 256、128 x 128、64 x 64、32 x 32 などなど…
1 x 1まで作成されます。キューブマップにこれを作っておくことで、IBLでのラフネス表現の時に便利なので今回は作成しておきました。


左に行くほど、ミップマップレベルが高い(低サイズの画像)
簡単につるつる、ざらざらの表現ができる。

解放もお忘れずに

Release関数も作っておきましょう。

CubeMapGenerator.cpp
void CubeMapGenerator::Release()
{
	SafeRelease(m_srvCubeMap);
	SafeRelease(m_rtvCubeMap);
	SafeRelease(m_dsv);
}

使用例

Generate関数が完成したので、実際に使用してみましょう。
上記で関数オブジェクトの話がありましたが、まさにここで関数オブジェクトdrawを作成してます。
下記のDrawWorld関数が、実際に毎フレーム実行される描画関数だとしましょう。

どこかのcpp

// 今回は雑にグローバル変数として作ります
CubeMapGenerator g_cubeMapGen;

// ゲームの世界を描画する関数
void DrawWorld()
{
	// 描画する内容をまとめた関数
	auto draw = []()
	{
		// 背景物描画
		// 空描画
		// キャラクター描画
		// などなど、ゲーム世界の描画を書きます。
	};

	// 座標(0, 2, 0)の位置で撮影し、キューブマップテクスチャを作成
	// このGenerate関数内でdrawが6回呼ばれます。
	g_cubeMapGen.Generate(512, { 0, 2, 0 }, draw);

	// 作成したキューブマップテクスチャを100番スロットにセットする
	d3dDeviceContext->PSSetShaderResources(100, 1, m_cubeMapGen.GetCubeMap());
	
	// カメラをセット
	・・・省略・・・
	
	// 世界を描画する
	draw();

	// 100番スロットのテクスチャを解除する
	ID3D11ShaderResourceView* nullSRV = nullptr;
	d3dDeviceContext->PSSetShaderResources(100, 1, &nullSRV);
}

まずは、関数オブジェクトdrawです。
このローカル関数内に、全ての物体の描画を書いておきます。
この描画関数drawを使って、キューブマップ生成で6回、世界の描画で1回、合計7回描画をしています。

Generate関数で作成したキューブマップテクスチャを世界の描画で使用したいので、作成後すぐにデバイスコンテキストへセットしています。とりあえず今回は100番スロットにセットしていますが、0~127なら別に何番でも良いです。これであとはシェーダーでキューブマップを使用できるようにプログラムすれば完成ですね。


作成したキューブマップを光源として使用するアルゴリズムであるIBL(Image Based Lighting)で描画してみた例

DDSファイルに保存してみよう

今回は毎フレームキューブマップを作成していますが、さすがに処理負荷が気になりますよね。
キューブマップが1つならまだいいかもですが、1ステージに複数のキューブマップを使うときもあるかもしれません。そうなると、キューブマップ数 x 6回の描画が実行されるため、処理負荷が無視できなくなってきます。これに対する対処方法ですが、

A. 初回だけ撮影する。
B. 撮影したキューブマップを画像ファイルに保存しておき、実行時に読み込んで使用する。

Bを実現するには、キューブマップをファイルへ保存する必要があるため、最後にこれを実装してみましょう。

CubeMapGenerator.cpp
void CubeMapGenerator::SaveToFile()
{
	if (m_rtvCubeMap == nullptr)return;

	// ビューからテクスチャリソース取得
	ID3D11Resource* cubeMapResource = nullptr;
	m_rtvCubeMap->GetResource(&cubeMapResource);
	cubeMapResource->Release();

	// テクスチャ―リソースから、イメージデータを作成
	DirectX::ScratchImage image;
	DirectX::CaptureTexture(
		d3dDevice,
		d3dDeviceContext,
		cubeMapResource,
		image
	);

	// イメージデータをファイルへ保存
	DirectX::SaveToDDSFile( image.GetImages(),
				image.GetImageCount(),
				image.GetMetadata(), DirectX::DDS_FLAGS_NONE,
				L"cubemap.dds");
}

キューブマップを保存するにはDDSという画像形式にする必要があります。SaveToDDSFileを使用することで、簡単にDDSファイルに保存できます。

おわりに

今回はリアルタイムキューブマップ生成を解説しました。
主に映り込みなどの反射の表現で使用することが多く、高品質な映像を作るためには重要なものですね。
さらにParallax Corrected Cubemapsを実装して自然な映り込み表現を行ったり、映り込み表現以外ではInterior Mappingなんかも面白い技術ですよね。

こういった技術を作品に組み込み、美麗なゲーム世界作りましょう!

神戸電子専門学校ゲーム技術研究部

Discussion