次の方法で共有


DirectX ゲームでのリソースの読み込み

ほとんどのゲームは、ある時点で、ローカル記憶域やその他のデータ ストリームからリソースとアセット (シェーダー、テクスチャ、定義済みのメッシュ、その他のグラフィックス データなど) を読み込みます。 ここでは、DirectX C/C++ ユニバーサル Windows プラットフォーム (UWP) ゲームで使用するためにこれらのファイルを読み込む際に考慮すべき事項の概要を説明します。

たとえば、ゲーム内のポリゴン オブジェクトのメッシュが別のツールで作成され、特定の形式にエクスポートされている可能性があります。 テクスチャについても同様です。その他についても同様で、フラットで圧縮されていないビットマップは、ほとんどのツールが同じように書き込むことが可能で、ほとんどのグラフィックス API で認識されますが、ゲームで使用する場合は非常に非効率的な場合があります。 ここでは、Direct3D で使用する 3 種類のグラフィック リソース (メッシュ (モデル)、テクスチャ (ビットマップ)、コンパイル済みシェーダー オブジェクト) を読み込むための基本的な手順について説明します。

知っておくべきこと

テクノロジ

  • 並列パターン ライブラリ (ppltasks.h)

前提条件

  • 基本的な Windows ランタイムを理解している
  • 非同期タスクを理解している
  • 3-D グラフィックス プログラミングの基本的な概念について理解している。

このサンプルには、リソースの読み込みと管理のための 3 つのコード ファイルも含まれています。 このトピックでは、これらのファイルで定義されているコード オブジェクトについて説明します。

  • BasicLoader.h/.cpp
  • BasicReaderWriter.h/.cpp
  • DDSTextureLoader.h/.cpp

これらのサンプルのコード一式は、次のリンク先にあります。

トピック 説明

BasicLoader のコード一式

グラフィックス メッシュ オブジェクトを変換してメモリに読み込むクラスとメソッドのコード一式。

BasicReaderWriter のコード一式

バイナリ データ ファイル全般の読み書きを行うクラスとメソッドのコード一式です。 BasicLoader クラスで使われます。

DDSTextureLoader のコード一式

メモリから DDS テクスチャを読み込むクラスとメソッドのコード一式です。

 

手順

非同期読み込み

非同期読み込みは、並列パターン ライブラリ (PPL) のタスク テンプレートを使用して処理されます。 タスクには、メソッド呼び出しと、その後で非同期呼び出しの完了後に結果を処理する LAMBDA が含まれており、通常は次の形式に従います。

task<generic return type>(async code to execute).then((parameters for lambda){ lambda code contents });

.then() 構文を使用してタスクを連結できるため、1 つの操作が完了すると、前の操作の結果に依存する別の非同期操作を実行できます。 こうすると、プレイヤーにほとんど見えない状態で、個別のスレッドで複雑なアセットを読み込み、変換、管理できます。

詳細については、「C++ での非同期プログラミング」を参照してください。

次に、非同期ファイル読み込みメソッド ReadDataAsync を宣言して作成するための基本的な構造を見てみましょう。

#include <ppltasks.h>

// ...
concurrency::task<Platform::Array<byte>^> ReadDataAsync(
        _In_ Platform::String^ filename);

// ...

using concurrency;

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
    )
{
    return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
    {
        return FileIO::ReadBufferAsync(file);
    }).then([=](IBuffer^ buffer)
    {
        auto fileData = ref new Platform::Array<byte>(buffer->Length);
        DataReader::FromBuffer(buffer)->ReadBytes(fileData);
        return fileData;
    });
}

このコードでは、コードが上記で定義した ReadDataAsync メソッドを呼び出すと、ファイル システムからバッファーを読み取るタスクが作成されます。 完了すると、連結されたタスクはバッファーを受け取り、静的な DataReader 型を使用してそのバッファーから配列にバイトをストリーミングします。

m_basicReaderWriter = ref new BasicReaderWriter();

// ...
return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
      // Perform some operation with the data when the async load completes.          
    });

