次の方法で共有


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) の タスク テンプレートを使用して処理されます。 タスク には、メソッド呼び出しの後に、非同期呼び出しの完了後に結果を処理するラムダが含まれており、通常は次の形式に従います。

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 自体はタスクとして定義されているため、ラムダを使用して、バイト配列が返されるときに特定の操作を実行できます 。たとえば、そのバイト データを使用できる DirectX 関数に渡すなどです。

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

ただし、すべての非同期読み込みが完了するまで、ゲームの本格的なスタートをしない方がいいです。 特定のフィールドなど、読み込みが完了したときにシグナル通知を行うメソッドをいくつか作成し、読み込み方法でラムダを使用して、完了時にそのシグナルを設定します。 読み込まれたリソースを使用するコンポーネントを開始する前に、変数を確認します。

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.
}

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

リソース ファイルを非同期的に読み込む方法を確認しました。 同期ファイルの読み込みははるかに簡単で、BasicReaderWriter の Complete コードと 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 ファイルは、次の情報を含むバイナリ ファイルです。

  • マジックナンバーである DWORD に 4 文字コード値 'DDS ' (0x20534444) が含まれています。

  • ファイル内のデータの説明。

    データは、DDS_HEADERを使用してヘッダーの説明と共に記述されます。ピクセル形式は DDS_PIXELFORMATを使用して定義されます。 DDS_HEADER および DDS_PIXELFORMAT 構造体は、非推奨のDDSURFACEDESC2、DDSCAPS2、DDPIXELFORMAT DirectDraw 7 構造体に置き換わる点に注意してください。 DDS_HEADER は、DDSURFACEDESC2とDDSCAPS2に相当するバイナリです。 DDS_PIXELFORMAT は、DDPIXELFORMAT と同等のバイナリです。

    DWORD               dwMagic;
    DDS_HEADER          header;
    

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

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

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

    BYTE bdata2[]
    

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

他のリソースの種類と同様に、ファイルからバイト ストリームとしてデータを読み取ります。 読み込みタスクが完了すると、ラムダ呼び出しによってコード (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
            );
    });
}

前のスニペットでは、ラムダはファイル名の拡張子が "dds" かどうかを確認します。 その場合は、DDS テクスチャであると想定します。 そうでない場合は、Windows イメージング コンポーネント (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) ファイルをバイト ストリームとして読み取ります。 そのタスクが完了すると、ラムダは 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を呼び出して頂点シェーダーを作成します。 その後、同じラムダでシェーダーの入力レイアウトを作成します。

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

注釈

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