Bekerja dengan shader dan sumber daya shader

Saatnya untuk mempelajari cara bekerja dengan sumber daya shader dan shader dalam mengembangkan game Microsoft DirectX Anda untuk Windows 8. Kami telah melihat cara menyiapkan perangkat grafis dan sumber daya, dan mungkin Anda bahkan telah mulai memodifikasi alurnya. Jadi sekarang mari kita lihat piksel dan shader vertex.

Jika Anda tidak terbiasa dengan bahasa shader, diskusi cepat berurutan. Shader adalah program kecil tingkat rendah yang dikompilasi dan dijalankan pada tahap tertentu dalam alur grafis. Spesialisasi mereka adalah operasi matematika floating-point yang sangat cepat. Program shader yang paling umum adalah:

  • Shader vertex—Dieksekusi untuk setiap puncak dalam adegan. Shader ini beroperasi pada elemen buffer vertex yang disediakan oleh aplikasi panggilan, dan secara minimal menghasilkan vektor posisi 4 komponen yang akan dirasterisasi menjadi posisi piksel.
  • Shader piksel—Dijalankan untuk setiap piksel dalam target render. Shader ini menerima koordinat raster dari tahap shader sebelumnya (dalam alur yang paling sederhana, ini akan menjadi shader vertex) dan mengembalikan warna (atau nilai 4 komponen lainnya) untuk posisi piksel tersebut, yang kemudian ditulis ke dalam target render.

Contoh ini mencakup vertex yang sangat dasar dan shader piksel yang hanya menggambar geometri, dan shader yang lebih kompleks yang menambahkan perhitungan pencahayaan dasar.

Program shader ditulis dalam Microsoft High Level Shader Language (HLSL). Sintaks HLSL terlihat seperti C, tetapi tanpa pointer. Program shader harus sangat ringkas dan efisien. Jika shader Anda dikompilasi ke terlalu banyak instruksi, shader tidak dapat dijalankan dan kesalahan dikembalikan. (Perhatikan bahwa jumlah pasti instruksi yang diizinkan adalah bagian dari tingkat fitur Direct3D.)

Di Direct3D, shader tidak dikompilasi pada durasi; mereka dikompilasi ketika sisa program dikompilasi. Saat Anda mengkompilasi aplikasi dengan Microsoft Visual Studio 2013, file HLSL dikompilasi ke file CSO (.cso) yang harus dimuat dan ditempatkan di memori GPU sebelum menggambar. Pastikan Anda menyertakan file CSO ini dengan aplikasi anda saat mengemasnya; mereka adalah aset seperti jala dan tekstur.

Memahami semantik HLSL

Penting untuk meluangkan waktu sejenak untuk mendiskusikan semantik HLSL sebelum kita melanjutkan, karena mereka sering menjadi titik kebingungan bagi pengembang Direct3D baru. Semantik HLSL adalah string yang mengidentifikasi nilai yang diteruskan antara aplikasi dan program shader. Meskipun dapat berupa salah satu dari berbagai kemungkinan string, praktik terbaiknya adalah menggunakan string seperti POSITION atau COLOR yang menunjukkan penggunaan. Anda menetapkan semantik ini saat membuat buffer konstanta atau tata letak input. Anda juga dapat menambahkan angka antara 0 dan 7 ke semantik sehingga Anda menggunakan register terpisah untuk nilai serupa. Misalnya: COLOR0, COLOR1, COLOR2...

Semantik yang diawali dengan "SV_" adalah semantik nilai sistem yang ditulis oleh program shader Anda; game Anda sendiri (berjalan pada CPU) tidak dapat memodifikasinya. Biasanya, semantik ini berisi nilai yang merupakan input atau output dari tahap shader lain dalam alur grafis, atau yang dihasilkan sepenuhnya oleh GPU.

Selain itu, SV_ semantik memiliki perilaku yang berbeda ketika digunakan untuk menentukan input ke atau output dari tahap shader. Misalnya, SV_POSITION (output) berisi data vertex yang diubah selama tahap shader vertex, dan SV_POSITION (input) berisi nilai posisi piksel yang diinterpolasi oleh GPU selama tahap rasterisasi.