ReadDataAsync の呼び出しを次に示します。 完了すると、コードは指定されたファイルから読み取られたバイト配列を受け取ります。 ReadDataAsync 自体はタスクとして定義されているため、LAMBDA を使用して、バイト配列が返されるときに特定の操作を実行できます。たとえば、そのバイト データを使用できる DirectX 関数に渡すなどです。

ゲームが十分にシンプルな場合は、ユーザーがゲームを開始するときに、このような方法でリソースを読み込みます。 これは、IFrameworkView::Run 実装の呼び出しシーケンスのある時点からメイン ゲーム ループを開始する前に行うことができます。 ここでも、リソースの読み込みメソッドを非同期的に呼び出して、ゲームをより迅速に開始できるようにします。そのため、プレイヤーは読み込みが完了するまで待たずに初期操作を開始できます。

ただし、すべての非同期読み込みが完了するまで、実際のゲームを開始する必要はありません。 特定のフィールドなど、読み込みが完了したときにシグナルを出すメソッドをいくつか作成し、読み込みメソッドで LAMBDA を使用して、完了時にそのシグナルをセットするようにします。 読み込まれたリソースを使用するコンポーネントを開始する前に、変数をチェックします。

ゲームの起動時に、BasicLoader.cpp で定義されている非同期メソッドを使用してシェーダー、メッシュ、テクスチャを読み込む例を次に示します。 すべての読み込みメソッドが完了したときに、ゲーム オブジェクト m_loadingComplete に特定のフィールドが設定されている点に注目してください。

void ResourceLoading::CreateDeviceResources()
{
    // DirectXBase is a common sample class that implements a basic view provider. 
    
    DirectXBase::CreateDeviceResources(); 

    // ...

    // This flag will keep track of whether or not all application
    // resources have been loaded.  Until all resources are loaded,
    // only the sample overlay will be drawn on the screen.
    m_loadingComplete = false;

    // Create a BasicLoader, and use it to asynchronously load all
    // application resources.  When an output value becomes non-null,
    // this indicates that the asynchronous operation has completed.
    BasicLoader^ loader = ref new BasicLoader(m_d3dDevice.Get());

    auto loadVertexShaderTask = loader->LoadShaderAsync(
        "SimpleVertexShader.cso",
        nullptr,
        0,
        &m_vertexShader,
        &m_inputLayout
        );

    auto loadPixelShaderTask = loader->LoadShaderAsync(
        "SimplePixelShader.cso",
        &m_pixelShader
        );

    auto loadTextureTask = loader->LoadTextureAsync(
        "reftexture.dds",
        nullptr,
        &m_textureSRV
        );

    auto loadMeshTask = loader->LoadMeshAsync(
        "refmesh.vbo",
        &m_vertexBuffer,
        &m_indexBuffer,
        nullptr,
        &m_indexCount
        );

    // The && operator can be used to create a single task that represents
    // a group of multiple tasks. The new task's completed handler will only
    // be called once all associated tasks have completed. In this case, the
    // new task represents a task to load various assets from the package.
    (loadVertexShaderTask && loadPixelShaderTask && loadTextureTask && loadMeshTask).then([=]()
    {
        m_loadingComplete = true;
    });

    // Create constant buffers and other graphics device-specific resources here.
}

すべてのタスクが完了したときにのみ、読み込み完了フラグをセットする LAMBDA がトリガーされるように、&& 演算子を使用してタスクが集約されている点に注意してください。 複数のフラグがある場合は、競合状態の可能性があることに注意してください。 たとえば、LAMBDA によって 2 つのフラグが同じ値に順番に設定される場合、別のスレッドは、2 番目のフラグが設定される前に両方のフラグを確認すると、最初のフラグの設定のみを認識する可能性があります。

リソース ファイルを非同期的に読み込む方法を説明しました。 同期ファイルの読み込みははるかに簡単で、その例は「BasicReaderWriter のコード一式」と「BasicLoader のコード一式」にあります。

もちろん、リソースやアセットの種類が異なると、グラフィックス パイプラインで使用する準備が整う前に、追加の処理や変換が必要になることがよくあります。 メッシュ、テクスチャ、シェーダーの 3 種類のリソースを見てみましょう。

