Memahami alur penyajian Direct3D 11

Sebelumnya, Anda melihat cara membuat jendela yang dapat Anda gunakan untuk menggambar di Bekerja dengan sumber daya perangkat DirectX. Sekarang, Anda belajar cara membangun alur grafis, dan di mana Anda dapat menghubungkannya.

Anda akan ingat bahwa ada dua antarmuka Direct3D yang menentukan alur grafis: ID3D11Device, yang menyediakan representasi virtual GPU dan sumber dayanya; dan ID3D11DeviceContext, yang mewakili pemrosesan grafis untuk alur. Biasanya, Anda menggunakan instans ID3D11Device untuk mengonfigurasi dan mendapatkan sumber daya GPU yang Anda butuhkan untuk mulai memproses grafik dalam adegan, dan Anda menggunakan ID3D11DeviceContext untuk memproses sumber daya tersebut pada setiap tahap shader yang sesuai dalam alur grafis. Anda biasanya jarang memanggil metode ID3D11Device —yaitu, hanya saat Anda menyiapkan adegan atau saat perangkat berubah. Di sisi lain, Anda akan memanggil ID3D11DeviceContext setiap kali Anda memproses bingkai untuk ditampilkan.

Contoh ini membuat dan mengonfigurasi alur grafis minimal yang cocok untuk menampilkan kubus berbayang puncak sederhana. Ini menunjukkan sekitar sekumpulan sumber daya terkecil yang diperlukan untuk ditampilkan. Saat Anda membaca info di sini, perhatikan batasan contoh yang diberikan di mana Anda mungkin harus memperluasnya untuk mendukung adegan yang ingin Anda render.

Contoh ini mencakup dua kelas C++ untuk grafis: kelas manajer sumber daya perangkat, dan kelas perender adegan 3D. Topik ini berfokus secara khusus pada perender adegan 3D.

Apa yang dilakukan perender kubus?

Alur grafis didefinisikan oleh kelas perender adegan 3D. Perender adegan dapat:

  • Tentukan buffer konstanta untuk menyimpan data seragam Anda.
  • Tentukan buffer vertex untuk menyimpan data vertex objek Anda, dan buffer indeks yang sesuai untuk memungkinkan shader vertex berjalan segitiga dengan benar.
  • Membuat sumber daya tekstur dan tampilan sumber daya.
  • Muat objek shader Anda.
  • Perbarui data grafik untuk menampilkan setiap bingkai.
  • Render (gambar) grafik ke rantai pertukaran.

Empat proses pertama biasanya menggunakan metode antarmuka ID3D11Device untuk menginisialisasi dan mengelola sumber daya grafis, dan dua proses terakhir menggunakan metode antarmuka ID3D11DeviceContext untuk mengelola dan menjalankan alur grafis.

Instans kelas Renderer dibuat dan dikelola sebagai variabel anggota pada kelas proyek utama. Instans DeviceResources dikelola sebagai pointer bersama di beberapa kelas, termasuk kelas proyek utama, kelas Penyedia tampilan aplikasi, dan Renderer. Jika Anda mengganti Renderer dengan kelas Anda sendiri, pertimbangkan untuk mendeklarasikan dan menetapkan instans DeviceResources sebagai anggota pointer bersama juga:

std::shared_ptr<DX::DeviceResources> m_deviceResources;

Cukup teruskan pointer ke konstruktor kelas (atau metode inisialisasi lainnya) setelah instans DeviceResources dibuat dalam metode Inisialisasi kelas Aplikasi . Anda juga dapat meneruskan referensi weak_ptr jika, Sebagai gantinya, Anda ingin kelas utama Anda memiliki instans DeviceResources sepenuhnya.

Membuat perender kubus

