Bagikan melalui


Bekerja dengan shader dan sumber daya shader

Saatnya untuk mempelajari cara bekerja dengan shader dan sumber daya 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 shader piksel dan vertex.

Jika Anda tidak terbiasa dengan bahasa shader, diskusi cepat perlu diadakan. 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:

  • Vertex Shader—Dijalankan untuk setiap verteks di dalam adegan. Shader ini beroperasi pada elemen buffer vertex yang disediakan oleh aplikasi yang memanggilnya, dan secara minimal menghasilkan vektor posisi dengan 4 komponen yang akan dirasterisasi menjadi posisi piksel.
  • piksel shader—Dijalankan untuk setiap piksel dalam sasaran rendering. Shader ini menerima koordinat raster dari tahap shader sebelumnya (dalam alur 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 shader vertex dan piksel yang sangat dasar 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 menjadi terlalu banyak instruksi, maka tidak dapat dijalankan, dan kesalahan akan ditampilkan. (Perhatikan bahwa jumlah pasti instruksi yang diizinkan adalah bagian dari tingkat fitur Direct3D.)

Di Direct3D, shader tidak dikompilasi pada waktu proses; 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 dalam aplikasi Anda saat mengemasnya; mereka adalah aset seperti mesh dan tekstur.

Memahami semantik HLSL

Penting untuk meluangkan waktu sejenak untuk membahas semantik HLSL sebelum kami 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 mereka bisa menjadi 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 nilai sistem semantik yang ditulis oleh program shader Anda; permainan Anda sendiri (berjalan pada CPU) tidak dapat mengubahnya. 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, semantik SV_ 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 vertex buffer. SV_POSITION menyediakan posisi piksel ke shader piksel dan tidak dapat ditulis oleh game Anda.
  • NORMAL(n) untuk data normal yang disediakan oleh penyangga vertex.
  • TEXCOORD(n) untuk data koordinat UV tekstur yang disuplai ke shader.
  • COLOR(n) untuk data warna RGBA yang disediakan ke shader. Perhatikan bahwa diperlakukan sama persis dengan data koordinat, termasuk menginterpolasi nilai selama rasterisasi; penandaan ini 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 struktur buffer konstan dinyatakan 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 di 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 sejalan dengan mendeklarasikan 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 16 byte. Untuk informasi selengkapnya tentang perataan dan pengemasan buffer konstan, lihat Aturan Pengemasan untuk Variabel Konstanta.

Sekarang, berikut cara buffer konstan dinyatakan 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*#*: Daftar untuk buffer tekstur (tbuffer ).
  • s*#*: Daftar untuk sampler. (Sampler menentukan perilaku pencarian untuk texel dalam sumber daya tekstur.)

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

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

Terserah Anda untuk menetapkan buffer konstanta ke register—saat Anda menyiapkan alur pemrosesan, Anda melampirkan buffer konstanta 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 konstan ke register 0, yang sesuai dengan penugasan buffer ke register (b0) dalam file HLSL.

Baca dari buffer vertex

Buffer vertex menyediakan data segitiga untuk objek citra ke vertex shader. Seperti halnya buffer konstanta, struct buffer vertex dideklarasikan dalam kode C++ dengan menggunakan aturan pengemasan yang serupa.

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

Tidak ada format standar untuk data vertex di Direct3D 11. Sebagai gantinya, kami menentukan tata letak data vertex kami sendiri menggunakan deskriptor; bidang data ditentukan menggunakan array struktur D3D11_INPUT_ELEMENT_DESC. Di sini, kami menampilkan 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 mungkin 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 tata letak input 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 pegangan yang membantu GPU mengidentifikasi elemen tersebut saat memproses simpul. Pilih nama umum yang bermakna untuk elemen vertex Anda.

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

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 di metode CreateCube.

Shader vertex mengembalikan struktur PS_INPUT, yang minimal harus berisi posisi vertex 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 pixel shader; shader vertex mengembalikan satu struktur untuk setiap vertex 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 mem-rasterisasi nilai output, dan sangat penting khususnya untuk pemrosesan data vektor output yang benar (vektor cahaya, normal per verteks dan tangen, dan lainnya).

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

Tinjau vertex shader

Contoh shader vertex ini sangat sederhana: menerima sebuah vertex (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 mengatur simpul objek untuk penggunaan bayangan Phong, mungkin akan tampak lebih seperti ini. Dalam hal ini, kami memanfaatkan fakta bahwa vektor dan normal diinterpolasi untuk memperkirakan permukaan agar 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

Contoh shader piksel ini adalah jumlah minimum absolut kode yang bisa Anda miliki dalam sebuah shader piksel. Dibutuhkan data warna piksel terinterpolasi yang dihasilkan selama rasterisasi dan mengembalikannya sebagai output, di mana data tersebut akan ditulis ke target render. Membosankan!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

Bagian pentingnya adalah semantik nilai sistem SV_TARGET pada nilai yang dikembalikan. Ini menunjukkan bahwa output akan ditulis ke target render utama, yaitu buffer tekstur yang disediakan ke rantai swap untuk tampilan. 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, kami tidak perlu menghitungnya secara per piksel. Namun, kita harus menormalkannya kembali karena cara kerja interpolasi; secara konseptual, kita perlu secara bertahap "memutar" vektor dari arah pada vertex A ke arah pada vertex B, mempertahankan panjangnya—sedangkan interpolasi 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, perangkat shader piksel mengambil buffer konstannya sendiri yang berisi informasi cahaya dan material. Tata letak input dalam shader vertex akan diperluas untuk menyertakan data normal, dan output dari shader vertex tersebut diharapkan menyertakan vektor yang diubah untuk vertex, cahaya, dan vertex normal dalam sistem koordinat tampilan.

Jika Anda memiliki buffer tekstur dan sampler dengan register yang ditetapkan ( masing-masingt dan s), 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 disampel dalam pass shader piksel akhir atau dalam frame tertentu sebagai bagian dari tahapan efek multi-stage. Multi-sampling adalah alat yang kuat dan tulang punggung 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