メッシュの読み込み

メッシュは頂点データであり、ゲーム内のコードによって手続き的に生成されるか、別のアプリ (3DStudio MAX や Alias WaveFront など) またはツールからファイルにエクスポートされます。 これらのメッシュは、キューブや球などのシンプルなプリミティブから、車や家、キャラクターまで、ゲーム内のモデルを表します。 形式によっては、色とアニメーションのデータを含むこともよくあります。 ここでは、頂点データのみを含むメッシュに焦点を当てます。

メッシュを正しく読み込むには、メッシュのファイル内のデータの形式を知っている必要があります。 上記のシンプルな BasicReaderWriter 型は、単にバイト ストリームとしてデータを読み取ります。バイト データがメッシュを表しているかどうかは関知しません。ましてや、別のアプリケーションによってエクスポートされた特定のメッシュ形式であることはわかりません。 メッシュ データをメモリに取り込む際に変換を実行する必要があります。

(アセット データは必ず、可能な限り内部表現に近い形式でパッケージ化するようにします。これにより、リソースの使用率が低下し、時間が節約されます。)

メッシュのファイルからバイト データを取得してみましょう。 この例の形式は、ファイルが .vbo のサフィックスが付いたサンプル固有の形式であることを前提としています。 (この形式も OpenGL の VBO 形式とは異なります)。頂点自体はそれぞれ BasicVertex 型 (obj2vbo コンバーター ツールのコードで定義されている構造体) にマップされます。 .vbo ファイル内の頂点データのレイアウトは次のようになります。

  • データ ストリームの最初の 32 ビット (4 バイト) には、uint32 値で表されたメッシュ内の頂点数 (numVertices) が格納されています。
  • データ ストリームの次の 32 ビット (4 バイト) には、uint32 値で表されたメッシュ内のインデックス数 (numIndices) が格納されています。
  • それ以降の (numVertices * sizeof(BasicVertex)) ビットには頂点データが含まれています。
  • データの最後の (numIndices * 16) ビットには、一連の uint16 値として表されたインデックス データが含まれています。

読み込んだメッシュ データのビットレベルのレイアウトを知ることが重要です。 また、必ずエンディアンに関して整合を取ってください。 すべての Windows 8 プラットフォームはリトル エンディアンです。

この例では、LoadMeshAsync メソッドから CreateMesh というメソッドを呼び出して、このビット レベルの解釈を実行します。

task<void> BasicLoader::LoadMeshAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11Buffer** vertexBuffer,
    _Out_ ID3D11Buffer** indexBuffer,
    _Out_opt_ uint32* vertexCount,
    _Out_opt_ uint32* indexCount
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ meshData)
    {
        CreateMesh(
            meshData->Data,
            vertexBuffer,
            indexBuffer,
            vertexCount,
            indexCount,
            filename
            );
    });
}

CreateMesh は、ファイルから読み込まれたバイト データを解釈します。そして、頂点リストとインデックス リストをそれぞれ ID3D11Device::CreateBuffer に渡し、D3D11_BIND_VERTEX_BUFFER または D3D11_BIND_INDEX_BUFFER を指定することによって、メッシュの頂点バッファーとインデックス バッファーを作成します。 BasicLoader で使用されるコードを次に示します。