Dalam contoh ini, kami mengatur kelas perender adegan dengan metode berikut:

  • CreateDeviceDependentResources: Dipanggil setiap kali adegan harus diinisialisasi atau dimulai ulang. Metode ini memuat data puncak awal, tekstur, shader, dan sumber daya lainnya, dan membangun buffer konstanta dan vertex awal. Biasanya, sebagian besar pekerjaan di sini dilakukan dengan metode ID3D11Device , bukan metode ID3D11DeviceContext .
  • CreateWindowSizeDependentResources: Dipanggil setiap kali status jendela berubah, seperti saat perubahan ukuran terjadi atau saat orientasi berubah. Metode ini membangun kembali matriks transformasi, seperti matriks untuk kamera Anda.
  • Pembaruan: Biasanya dipanggil dari bagian program yang mengelola status permainan langsung; dalam contoh ini, kami hanya menyebutnya dari kelas Utama . Mintalah metode ini membaca dari informasi status permainan apa pun yang memengaruhi penyajian, seperti pembaruan pada posisi objek atau bingkai animasi, ditambah data game global apa pun seperti tingkat cahaya atau perubahan pada fisika game. Input ini digunakan untuk memperbarui buffer konstanta per bingkai dan data objek.
  • Render: Biasanya dipanggil dari bagian program yang mengelola perulangan game; dalam hal ini, itu dipanggil dari kelas Utama . Metode ini membangun alur grafis: mengikat shader, mengikat buffer dan sumber daya ke tahap shader, dan memanggil gambar untuk bingkai saat ini.

Metode ini terdiri dari isi perilaku untuk merender adegan dengan Direct3D menggunakan aset Anda. Jika Anda memperluas contoh ini dengan kelas penyajian baru, deklarasikan pada kelas proyek utama. Jadi ini:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

menjadi ini:

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

Sekali lagi, perhatikan bahwa contoh ini mengasumsikan bahwa metode memiliki tanda tangan yang sama dalam implementasi Anda. Jika tanda tangan telah berubah, tinjau perulangan Utama dan buat perubahan yang sesuai.

Mari kita lihat metode penyajian adegan secara lebih rinci.

Membuat sumber daya dependen perangkat

CreateDeviceDependentResources mengonsolidasikan semua operasi untuk menginisialisasi adegan dan sumber dayanya menggunakan panggilan ID3D11Device . Metode ini mengasumsikan bahwa perangkat Direct3D baru saja diinisialisasi (atau telah dibuat ulang) untuk adegan. Ini membuat ulang atau memuat ulang semua sumber daya grafis khusus adegan, seperti vertex dan shader piksel, buffer puncak dan indeks untuk objek, dan sumber daya lainnya (misalnya, sebagai tekstur dan tampilan yang sesuai).

Berikut adalah contoh kode untuk CreateDeviceDependentResources:

void Renderer::CreateDeviceDependentResources()
{
    // Compile shaders using the Effects library.
    auto CreateShadersTask = Concurrency::create_task(
            [this]( )
            {
                CreateShaders();
            }
        );

    // Load the geometry for the spinning cube.
    auto CreateCubeTask = CreateShadersTask.then(
            [this]()
            {
                CreateCube();
            }
        );
}

void Renderer::CreateWindowSizeDependentResources()
{
    // Create the view matrix and the perspective matrix.
    CreateViewAndPerspective();
}

Setiap kali Anda memuat sumber daya dari disk—sumber daya seperti file atau tekstur objek shader yang dikompilasi (CSO, atau .cso) — lakukan secara asinkron. Ini memungkinkan Anda untuk menjaga pekerjaan lain tetap berjalan pada saat yang sama (seperti tugas penyiapan lainnya), dan karena perulangan utama tidak diblokir, Anda dapat terus menampilkan sesuatu yang menarik secara visual kepada pengguna (seperti animasi pemuatan untuk game Anda). Contoh ini menggunakan API Konkurensi::Tugas yang tersedia mulai dari Windows 8; perhatikan sintaks lambda yang digunakan untuk merangkum tugas pemuatan asinkron. Lambda ini mewakili fungsi yang disebut off-thread, sehingga pointer ke objek kelas saat ini (ini) ditangkap secara eksplisit.

Berikut adalah contoh bagaimana Anda dapat memuat bytecode shader:

HRESULT hr = S_OK;

// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();

// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE* vShader, * pShader;
BYTE* bytes;

size_t destSize = 4096;
size_t bytesRead = 0;
bytes = new BYTE[destSize];

