Apa lapisan abstraksi yang lebih baik untuk manajemen data D3D9 dan OpenGL?

8

Kode rendering saya selalu OpenGL. Saya sekarang perlu mendukung platform yang tidak memiliki OpenGL, jadi saya harus menambahkan lapisan abstraksi yang membungkus OpenGL dan Direct3D 9. Saya akan mendukung Direct3D 11 nanti.

TL; DR: perbedaan antara OpenGL dan Direct3D menyebabkan redundansi untuk programmer, dan tata letak data terasa serpihan.

Untuk saat ini, API saya bekerja sedikit seperti ini. Beginilah cara shader dibuat:

Shader *shader = Shader::Create(
    " ... GLSL vertex shader ... ", " ... GLSL pixel shader ... ",
    " ... HLSL vertex shader ... ", " ... HLSL pixel shader ... ");
ShaderAttrib a1 = shader->GetAttribLocation("Point", VertexUsage::Position, 0);
ShaderAttrib a2 = shader->GetAttribLocation("TexCoord", VertexUsage::TexCoord, 0);
ShaderAttrib a3 = shader->GetAttribLocation("Data", VertexUsage::TexCoord, 1);
ShaderUniform u1 = shader->GetUniformLocation("WorldMatrix");
ShaderUniform u2 = shader->GetUniformLocation("Zoom");

Sudah ada masalah di sini: begitu Direct3D shader dikompilasi, tidak ada cara untuk menanyakan atribut input dengan namanya; rupanya hanya semantik yang tetap bermakna. Inilah sebabnya mengapa GetAttribLocationada argumen ekstra ini, yang disembunyikan di ShaderAttrib.

Sekarang ini adalah bagaimana saya membuat deklarasi vertex dan dua buffer vertex:

VertexDeclaration *decl = VertexDeclaration::Create(
        VertexStream<vec3,vec2>(VertexUsage::Position, 0,
                                VertexUsage::TexCoord, 0),
        VertexStream<vec4>(VertexUsage::TexCoord, 1));