void BasicLoader::CreateMesh(
    _In_ byte* meshData,
    _Out_ ID3D11Buffer** vertexBuffer,
    _Out_ ID3D11Buffer** indexBuffer,
    _Out_opt_ uint32* vertexCount,
    _Out_opt_ uint32* indexCount,
    _In_opt_ Platform::String^ debugName
    )
{
    // The first 4 bytes of the BasicMesh format define the number of vertices in the mesh.
    uint32 numVertices = *reinterpret_cast<uint32*>(meshData);

    // The following 4 bytes define the number of indices in the mesh.
    uint32 numIndices = *reinterpret_cast<uint32*>(meshData + sizeof(uint32));

    // The next segment of the BasicMesh format contains the vertices of the mesh.
    BasicVertex* vertices = reinterpret_cast<BasicVertex*>(meshData + sizeof(uint32) * 2);

    // The last segment of the BasicMesh format contains the indices of the mesh.
    uint16* indices = reinterpret_cast<uint16*>(meshData + sizeof(uint32) * 2 + sizeof(BasicVertex) * numVertices);

    // Create the vertex and index buffers with the mesh data.

    D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
    vertexBufferData.pSysMem = vertices;
    vertexBufferData.SysMemPitch = 0;
    vertexBufferData.SysMemSlicePitch = 0;
    CD3D11_BUFFER_DESC vertexBufferDesc(numVertices * sizeof(BasicVertex), D3D11_BIND_VERTEX_BUFFER);

    m_d3dDevice->CreateBuffer(
            &vertexBufferDesc,
            &vertexBufferData,
            vertexBuffer
            );
    
    D3D11_SUBRESOURCE_DATA indexBufferData = {0};
    indexBufferData.pSysMem = indices;
    indexBufferData.SysMemPitch = 0;
    indexBufferData.SysMemSlicePitch = 0;
    CD3D11_BUFFER_DESC indexBufferDesc(numIndices * sizeof(uint16), D3D11_BIND_INDEX_BUFFER);
    
    m_d3dDevice->CreateBuffer(
            &indexBufferDesc,
            &indexBufferData,
            indexBuffer
            );
  
    if (vertexCount != nullptr)
    {
        *vertexCount = numVertices;
    }
    if (indexCount != nullptr)
    {
        *indexCount = numIndices;
    }
}

通常は、ゲームで使用するすべてのメッシュに対して頂点/インデックス バッファー ペアを作成します。 メッシュを読み込む場所とタイミングはユーザーが選択します。 多数のメッシュがある場合は、事前に定義された特定の読み込み状態中など、ゲーム内の特定のポイントでディスクから一部のみを読み込みたいことがあります。 地形データなどの大規模なメッシュの場合は、キャッシュから頂点をストリーミングできますが、手順が複雑で、このトピックの範囲外です。

ここでも、頂点データ形式を知る必要があります。 モデルの作成に使用されるツール全体では、頂点データを表す方法が多数あります。 また、頂点データの Direct3D への入力レイアウト (三角形リストやストリップなど) を表すさまざまな方法もあります。 頂点データの詳細については、「Direct3D 11 のバッファーの概要」と「プリミティブ」を参照してください。

次に、テクスチャの読み込みを見てみましょう。

テクスチャの読み込み

ゲーム内で最も一般的なアセットであり、ディスク上およびメモリ内のほとんどのファイルを構成するアセットはテクスチャです。 メッシュと同様に、テクスチャはさまざまな形式で提供される可能性があり、読み込むときに Direct3D で使用できる形式に変換します。 テクスチャもさまざまな型で提供され、さまざまな効果を作成するために使用されます。 テクスチャの MIP レベルを使用して、距離オブジェクトの外観とパフォーマンスを向上させることができます。ダートマップとライトマップは、基本テクスチャの上にエフェクトと詳細を重ねるために使用されます。また、法線マップは、ピクセル単位の照明計算で使用されます。 最新のゲームでは、一般的なシーンには何千もの個別のテクスチャが含まれる可能性があり、コードでそれらすべてを効果的に管理する必要があります。

また、メッシュと同様に、メモリ使用量を効率的にするために使用される特定の形式がいくつかあります。 テクスチャは GPU (およびシステム) メモリの大部分を簡単に消費してしまう可能性があるため、多くの場合、何らかの方法で圧縮されます。 ゲームのテクスチャに圧縮を使用することは必須ではありません。また、理解できる形式 (Texture2D ビットマップなど) で Direct3D シェーダーにデータを提供する限り、任意の圧縮/伸張アルゴリズムを使用できます。

Direct3D では DXT テクスチャ圧縮アルゴリズムがサポートされますが、プレイヤーのグラフィックス ハードウェアでサポートされていない DXT 形式がある可能性があります。 DDS ファイルには DXT テクスチャが (およびその他のテクスチャ圧縮形式も) 含まれており、サフィックスは .dds です。