fopen_s(&vShader, "CubeVertexShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, vShader);
hr = device->CreateVertexShader(
    bytes,
    bytesRead,
    nullptr,
    &m_pVertexShader
    );

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

delete bytes;


bytes = new BYTE[destSize];
bytesRead = 0;
fopen_s(&pShader, "CubePixelShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, pShader);
hr = device->CreatePixelShader(
    bytes,
    bytesRead,
    nullptr,
    m_pPixelShader.GetAddressOf()
    );

delete bytes;

CD3D11_BUFFER_DESC cbDesc(
    sizeof(ConstantBufferStruct),
    D3D11_BIND_CONSTANT_BUFFER
    );

hr = device->CreateBuffer(
    &cbDesc,
    nullptr,
    m_pConstantBuffer.GetAddressOf()
    );

fclose(vShader);
fclose(pShader);

Berikut adalah contoh cara membuat puncak dan buffer indeks:

HRESULT Renderer::CreateCube()
{
    HRESULT hr = S_OK;

    // Use the Direct3D device to load resources into graphics memory.
    ID3D11Device* device = m_deviceResources->GetDevice();

    // Create cube geometry.
    VertexPositionColor CubeVertices[] =
    {
        {DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  0,   0,   0),},
        {DirectX::XMFLOAT3(-0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  0,   0,   1),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  0,   1,   0),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  0,   1,   1),},

        {DirectX::XMFLOAT3( 0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  1,   0,   0),},
        {DirectX::XMFLOAT3( 0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  1,   0,   1),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  1,   1,   0),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  1,   1,   1),},
    };
    
    // Create vertex buffer:
    
    CD3D11_BUFFER_DESC vDesc(
        sizeof(CubeVertices),
        D3D11_BIND_VERTEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA vData;
    ZeroMemory(&vData, sizeof(D3D11_SUBRESOURCE_DATA));
    vData.pSysMem = CubeVertices;
    vData.SysMemPitch = 0;
    vData.SysMemSlicePitch = 0;

    hr = device->CreateBuffer(
        &vDesc,
        &vData,
        &m_pVertexBuffer
        );

    // Create index buffer:
    unsigned short CubeIndices [] = 
    {
        0,2,1, // -x
        1,2,3,

        4,5,6, // +x
        5,7,6,

        0,1,5, // -y
        0,5,4,

        2,6,7, // +y
        2,7,3,

        0,4,6, // -z
        0,6,2,

        1,3,7, // +z
        1,7,5,
    };

    m_indexCount = ARRAYSIZE(CubeIndices);

    CD3D11_BUFFER_DESC iDesc(
        sizeof(CubeIndices),
        D3D11_BIND_INDEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA iData;
    ZeroMemory(&iData, sizeof(D3D11_SUBRESOURCE_DATA));
    iData.pSysMem = CubeIndices;
    iData.SysMemPitch = 0;
    iData.SysMemSlicePitch = 0;
    
    hr = device->CreateBuffer(
        &iDesc,
        &iData,
        &m_pIndexBuffer
        );

    return hr;
}

Contoh ini tidak memuat jala atau tekstur apa pun. Anda harus membuat metode untuk memuat jenis jala dan tekstur yang khusus untuk game Anda, dan memanggilnya secara asinkron.

Isi nilai awal untuk buffer konstanta per adegan Anda di sini juga. Contoh buffer konstanta per adegan termasuk lampu tetap, atau elemen dan data adegan statis lainnya.

Menerapkan metode CreateWindowSizeDependentResources

Metode CreateWindowSizeDependentResources dipanggil setiap kali ukuran jendela, orientasi, atau resolusi berubah.

Sumber daya ukuran jendela diperbarui seperti itu: Proc pesan statis mendapatkan salah satu dari beberapa kemungkinan peristiwa yang menunjukkan perubahan status jendela. Perulangan utama Anda kemudian diberitahu tentang peristiwa dan panggilan CreateWindowSizeDependentResources pada instans kelas utama, yang kemudian memanggil implementasi CreateWindowSizeDependentResources pada kelas perender adegan.

Tugas utama metode ini adalah memastikan visual tidak menjadi bingung atau tidak valid karena perubahan properti jendela. Dalam contoh ini, kami memperbarui matriks proyek dengan bidang tampilan baru (FOV) untuk jendela yang diubah ukurannya atau bereorientasi.

Kami sudah melihat kode untuk membuat sumber daya jendela di DeviceResources - itu adalah rantai pertukaran (dengan buffer belakang) dan merender tampilan target. Berikut cara perender membuat transformasi yang bergantung pada rasio aspek:

void Renderer::CreateViewAndPerspective()
{
    // Use DirectXMath to create view and perspective matrices.

    DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.7f, 1.5f, 0.f);
    DirectX::XMVECTOR at  = DirectX::XMVectorSet(0.0f,-0.1f, 0.0f, 0.f);
    DirectX::XMVECTOR up  = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.f);

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.view,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixLookAtRH(
                eye,
                at,
                up
                )
            )
        );

    float aspectRatioX = m_deviceResources->GetAspectRatio();
    float aspectRatioY = aspectRatioX < (16.0f / 9.0f) ? aspectRatioX / (16.0f / 9.0f) : 1.0f;

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.projection,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixPerspectiveFovRH(
                2.0f * std::atan(std::tan(DirectX::XMConvertToRadians(70) * 0.5f) / aspectRatioY),
                aspectRatioX,
                0.01f,
                100.0f
                )
            )
        );
}

