Render grafik 3D dengan OpenGL

Pendahuluan


Rendering grafik 3D bukanlah tugas yang mudah, tetapi sangat menarik dan mengasyikkan. Artikel ini diperuntukkan bagi mereka yang baru mulai berkenalan dengan OpenGL atau bagi mereka yang tertarik dengan cara kerja jalur pipa grafis dan seperti apa mereka. Artikel ini tidak akan memberikan instruksi yang tepat tentang cara membuat konteks dan jendela OpenGL, atau cara menulis aplikasi jendela OpenGL pertama Anda. Ini karena fitur dari setiap bahasa pemrograman dan pilihan pustaka atau kerangka kerja untuk bekerja dengan OpenGL (saya akan menggunakan C ++ dan GLFW ), terutama karena mudah untuk menemukan tutorial di jaringan untuk bahasa yang Anda minati. Semua contoh yang diberikan dalam artikel akan berfungsi dalam bahasa lain dengan semantik perintah yang sedikit berubah, mengapa demikian, saya akan ceritakan sedikit kemudian.

Apa itu OpenGL?


OpenGL adalah spesifikasi yang mendefinisikan antarmuka perangkat lunak platform-independen untuk menulis aplikasi menggunakan grafis komputer dua dimensi dan tiga dimensi. OpenGL bukan implementasi, tetapi hanya menjelaskan set instruksi yang harus diimplementasikan, yaitu adalah API.


Setiap versi OpenGL memiliki spesifikasi sendiri, kami akan bekerja dari versi 3.3 hingga versi 4.6, karena semua inovasi dari versi 3.3 memengaruhi aspek-aspek yang tidak terlalu penting bagi kami. Sebelum Anda mulai menulis aplikasi OpenGL pertama Anda, saya sarankan Anda mencari tahu versi yang didukung driver Anda (Anda dapat melakukan ini di situs vendor kartu video Anda) dan memperbarui driver ke versi terbaru.


Perangkat OpenGL


OpenGL dapat dibandingkan dengan mesin negara besar, yang memiliki banyak status dan fungsi untuk mengubahnya. Keadaan OpenGL pada dasarnya merujuk pada konteks OpenGL. Saat bekerja dengan OpenGL, kami akan melalui beberapa fungsi yang mengubah keadaan yang akan mengubah konteks, dan melakukan tindakan tergantung pada kondisi OpenGL saat ini.


Sebagai contoh, jika kita memberi OpenGL perintah untuk menggunakan baris alih-alih segitiga sebelum rendering, maka OpenGL akan menggunakan baris untuk semua rendering berikutnya sampai kita mengubah opsi ini atau mengubah konteks.


Objek di OpenGL


Pustaka OpenGL ditulis dalam C dan memiliki banyak API untuknya untuk bahasa yang berbeda, namun demikian pustaka C. Banyak konstruksi dari C tidak diterjemahkan ke bahasa tingkat tinggi, jadi OpenGL dikembangkan menggunakan sejumlah besar abstraksi, salah satu abstraksi ini adalah objek.


Objek di OpenGL adalah sekumpulan opsi yang menentukan statusnya. Objek apa pun di OpenGL dapat dijelaskan oleh id dan serangkaian opsi yang menjadi tanggung jawabnya. Tentu saja, setiap jenis objek memiliki opsi sendiri dan upaya untuk mengkonfigurasi opsi yang tidak ada untuk objek tersebut akan menyebabkan kesalahan. Disinilah letak ketidaknyamanan menggunakan OpenGL: seperangkat opsi dijelaskan dengan struktur yang mirip C, pengenal yang sering berupa angka, yang tidak memungkinkan programmer menemukan kesalahan pada tahap kompilasi, karena kode yang salah dan benar secara semantik tidak bisa dibedakan.


glGenObject(&objectId); glBindObject(GL_TAGRGET, objectId); glSetObjectOption(GL_TARGET, GL_CORRECT_OPTION, correct_option); //Ok glSetObjectOption(GL_TARGET, GL_WRONG_OPTION, wrong_option); //  , ..    

