Memuat sumber daya di game DirectX Anda

Sebagian besar game, pada titik tertentu, memuat sumber daya dan aset (seperti shader, tekstur, jerat yang telah ditentukan atau data grafis lainnya) dari penyimpanan lokal atau aliran data lainnya. Di sini, kami memandu Anda melalui tampilan tingkat tinggi tentang apa yang harus Anda pertimbangkan saat memuat file-file ini untuk digunakan dalam game DirectX C / C ++ Universal Windows Platform (UWP) Anda.

Misalnya, jerat untuk objek poligonal dalam game Anda mungkin telah dibuat dengan alat lain, dan diekspor ke format tertentu. Hal yang sama berlaku untuk tekstur, dan lebih dari itu: sementara bitmap datar dan tidak terkompresi dapat umum ditulis oleh sebagian besar alat dan dipahami oleh sebagian besar API grafis, itu bisa sangat tidak efisien untuk digunakan dalam game Anda. Di sini, kami memandu Anda melalui langkah-langkah dasar untuk memuat tiga jenis sumber daya grafis yang berbeda untuk digunakan dengan Direct3D: jerat (model), tekstur (bitmap), dan objek shader yang dikompilasi.

Apa yang perlu Anda ketahui

Teknologi

  • Pustaka Pola Paralel (ppltasks.h)

Prasyarat

  • Memahami runtime Windows dasar
  • Memahami tugas asinkron
  • Memahami konsep dasar pemrograman grafis 3-D.

Sampel ini juga mencakup tiga file kode untuk pemuatan dan pengelolaan sumber daya. Anda akan menemukan objek kode yang ditentukan dalam file-file ini di seluruh topik ini.

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

Kode lengkap untuk sampel ini dapat ditemukan di tautan berikut.

Topik Deskripsi

Kode lengkap untuk BasicLoader

Kode lengkap untuk kelas dan metode yang mengubah dan memuat objek mesh grafis menjadi memori.

Kode lengkap untuk BasicReaderWriter

Kode lengkap untuk kelas dan metode untuk membaca dan menulis file data biner secara umum. Digunakan oleh kelas BasicLoader .

Kode lengkap untuk DDSTextureLoader

Kode lengkap untuk kelas dan metode yang memuat tekstur DDS dari memori.

 

Instruksi

Pemuatan asinkron

Pemuatan asinkron ditangani menggunakan templat tugas dari Parallel Patterns Library (PPL). Tugas berisi panggilan metode yang diikuti oleh lambda yang memproses hasil panggilan async setelah selesai, dan biasanya mengikuti format:

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

Tugas dapat dirantai bersama menggunakan sintaks .then(), sehingga ketika satu operasi selesai, operasi async lain yang bergantung pada hasil operasi sebelumnya dapat dijalankan. Dengan cara ini, Anda dapat memuat, mengonversi, dan mengelola aset kompleks pada utas terpisah dengan cara yang tampak hampir tidak terlihat oleh pemain.

Untuk lebih jelasnya, baca pemrograman asinkron di C ++.

Sekarang, mari kita lihat struktur dasar untuk mendeklarasikan dan membuat metode pemuatan file async, 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;
    });
}

Dalam kode ini, ketika kode Anda memanggil metode ReadDataAsync yang didefinisikan di atas, tugas dibuat untuk membaca buffer dari sistem file. Setelah selesai, tugas yang dirantai mengambil buffer dan mengalirkan byte dari buffer itu ke dalam array menggunakan tipe DataReader statis.

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

Berikut adalah panggilan yang Anda buat ke ReadDataAsync. Setelah selesai, kode Anda menerima array byte yang dibaca dari file yang disediakan. Karena ReadDataAsync sendiri didefinisikan sebagai tugas, Anda dapat menggunakan lambda untuk melakukan operasi tertentu ketika array byte dikembalikan, seperti meneruskan data byte itu ke fungsi DirectX yang dapat menggunakannya.

