【3Dゲーム開発#08】3Dから2Dに落とす「ビュー変換・透視投影変換」

3Dゲーム開発

いよいよ(やっと)今回から3D描画のほうにはいっていきます。

3D描画では奥行き(z)要素が加わりますが、人間の視覚と同様の描画をするには、単純に奥行きをいれればいいというだけのことではありません。

また、3Dゲームではカメラ操作ができるのが一般的です。

まずは、それら踏まえた3D描画の基礎「ビュー変換・透視投影変換」を見ていきましょう。

(コードは要点を抜粋して記載しております。最後にすべてのコードが掲載されていますので詳細はそちらを確認ください)

前回の記事:

【3Dゲーム開発#07】BlendStateを設定して半透明(アルファブレンド)処理
前回、頂点カラーを設定してポリゴンに色をつけられるようになりました。その際、ColorはRGBAの値で設定しましたが、A(=Alpha)値については1.0(不透明)固定のままいじりませんでした。というのも、Alpha値に関しては、単純にシェ...

3Dゲーム開発記事のまとめ:「3Dゲーム開発」

3D描画に必要なこと

透視投影変換(射影変換)

3Dになって大きく変わるのが、「遠近感を出す計算」が入ることです。

これを透視投影(Perspective Projection)といいます。

人間の目で見たとき、遠くのものは小さく、近くのものは大きく見えます。

この遠近感を擬似的に計算で再現して描画するのが「透視投影変換」です。

perspective_minecraft

参考:Minecraftの画像。近いキャラや岩は大きい、遠いキャラや岩は小さい。

↑マイクラの画像のように、3Dゲームでは人間の視覚と同様の遠近感が当然のように表現されていますが、これは「透視投影変換」がされた結果となります。

視点の制御とビュー変換

2Dゲームではスクリーン内の座標系で、オブジェクトをどう動かすか、ということに閉じてゲームを作ることも多いですが、3Dゲームとなるとなかなかそうはいきません。

通常、「どこからどこを見ているか」という視点(カメラ)の制御を導入します。

今どこに視点があって、どこを見ている(注視点)から、そこの世界が描画される、という、かなり現実に近い考え方でゲーム画面描画を行っていきます。

この視点から見た世界を、スクリーンに合わせて描画するためにも、座標変換が必要になります。

例えば、真後ろをみているなら世界をY軸で180度回してあげれば、真後ろの世界が見えるようになる、というようなことですね。

こういった、視点に合わせた部分をスクリーンに映すように座標変換してあげることを「ビュー変換」といいます。

実装

透視投影変換の実装

遠近感を出すための「透視投影変換」を行うためには、クリップ距離、画角、といった設定が必要になります。

frustum

参考:viewports-and-clipping

これは、奥行きのあるものをスクリーン(2D)に落とし込むため、どこからどこまでの奥行きと、見える範囲角度を計算するか、を決める必要があるためです。

この計算の詳細については、↑の参考リンクなどにすると良いですが、実際の計算ではDirectXMathが提供する↓のAPIで計算するだけでOKです。

↓のAPIを使います。

XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH(
  [in] float FovAngleY,
  [in] float AspectRatio,
  [in] float NearZ,
  [in] float FarZ
) noexcept;

ということで、これら計算のために、Redererに以下の透視投影変換計算のローカルメソッドと、必要パラメータを追加します。

パラメータは、現状では setupProjectionTransform でのみ使うので固定値でもかまいませんが、管理上のわかりやすさやゲームで変化する場合などもあるので、メンバに一応持たせておきます。

class Renderer
{
...
    bool setupProjectionTransform();
...
    float   nearClipDist_ = 0.f;
    float   farClipDist_ = 0.f;
    float   fov_ = 0.f;
};
// 透視投影視錐台 のパラメータ設定
Renderer::Renderer()
    : nearClipDist_(0.1f)
    , farClipDist_(1000.f)
    , fov_(XMConvertToRadians(30.f))
{
}

bool Renderer::Initialize(HWND hWindow)
{
    ...
    setupProjectionTransform();
    ...
}

/*
 *    透視投影行列を設定
 */