DDS ファイルは次の情報が含まれるバイナリ ファイルです。

  • 4 文字コード値 'DDS ' (0x20534444) を内容とする DWORD (マジックナンバー)。

  • データ ソース内のデータの説明。

    データは、DDS_HEADER を使ってヘッダーの説明と一緒に説明されます。ピクセル形式は、DDS_PIXELFORMAT を使って定義されます。 DDS_HEADER 構造体と DDS_PIXELFORMAT 構造体は、推奨されなくなった DirectDraw 7 の DDSURFACEDESC2 構造体、DDSCAPS2 構造体、および DDPIXELFORMAT 構造体の代わりに使います。 DDS_HEADER は、DDSURFACEDESC2 と DDSCAPS2 の機能をバイナリで実現します。 DDS_PIXELFORMAT は、DDPIXELFORMAT の機能をバイナリで実現します。

    DWORD               dwMagic;
    DDS_HEADER          header;
    

    DDS_PIXELFORMATdwFlags の値を DDPF_FOURCC に設定し、dwFourCC を "DX10" に設定していると、DDS_HEADER_DXT10 構造体が追加で生成され、浮動小数点形式や sRGB 形式などの RGB ピクセル形式では表現できない DXGI 形式やテクスチャ配列がこの構造体に格納されます。この DDS_HEADER_DXT10 構造体が存在する場合、データ記述の全体は次のようになります。

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • メイン サーフェス データを格納するバイトの配列へのポインター。

    BYTE bdata[]
    
  • ミップマップ レベル、キューブ マップ内の面、ボリューム テクスチャの深度など、残りのサーフェスを格納するバイト配列へのポインター。 テクスチャキューブ マップ、またはボリューム テクスチャの DDS ファイル レイアウトの詳細については、それぞれのリンクを参照してください。

    BYTE bdata2[]
    

多くのツールが DDS 形式にエクスポートします。 テクスチャをこの形式にエクスポートするツールがない場合は、作成することを検討してください。 DDS 形式の詳細とコードでの使用方法については、「DDS のプログラミング ガイド」を参照してください。 この例では DDS を使用します。

その他のリソースの種類と同様に、ファイルからバイト ストリームとしてデータを読み取ります。 読み込みタスクが完了すると、LAMBDA 呼び出しによってコード (CreateTexture メソッド) が実行されて、Direct3D で使用できる形式にバイト ストリームが処理されます。

task<void> BasicLoader::LoadTextureAsync(
    _In_ Platform::String^ filename,
    _Out_opt_ ID3D11Texture2D** texture,
    _Out_opt_ ID3D11ShaderResourceView** textureView
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ textureData)
    {
        CreateTexture(
            GetExtension(filename) == "dds",
            textureData->Data,
            textureData->Length,
            texture,
            textureView,
            filename
            );
    });
}

前のスニペットでは、LAMBDA はファイル名の拡張子が "dds" かどうかをチェックします。 拡張子が "dds" であれば、DDS テクスチャであると想定します。 そうでない場合は、Windows Imaging Component (WIC) API を使用して形式を検出し、データをビットマップとしてデコードします。 どちらの場合でも、結果は Texture2D ビットマップ (またはエラー) になります。

