前回
IMGUI, インプット
IMGUI
IMGUIは、UIを素早く簡単に実装できるオープンソースライブラリです。
DirectXだけでなく、OpenGLやVulkanなどの他のグラフィックスAPIでも簡単に使用できます。特徴として、毎フレームUIが一時的に作成され、それが画面に描画されます。
これを利用すれば、デバッグ情報を表示するためのUIやパラメータ調整のためのUIを簡単に作成することができます。
IMGUIをプロジェクトに組み込む
https://github.com/ocornut/imgui/tree/master/backends
上記のIMGUIのGitHubページからコードをZIPでダウンロードして解凍してください。
そして、プロジェクトフォルダにImguiというフォルダを作成してください。
このフォルダにIMGUIのソースコードフォルダにあるソースコードファイルをコピーしてください。
cppファイルとhファイルだけをコピーすれば大丈夫です。
そして、backendsフォルダからimgui_impl_dx12.cpp、imgui_impl_dx12.h、imgui_impl_win32.cpp、imgui_impl_win32.hファイルもコピーしてください。
プロジェクトを開いて、ソリューションエクスプローラーで追加したImguiフォルダをプロジェクトに追加してください。
これでプロジェクトでImguiを使用できるようになりました。
IMGUIを使ってみよう
#include "Imgui/imgui.h"
#include "Imgui/imgui_impl_dx12.h"
#include "Imgui/imgui_impl_win32.h"
DirectX12でIMGUIを使用するには、上記のようなヘッダーをインクルードする必要があります。
実際の使用方法は難しくありません。
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
//ここにIMGUIコード作成
ImGui::Render();
dx->CommandList()->SetDescriptorHeaps(1, dx->GetHeapForImgui().GetAddressOf());
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), dx->CommandList().Get());
上記のコードでdxはDx12Wrapperのポインタです。
上記のようなコードをバックバッファのPresent処理を行う前に実行してください。
IMGUIに関する処理を別のクラスとして作成して管理するようにしました。
IMGUIコードをどのように管理するかについては、直接構成してみるのが良いでしょう。
ここでは簡単な例を一つだけ紹介します。
ImGui::Begin("Transform");
DirectX::XMFLOAT3 position = transform.GetPosition();
DirectX::XMFLOAT3 rotation = transform.GetRotation();
DirectX::XMFLOAT3 scale = transform.GetScale();
float positionArray[] = { position.x, position.y, position.z };
float rotationArray[] = { rotation.x, rotation.y, rotation.z };
float scaleArray[] = { scale.x, scale.y, scale.z };
if (ImGui::DragFloat3("Position ## Inspector", positionArray, 0.01f))
{
transform.SetPosition(positionArray[0], positionArray[1], positionArray[2]);
}
if (ImGui::DragFloat3("Rotation ## Inspector", rotationArray, 0.01f))
{
transform.SetRotation(rotationArray[0], rotationArray[1], rotationArray[2]);
}
if (ImGui::DragFloat3("Scale ## Inspector", scaleArray, 0.01f))
{
transform.SetScale(scaleArray[0], scaleArray[1], scaleArray[2]);
}
ImGui::End();
これは前回追加したTransformクラスのオブジェクトを直接調整できるウィンドウを作成するコードです。
これ以外にも、多くの方法で望むUIを作成することができます。
以前に追加した内容を直接ランタイムでパラメータを変更できるように修正したり、今後追加する内容もIMGUIに連携させてパラメータを調整できるようにすることができるでしょう。
インプット
今回は、キーボードやマウスの入力を処理できるようにしてみましょう。
これまでは、カメラが固定された視点にありました。
だから、カメラの位置を変更するには、直接コードで位置を変更する必要がありました。
そこで今回は、キーボードとマウスを直接使用してカメラを移動させたり回転させたりできるようにします。
キーボードやマウスの入力を処理するために、DirectInputを使用します。
DirectInputはDirect X APIの一部で、主に高速で低レベルの入力処理に使用されます。
しかし、調べてみるとDirectInputのバージョンアップデートは長い間行われておらず、最近ではXInputが使用されているようです。
私は以前DirectInputを使用した経験があるので、今回もこれを使用しました。
もし、より最新のAPIを使用したいと思われる場合は、XInputを調べて使用するのが良いでしょう。
私はInputというクラスで入力に関連する内容を管理するようにしました。
シングルトンとして作成し、どこからでもアクセス可能にしました。
#define DIRECTINPUT_VERSION 0x0800
#pragma comment(lib,"dinput8.lib")
#pragma comment(lib,"dxguid.lib")
#include <dinput.h>
DirectInputを使用するためにライブラリをリンクし、ヘッダーをインクルードします。
DIRECTINPUT_VERSIONはDirectInputの8.0バージョンを設定するためのマクロです。
8.0が最後のバージョンです。
class Input
{
public:
static Input* Instance();
bool Initialize(HINSTANCE hInstance, HWND hwnd);
bool Update();
void SetWMMouse(int x, int y);
bool GetMouseDown(int index);
bool GetMousePressed(int index);
float GetMouseX() const;
float GetMouseY() const;
float GetMousePositionX() const;
float GetMousePositionY() const;
bool GetKeyPressed(int index);
private:
Input();
~Input();
bool UpdateMouse();
bool UpdateKeyBoard();
private:
static Input* mInstance;
IDirectInput8* mDirectInput = nullptr;
IDirectInputDevice8* mKeyBoard = nullptr;
IDirectInputDevice8* mMouse = nullptr;
BYTE mMouseButtonGetKeyDownFlag[4] = { 0, };
unsigned char mKeyBoardState[256];
DIMOUSESTATE mMouseState;
int mWMMouseX;
int mWMMouseY;
};
Inputクラスの宣言部です。
詳細な内容はメソッドを見ながら説明します。
Input* Input::mInstance = nullptr;
Input* Input::Instance()
{
if (mInstance == nullptr)
{
mInstance = new Input();
}
return mInstance;
}
シングルトンパターンを実装したメソッドです。
bool Input::Initialize(HINSTANCE hInstance, HWND hwnd)
{
HRESULT result = DirectInput8Create(hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, reinterpret_cast<void**>(&mDirectInput), nullptr);
if (FAILED(result) == true)
{
return false;
}
result = mDirectInput->CreateDevice(GUID_SysKeyboard, &mKeyBoard, nullptr);
if (FAILED(result) == true)
{
return false;
}
result = mKeyBoard->SetDataFormat(&c_dfDIKeyboard);
if (FAILED(result) == true)
{
return false;
}
result = mKeyBoard->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
if (FAILED(result) == true)
{
return false;
}
result = mKeyBoard->Acquire();
if (FAILED(result) == true)
{
return false;
}
result = mDirectInput->CreateDevice(GUID_SysMouse, &mMouse, nullptr);
if (FAILED(result) == true)
{
return false;
}
result = mMouse->SetDataFormat(&c_dfDIMouse);
if (FAILED(result) == true)
{
return false;
}
result = mMouse->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
if (FAILED(result) == true)
{
return false;
}
result = mMouse->Acquire();
if (FAILED(result) == true)
{
return false;
}
return true;
}
DirectInputインターフェースおよびキーボードとマウスデバイスを初期化するメソッドです。
HRESULT result = DirectInput8Create(hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, reinterpret_cast<void**>(&mDirectInput), nullptr);
if (FAILED(result) == true)
{
return false;
}
DirectInputのインターフェースを作成します。
DIRECTINPUT_VERSIONを渡してバージョンを指定します。
作成に成功すると、mDirectInputにインターフェースが格納されます。
result = mDirectInput->CreateDevice(GUID_SysKeyboard, &mKeyBoard, nullptr);
if (FAILED(result) == true)
{
return false;
}
result = mKeyBoard->SetDataFormat(&c_dfDIKeyboard);
if (FAILED(result) == true)
{
return false;
}
result = mKeyBoard->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
if (FAILED(result) == true)
{
return false;
}
result = mKeyBoard->Acquire();
if (FAILED(result) == true)
{
return false;
}
キーボードデバイスを初期化します。
CreateDeviceでキーボードデバイスを作成します。作成に成功すると、mKeyBoardにキーボードのインターフェースが保存されます。
SetDataFormatを通じて、キーボードの入力をDirectInputが理解できるように設定します。
SetCooperativeLevelでデバイスの協調レベルを設定します。
DISCL_FOREGROUNDは、このプログラムのウィンドウがアクティブな状態でのみ入力が処理されるようにし、
DISCL_NONEXCLUSIVEは、このプログラムがキーボードを独占しないようにします。
最後に、Acquireでデバイスを有効化して入力を受け付けられるようにします。
result = mDirectInput->CreateDevice(GUID_SysMouse, &mMouse, nullptr);
if (FAILED(result) == true)
{
return false;
}
result = mMouse->SetDataFormat(&c_dfDIMouse);
if (FAILED(result) == true)
{
return false;
}
result = mMouse->SetCooperativeLevel(hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
if (FAILED(result) == true)
{
return false;
}
result = mMouse->Acquire();
if (FAILED(result) == true)
{
return false;
}
マウスデバイスを初期化します。
mMouseにマウスデバイスのインターフェースが保存されるだけで、他の内容はキーボードと似ています。
キーボードとマウスデバイスの初期化が完了したので、インターフェースを通じて毎フレームデバイスの入力状態を更新しましょう。
bool Input::Update()
{
bool result = UpdateMouse();
if (result == false)
{
return result;
}
result = UpdateKeyBoard();
if (result == false)
{
return result;
}
return result;
}
このメソッドを毎フレーム呼び出して入力状態を更新します。
まずはマウスの方から見ていきましょう。
bool Input::UpdateMouse()
{
HRESULT result = mMouse->GetDeviceState(sizeof(DIMOUSESTATE), (LPVOID)&mMouseState);
if (FAILED(result) == true)
{
if (result == DIERR_INPUTLOST || result == DIERR_NOTACQUIRED)
{
mMouse->Acquire();
}
else
{
return false;
}
}
for (int i = 0; i < 4; i++)
{
if ((mMouseState.rgbButtons[i] & 0x80) == false)
{
mMouseButtonGetKeyDownFlag[i] = 0;
}
}
return true;
}
mMouseを通してマウスの入力状態を取得し、mMouseStateに保存しています。
もしデバイスが無効になっているか接続されていない場合は、再度アクティブ化を試みます。
mMouseButtonGetKeyDownFlagはマウスボタンのプッシュダウンイベントを記録するためのものです。
マウスボタンが押されると、mMouseState.rgbButtonsの値の最上位ビットが1に設定されます。
そのため、0x80と比較して押されたかどうかを確認できます。
bool Input::UpdateKeyBoard()
{
HRESULT result = mKeyBoard->GetDeviceState(sizeof(mKeyBoardState), (LPVOID)&mKeyBoardState);
if (FAILED(result) == true)
{
if (result == DIERR_INPUTLOST || result == DIERR_NOTACQUIRED)
{
mKeyBoard->Acquire();
}
else
{
return false;
}
}
return true;
}
mKeyBoardを通してキーボードの入力状態をmKeyBoardStateに保存しています。
mKeyBoardStateはキーボードキーのインデックスで知りたいキーの入力状態を知ることができます。
これから入力状態に関する情報を取得するメソッドを見ていきましょう。
bool Input::GetMouseDown(int index)
{
if (index < 0 || index >= 4)
{
return false;
}
if (mMouseButtonGetKeyDownFlag[index] == 1)
{
return false;
}
if (mMouseState.rgbButtons[index] & 0x80)
{
mMouseButtonGetKeyDownFlag[index] = 1;
return true;
}
return false;
}
マウスボタンは0番から3番まであります。
0番は左クリック
1番は右クリック
2番はホイールクリック
3番は追加ボタンです。
GetMouseDownはマウスボタンのプッシュダウンイベントが発生したかどうかを返します。
mMouseButtonGetKeyDownFlagの値が1の場合、すでにボタンが押されていたということなのでfalseを返します。
そうでない場合、mMouseState.rgbButtonsと0x80を比較して最上位ビットが1であればmMouseButtonGetKeyDownFlagを1に設定し、trueを返します。
bool Input::GetMousePressed(int index)
{
if (index < 0 || index >= 4)
{
return false;
}
if (mMouseState.rgbButtons[index] & 0x80)
{
return true;
}
return false;
}
このメソッドは、マウスボタンが現在押されているかどうかを返します。
float Input::GetMouseX() const
{
return static_cast<float>(mMouseState.lX);
}
float Input::GetMouseY() const
{
return static_cast<float>(mMouseState.lY);
}
mMouseStateのlx、ly値は、マウスが前の位置から移動したピクセル数を表します。
FPSゲームの視点回転などを実装するのに適しています。
bool Input::GetKeyPressed(int index)
{
if (index < 0 || index >= 256)
{
return false;
}
if (mKeyBoardState[index] & 0x80)
{
return true;
}
return false;
}
キーボードのキーが押されているかどうかもmKeyBoardStateの値を比較して判断します。
キーのインデックスもマクロで準備されています。
WASDキーはそれぞれDIK_W、DIK_A、DIK_S、DIK_Dと定義されています。
さらに、マウスの移動量は取得できますが、プログラムウィンドウ内のカーソル位置は取得できません。
そのため、ウィンドウメッセージを通じてInputオブジェクトに設定するメソッドを追加しました。
void Input::SetWMMouse(int x, int y)
{
mWMMouseX = x;
mWMMouseY = y;
}
float Input::GetMousePositionX() const
{
return mWMMouseX;
}
float Input::GetMousePositionY() const
{
return mWMMouseY;
}
LRESULT WindowProcedure(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
if (msg == WM_DESTROY)
{
PostQuitMessage(0);
return 0;
}
if (msg == WM_MOUSEMOVE)
{
Input::Instance()->SetWMMouse(static_cast<int>(LOWORD(lparam)), static_cast<int>(HIWORD(lparam)));
}
ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam);
return DefWindowProc(hwnd, msg, wparam, lparam);
}
ウィンドウプロシージャでWM_MOUSEMOVEメッセージが検出されたときにSetWMMouseを呼び出し、マウスの位置値を保存するようにします。
Input::~Input()
{
if (mMouse != nullptr)
{
mMouse->Unacquire();
mMouse->Release();
mMouse = nullptr;
}
if (mKeyBoard != nullptr)
{
mKeyBoard->Unacquire();
mKeyBoard->Release();
mKeyBoard = nullptr;
}
if (mDirectInput != nullptr)
{
mDirectInput->Release();
mDirectInput = nullptr;
}
}
プログラムが終了する際にデバイスに関連するオブジェクトを解放するためのデストラクタを作成します。
Inputクラスの準備ができたので、これを使ってカメラを動かしてみましょう。
if (Input::Instance()->GetMousePressed(1) == true)
{
float deltaFloat = Time::GetDeltaTime();
float mouseX = Input::Instance()->GetMouseX();
float mouseY = Input::Instance()->GetMouseY();
float senstive = 0.025f;
DirectX::XMFLOAT3 rotation = mCameraTransform->GetRotation();
rotation.y = rotation.y + (senstive * deltaFloat * mouseX);
rotation.x = rotation.x + (senstive * deltaFloat * mouseY);
mCameraTransform->SetRotation(rotation.x, rotation.y, rotation.z);
bool inputWASD = false;
DirectX::XMVECTOR cameraForward = XMLoadFloat3(&mCameraTransform->GetForward());
DirectX::XMVECTOR cameraRight = XMLoadFloat3(&mCameraTransform->GetRight());
DirectX::XMVECTOR moveVector = XMVectorZero();
if (Input::Instance()->GetKeyPressed(DIK_W) == true)
{
moveVector += cameraForward;
inputWASD = true;
}
if (Input::Instance()->GetKeyPressed(DIK_S) == true)
{
moveVector -= cameraForward;
inputWASD = true;
}
if (Input::Instance()->GetKeyPressed(DIK_A) == true)
{
moveVector -= cameraRight;
inputWASD = true;
}
if (Input::Instance()->GetKeyPressed(DIK_D) == true)
{
moveVector += cameraRight;
inputWASD = true;
}
if (inputWASD == true)
{
moveVector = XMVector3Normalize(moveVector);
mCameraTransform->AddTranslation(moveVector.m128_f32[0], moveVector.m128_f32[1], moveVector.m128_f32[2]);
}
}
マウスの右クリックを押している間だけカメラ操作が可能になるようにします。
マウスの移動値を取得してカメラを回転させ、WASDキーでカメラが移動するように処理しました。
まとめ
今回はIMGUIと入力に関する機能を追加しました。
IMGUIを追加したことで、今後追加される機能のパラメータなどをランタイムで調整できるようになります。
例えば、マテリアルを追加してマテリアルの色などをリアルタイムで変更する機能も作れるでしょう。
私は自分なりに必要な機能を作って使用しています。
今後追加される新機能とIMGUIの連携については触れませんので、自分で作ってみてください。
入力機能については、さらにオブジェクトピッキング機能を追加することもできそうです。
次の記事では、FBXモデルファイルをロードして描画できるようにします。
前回