Jika permainan Anda cukup sederhana, muat sumber daya Anda dengan metode seperti ini ketika pengguna memulai permainan. Anda dapat melakukan ini sebelum memulai loop game utama dari beberapa titik dalam urutan panggilan implementasi IFrameworkView::Run Anda. Sekali lagi, Anda memanggil metode pemuatan sumber daya Anda secara asinkron sehingga permainan dapat dimulai lebih cepat sehingga pemain tidak perlu menunggu sampai pemuatan selesai sebelum terlibat dalam interaksi awal.

Namun, Anda tidak ingin memulai permainan dengan benar sampai semua pemuatan asinkron selesai! Buat beberapa metode untuk memberi sinyal saat pemuatan selesai, seperti bidang tertentu, dan gunakan lambda pada metode pemuatan Anda untuk mengatur sinyal itu setelah selesai. Periksa variabel sebelum memulai komponen apa pun yang menggunakan sumber daya yang dimuat tersebut.

Berikut adalah contoh menggunakan metode async yang didefinisikan dalam BasicLoader.cpp untuk memuat shader, mesh, dan tekstur saat permainan dimulai. Perhatikan bahwa itu menetapkan bidang tertentu pada objek permainan, m_loadingComplete, ketika semua metode pemuatan selesai.

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

Perhatikan bahwa tugas telah digabungkan menggunakan && operator sedemikian rupa sehingga lambda yang menetapkan bendera lengkap pemuatan dipicu hanya ketika semua tugas selesai. Perhatikan bahwa jika Anda memiliki beberapa bendera, Anda memiliki kemungkinan kondisi balapan. Misalnya, jika lambda menetapkan dua bendera secara berurutan ke nilai yang sama, thread lain mungkin hanya melihat bendera pertama yang ditetapkan jika memeriksanya sebelum bendera kedua ditetapkan.

Anda telah melihat cara memuat file sumber daya secara asinkron. Beban file sinkron jauh lebih sederhana, dan Anda dapat menemukan contoh di kode Lengkap untuk BasicReaderWriter dan Kode lengkap untuk BasicLoader.

Tentu saja, jenis sumber daya dan aset yang berbeda sering memerlukan pemrosesan atau konversi tambahan sebelum siap digunakan dalam alur grafis Anda. Mari kita lihat tiga jenis sumber daya tertentu: jerat, tekstur, dan shader.

Memuat jerat

Jerat adalah data simpul, baik yang dihasilkan secara prosedural dengan kode dalam game Anda atau diekspor ke file dari aplikasi lain (seperti 3DStudio MAX atau Alias WaveFront) atau alat. Jerat ini mewakili model dalam permainan Anda, dari primitif sederhana seperti kubus dan bola hingga mobil dan rumah dan karakter. Mereka sering berisi data warna dan animasi, juga, tergantung pada formatnya. Kami akan fokus pada jerat yang hanya berisi data simpul.

Untuk memuat mesh dengan benar, Anda harus mengetahui format data dalam file untuk mesh. Jenis BasicReaderWriter sederhana kami di atas hanya membaca data sebagai aliran byte; tidak tahu bahwa data byte mewakili mesh, apalagi format mesh tertentu seperti yang diekspor oleh aplikasi lain! Anda harus melakukan konversi saat anda membawa data mesh ke dalam memori.

(Anda harus selalu mencoba mengemas data aset dalam format yang sedekat mungkin dengan representasi internal. Melakukan hal itu akan mengurangi pemanfaatan sumber daya dan menghemat waktu.)

Mari kita dapatkan data byte dari file mesh. Format dalam contoh mengasumsikan bahwa file adalah format khusus sampel yang diakhiri dengan .vbo. (Sekali lagi, format ini tidak sama dengan format VBO OpenGL.) Setiap simpul itu sendiri memetakan ke tipe BasicVertex , yang merupakan struktur yang didefinisikan dalam kode untuk alat konverter obj2vbo. Tata letak data simpul dalam file .vbo terlihat seperti ini:

  • 32 bit pertama (4 byte) dari aliran data berisi jumlah simpul (numVertices) dalam mesh, direpresentasikan sebagai nilai uint32.
  • 32 bit berikutnya (4 byte) dari aliran data berisi jumlah indeks dalam mesh (numIndices), direpresentasikan sebagai nilai uint32.
  • Setelah itu, bit berikutnya (numVertices * sizeof(BasicVertex)) berisi data simpul.
  • Bit terakhir (numIndices * 16) berisi data indeks, direpresentasikan sebagai urutan nilai uint16.