Jika adegan Anda memiliki tata letak komponen tertentu yang tergantung pada rasio aspek, ini adalah tempat untuk mengatur ulang agar sesuai dengan rasio aspek tersebut. Anda mungkin ingin mengubah konfigurasi perilaku pasca-pemrosesan di sini juga.

Menerapkan metode Pembaruan

Metode Pembaruan dipanggil sekali per perulangan game - dalam contoh ini, metode ini dipanggil dengan metode kelas utama dengan nama yang sama. Ini memiliki tujuan sederhana: memperbarui geometri adegan dan status permainan berdasarkan jumlah waktu yang berlalu (atau langkah-langkah waktu yang berlalu) sejak bingkai sebelumnya. Dalam contoh ini, kita cukup memutar kubus sekali per bingkai. Dalam adegan game nyata, metode ini berisi lebih banyak kode untuk memeriksa status permainan, memperbarui buffer konstanta per bingkai (atau dinamis lainnya), buffer geometri, dan aset dalam memori lainnya yang sesuai. Karena komunikasi antara CPU dan GPU menimbulkan overhead, pastikan Anda hanya memperbarui buffer yang benar-benar telah berubah sejak bingkai terakhir - buffer konstanta Anda dapat dikelompokkan, atau dipisahkan, sesuai kebutuhan untuk membuat ini lebih efisien.

void Renderer::Update()
{
    // Rotate the cube 1 degree per frame.
    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.world,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixRotationY(
                DirectX::XMConvertToRadians(
                    (float) m_frameCount++
                    )
                )
            )
        );

    if (m_frameCount == MAXUINT)  m_frameCount = 0;
}

Dalam hal ini, Putar pembaruan buffer konstanta dengan matriks transformasi baru untuk kubus. Matriks akan dikalikan per vertex selama tahap shader vertex. Karena metode ini dipanggil dengan setiap bingkai, ini adalah tempat yang baik untuk menggabungkan metode apa pun yang memperbarui buffer konstanta dan puncak dinamis Anda, atau untuk melakukan operasi lain yang menyiapkan objek dalam adegan untuk transformasi oleh alur grafis.

Menerapkan metode Render

Metode ini dipanggil sekali per perulangan game setelah memanggil Update. Seperti Pembaruan, metode Render juga dipanggil dari kelas utama. Ini adalah metode di mana alur grafis dibangun dan diproses untuk bingkai menggunakan metode pada instans ID3D11DeviceContext . Ini berpuncak dalam panggilan terakhir ke ID3D11DeviceContext::D rawIndexed. Penting untuk dipahami bahwa panggilan ini (atau panggilan Draw* serupa lainnya yang ditentukan pada ID3D11DeviceContext) benar-benar menjalankan alur. Secara khusus, ini adalah ketika Direct3D berkomunikasi dengan GPU untuk mengatur status gambar, menjalankan setiap tahap alur, dan menulis hasil piksel ke dalam sumber daya buffer target render untuk ditampilkan oleh rantai pertukaran. Karena komunikasi antara CPU dan GPU menimbulkan overhead, gabungkan beberapa panggilan gambar menjadi satu jika Anda bisa, terutama jika adegan Anda memiliki banyak objek yang dirender.