Anda akan menemukan kode seperti itu sangat sering, jadi ketika Anda terbiasa dengan apa itu seperti mengatur mesin negara, itu akan menjadi jauh lebih mudah bagi Anda. Kode ini hanya menunjukkan contoh cara kerja OpenGL. Selanjutnya, contoh nyata akan disajikan.


Tapi ada plusnya. Fitur utama dari objek-objek ini adalah bahwa kita dapat mendeklarasikan banyak objek dalam aplikasi kita, mengatur opsi-opsi mereka, dan setiap kali kita memulai operasi menggunakan keadaan OpenGL, kita hanya dapat mengikat objek dengan pengaturan pilihan kita. Misalnya, ini mungkin objek dengan data model 3D atau sesuatu yang ingin kita gambar pada model ini. Memiliki banyak objek memudahkan untuk beralih di antara mereka selama proses rendering. Dengan pendekatan ini, kita dapat mengonfigurasi banyak objek yang diperlukan untuk rendering dan menggunakan status mereka tanpa kehilangan waktu yang berharga di antara frame.


Untuk mulai bekerja dengan OpenGL Anda perlu berkenalan dengan beberapa objek dasar yang tanpanya kami tidak dapat menampilkan apa pun. Menggunakan objek-objek ini sebagai contoh, kita akan mengerti bagaimana cara mengikat data dan instruksi yang dapat dieksekusi di OpenGL.


Objek dasar: Program shader dan shader. =


Shader adalah program kecil yang berjalan pada akselerator grafis (GPU) pada titik tertentu dalam pipa grafis. Jika kita mempertimbangkan shader secara abstrak, kita dapat mengatakan bahwa ini adalah tahapan dari pipeline grafik, yang:

  1. Ketahui di mana mendapatkan data untuk diproses.
  2. Ketahui cara memproses input data.
  3. Mereka tahu di mana harus menulis data untuk diproses lebih lanjut.

Tapi seperti apa grafik pipa itu? Sangat sederhana, seperti ini:



Sejauh ini, dalam skema ini, kami hanya tertarik pada vertikal utama, yang dimulai dengan Spesifikasi Vertex dan berakhir dengan Frame Buffer. Seperti disebutkan sebelumnya, masing-masing shader memiliki parameter input dan output sendiri, yang berbeda dalam jenis dan jumlah parameter.
Kami jelaskan secara singkat setiap tahap pipa untuk memahami apa yang dilakukannya:

  1. Vertex Shader - diperlukan untuk memproses data koordinat 3D dan semua parameter input lainnya. Paling sering, shader vertex menghitung posisi verteks relatif terhadap layar, menghitung normals (jika perlu) dan menghasilkan data input ke shader lain.
  2. Tessellation shader dan tessellation control shader - kedua shader ini bertanggung jawab untuk merinci primitif yang berasal dari vertex shader dan menyiapkan data untuk diproses dalam shader geometrik. Sulit untuk menggambarkan apa yang mampu dilakukan oleh dua bayangan ini dalam dua kalimat, tetapi bagi pembaca untuk memiliki sedikit ide, saya akan memberikan beberapa gambar dengan tumpang tindih tingkat rendah dan tinggi:

    Saya menyarankan Anda untuk membaca artikel ini jika Anda ingin tahu lebih banyak tentang tessellation. Dalam seri artikel ini kita akan membahas tessellation, tetapi tidak akan segera.
  3. Geometris shader - bertanggung jawab atas pembentukan primitif geometri dari hasil shading tessellation. Dengan menggunakan shader geometris, Anda dapat membuat primitif baru dari primitif OpenGL dasar (GL_LINES, GL_POINT, GL_TRIANGLES, dll.), Misalnya, dengan menggunakan geometri shader, Anda dapat membuat efek partikel dengan menjelaskan partikel hanya dengan menggambarkan partikel hanya dengan warna, pusat cluster, jari-jari dan kepadatan.
  4. Shader rasterisasi adalah salah satu shader yang tidak dapat diprogram. Berbicara dalam bahasa yang dapat dimengerti, itu menerjemahkan semua primitif grafis keluaran menjadi fragmen (piksel), yaitu menentukan posisi mereka di layar.
  5. Shader fragmen adalah tahap terakhir dari pipa grafis. Shader fragmen menghitung warna fragmen (piksel) yang akan ditetapkan dalam buffer bingkai saat ini. Paling sering, dalam shader fragmen, bayangan dan pencahayaan fragmen, pemetaan tekstur dan peta normal dihitung - semua teknik ini memungkinkan Anda untuk mencapai hasil yang sangat indah.