Berikut adalah beberapa semantik HLSL umum:

  • POSITION(n) untuk data buffer vertex. SV_POSITION menyediakan posisi piksel ke shader piksel dan tidak dapat ditulis oleh permainan Anda.
  • NORMAL(n) untuk data normal yang disediakan oleh buffer vertex.
  • TEXCOORD(n) untuk data koordinat UV tekstur yang disediakan ke shader.
  • COLOR(n) untuk data warna RGBA yang disediakan ke shader. Perhatikan bahwa diperlakukan secara identik dengan mengoordinasikan data, termasuk menginterpolasi nilai selama rasterisasi; semantik hanya membantu Anda mengidentifikasi bahwa itu adalah data warna.
  • SV_Target[n] untuk menulis dari shader piksel ke tekstur target atau buffer piksel lainnya.

Kita akan melihat beberapa contoh semantik HLSL saat meninjau contohnya.

Membaca dari buffer konstanta

Shader apa pun dapat membaca dari buffer konstan jika buffer tersebut dilampirkan ke tahapnya sebagai sumber daya. Dalam contoh ini, hanya shader vertex yang diberi buffer konstanta.

Buffer konstanta dideklarasikan di dua tempat: dalam kode C++, dan dalam file HLSL yang sesuai yang akan mengaksesnya.

Berikut adalah bagaimana struct buffer konstanta dideklarasikan dalam kode C++.

typedef struct _constantBufferStruct {
    DirectX::XMFLOAT4X4 world;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;

Saat mendeklarasikan struktur untuk buffer konstanta dalam kode C++Anda, pastikan bahwa semua data diselaraskan dengan benar sepanjang batas 16 byte. Cara term mudah untuk melakukan ini adalah dengan menggunakan jenis DirectXMath , seperti XMFLOAT4 atau XMFLOAT4X4, seperti yang terlihat dalam kode contoh. Anda juga dapat melindungi dari buffer yang tidak sejajar dengan menyatakan pernyataan statis:

// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");

Baris kode ini akan menyebabkan kesalahan pada waktu kompilasi jika ConstantBufferStruct tidak selaras dengan 16 byte. Untuk informasi selengkapnya tentang perataan dan pengemasan buffer konstan, lihat Aturan Pengemasan untuk Variabel Konstanta.

Sekarang, berikut adalah bagaimana buffer konstanta dideklarasikan dalam vertex shader HLSL.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix mWorld;      // world matrix for object
    matrix View;        // view matrix
    matrix Projection;  // projection matrix
};

Semua buffer—konstanta, tekstur, sampler, atau lainnya—harus memiliki register yang ditentukan sehingga GPU dapat mengaksesnya. Setiap tahap shader memungkinkan hingga 15 buffer konstanta, dan setiap buffer dapat menampung hingga 4.096 variabel konstan. Sintaksis deklarasi penggunaan register adalah sebagai berikut:

  • b*#*: Daftar untuk buffer konstanta (cbuffer).
  • t*#*: Register untuk buffer tekstur (tbuffer).
  • s*#*: Register untuk sampler. (Sampler menentukan perilaku pencarian untuk texel di sumber daya tekstur.)

Misalnya, HLSL untuk shader piksel mungkin mengambil tekstur dan sampler sebagai input dengan deklarasi seperti ini.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

Terserah Anda untuk menetapkan buffer konstanta untuk mendaftar—saat Anda menyiapkan alur, Anda melampirkan buffer konstan ke slot yang sama dengan yang Anda tetapkan dalam file HLSL. Misalnya, dalam topik sebelumnya panggilan ke VSSetConstantBuffers menunjukkan '0' untuk parameter pertama. Itu memberi tahu Direct3D untuk melampirkan sumber daya buffer konstanta untuk mendaftarkan 0, yang cocok dengan penugasan buffer untuk mendaftar (b0) dalam file HLSL.

Membaca dari buffer vertex

Buffer vertex memasok data segitiga untuk objek adegan ke shader vertex. Seperti halnya buffer konstanta, struct buffer vertex dideklarasikan dalam kode C++, menggunakan aturan pengemasan yang sama.

typedef struct _vertexPositionColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 color;
} VertexPositionColor;

Tidak ada format standar untuk data vertex di Direct3D 11. Sebaliknya, kita menentukan tata letak data vertex kita sendiri menggunakan deskriptor; bidang data ditentukan menggunakan array struktur D3D11_INPUT_ELEMENT_DESC . Di sini, kami menunjukkan tata letak input sederhana yang menjelaskan format vertex yang sama dengan struktur sebelumnya:

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

Jika Anda menambahkan data ke format vertex saat memodifikasi kode contoh, pastikan untuk memperbarui tata letak input juga, atau shader tidak akan dapat menafsirkannya. Anda dapat mengubah tata letak puncak seperti ini:

typedef struct _vertexPositionColorTangent
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;