bool Renderer::setupProjectionTransform()
{
    XMMATRIX mat = XMMatrixPerspectiveFovLH(
        fov_,
        static_cast<float>(screenWidth_) / static_cast<float>(screenHeight_),   // アスペクト比
        nearClipDist_,
        farClipDist_);
    mat = XMMatrixTranspose(mat);

    auto& cb = GetRenderParam().CbProjectionSet;
    XMStoreFloat4x4(&cb.Data.Projection, mat);

    D3D11_MAPPED_SUBRESOURCE mappedResource;
    auto pDeviceContext = GetDeviceContext();
    // CBufferにひもづくハードウェアリソースマップ取得(ロックして取得)
    HRESULT hr = pDeviceContext->Map(
        cb.pBuffer,
        0,
        D3D11_MAP_WRITE_DISCARD,
        0,
        &mappedResource);
    if (FAILED(hr)) {
        return false;
    }
    CopyMemory(mappedResource.pData, &cb.Data, sizeof(cb.Data));
    // マップ解除
    pDeviceContext->Unmap(cb.pBuffer, 0);

    // VSにProjectionMatrixをセット(ここで1度セットして以後不変)
    pDeviceContext->VSSetConstantBuffers(2, 1, &cb.pBuffer);

    return true;
}

透視投影変換はシェーダーで処理するので、2D変換でもやったようにコンスタントバッファを用意して↑のsetupProjectionTransform内でセットしています。

struct RenderParam
{
    ...
    CbProjectionSet CbProjectionSet;
    ...
}

bool RenderParam::initConstantBuffer(Renderer& renderer)
{
    ...
    // View変換用定数バッファの作成
    cBufferDesc.ByteWidth = sizeof(CbView); // バッファ・サイズ
    hr = renderer.GetDevice()->CreateBuffer(&cBufferDesc, nullptr, &CbViewSet.pBuffer);
    if (FAILED(hr)) {
        // DXTRACE_ERR(L"InitDirect3D g_pD3DDevice->CreateBuffer", hr);
        return false;
    }
    ...
}
struct CbProjection
{
    XMFLOAT4X4  Projection;
};

struct CbProjectionSet
{
    CbProjection    Data;
    ID3D11Buffer*   pBuffer = nullptr;
};

ビュー変換の実装(Cameraの実装)

ビュー変換で必要な「視点・注視点」はいわゆる「カメラ」の処理そのものです。

ということでCameraクラスを追加して、視点・注視点(どこからどこを見るか)を管理します。

そのため、Cameraクラスには、視点(eyePos)、注視点(focusPos)があり、さらにカメラの上向きはどっちかというベクトル(up)をもたせます。

upは、上下逆さまにしたいとか、そうゆうときに使うものなので、通常のゲーム制作では(0,1,0)のまま固定で変わることはありません。

逆に、eyePos、focusPosは、3Dゲームで一般的な、カメラ操作があれば、常時変わり続けるようなパラメータ、ということになります。

それらから求まるビュー変換行列は、実際にはDirectXMathのAPI↓を使うだけでOKです。

XMMATRIX XM_CALLCONV XMMatrixLookAtLH(
  [in] FXMVECTOR EyePosition,
  [in] FXMVECTOR FocusPosition,
  [in] FXMVECTOR UpDirection
) noexcept;

XMMatrixLookAtLH function (directxmath.h)

最終的にCameraの実装は以下のようになっています。

Updateで、最後のサンプル画像用の視点のy座標移動をしています。

class Camera
{
public:
    Camera();
    ~Camera();

    void Update();

    XMMATRIX GetViewMatrix() const;

private:
    XMFLOAT3    eyePos_ = { 0, 0, 0};
    XMFLOAT3    focutPos_ = { 0, 0, 0 };
    XMFLOAT3    up_ = { 0, 1.f, 0 };
};
Camera::Camera()
    : eyePos_(0, 0, -5)
    , focutPos_(0, 0, 0)
{
}

Camera::~Camera()
{

}

void Camera::Update()
{
    // 0.03は適当なので環境に合わせて変えてOK
    eyePos_.y += 0.03f;
    if (eyePos_.y > 10.f) {
        eyePos_.y = 0.f;
    }
}

/*
 *    View変換行列取得
 */
DirectX::XMMATRIX Camera::GetViewMatrix() const
{
    // ビュー変換行列を求める
    XMMATRIX viewMat = XMMatrixLookAtLH(
        XMLoadFloat3(&eyePos_), XMLoadFloat3(&focutPos_), XMLoadFloat3(&up_));
    return viewMat;
}

 

Cameraから取得したViewMatrixは、ワールド変換・透視投影変換と同様にシェーダーで計算に使うので、ConstantBufferの用意をして、Rendererでコンスタントバッファにセットできるようにします。

/*
 *    View変換用コンスタントバッファ
 */
struct CbView
{
    XMFLOAT4X4  View;
};