OpenGL shaders ditulis dalam bahasa GLSL mirip C dari mana mereka dikompilasi dan dihubungkan ke program shader. Sudah pada tahap ini, tampaknya menulis program shader adalah tugas yang sangat memakan waktu, karena Anda perlu menentukan 5 langkah dari pipa grafis dan menghubungkannya bersama. Untungnya, ini tidak demikian: tessellation dan geometri shader didefinisikan dalam pipa grafis secara default, yang memungkinkan kita untuk mendefinisikan hanya dua shader - vertex dan shader fragmen (kadang-kadang disebut pixel shader). Yang terbaik untuk mempertimbangkan dua bayangan ini dengan contoh klasik:


Vertex shader
 #version 450 layout (location = 0) in vec3 vertexCords; layout (location = 1) in vec3 color; out vec3 Color; void main(){ gl_Position = vec4(vertexCords,1.0f) ; Color = color; } 


Shader fragmen
 #version 450 in vec3 Color; out vec4 out_fragColor; void main(){ out_fragColor = Color; } 


Contoh perakitan shader
 unsigned int vShader = glCreateShader(GL_SHADER_VERTEX); //    glShaderSource(vShader,&vShaderSource); //  glCompileShader(vShader); //  //        unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vShader); //    glAttachShader(shaderProgram, fShader); //    glLinkProgram(shaderProgram); //  


Dua shader sederhana ini tidak menghitung apa-apa, mereka hanya melewatkan data ke dalam pipa. Mari kita perhatikan bagaimana vertex dan fragmen shaders terhubung: di vertex shader, variabel Color dideklarasikan ke mana warna akan ditulis setelah fungsi utama dieksekusi, sementara di fragmen shader variabel yang sama persis dengan kualifikasi yang dinyatakan, yaitu. seperti yang dijelaskan sebelumnya, shader fragmen menerima data dari verteks dengan cara mendorong data lebih jauh melalui pipa (tetapi sebenarnya tidak begitu sederhana).

Catatan: Jika Anda tidak mendeklarasikan dan menginisialisasi variabel tipe vec4 di shader fragmen, maka tidak ada yang akan ditampilkan di layar.

Pembaca yang penuh perhatian telah memperhatikan deklarasi variabel input tipe vec3 dengan kualifikasi tata letak yang aneh di awal vertex shader, logis untuk menganggap bahwa ini adalah input, tetapi dari mana kita mendapatkannya?

Objek Dasar: Buffer dan Array Vertex


Saya pikir tidak ada gunanya menjelaskan objek buffer, sebaiknya kita mempertimbangkan cara membuat dan mengisi buffer di OpenGL.
 float vertices[] = { // // -0.8f, -0.8f, 0.0f, 1.0f, 0.0f, 0.0f, 0.8f, -0.8f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.8f, 0.0f, 0.0f, 0.0f, 1.0f }; unsigned int VBO; //vertex buffer object glGenBuffers(1,&VBO); glBindBuffer(GL_SOME_BUFFER_TARGET,VBO); glBufferData(GL_SOME_BUFFER_TARGET, sizeof(vertices), vertices, GL_STATIC_DRAW); 

Tidak ada yang sulit dalam hal ini, kami lampirkan buffer yang dihasilkan ke target yang diinginkan (nanti kita akan menemukan yang mana) dan memuat data yang menunjukkan ukuran dan jenis penggunaannya.


GL_STATIC_DRAW - data dalam buffer tidak akan diubah.
GL_DYNAMIC_DRAW - data dalam buffer akan berubah, tetapi tidak sering.
GL_STREAM_DRAW - data dalam buffer akan berubah setiap panggilan undian.

Sangat bagus, sekarang data kami terletak di memori GPU, program shader dikompilasi dan dihubungkan, tetapi ada satu peringatan: bagaimana program tahu di mana mendapatkan data input untuk vertex shader? Kami mengunduh data, tetapi tidak menunjukkan dari mana program shader akan mendapatkannya. Masalah ini diselesaikan dengan jenis objek OpenGL yang terpisah - array vertex.


gambar
Gambar diambil dari tutorial ini.

Seperti halnya buffer, array vertex paling baik dilihat menggunakan contoh konfigurasinya.


  unsigned int VBO, VAO; glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); //    glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //     () glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr); //     () glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), reinterpret_cast<void*> (sizeof(float) * 3)); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); 