VertexBuffer *vb1 = new VertexBuffer(NUM * (sizeof(vec3) + sizeof(vec2));
VertexBuffer *vb2 = new VertexBuffer(NUM * sizeof(vec4));

Masalah lain: informasi VertexUsage::Position, 0tersebut sama sekali tidak berguna untuk backend OpenGL / GLSL karena tidak peduli dengan semantik.

Setelah buffer vertex diisi atau diarahkan ke data, ini adalah kode rendering:

shader->Bind();
shader->SetUniform(u1, GetWorldMatrix());
shader->SetUniform(u2, blah);
decl->Bind();
decl->SetStream(vb1, a1, a2);
decl->SetStream(vb2, a3);
decl->DrawPrimitives(VertexPrimitive::Triangle, NUM / 3);
decl->Unbind();
shader->Unbind();

Anda melihat bahwa declitu sedikit lebih dari sekedar deklarasi vertex D3D-like, itu agak mengurus rendering juga. Apakah ini masuk akal? Apa yang akan menjadi desain yang lebih bersih? Atau sumber inspirasi yang bagus?

sam hocevar
sumber
Versi OpenGL apa yang Anda targetkan?
Nicol Bolas
@NicolBolas sampai sekarang saya menggunakan OpenGL 2.1 dan OpenGL ES 2.0 dan saya berencana untuk mendukung OpenGL 3.3 atau 4.0, tetapi saya belum memutuskan apakah saya akan menghentikan dukungan untuk versi sebelumnya. Masalah saya saat ini adalah bahwa saya juga menggunakan subset dari OpenGL tua di PS3, yang suboptimal melainkan nyaman ...
sam Hocevar
Anda mungkin sudah mengetahui hal ini, tetapi periksa sumber Ogre untuk melihat bagaimana mereka melakukannya ogre3d.org
Aralox
4
@Aralox: OGRE adalah kekacauan yang dipenuhi oleh Singleton dan saya tidak akan pernah menyarankan siapa pun untuk mengikuti desain mereka.
DeadMG

Jawaban:

8

Pada dasarnya Anda mengalami situasi yang membuat NVIDIA Cg menjadi perangkat lunak yang sangat menarik (selain fakta bahwa NVIDIA Cg tidak mendukung GL | ES, yang Anda katakan sedang Anda gunakan).

Perhatikan juga bahwa Anda seharusnya tidak menggunakan glGetAttribLocation. Fungsi itu adalah juju buruk dari hari-hari awal GLSL sebelum orang-orang yang bertanggung jawab atas GL benar-benar mulai memahami bagaimana bahasa shading yang baik harus bekerja. Ini tidak ditinggalkan karena memiliki penggunaan sesekali, tetapi secara umum, lebih suka glBindAttibLocation atau ekstensi lokasi atribut eksplisit (inti dalam GL 3.3+).

Menangani perbedaan dalam bahasa shader sejauh ini merupakan bagian tersulit dari perangkat lunak porting antara GL dan D3D. Masalah API yang Anda hadapi mengenai definisi tata letak simpul juga dapat dilihat sebagai masalah bahasa shader, karena versi GLSL sebelum 3.30 tidak mendukung lokasi atribut eksplisit (serupa dengan atribut semantik di HLSL) dan versi GLSL sebelum 4.10 iirc tidak mendukung binding seragam eksplisit.

Pendekatan "terbaik" adalah memiliki pustaka bahasa tingkat tinggi dan format data shading yang merangkum paket shader Anda. JANGAN hanya memberi makan sekelompok GLSL / HLSL mentah ke kelas Shader yang tipis dan berharap untuk dapat menghasilkan segala jenis API yang waras.

Sebaliknya, letakkan shader Anda ke dalam file. Bungkus mereka dalam sedikit meta-data. Anda bisa menggunakan XML, dan menulis paket shader seperti:

<shader name="bloom">
  <profile type="glsl" version="1.30">
    <source type="vertex"><![CDATA[
      glsl vertex shader code goes here
    ]]></source>
    <source type="fragment"><![CDATA[
      glsl fragment shader code goes here
    ]]></source>
  </profile>
  <profile type="hlsl" version="sm3">
    <source type="fx"><![CDATA[
      hlsl effects code goes here
      you could also split up the source elements for hlsl
    ]]></source>
  </profile>
</shader>

Menulis parser minimal untuk itu sepele (cukup gunakan TinyXML misalnya). Biarkan pustaka shader Anda memuat paket itu, pilih profil yang sesuai untuk target renderer Anda saat ini, dan kompilasi shader.

Perhatikan juga bahwa jika Anda mau, Anda dapat menyimpan sumber di luar definisi shader, tetapi masih memiliki file. Masukkan saja nama file alih-alih sumber ke elemen sumber. Ini mungkin bermanfaat jika Anda berencana mengkompilasi shader, misalnya.

Bagian yang sulit sekarang tentu saja berurusan dengan GLSL dan kekurangannya. Masalahnya adalah Anda harus mengikat lokasi atribut ke sesuatu yang mirip dengan semantik HLSL. Ini dapat dilakukan dengan mendefinisikan semantik tersebut di API Anda dan kemudian menggunakan glBindAttribLocation sebelum menautkan profil GLSL. Kerangka paket shader Anda dapat menangani hal ini secara eksplisit, sama sekali tidak perlu API grafik Anda untuk mengekspos detailnya.

Anda dapat melakukannya dengan memperluas format XML di atas dengan beberapa elemen baru di profil GLSL untuk secara eksplisit menentukan lokasi atribut, misalnya

<shader name="bloom">
  <profile type="glsl" version="1.30">
    <attrib name="inPosition" semantic="POSITION"/>
    <attrib name="inColor" semantic="COLOR0"/>
    <source type="vertex"><![CDATA[
      #version 150
      in vec4 inPosition;
      in vec4 inColor;

      out vec4 vColor;

      void main() {
        vColor = inColor;
        gl_Position = position;
      }
    ]]></source>
  </profile>
</shader>

Kode paket shader Anda akan membaca semua elemen attrib di XML, ambil nama dan semantik darinya, cari indeks atribut yang telah ditentukan untuk setiap semantik, dan kemudian secara otomatis panggil glBindAttribLocation untuk Anda saat menautkan shader.

Hasil akhirnya adalah bahwa API Anda sekarang dapat terasa jauh lebih baik daripada kode GL lama Anda yang mungkin pernah terlihat, dan bahkan sedikit lebih bersih daripada D3D11 akan memungkinkan:

// simple example, easily improved
VertexLayout layout = api->createLayout();
layout.bind(gfx::POSITION, buffer0, gfx::FLOATx4, sizeof(Vertex), offsetof(Vertex, position));
layout.bind(gfx::COLOR0, buffer0, gfx::UBYTEx4, sizeof(Vertex), offsetof(Vertex, color));

Perhatikan juga bahwa Anda tidak benar - benar membutuhkan format paket shader. Jika Anda ingin menjaga hal-hal sederhana, Anda bebas hanya memiliki jenis fungsi loadShader (const char * name) yang secara otomatis mengambil nama.vs dan name.fs file GLSL dalam mode GL dan mengkompilasi dan menautkannya. Namun, Anda benar-benar menginginkan metadata atribut itu. Dalam kasus sederhana, Anda dapat menambah kode GLSL Anda dengan komentar khusus yang mudah diurai, seperti:

#version 150

/// ATTRIB(inPosition,POSITION)
in vec4 inPosition;
/// ATTRIB(inColor,COLOR0)
in vec4 inColor;

out vec4 vColor

void main() {
  vColor = inColor;
  gl_Position = inPosition;
}

Anda bisa menjadi semewah yang Anda inginkan dengan menguraikan komentar. Lebih dari beberapa mesin profesional akan melangkah lebih jauh untuk membuat ekstensi bahasa kecil yang mereka parsing dan modifikasi, bahkan, seperti hanya menambahkan deklarasi semantik gaya HLSL secara langsung. Jika pengetahuan Anda tentang parsing kuat, Anda harus dapat menemukan deklarasi yang diperluas itu, mengekstrak informasi tambahan, dan kemudian mengganti teks dengan kode yang kompatibel dengan GLSL.

Tidak peduli bagaimana Anda melakukannya, versi singkatnya adalah untuk menambah GLSL Anda dengan informasi semantik atribut yang hilang dan meminta abstraksi loader shader Anda dengan memanggil glBindAttribLocation untuk memperbaiki keadaan dan menjadikannya lebih seperti versi GLSL modern dan HLSL yang mudah dan efisien.

Sean Middleditch
sumber
Terima kasih atas jawaban yang sangat komprehensif. Saran tambahan tentang komentar semantik itu sederhana namun sangat masuk akal!
sam hocevar
Saya akhirnya menerima jawaban Anda, bahkan jika orang lain terbukti sangat membantu. Saya menghabiskan banyak waktu untuk merenungkan bagaimana melakukannya dengan benar, dan akhirnya saya menulis parser GLSL / HLSL lengkap yang membantu saya meniru lokasi atribut eksplisit ketika tidak didukung.
sam hocevar
5

Pertama, saya sarankan menggunakan VertexBuffer<T>untuk meningkatkan keamanan tipe, tetapi kedua, saya pikir perbedaan antara kedua API terlalu banyak pada level ini. Saya pribadi akan sepenuhnya merangkum penyaji di belakang antarmuka yang tidak berurusan dengan hal-hal seperti deklarasi vertex atau pengaturan atribut shader.

DeadMG
sumber
Diperbantukan; lapisan abstraksi Anda saat ini berada pada level yang terlalu rendah dan perlu lebih tinggi untuk benar-benar dapat mengatasi perbedaan API.
Maximus Minimus
2

Secara pribadi, saya akan membuat (dan menegakkan) konvensi standar untuk indeks atribut. Indeks GL 0 adalah posisi. Indeks GL 1 adalah warnanya. Indeks 2 adalah normal, dengan 3 dan 4 untuk garis singgung dan binormal (jika perlu). Indeks 5-7 adalah koordinat tekstur. Mungkin 8 dan 9 untuk bobot tulang. 10 dapat menjadi warna kedua jika perlu. Jika Anda tidak dapat menggunakan GL_ARB_explicit_attrib_locationatau GL 3.3+, maka Anda juga harus menetapkan penamaan atribut standar konvensi .

Dengan begitu, D3D memiliki konvensi dan OpenGL memiliki konvensi. Jadi pengguna bahkan tidak perlu bertanya apa indeks "posisi" itu; mereka tahu itu 0. Dan abstraksi Anda tahu bahwa 0 berarti, di tanah D3D VertexUsage::Position,.

Nicol Bolas
sumber