テクスチャーを使った描画
前回のDirectX11の記事ではシェーダを使って矩形を描画するところまで行いました。
今回は、更に矩形にテクスチャーを張るところまでやっていこうかと思います。
DirectX11ではテクスチャーの読み込み処理などはサポートされていません。
以前はD3DXというヘルパー機能に含まれていましたが、D3DXも廃止となりました。
なので先ずは読み込み部分を何とかする方法から説明していきます。
DirectXTex
上でも書いたようにDirectX11ではテクスチャー読み込みなどの機能は無くなりました。
とはいってもMicrosoft様は全くサポートを無くした訳ではありません。
なんとテクスチャー周りの実装をDirectXTexというライブラリで用意してくれています。
・ソースコード
https://github.com/Microsoft/DirectXTex
ソースコードはGitHubで公開されています。
Gitで取得してもいいのですが、現在の最新盤を取得するだけなら[Clone or download]から[Download ZIP]でダウンロードできます。
ダウンロード後は自前でビルドする必要がありますが、
Visual Studioで立ち上げて必要な構成でビルドするだけです。
コードが全て公開されていますので、自前でテクスチャー読み込みを作成する参考にもなります。
DirectXTexのビルド方法
DirectXTexのビルド方法を簡単に説明しておきます。
今回はVisual Studio 2019(Windows10)での方法です。
先ず、ダウロードしたDirectXTexフォルダ内の下記を開きます。
・DirectXTex_Desktop_2019_Win10.sln
紛らわしいのですが、DirectXTex_Windows10.slnはWindows Store向けのプロジェクトとなります。
このサイトでは基本的にDesktopアプリケーションについての説明を記載していますのでDesktop_2019の方を使います。
今回はx64でDirectX11のプロジェクトを作成しているのでx64のDebugとReleaseでそれぞれビルドします。
ビルド後は下記のフォルダにDebugとReleaseのビルド結果が出力されます。
[プロジェクト]/DirectXTex/Bin/Desktop_2019_Win10/x64
それぞれDebugとReleaseのフォルダから下記のファイルを自分のプロジェクトにコピーします。
- DirectXTex.lib
- DirectXTex.pdb
後は、DirectXTexのヘッダーをコピーして持って来れば準備は完了です。
必要なヘッダーは下記の通りです。
[プロジェクト]/DirectXTex
- DirectXTex.h
- DirectXTex.inl
これらをプロジェクト内の任意のフォルダにコピーして使用します。
今回はDirectX用のプロジェクトの下記に配置します。
[ソリューションのあるディレクトリ]/ThirdParty/DirectXTex
DirectXTex
┣ヘッダー類
┗ x64 ━ DebugとReleaseフォルダ ━ libファイル類
これでビルドして必要なものは一通り揃います。
プロジェクトの設定
DirectXTexを自分のプロジェクトに配置の次はプロジェクトの設定を行います。
・includeパスの追加
[C/C++] -> [全般] -> [追加のインクルードディレクトリ]
ここに$(SolutionDir)ThirdPartyと値を追加します。
・ライブラリパスの追加
[C/C++] -> [全般] -> [追加のライブラリディレクトリ]
ここに$(SolutionDir)ThirdPartyと値を追加します。
これで使用するための準備が整います!
ビルドして配置してプロジェクトの設定と色々とありましたが、ついにここからテクスチャーの読み込みを実装です。
テクスチャーの管理クラス
テクスチャーを読み込んで使用するために、先ずはTextureクラスを作成していきます。
今回のクラスでは下記の機能を実装いていきます。
- 画像データの読み込み
- シェーダリソースの作成
- サンプラーステートの作成
サンプラーステートは別途管理した方がいい場合もありますが、今回はまとめて実装していきます。
Texture.h
// テクスチャークラス
class Texture
{
public:
Texture();
// 初期化、解放
bool Initialize(DirectX11& directX11, const char* pFilename);
void Finalize();
ID3D11ShaderResourceView* GetResourceView() {
return m_pShaderResourceView.Get();
}
ID3D11SamplerState* GetSamplerState() {
return m_pSamplerState.Get();
}
UINT GetWidth() { return m_Width; }
UINT GetHeight() { return m_Height; }
private:
// サンプラーステートの作成
bool CreateSampler(DirectX11& directX11);
private:
ComPtr<ID3D11ShaderResourceView> m_pShaderResourceView;
ComPtr<ID3D11SamplerState> m_pSamplerState;
DXGI_FORMAT m_Format;
UINT m_Width;
UINT m_Height;
};
今回は、テクスチャークラスの管理にComPtrを使用しています。
これはスマートポインタと呼ばれるものでCOMオブジェクトの生存管理をしてくれます。
DirectXはCOMオブジェクトとして実装されています。
なのでComPtrを使用することで管理を楽にすることができます。
使い方は簡単です。
#include <wrl.h>
using namespace Microsoft::WRL;
上記のようにwrl.hのインクルードとネームスペースを省略するためのusingを書けば完了です。
ではこの調子でクラスの実装部分も見ていきましょう。
Texture.cpp
#include "Texture.h"
#include <DirectXTex/DirectXTex.h>
// DirectXTexのライブラリをリンク
#if _DEBUG
#pragma comment(lib, "DirectXTex/x64/Debug/DirectXTex.lib")
#else
#pragma comment(lib, "DirectXTex/x64/Release/DirectXTex.lib")
#endif
using namespace DirectX;
Texture::Texture()
: m_Width(0)
, m_Height(0)
{}
bool Texture::Initialize(DirectX11& directX11, const char* pFilename)
{
auto pDevice = directX11.GetDevice();
Finalize();
const char* pExtension = "";
for (size_t i = strlen(pFilename); i != 0; i--)
{
if (pFilename[i-1] == '.')
{
pExtension = &pFilename[i];
}
}
WCHAR path[256];
size_t len = 0;
mbstowcs_s(&len, path, 256, pFilename, _TRUNCATE);
TexMetadata metadata;
ScratchImage image;
HRESULT hr;
// DDSファイルの読み込み
if (strcmp(pExtension, "dds") == 0)
{
hr = LoadFromDDSFile(path, DDS_FLAGS::DDS_FLAGS_NONE, &metadata, image);
}
// TGAファイルの読み込み
else if (strcmp(pExtension, "tga") == 0)
{
hr = LoadFromTGAFile(path, &metadata, image);
}
// WICファイル(bmp, jpg, png等)の読み込み
else
{
hr = LoadFromWICFile(path, WIC_FLAGS::WIC_FLAGS_NONE, &metadata, image);
}
if (FAILED(hr))
{
return false;
}
ComPtr<ID3D11ShaderResourceView> pView;
// 画像からシェーダリソースViewの作成
hr = CreateShaderResourceView(pDevice, image.GetImages(), image.GetImageCount(), metadata, &pView);
if (FAILED(hr)) {
return false;
}
if (!CreateSampler(directX11)) {
return false;
}
m_Format = metadata.format;
m_Width = static_cast<UINT>(metadata.width);
m_Height = static_cast<UINT>(metadata.height);
m_pShaderResourceView.Swap(pView);
return true;
}
void Texture::Finalize()
{
m_pShaderResourceView.Reset();
m_pSamplerState.Reset();
}
bool Texture::CreateSampler(DirectX11& directX11)
{
auto pDevice = directX11.GetDevice();
// SamplerState作成
D3D11_SAMPLER_DESC sampDesc{};
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
ComPtr<ID3D11SamplerState> pSamplerState;
HRESULT hr = pDevice->CreateSamplerState(&sampDesc, &pSamplerState);
if (FAILED(hr)) {
return false;
}
m_pSamplerState.Swap(pSamplerState);
return true;
}
一気に記述していきましたが、これでテクスチャーの管理が行えます。
ComPtrを使用しているのでFinalizeは無くても解放はされます。
ただ解放タイミングの制御するためにはあった方が無難です。
対応しているファイルはbmp, png, jpgなどからddsやtga辺りまで対応しています。
シェーダの実装
テクスチャーを使用した描画を行うにはシェーダを修正する必要があります。
Shader.hlsl
struct VS_INPUT
{
float3 Position : POSITION;
float4 Color : COLOR;
float2 UV : TEXCOORD; // テクスチャーのUV値
};
struct VS_OUTPUT
{
float4 Position : SV_POSITION;
float4 Color : TEXCOORD0;
float2 UV : TEXCOORD1; // テクスチャーのUV値
};
void VS(
in VS_INPUT In,
out VS_OUTPUT Out
)
{
Out.Position.xyz = In.Position.xyz;
Out.Position.w = 1.0f;
Out.Color = In.Color;
// UV値をPixelShaderに流す
Out.UV = In.UV;
}
typedef VS_OUTPUT PS_INPUT;
// テクスチャーとサンプラーステートの定義
Texture2D diffuse : register(t0);
SamplerState samplerDiffuse : register(s0);
void PS(
in PS_INPUT In,
out float4 OutColor : SV_Target0
)
{
// UV値を元にテクスチャーから色をサンプリングする
float4 tex = diffuse.Sample(samplerDiffuse, In.UV);
// テクスチャーと頂点カラーを掛け合わせる
OutColor = In.Color * tex;
}
シェーダの実装で変更した個所は主に2点です。
- UV値の入力値を追加
- テクスチャーとサンプラーステートからのサンプリング処理を追加
これだけでシェーダの実装は以外と簡単にできます。
Texture2D diffuse : register(t0);
SamplerState samplerDiffuse : register(s0);
この部分の各行の末尾にある(t0)と(s0)はレジスタ番号を指定しています。
C++のコードからはスロット番号という形で指定するようになっています。
初期化から描画までの処理
テクスチャーを使って描画するために必要なことは大きくは2点です。
- 頂点データにUV情報の追加
- テクスチャーの読み込みと設定
たったこれだけ対応すればテクスチャーを表示することができます。
VertexBuffer vertexBuffer;
VertexShader vertexShader;
PixelShader pixelShader;
InputLayout inputLayout;
// テクスチャー管理
Texture texture;
// 頂点データ
struct Vertex
{
float x, y, z;
UINT color;
float u, v;
};
bool Initialize(DirectX11& directX11)
{
// 左上から右下まで全体を切り取るようにUV情報を追加
Vertex vertex[4]{
{ -0.5f, -0.5f, 0.0f, 0xffffffff, 1.0f, 1.0f },
{ -0.5f, 0.5f, 0.0f, 0xffffffff, 1.0f, 0.0f },
{ 0.5f, -0.5f, 0.0f, 0xffffffff, 0.0f, 1.0f },
{ 0.5f, 0.5f, 0.0f, 0xffffffff, 0.0f, 0.0f },
};
vertexBuffer.Initialize(directX11, sizeof(vertex), vertex);
if (!vertexShader.Initialize(directX11, "assets/shader.hlsl", "VS"))
{
MessageBox(NULL, "VertexShader", "Error", MB_OK);
return false;
}
if (!pixelShader.Initialize(directX11, "assets/shader.hlsl", "PS"))
{
MessageBox(NULL, "PixelShader", "Error", MB_OK);
return false;
}
// 入力レイアウト(UV値を頂点レイアウトに含める)
D3D11_INPUT_ELEMENT_DESC layout3D[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 4 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 4 * 4, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = ARRAYSIZE(layout3D);
if (!inputLayout.Initialize(directX11, vertexShader, layout3D, numElements))
{
return false;
}
if (!texture.Initialize(directX11, "assets/food.png"))
{
return false;
}
return true;
}
void Finalize()
{
texture.Finalize();
inputLayout.Finalize();
vertexBuffer.Finalize();
vertexShader.Finalize();
pixelShader.Finalize();
}
void RenderTest(DirectX11& directX11)
{
ID3D11DeviceContext* pContext = directX11.GetContext();
ID3D11RenderTargetView* pTarget = directX11.GetRenderTargetView();
D3D11_VIEWPORT ViewPort;
ViewPort.TopLeftX = 0;
ViewPort.TopLeftY = 0;
ViewPort.Width = 1280;
ViewPort.Height = 720;
ViewPort.MinDepth = 0.0f;
ViewPort.MaxDepth = 1.0f;
pContext->OMSetRenderTargets(1, &pTarget, NULL);
pContext->RSSetViewports(1, &ViewPort);
pContext->VSSetShader(vertexShader.GetShader(), NULL, 0);
pContext->PSSetShader(pixelShader.GetShader(), NULL, 0);
auto pView = texture.GetResourceView();
auto pSampler = texture.GetSamplerState();
// テクスチャーとサンプラーステートをスロット(レジスタ)0番に設定
pContext->PSSetShaderResources(0, 1, &pView);
pContext->PSSetSamplers(0, 1, &pSampler);
pContext->IASetInputLayout(inputLayout.GetInputLayout());
pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
UINT offset = 0;
UINT stride = sizeof(Vertex);
ID3D11Buffer* pVBuffer = vertexBuffer.GetBuffer();
pContext->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset);
pContext->Draw(4, 0);
}
前回のシェーダを使った描画からそこまで大掛かりな変更は加えていません。
これだけでテクスチャーを使った描画ができるようになります。
実際にこれを実行すると下記のような結果になります。
美味しそうな麻婆麺が表示されましたね!
今回は単純に画像をそのまま表示しているだけです。
ゲームでは1枚のテクスチャーを部分的に切り取って表示することでUIやアニメーション的な処理を行うこともあります。
その辺りは機会があれば別途記事にしていこうかと思います。
サンプルプログラム
今回のテクスチャーの描画までを行ったプロジェクトをアップしています。
必要に応じて参考にしてください。