void BasicLoader::CreateTexture(
    _In_ bool decodeAsDDS,
    _In_reads_bytes_(dataSize) byte* data,
    _In_ uint32 dataSize,
    _Out_opt_ ID3D11Texture2D** texture,
    _Out_opt_ ID3D11ShaderResourceView** textureView,
    _In_opt_ Platform::String^ debugName
    )
{
    ComPtr<ID3D11ShaderResourceView> shaderResourceView;
    ComPtr<ID3D11Texture2D> texture2D;

    if (decodeAsDDS)
    {
        ComPtr<ID3D11Resource> resource;

        if (textureView == nullptr)
        {
            CreateDDSTextureFromMemory(
                m_d3dDevice.Get(),
                data,
                dataSize,
                &resource,
                nullptr
                );
        }
        else
        {
            CreateDDSTextureFromMemory(
                m_d3dDevice.Get(),
                data,
                dataSize,
                &resource,
                &shaderResourceView
                );
        }

        resource.As(&texture2D);
    }
    else
    {
        if (m_wicFactory.Get() == nullptr)
        {
            // A WIC factory object is required in order to load texture
            // assets stored in non-DDS formats.  If BasicLoader was not
            // initialized with one, create one as needed.
            CoCreateInstance(
                    CLSID_WICImagingFactory,
                    nullptr,
                    CLSCTX_INPROC_SERVER,
                    IID_PPV_ARGS(&m_wicFactory));
        }

        ComPtr<IWICStream> stream;
        m_wicFactory->CreateStream(&stream);

        stream->InitializeFromMemory(
                data,
                dataSize);

        ComPtr<IWICBitmapDecoder> bitmapDecoder;
        m_wicFactory->CreateDecoderFromStream(
                stream.Get(),
                nullptr,
                WICDecodeMetadataCacheOnDemand,
                &bitmapDecoder);

        ComPtr<IWICBitmapFrameDecode> bitmapFrame;
        bitmapDecoder->GetFrame(0, &bitmapFrame);

        ComPtr<IWICFormatConverter> formatConverter;
        m_wicFactory->CreateFormatConverter(&formatConverter);

        formatConverter->Initialize(
                bitmapFrame.Get(),
                GUID_WICPixelFormat32bppPBGRA,
                WICBitmapDitherTypeNone,
                nullptr,
                0.0,
                WICBitmapPaletteTypeCustom);

        uint32 width;
        uint32 height;
        bitmapFrame->GetSize(&width, &height);

        std::unique_ptr<byte[]> bitmapPixels(new byte[width * height * 4]);
        formatConverter->CopyPixels(
                nullptr,
                width * 4,
                width * height * 4,
                bitmapPixels.get());

        D3D11_SUBRESOURCE_DATA initialData;
        ZeroMemory(&initialData, sizeof(initialData));
        initialData.pSysMem = bitmapPixels.get();
        initialData.SysMemPitch = width * 4;
        initialData.SysMemSlicePitch = 0;

        CD3D11_TEXTURE2D_DESC textureDesc(
            DXGI_FORMAT_B8G8R8A8_UNORM,
            width,
            height,
            1,
            1
            );

        m_d3dDevice->CreateTexture2D(
                &textureDesc,
                &initialData,
                &texture2D);

        if (textureView != nullptr)
        {
            CD3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc(
                texture2D.Get(),
                D3D11_SRV_DIMENSION_TEXTURE2D
                );

            m_d3dDevice->CreateShaderResourceView(
                    texture2D.Get(),
                    &shaderResourceViewDesc,
                    &shaderResourceView);
        }
    }


    if (texture != nullptr)
    {
        *texture = texture2D.Detach();
    }
    if (textureView != nullptr)
    {
        *textureView = shaderResourceView.Detach();
    }
}

このコードが完了すると、画像ファイルから読み込まれた Texture2D がメモリに格納されます。 メッシュと同様に、おそらくゲームや特定のシーンにテクスチャが多数存在します。 ゲームまたはレベルの開始時にすべて読み込むのではなく、シーンごとまたはレベルごとに定期的にアクセスされるテクスチャのキャッシュを作成することを検討してください。

(上記のサンプルで呼び出された CreateDDSTextureFromMemory メソッドは、「DDSTextureLoader のコード一式」で完全に調べることができます。)

また、個々のテクスチャまたはテクスチャ "スキン" は、特定のメッシュ ポリゴンまたはサーフェスにマップされる場合があります。 このマッピング データは、通常、アーティストまたはデザイナーがモデルとテクスチャを作成するのに使用するツールによってエクスポートされます。 エクスポートしたデータを読み込むときにこの情報も必ずキャプチャしてください。フラグメント シェーディングを実行するときに適切なテクスチャを対応するサーフェスにマップするために使用するからです。

シェーダーの読み込み