void Renderer::Render()
{
    // Use the Direct3D device context to draw.
    ID3D11DeviceContext* context = m_deviceResources->GetDeviceContext();

    ID3D11RenderTargetView* renderTarget = m_deviceResources->GetRenderTarget();
    ID3D11DepthStencilView* depthStencil = m_deviceResources->GetDepthStencil();

    context->UpdateSubresource(
        m_pConstantBuffer.Get(),
        0,
        nullptr,
        &m_constantBufferData,
        0,
        0
        );

    // Clear the render target and the z-buffer.
    const float teal [] = { 0.098f, 0.439f, 0.439f, 1.000f };
    context->ClearRenderTargetView(
        renderTarget,
        teal
        );
    context->ClearDepthStencilView(
        depthStencil,
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
        1.0f,
        0);

    // Set the render target.
    context->OMSetRenderTargets(
        1,
        &renderTarget,
        depthStencil
        );

    // Set up the IA stage by setting the input topology and layout.
    UINT stride = sizeof(VertexPositionColor);
    UINT offset = 0;

    context->IASetVertexBuffers(
        0,
        1,
        m_pVertexBuffer.GetAddressOf(),
        &stride,
        &offset
        );

    context->IASetIndexBuffer(
        m_pIndexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
        );
    
    context->IASetPrimitiveTopology(
        D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
        );

    context->IASetInputLayout(m_pInputLayout.Get());

    // Set up the vertex shader stage.
    context->VSSetShader(
        m_pVertexShader.Get(),
        nullptr,
        0
        );

    context->VSSetConstantBuffers(
        0,
        1,
        m_pConstantBuffer.GetAddressOf()
        );

    // Set up the pixel shader stage.
    context->PSSetShader(
        m_pPixelShader.Get(),
        nullptr,
        0
        );

    // Calling Draw tells Direct3D to start sending commands to the graphics device.
    context->DrawIndexed(
        m_indexCount,
        0,
        0
        );
}

Adalah praktik yang baik untuk mengatur berbagai tahapan alur grafis pada konteks secara berurutan. Biasanya, urutannya adalah:

  • Refresh sumber daya buffer konstan dengan data baru sesuai kebutuhan (menggunakan data dari Pembaruan).
  • Perakitan input (IA): Di sinilah kami melampirkan buffer puncak dan indeks yang menentukan geometri adegan. Anda perlu melampirkan setiap puncak dan buffer indeks untuk setiap objek dalam adegan. Karena contoh ini hanya memiliki kubus, itu cukup sederhana.
  • Vertex shader (VS): Lampirkan shader vertex apa pun yang akan mengubah data di buffer vertex, dan melampirkan buffer konstan untuk shader vertex.
  • Pixel shader (PS): Lampirkan shader piksel apa pun yang akan melakukan operasi per piksel dalam adegan yang dirasterisasi, dan melampirkan sumber daya perangkat untuk shader piksel (buffer konstanta, tekstur, dan sebagainya).
  • Penggabungan output (OM): Ini adalah tahap di mana piksel dipadukan, setelah shader selesai. Ini adalah pengecualian untuk aturan, karena Anda melampirkan stensil kedalaman Anda dan merender target sebelum mengatur salah satu tahap lainnya. Anda mungkin memiliki beberapa stensil dan target jika Anda memiliki vertex dan shader piksel tambahan yang menghasilkan tekstur seperti peta bayangan, peta tinggi, atau teknik pengambilan sampel lainnya - dalam hal ini, setiap pass gambar akan memerlukan target yang sesuai yang ditetapkan sebelum Anda memanggil fungsi gambar.

Selanjutnya, di bagian akhir (Bekerja dengan shader dan sumber daya shader), kita akan melihat shader dan membahas bagaimana Direct3D mengeksekusinya.

Berikutnya

Bekerja dengan shader dan sumber daya shader