struct CbViewSet
{
    CbView  Data;
    ID3D11Buffer*   pBuffer = nullptr;
};
bool Renderer::SetupViewTransform(const XMMATRIX& viewMat)
{
    auto& cb = GetRenderParam().CbViewSet;
    XMStoreFloat4x4(&cb.Data.View, XMMatrixTranspose(viewMat));

    D3D11_MAPPED_SUBRESOURCE mappedResource;
    auto pDeviceContext = GetDeviceContext();
    // CBufferにひもづくハードウェアリソースマップ取得(ロックして取得)
    HRESULT hr = pDeviceContext->Map(
        cb.pBuffer,
        0,
        D3D11_MAP_WRITE_DISCARD,
        0,
        &mappedResource);
    if (FAILED(hr)) {
        //DXTRACE_ERR(L"DrawSceneGraph failed", hr);
        return false;
    }
    CopyMemory(mappedResource.pData, &cb.Data, sizeof(cb.Data));
    // マップ解除
    pDeviceContext->Unmap(cb.pBuffer, 0);

    // VSにViewMatrixをセット
    pDeviceContext->VSSetConstantBuffers(1, 1, &cb.pBuffer);

    return true;
}

描画物の管理「SceneManager」追加

今回から、視点を決める「カメラ(Camera)」を追加します。

それを踏まえ、「なにをどう表示するか」という要素(シーンの管理)が増えてきたので、今回からSceneManagerを作って、そちらで

  • 描画物(Triangle)
  • カメラ

を管理するようにしましょう。

SceneManagerでは、カメラやオブジェクトの更新(Update)、その描画(Draw)を行うのでそれぞれメソッドを実装しておきます。

ざっと実装して今回は以下のようなコードとなります。

class SceneManager
{
public:
    SceneManager();
    ~SceneManager();

    void Initialize(Renderer& renderer);
    void Terminate();

    void Update();
    void Draw();
private:
    Renderer* pRenderer_ = nullptr;
    Camera  camera_;
    Triangle    sampleTriangle_;

};
SceneManager::SceneManager()
{
}

SceneManager::~SceneManager()
{
}

void SceneManager::Initialize(Renderer& renderer)
{
    pRenderer_ = &renderer;

    sampleTriangle_.CreateVertexBuffer(renderer);
}

void SceneManager::Terminate()
{
    sampleTriangle_.DestroyVertexBuffer();
}

void SceneManager::Update()
{
    camera_.Update();
}

void SceneManager::Draw()
{
    auto viewMatrix = camera_.GetViewMatrix();
    pRenderer_->SetupViewTransform(viewMatrix);

    sampleTriangle_.Draw(*pRenderer_);
}

SceneManagerの呼び出し組み込み

SceneManagerはApplicationにもたせて、Applicationからそれぞれの関数を呼び出します。

また、カメラなどUpdate処理も入ってきたので、あまりに高速にgameLoopが回りすぎても、無駄な負荷(画面描画より早い更新)となりますので、今回からgameLoopの最後にSleepを入れておきます。

Sleep(10) で10msecのスリープです、現在の処理ならこれでも60FPSに十分なループ処理速度となるでしょう。

class Application
{
    ...
    SceneManager    sceneManager_;
    ...
};
void Application::Initialize(HINSTANCE hInst)
{
    ...
    sceneManager_.Initialize(renderer_);
}

void Application::Terminate()
{
    sceneManager_.Terminate();
    ...
}

bool Application::gameLoop()
{
    ...
    sceneManager_.Update();
    ...
    sceneManager_.Draw();
    ...

    Sleep(10);
    return true;
}

シェーダーの計算の変更

ここまで用意した「透視投影変換」「ビュー変換」をシェーダーで実際に計算で使うようにします。

それぞれコンスタントバッファにセットしてあり、以下のような計算にすればよいです。

cbuffer cbTransform : register(b0) { 
    matrix Transform;
};

// Slot1 View変換
cbuffer cbView : register(b1) {
    matrix View;
};

// Slot2 投影変換
cbuffer cbProjection : register(b2) {
    matrix Projection;
};

struct VS_INPUT {
    float3 Pos : POSITION;   // 頂点座標(モデル座標系)
    float4 Col : COLOR;      // 頂点色
};

struct VS_OUTPUT {
    float4 Pos : SV_POSITION;
    float4 Col : COLOR;
};

VS_OUTPUT main(VS_INPUT input)
{
    VS_OUTPUT output;

    float4 pos = float4(input.Pos, 1.0);
    pos = mul(pos, Transform);
    pos = mul(pos, View);
    output.Pos = mul(pos, Projection);

    output.Col = input.Col;

    return output;
}

結果

視点(Camera座標)を上に移動させて、三角形を描画したのが以下となります。

視点が三角形から遠くなり三角形が小さくなり、視点が上がることで見下ろす形となっていることがわかります。

perspective_projection

ソースコード

以下が今回の実装があるコードです。

https://github.com/prog-log/GameDev/blob/master/05-3DTransform

コメント

タイトルとURLをコピーしました