シェーダーは、メモリに読み込まれてグラフィックス パイプラインの特定のステージで呼び出される高レベル シェーダー言語 (HLSL) ファイルにコンパイルされます。 最も一般的で重要なシェーダーは頂点シェーダーとピクセル シェーダーです。両者はそれぞれ、メッシュの個々の頂点の処理と、シーンのビューポート内のピクセルの処理を行います。 HLSL コードは、ジオメトリを変換し、照明効果とテクスチャを適用し、レンダリングされたシーンについての後処理をするために実行されます。

Direct3D ゲームにはさまざまなシェーダーがある可能性があります。それぞれが別々の CSO (コンパイル済みシェーダー オブジェクト、.cso) ファイルにコンパイルされます。 通常は、動的に読み込む必要があるほど多くはありません。ほとんどの場合、ゲームの開始時またはレベルごと (雨の効果用のシェーダーなど) に読み込むことができます。

BasicLoader クラスにあるコードは、頂点シェーダー、ジオメトリ シェーダー、ピクセル シェーダー、ハル シェーダーなど、さまざまなシェーダーについて多数のオーバーロードができるようにします。 以下のコードでは、例としてピクセル シェーダーについて説明します。 (「BasicLoader のコード一式」でコード一式を確認できます)。

concurrency::task<void> LoadShaderAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11PixelShader** shader
    );

// ...

task<void> BasicLoader::LoadShaderAsync(
    _In_ Platform::String^ filename,
    _Out_ ID3D11PixelShader** shader
    )
{
    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
        
       m_d3dDevice->CreatePixelShader(
                bytecode->Data,
                bytecode->Length,
                nullptr,
                shader);
    });
}

この例では、BasicReaderWriter インスタンス (m_basicReaderWriter) を使って、指定されたコンパイル済みシェーダー オブジェクト (.cso) ファイルをバイト ストリームとして読み取っています。 そのタスクが完了すると、LAMBDA によって ID3D11Device::CreatePixelShader が呼び出され、ファイルからバイト データが読み込まれます。 コールバックでは、読み込みが成功したことを示す何らかのフラグを設定する必要があります。また、シェーダーを実行する前にコードでこのフラグをチェックする必要があります。

頂点シェーダーはもう少し複雑です。 頂点シェーダーの場合は、頂点データを定義する別の入力レイアウトも読み込みます。 次のコードを使用して、カスタム頂点入力レイアウトと共に頂点シェーダーを非同期的に読み込むことができます。 メッシュから読み込んだ頂点情報がこの入力レイアウトで正しく表現できることを確認してください。

頂点シェーダーを読み込む前に、入力レイアウトを作成しましょう。

void BasicLoader::CreateInputLayout(
    _In_reads_bytes_(bytecodeSize) byte* bytecode,
    _In_ uint32 bytecodeSize,
    _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC* layoutDesc,
    _In_ uint32 layoutDescNumElements,
    _Out_ ID3D11InputLayout** layout
    )
{
    if (layoutDesc == nullptr)
    {
        // If no input layout is specified, use the BasicVertex layout.
        const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "NORMAL",   0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,    0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        };

        m_d3dDevice->CreateInputLayout(
                basicVertexLayoutDesc,
                ARRAYSIZE(basicVertexLayoutDesc),
                bytecode,
                bytecodeSize,
                layout);
    }
    else
    {
        m_d3dDevice->CreateInputLayout(
                layoutDesc,
                layoutDescNumElements,
                bytecode,
                bytecodeSize,
                layout);
    }
}

この特定のレイアウトでは、各頂点には頂点シェーダーによって処理される次のデータがあります。

  • モデルの座標空間内の 3D 座標位置 (x, y, z)。32 ビット浮動小数点値の 3 つの値の組として表されます。
  • 頂点の法線ベクトル。これも 3 つの 32 ビット浮動小数点値として表されます。
  • 変換された 2D テクスチャ座標値 (u, v)。32 ビット浮動小数点値のペアとして表されます。