Intinya adalah ini: ketahui tata letak bit-level dari data mesh yang telah Anda muat. Juga, pastikan Anda konsisten dengan endian-ness. Semua platform Windows 8 adalah little-endian.

Dalam contoh, Anda memanggil metode, CreateMesh, dari metode LoadMeshAsync untuk melakukan interpretasi tingkat bit ini.

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 menafsirkan data byte yang dimuat dari file, dan membuat buffer simpul dan buffer indeks untuk mesh dengan meneruskan daftar simpul dan indeks, masing-masing, ke ID3D11Device::CreateBuffer dan menentukan D3D11_BIND_VERTEX_BUFFER atau D3D11_BIND_INDEX_BUFFER. Berikut adalah kode yang digunakan dalam 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;
    }
}

Anda biasanya membuat pasangan penyangga simpul / indeks untuk setiap mesh yang Anda gunakan dalam permainan Anda. Di mana dan kapan Anda memuat jerat terserah Anda. Jika Anda memiliki banyak jerat, Anda mungkin hanya ingin memuat beberapa dari disk pada titik-titik tertentu dalam permainan, seperti selama status pemuatan tertentu yang telah ditentukan sebelumnya. Untuk jerat besar, seperti data medan, Anda dapat mengalirkan simpul dari cache, tetapi itu adalah prosedur yang lebih kompleks dan bukan dalam lingkup topik ini.

Sekali lagi, ketahui format data simpul Anda! Ada banyak, banyak cara untuk mewakili data simpul di seluruh alat yang digunakan untuk membuat model. Ada juga banyak cara berbeda untuk mewakili tata letak input data simpul ke Direct3D, seperti daftar segitiga dan strip. Untuk informasi selengkapnya tentang data simpul, baca Pengantar Buffer di Direct3D 11 dan Primitif.

Selanjutnya, mari kita lihat tekstur pemuatan.

Memuat tekstur

Aset yang paling umum dalam permainan — dan yang terdiri dari sebagian besar file pada disk dan memori — adalah tekstur. Seperti jerat, tekstur dapat datang dalam berbagai format, dan Anda mengubahnya menjadi format yang dapat digunakan Direct3D saat Anda memuatnya. Tekstur juga datang dalam berbagai jenis dan digunakan untuk membuat efek yang berbeda. Tingkat MIP untuk tekstur dapat digunakan untuk meningkatkan tampilan dan kinerja objek jarak; peta kotoran dan cahaya digunakan untuk melapisi efek dan detail di atas tekstur dasar; dan peta normal digunakan dalam perhitungan pencahayaan per piksel. Dalam permainan modern, adegan khas berpotensi memiliki ribuan tekstur individu, dan kode Anda harus secara efektif mengelola semuanya!

Juga seperti jerat, ada sejumlah format khusus yang digunakan untuk membuat penggunaan memori menjadi efisien. Karena tekstur dapat dengan mudah mengkonsumsi sebagian besar memori GPU (dan sistem), mereka sering dikompresi dengan cara tertentu. Anda tidak diharuskan menggunakan kompresi pada tekstur game Anda, dan Anda dapat menggunakan algoritma kompresi / dekompresi apa pun yang Anda inginkan selama Anda memberikan peningtut jarak Direct3D dengan data dalam format yang dapat dipahaminya (seperti bitmap Texture2D ).

Direct3D memberikan dukungan untuk algoritma kompresi tekstur DXT, meskipun setiap format DXT mungkin tidak didukung dalam perangkat keras grafis pemain. File DDS berisi tekstur DXT (dan format kompresi tekstur lainnya juga), dan diakhiri dengan .dds.