Dalam hal ini, Anda akan memodifikasi definisi input-layout sebagai berikut.

D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
    { "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 },

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

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

Masing-masing definisi elemen input-layout diawali dengan string, seperti "POSITION" atau "NORMAL"—yaitu semantik yang kita bahas sebelumnya dalam topik ini. Ini seperti handel yang membantu GPU mengidentifikasi elemen tersebut saat memproses vertex. Pilih nama umum yang bermakna untuk elemen vertex Anda.

Sama seperti buffer konstan, shader vertex memiliki definisi buffer yang sesuai untuk elemen vertex masuk. (Itulah sebabnya kami memberikan referensi ke sumber daya shader vertex saat membuat tata letak input - Direct3D memvalidasi tata letak data per vertex dengan struct input shader.) Perhatikan bagaimana semantik cocok antara definisi tata letak input dan deklarasi buffer HLSL ini. Namun, COLOR memiliki "0" yang ditambahkan ke dalamnya. Tidak perlu menambahkan 0 jika Anda hanya memiliki satu elemen yang COLOR dideklarasikan dalam tata letak, tetapi ini adalah praktik yang baik untuk menambahkannya jika Anda memilih untuk menambahkan lebih banyak elemen warna di masa mendatang.

struct VS_INPUT
{
    float3 vPos   : POSITION;
    float3 vColor : COLOR0;
};

Meneruskan data antar shader

Shader mengambil jenis input dan mengembalikan jenis output dari fungsi utamanya setelah eksekusi. Untuk shader vertex yang ditentukan di bagian sebelumnya, jenis input adalah struktur VS_INPUT, dan kami menentukan tata letak input yang cocok dan struktur C++. Array struct ini digunakan untuk membuat buffer vertex dalam metode CreateCube .

Shader vertex mengembalikan struktur PS_INPUT, yang minimal harus berisi posisi puncak akhir 4 komponen (float4). Nilai posisi ini harus memiliki nilai sistem semantik, SV_POSITION, dideklarasikan untuk itu sehingga GPU memiliki data yang diperlukan untuk melakukan langkah gambar berikutnya. Perhatikan bahwa tidak ada korespondensi 1:1 antara output shader vertex dan input shader piksel; shader vertex mengembalikan satu struktur untuk setiap puncak yang diberikannya, tetapi shader piksel berjalan sekali untuk setiap piksel. Itu karena data per vertex pertama kali melewati tahap rasterisasi. Tahap ini memutuskan piksel mana yang "mencakup" geometri yang Anda gambar, menghitung data terinterpolasi per vertex untuk setiap piksel, lalu memanggil shader piksel sekali untuk masing-masing piksel tersebut. Interpolasi adalah perilaku default saat memerkosterisasi nilai output, dan sangat penting khususnya untuk pemrosesan data vektor output yang benar (vektor cahaya, normal per puncak dan tangen, dan lainnya).

struct PS_INPUT
{
    float4 Position : SV_POSITION;  // interpolated vertex position (system value)
    float4 Color    : COLOR0;       // interpolated diffuse color
};

Tinjau shader puncak

Contoh shader vertex sangat sederhana: mengambil puncak (posisi dan warna), mengubah posisi dari koordinat model menjadi koordinat yang diproyeksikan perspektif, dan mengembalikannya (bersama dengan warna) ke rasterizer. Perhatikan bahwa nilai warna diinterpolasi tepat bersama dengan data posisi, memberikan nilai yang berbeda untuk setiap piksel meskipun shader vertex tidak melakukan perhitungan apa pun pada nilai warna.

VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
    VS_OUTPUT Output;

    float4 pos = float4(input.vPos, 1.0f);

    // Transform the position from object space to homogeneous projection space
    pos = mul(pos, mWorld);
    pos = mul(pos, View);
    pos = mul(pos, Projection);
    Output.Position = pos;

    // Just pass through the color data
    Output.Color = float4(input.vColor, 1.0f);

    return Output;
}

Shader vertex yang lebih kompleks, seperti yang menyiapkan simpul objek untuk bayangan Phong, mungkin terlihat lebih seperti ini. Dalam hal ini, kami memanfaatkan fakta bahwa vektor dan normal diinterpolasi ke perkiraan permukaan yang tampak halus.

// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

cbuffer LightConstantBuffer : register(b1)
{
    float4 lightPos;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
};

// Per-pixel color data passed through the pixel shader.

struct PixelShaderInput
{
    float4 position : SV_POSITION; 
    float3 outVec : POSITION0;
    float3 outNormal : NORMAL0;
    float3 outLightVec : POSITION1;
};