これらの頂点ごとの入力要素は HLSL セマンティクスと呼ばれ、コンパイルされたシェーダー オブジェクトとの間でデータを受け渡しするために使用される定義済みのレジスタのセットです。 パイプラインは、読み込んだメッシュ内のすべての頂点に対して頂点シェーダーをそれぞれ 1 回実行します。 セマンティクスは、実行時の頂点シェーダーへの入力 (および頂点シェーダーからの出力) を定義し、シェーダーの HLSL コードで頂点ごとの計算にこのデータを提供します。

次に、頂点シェーダー オブジェクトを読み込みます。

concurrency::task<void> LoadShaderAsync(
        _In_ Platform::String^ filename,
        _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
        _In_ uint32 layoutDescNumElements,
        _Out_ ID3D11VertexShader** shader,
        _Out_opt_ ID3D11InputLayout** layout
        );

// ...

task<void> BasicLoader::LoadShaderAsync(
    _In_ Platform::String^ filename,
    _In_reads_opt_(layoutDescNumElements) D3D11_INPUT_ELEMENT_DESC layoutDesc[],
    _In_ uint32 layoutDescNumElements,
    _Out_ ID3D11VertexShader** shader,
    _Out_opt_ ID3D11InputLayout** layout
    )
{
    // This method assumes that the lifetime of input arguments may be shorter
    // than the duration of this task.  In order to ensure accurate results, a
    // copy of all arguments passed by pointer must be made.  The method then
    // ensures that the lifetime of the copied data exceeds that of the task.

    // Create copies of the layoutDesc array as well as the SemanticName strings,
    // both of which are pointers to data whose lifetimes may be shorter than that
    // of this method's task.
    shared_ptr<vector<D3D11_INPUT_ELEMENT_DESC>> layoutDescCopy;
    shared_ptr<vector<string>> layoutDescSemanticNamesCopy;
    if (layoutDesc != nullptr)
    {
        layoutDescCopy.reset(
            new vector<D3D11_INPUT_ELEMENT_DESC>(
                layoutDesc,
                layoutDesc + layoutDescNumElements
                )
            );

        layoutDescSemanticNamesCopy.reset(
            new vector<string>(layoutDescNumElements)
            );

        for (uint32 i = 0; i < layoutDescNumElements; i++)
        {
            layoutDescSemanticNamesCopy->at(i).assign(layoutDesc[i].SemanticName);
        }
    }

    return m_basicReaderWriter->ReadDataAsync(filename).then([=](const Platform::Array<byte>^ bytecode)
    {
       m_d3dDevice->CreateVertexShader(
                bytecode->Data,
                bytecode->Length,
                nullptr,
                shader);

        if (layout != nullptr)
        {
            if (layoutDesc != nullptr)
            {
                // Reassign the SemanticName elements of the layoutDesc array copy to point
                // to the corresponding copied strings. Performing the assignment inside the
                // lambda body ensures that the lambda will take a reference to the shared_ptr
                // that holds the data.  This will guarantee that the data is still valid when
                // CreateInputLayout is called.
                for (uint32 i = 0; i < layoutDescNumElements; i++)
                {
                    layoutDescCopy->at(i).SemanticName = layoutDescSemanticNamesCopy->at(i).c_str();
                }
            }

            CreateInputLayout(
                bytecode->Data,
                bytecode->Length,
                layoutDesc == nullptr ? nullptr : layoutDescCopy->data(),
                layoutDescNumElements,
                layout);   
        }
    });
}

このコードでは、頂点シェーダーの CSO ファイルのバイト データを読み取ったら、ID3D11Device::CreateVertexShader を呼び出して頂点シェーダーを作成します。 その後、同じ LAMBDA でシェーダーの入力レイアウトを作成します。

ハル シェーダーやジオメトリ シェーダーなど、他の種類のシェーダーでも、特定の構成が必要な場合があります。 さまざまなシェーダー読み込みメソッドのコード一式は、「BasicLoader のコード一式」と「Direct3D リソース読み込みサンプル」にあります。

解説

ここまでで、メッシュ、テクスチャ、コンパイルされたシェーダーなど、一般的なゲーム リソースとアセットを非同期的に読み込むためのメソッドを理解し、作成または変更できるようになっているはずです。