Membuat array vertex tidak berbeda dengan membuat objek OpenGL lainnya, yang paling menarik dimulai setelah baris:

 glBindVertexArray(VAO); 
Vertex array (VAO) mengingat semua binding dan konfigurasi yang dilakukan dengannya, termasuk pengikatan objek buffer untuk pembongkaran data. Dalam contoh ini, hanya ada satu objek seperti itu, tetapi dalam praktiknya bisa ada beberapa. Setelah itu, atribut vertex dengan nomor tertentu dikonfigurasi:


  glBindBuffer(GL_ARRAY_BUFFER, VBO); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr); 

Dari mana kami mendapatkan nomor ini? Ingat kualifikasi tata letak untuk variabel input vertex shader? Merekalah yang menentukan atribut vertex mana variabel input akan terikat. Sekarang secara singkat membahas argumen fungsi sehingga tidak ada pertanyaan yang tidak perlu:
  1. Nomor atribut yang ingin kita konfigurasi.
  2. Jumlah barang yang ingin kami ambil. (Karena variabel input vertex shader dengan tata letak = 0 adalah tipe vec3, maka kita mengambil 3 elemen tipe float)
  3. Jenis barang.
  4. Apakah perlu untuk menormalkan elemen, jika itu adalah vektor.
  5. Offset untuk verteks berikutnya (karena kita memiliki koordinat dan warna terletak secara berurutan dan masing-masing memiliki tipe vec3, maka kita menggeser dengan 6 * sizeof (float) = 24 byte).
  6. Argumen terakhir menunjukkan offset apa yang harus diambil untuk verteks pertama. (untuk koordinat, argumen ini adalah 0 byte, untuk warna 12 byte)

Sekarang kita siap untuk membuat gambar pertama kita


Ingatlah untuk mengikat VAO dan program shader sebelum memohon render.
 { // your render loop glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES,0,3); //        } 


Jika Anda melakukan semuanya dengan benar, maka Anda harus mendapatkan hasil ini:



Hasilnya mengesankan, tetapi dari mana asal gradien terisi dalam segitiga, karena kami hanya mengindikasikan 3 warna: merah, biru dan hijau untuk masing-masing simpul individu? Ini adalah keajaiban shader rasterization: faktanya adalah bahwa nilai Color yang kita atur di vertex tidak masuk ke fragmen shader. Kami mentransmisikan hanya 3 simpul, tetapi lebih banyak fragmen yang dihasilkan (jumlah fragmen persis sama banyaknya dengan piksel yang diisi). Oleh karena itu, untuk setiap fragmen, rata-rata dari tiga nilai Warna diambil, tergantung pada seberapa dekat itu dengan masing-masing simpul. Ini sangat baik terlihat di sudut-sudut segitiga, di mana fragmen mengambil nilai warna yang kami tunjukkan dalam data vertex.

Ke depan, saya akan mengatakan bahwa koordinat tekstur ditransmisikan dengan cara yang sama, yang membuatnya mudah untuk overlay tekstur pada primitif kami.

Saya pikir ini layak untuk menyelesaikan artikel ini, yang paling sulit adalah di belakang kita, tetapi yang paling menarik baru saja dimulai. Jika Anda memiliki pertanyaan atau Anda melihat kesalahan dalam artikel, tulis tentang itu di komentar, saya akan sangat berterima kasih.


Pada artikel selanjutnya, kita akan melihat transformasi, belajar tentang variabel yang berbeda, dan belajar bagaimana memaksakan tekstur pada primitif.

Source: https://habr.com/ru/post/id467599/


All Articles