PixelShaderInput main(VertexShaderInput input)
{
    // Inefficient -- doing this only for instruction. Normally, you would
 // premultiply them on the CPU and place them in the cbuffer.
    matrix mvMatrix = mul(model, view);
    matrix mvpMatrix = mul(mvMatrix, projection);

    PixelShaderInput output;

    float4 pos = float4(input.pos, 1.0f);
    float4 normal = float4(input.normal, 1.0f);
    float4 light = float4(lightPos.xyz, 1.0f);

    // 
    float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);

    // Transform the vertex position into projected space.
    output.gl_Position = mul(pos, mvpMatrix);
    output.outNormal = mul(normal, mvMatrix).xyz;
    output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
    output.outLightVec = mul(light, mvMatrix).xyz;

    return output;
}

Meninjau shader piksel

Shader piksel dalam contoh ini sangat mungkin merupakan jumlah minimum absolut kode yang dapat Anda miliki dalam shader piksel. Dibutuhkan data warna piksel terinterpolasi yang dihasilkan selama rasterisasi dan mengembalikannya sebagai output, di mana data akan ditulis ke target render. Membosankan!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

Bagian pentingnya adalah SV_TARGET semantik nilai sistem pada nilai yang dikembalikan. Ini menunjukkan bahwa output akan ditulis ke target render utama, yang merupakan buffer tekstur yang disediakan ke rantai pertukaran untuk ditampilkan. Ini diperlukan untuk shader piksel - tanpa data warna dari shader piksel, Direct3D tidak akan memiliki apa pun untuk ditampilkan!

Contoh shader piksel yang lebih kompleks untuk melakukan bayangan Phong mungkin terlihat seperti ini. Karena vektor dan normal diinterpolasi, kita tidak perlu menghitungnya berdasarkan per piksel. Namun, kita harus menormalkannya kembali karena cara kerja interpolasi; secara konseptual, kita perlu secara bertahap "memutar" vektor dari arah di puncak A ke arah puncak B, mempertahankan panjangnya — interpolasi wheras malah memotong garis lurus antara dua titik akhir vektor.

cbuffer MaterialConstantBuffer : register(b2)
{
    float4 lightColor;
    float4 Ka;
    float4 Kd;
    float4 Ks;
    float4 shininess;
};

struct PixelShaderInput
{
    float4 position : SV_POSITION;
    float3 outVec : POSITION0;
    float3 normal : NORMAL0;
    float3 light : POSITION1;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 L = normalize(input.light);
    float3 V = normalize(input.outVec);
    float3 R = normalize(reflect(L, input.normal));

    float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
    diffuse = saturate(diffuse);

    float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
    specular = saturate(specular);

    float4 finalColor = diffuse + specular;

    return finalColor;
}

Dalam contoh lain, shader piksel mengambil buffer konstannya sendiri yang berisi informasi cahaya dan material. Tata letak input dalam shader puncak akan diperluas untuk menyertakan data normal, dan output dari shader vertex tersebut diharapkan mencakup vektor yang diubah untuk vertex, cahaya, dan vertex normal dalam tampilan sistem koordinat.

Jika Anda memiliki buffer tekstur dan sampler dengan register yang ditetapkan (t dan s, masing-masing), Anda juga dapat mengaksesnya di shader piksel.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    float3 lightDirection = normalize(float3(1, -1, 0));
    float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
    float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
    return texelColor * lightMagnitude;
}

Shader adalah alat yang sangat kuat yang dapat digunakan untuk menghasilkan sumber daya prosedural seperti peta bayangan atau tekstur kebisingan. Bahkan, teknik canggih mengharuskan Anda memikirkan tekstur secara lebih abstrak, bukan sebagai elemen visual tetapi sebagai buffer. Mereka menyimpan data seperti informasi tinggi, atau data lain yang dapat diambil sampelnya di pass shader piksel akhir atau dalam bingkai tertentu sebagai bagian dari pass efek multi-tahap. Multi-pengambilan sampel adalah alat yang ampuh dan tulang punggung dari banyak efek visual modern.

Langkah berikutnya

Semoga Anda nyaman dengan DirectX 11di titik ini dan siap untuk mulai mengerjakan proyek Anda. Berikut adalah beberapa tautan untuk membantu menjawab pertanyaan lain yang mungkin Anda miliki tentang pengembangan dengan DirectX dan C++:

Bekerja dengan sumber daya perangkat DirectX

Memahami alur penyajian Direct3D 11