File DDS adalah file biner yang berisi informasi berikut:

  • DWORD (angka ajaib) yang berisi nilai kode empat karakter 'DDS' (0x20534444).

  • Deskripsi data dalam file.

    Data dijelaskan dengan deskripsi header menggunakan DDS_HEADER; format piksel ditentukan menggunakan DDS_PIXELFORMAT. Perhatikan bahwa struktur DDS_HEADER dan DDS_PIXELFORMAT menggantikan struktur DDSURFACEDESC2, DDSCAPS2 dan DDPIXELFORMAT DirectDraw 7 yang tidak digunakan lagi. DDS_HEADER adalah setara biner dari DDSURFACEDESC2 dan DDSCAPS2. DDS_PIXELFORMAT adalah setara biner dari DDPIXELFORMAT.

    DWORD               dwMagic;
    DDS_HEADER          header;
    

    Jika nilai dwFlags dalam DDS_PIXELFORMAT diatur ke DDPF_FOURCC dan dwFourCC diatur ke "DX10" struktur DDS_HEADER_DXT10 tambahan akan hadir untuk mengakomodasi array tekstur atau format DXGI yang tidak dapat dinyatakan sebagai format piksel RGB seperti format floating point, format sRGB dll. Ketika struktur DDS_HEADER_DXT10 hadir, seluruh deskripsi data akan terlihat seperti ini.

    DWORD               dwMagic;
    DDS_HEADER          header;
    DDS_HEADER_DXT10    header10;
    
  • Penunjuk ke array byte yang berisi data permukaan utama.

    BYTE bdata[]
    
  • Penunjuk ke array byte yang berisi permukaan yang tersisa seperti; tingkat mipmap, wajah dalam peta kubus, kedalaman dalam tekstur volume. Ikuti tautan ini untuk informasi lebih lanjut tentang tata letak file DDS untuk: tekstur, peta kubus, atau tekstur volume.

    BYTE bdata2[]
    

Banyak alat diekspor ke format DDS. Jika Anda tidak memiliki alat untuk mengekspor tekstur ke format ini, pertimbangkan untuk membuatnya. Untuk detail lebih lanjut tentang format DDS dan cara bekerja dengannya dalam kode Anda, baca Panduan Pemrograman untuk DDS. Dalam contoh kita, kita akan menggunakan DDS.

Seperti jenis sumber daya lainnya, Anda membaca data dari file sebagai aliran byte. Setelah tugas pemuatan Anda selesai, panggilan lambda menjalankan kode (metode CreateTexture ) untuk memproses aliran byte ke dalam format yang dapat digunakan 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
            );
    });
}

Pada cuplikan sebelumnya, lambda memeriksa untuk melihat apakah nama file memiliki ekstensi "dds". Jika ya, Anda berasumsi bahwa itu adalah tekstur DDS. Jika tidak, gunakan API Windows Imaging Component (WIC) untuk menemukan format dan memecahkan kode data sebagai bitmap. Either way, hasilnya adalah bitmap Texture2D (atau kesalahan).

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();
    }
}

Ketika kode ini selesai, Anda memiliki Texture2D dalam memori, dimuat dari file gambar. Seperti jerat, Anda mungkin memiliki banyak dari mereka dalam permainan Anda dan dalam adegan tertentu. Pertimbangkan untuk membuat cache untuk tekstur yang diakses secara teratur per adegan atau per level, daripada memuat semuanya saat game atau level dimulai.

(Metode CreateDDSTextureFromMemory yang disebut dalam sampel di atas dapat dieksplorasi secara penuh dalam kode Lengkap untuk DDSTextureLoader.)

Juga, tekstur individu atau tekstur "kulit" dapat memetakan poligon mesh tertentu atau permukaan. Data pemetaan ini biasanya diekspor oleh alat yang digunakan artis atau desainer untuk membuat model dan tekstur. Pastikan Anda menangkap informasi ini juga saat Memuat data yang diekspor, karena Anda akan menggunakannya memetakan tekstur yang benar ke permukaan yang sesuai saat Anda melakukan bayangan fragmen.

Memuat shader

Shaders dikompilasi High Level Shader Language (HLSL) file yang dimuat ke dalam memori dan dipanggil pada tahap tertentu dari pipa grafis. Shader yang paling umum dan penting adalah shader simpul dan piksel, yang memproses simpul individu mesh Anda dan piksel di viewport adegan ( s ), masing-masing. Kode HLSL dijalankan untuk mengubah geometri, menerapkan efek pencahayaan dan tekstur, dan melakukan pasca-pemrosesan pada adegan yang diberikan.

Sebuah game Direct3D dapat memiliki sejumlah shader yang berbeda, masing-masing dikompilasi menjadi CSO terpisah (Compiled Shader Object, .cso) file. Biasanya, Anda tidak memiliki begitu banyak sehingga Anda perlu memuatnya secara dinamis, dan dalam banyak kasus, Anda cukup memuatnya saat permainan dimulai, atau berdasarkan per level (seperti shader untuk efek hujan).

Kode di kelas BasicLoader menyediakan sejumlah kelebihan beban untuk shader yang berbeda, termasuk simpul, geometri, piksel, dan shader lambung. Kode di bawah ini mencakup shader piksel sebagai contoh. (Anda dapat meninjau kode lengkap dalam kode Lengkap untuk 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);
    });
}

Dalam contoh ini, Anda menggunakan instance BasicReaderWriter (m_basicReaderWriter) untuk membaca dalam file shader object (.cso) yang dikompilasi sebagai aliran byte. Setelah tugas itu selesai, lambda memanggil ID3D11Device::CreatePixelShader dengan data byte yang dimuat dari file. Callback Anda harus menetapkan beberapa bendera yang menunjukkan bahwa beban berhasil, dan kode Anda harus memeriksa bendera ini sebelum menjalankan shader.

Shader vertex sedikit lebih kompleks. Untuk shader simpul, Anda juga memuat tata letak input terpisah yang menentukan data simpul. Kode berikut dapat digunakan untuk memuat shader simpul secara asinkron bersama dengan tata letak input simpul kustom. Pastikan bahwa informasi simpul yang Anda muat dari jerat Anda dapat diwakili dengan benar oleh tata letak input ini!

Mari kita buat tata letak input sebelum Anda memuat shader simpul.

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);
    }
}

Dalam tata letak khusus ini, setiap simpul memiliki data berikut yang diproses oleh shader simpul:

  • Posisi koordinat 3D (x, y, z) di ruang koordinat model, direpresentasikan sebagai trio nilai floating point 32-bit.
  • Vektor normal untuk simpul, juga direpresentasikan sebagai tiga nilai floating point 32-bit.
  • Nilai koordinat tekstur 2D yang diubah (u, v), direpresentasikan sebagai sepasang nilai mengambang 32-bit.

Elemen input per-simpul ini disebut semantik HLSL, dan mereka adalah satu set register yang ditentukan yang digunakan untuk meneruskan data ke dan dari objek shader yang Dikompilasi. Alur Anda menjalankan shader simpul sekali untuk setiap simpul di mesh yang telah Anda muat. Semantik menentukan input ke (dan output dari) shader simpul saat berjalan, dan menyediakan data ini untuk perhitungan per simpul Anda dalam kode HLSL shader Anda.

Sekarang, muat objek shader simpul.

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);   
        }
    });
}

Dalam kode ini, setelah Anda membaca data byte untuk file CSO vertex shader, Anda membuat shader simpul dengan memanggil ID3D11Device::CreateVertexShader. Setelah itu, Anda membuat tata letak input untuk shader di lambda yang sama.

Jenis shader lainnya, seperti hull dan geometri shader, juga dapat memerlukan konfigurasi khusus. Kode lengkap untuk berbagai metode pemuatan shader disediakan dalam kode Lengkap untuk BasicLoader dan dalam sampel pemuatan sumber daya Direct3D.

Keterangan

Pada titik ini, Anda harus memahami dan dapat membuat atau memodifikasi metode untuk memuat sumber daya dan aset game umum secara asinkron, seperti jerat, tekstur, dan shader yang dikompilasi.