Pengantar OpenGL: Tutorial Rendering Teks 3D

Diterbitkan: 2022-03-11

Dengan tersedianya alat seperti DirectX dan OpenGL, menulis aplikasi desktop yang merender elemen 3D tidak terlalu sulit saat ini. Namun, seperti banyak teknologi, terkadang ada kendala yang membuat developer kesulitan untuk masuk ke dalam ceruk ini. Seiring waktu, persaingan antara DirectX dan OpenGL telah menyebabkan teknologi ini menjadi lebih mudah diakses oleh pengembang, bersama dengan dokumentasi yang lebih baik dan proses yang lebih mudah untuk menjadi pengembang DirectX atau OpenGL yang terampil.

DirectX, diperkenalkan dan dikelola oleh Microsoft, adalah teknologi khusus untuk platform Windows. Di sisi lain, OpenGL adalah API lintas platform untuk arena grafis 3D yang spesifikasinya dikelola oleh Khronos Group.

pengenalan opengl

Dalam pengantar OpenGL ini, saya akan menjelaskan cara menulis aplikasi yang sangat sederhana untuk merender model teks 3D. Kami akan menggunakan Qt/Qt Creator untuk mengimplementasikan UI, sehingga memudahkan untuk mengkompilasi dan menjalankan aplikasi ini di berbagai platform. Kode sumber prototipe yang dibuat untuk artikel ini tersedia di GitHub.

Tujuan dari aplikasi sederhana ini adalah untuk menghasilkan model 3D, menyimpannya ke file dengan format sederhana, dan untuk membuka dan menampilkannya di layar. Model 3D dalam adegan yang dirender akan dapat diputar dan diperbesar, untuk memberikan kesan kedalaman dan dimensi yang lebih baik.

Prasyarat

Sebelum memulai, kita perlu mempersiapkan lingkungan pengembangan kita dengan beberapa alat yang berguna untuk proyek ini. Hal pertama yang kita butuhkan adalah kerangka kerja Qt dan utilitas yang relevan, yang dapat diunduh dari www.qt.io. Mungkin juga tersedia melalui manajer paket standar sistem operasi Anda; jika itu masalahnya, Anda mungkin ingin mencobanya terlebih dahulu. Artikel ini memerlukan beberapa keakraban dengan kerangka kerja Qt. Namun, jika Anda tidak terbiasa dengan kerangka kerja, jangan merasa putus asa untuk mengikutinya, karena prototipe bergantung pada beberapa fitur kerangka kerja yang cukup sepele.

Anda juga dapat menggunakan Microsoft Visual Studio 2013 di Windows. Dalam hal ini, pastikan Anda menggunakan Qt Addin yang sesuai untuk Visual Studio.

Pada titik ini, Anda mungkin ingin mengkloning repositori dari GitHub dan mengikutinya saat Anda membaca artikel ini.

Ikhtisar OpenGL

Kita akan mulai dengan membuat proyek aplikasi Qt sederhana dengan widget dokumen tunggal. Karena ini adalah widget sederhana, kompilasi dan menjalankannya tidak akan menghasilkan sesuatu yang berguna. Dengan Qt designer, kita akan menambahkan menu “File” dengan empat item: “New…”, “Open…”, “Close”, dan “Exit”. Anda dapat menemukan kode yang mengikat item menu ini ke tindakan yang sesuai di repositori.

Mengklik "Baru ..." akan memunculkan dialog yang akan terlihat seperti ini:

buka sembulan

Di sini, pengguna dapat memasukkan beberapa teks, memilih font, mengubah tinggi model yang dihasilkan, dan menghasilkan model 3D. Mengklik "Buat" akan menyimpan model, dan juga harus membukanya jika pengguna memilih opsi yang sesuai dari sudut kiri bawah. Seperti yang Anda tahu, tujuannya di sini adalah untuk mengubah beberapa teks yang dimasukkan pengguna menjadi model 3D dan menampilkannya di layar.

Proyek ini akan memiliki struktur sederhana, dan komponennya akan dipecah menjadi beberapa file C++ dan header:

c++ dan file header

createcharmodeldlg.h/cpp

File berisi objek turunan QDialog. Ini mengimplementasikan widget dialog yang memungkinkan pengguna mengetik teks, memilih font, dan memilih apakah akan menyimpan hasilnya ke dalam file dan/atau menampilkannya dalam 3D.

gl_widget.h/cpp

Berisi implementasi objek turunan QOpenGLWidget. Widget ini digunakan untuk membuat adegan 3D.

mainwindow.h/cpp

Berisi implementasi widget aplikasi utama. File-file ini dibiarkan tidak berubah sejak dibuat oleh wizard Qt Creator.

main.cpp

Berisi fungsi main(…), yang membuat widget aplikasi utama dan menampilkannya di layar.

model2d_processing.h/cpp

Berisi fungsionalitas pembuatan adegan 2D.

model3d.h/cpp

Berisi struktur yang menyimpan objek model 3D dan memungkinkan operasi untuk mengerjakannya (menyimpan, memuat, dll.).

model_creator.h/cpp

Berisi implementasi kelas yang memungkinkan pembuatan objek model adegan 3D.

Implementasi OpenGL

Untuk singkatnya, kami akan melewatkan detail yang jelas dari penerapan antarmuka pengguna dengan Qt Designer, dan kode yang mendefinisikan perilaku elemen interaktif. Tentu ada beberapa aspek yang lebih menarik dari aplikasi prototipe ini, yang tidak hanya penting tetapi juga relevan dengan pengkodean dan rendering model 3D yang ingin kita bahas. Misalnya, langkah pertama untuk mengonversi teks ke model 3D dalam prototipe ini melibatkan konversi teks ke gambar monokrom 2D. Setelah gambar ini dibuat, dimungkinkan untuk mengetahui piksel gambar mana yang membentuk teks, dan mana yang hanya ruang "kosong". Ada beberapa cara sederhana untuk merender teks dasar menggunakan OpenGL, tetapi kami menggunakan pendekatan ini untuk membahas beberapa detail seluk beluk rendering 3D dengan OpenGL.

Untuk menghasilkan gambar ini, kami membuat instance objek QImage dengan flag QImage::Format_Mono. Karena yang perlu kita ketahui hanyalah piksel mana yang merupakan bagian dari teks dan mana yang bukan, gambar monokrom akan berfungsi dengan baik. Ketika pengguna memasukkan beberapa teks, kami secara sinkron memperbarui objek QImage ini. Berdasarkan ukuran font dan lebar gambar, kami mencoba yang terbaik untuk menyesuaikan teks dengan tinggi yang ditentukan pengguna.

Selanjutnya, kami menghitung semua piksel yang merupakan bagian dari teks - dalam hal ini, piksel hitam. Setiap piksel di sini diperlakukan sebagai unit persegi yang terpisah. Berdasarkan ini, kami dapat membuat daftar segitiga, menghitung koordinat simpulnya, dan menyimpannya dalam file model 3D kami.

Sekarang kita memiliki format file model 3D sederhana kita sendiri, kita dapat mulai fokus pada renderingnya. Untuk rendering 3D berbasis OpenGL, Qt menyediakan widget bernama QOpenGLWidget. Untuk menggunakan widget ini, tiga fungsi dapat diganti:

  • initializeGl() - di sinilah kode inisialisasi berjalan
  • paintGl() - metode ini dipanggil setiap kali widget digambar ulang
  • resizeGl(int w, int h) - metode ini dipanggil dengan lebar dan tinggi widget setiap kali ukurannya diubah

format file model 3d

Kami akan menginisialisasi widget dengan mengatur konfigurasi shader yang sesuai dalam metode initializeGl.

 glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);

Baris pertama membuat program hanya menampilkan piksel yang dirender yang lebih dekat dengan kita, bukan piksel yang berada di belakang piksel lain dan tidak terlihat. Baris kedua menentukan teknik bayangan datar. Baris ketiga membuat program membuat segitiga terlepas dari arah mana normalnya menunjuk.

Setelah diinisialisasi, kami merender model pada tampilan setiap kali paintGl dipanggil. Sebelum kita mengganti metode paintGl, kita harus menyiapkan buffernya. Untuk melakukan itu, pertama-tama kita membuat pegangan buffer. Kami kemudian mengikat pegangan ke salah satu titik pengikatan, menyalin data sumber ke buffer, dan akhirnya kami memberi tahu program untuk melepaskan buffer:

 // Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);

Di dalam metode paintGl utama, kami menggunakan larik simpul dan larik data normal untuk menggambar segitiga untuk setiap bingkai:

 QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);

Untuk meningkatkan kinerja, kami menggunakan Vertex Buffer Object (VBO) dalam aplikasi prototipe kami. Ini memungkinkan kami menyimpan data dalam memori video dan menggunakannya secara langsung untuk rendering. Metode alternatif untuk ini melibatkan penyediaan data (koordinat titik, normal dan warna) dari kode rendering:

 glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();

Ini mungkin tampak seperti solusi yang lebih sederhana; namun, ini memiliki implikasi kinerja yang serius, karena ini memerlukan data untuk berjalan melalui bus memori video - proses yang relatif lebih lambat. Setelah menerapkan metode paintGl, kita harus memperhatikan shader:

 m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));

Dengan OpenGL, shader diimplementasikan menggunakan bahasa yang dikenal sebagai GLSL. Bahasa ini dirancang untuk memudahkan manipulasi data 3D sebelum dirender. Di sini, kita membutuhkan dua shader: vertex shader dan fragment shader. Dalam vertex shader, kita akan mengubah koordinat dengan matriks transformasi untuk menerapkan rotasi dan zoom, dan untuk menghitung warna. Dalam shader fragmen, kami akan menetapkan warna pada fragmen. Program shader ini kemudian harus dikompilasi dan dihubungkan dengan konteksnya. OpenGL menyediakan cara sederhana untuk menjembatani dua lingkungan sehingga parameter di dalam program dapat diakses atau ditetapkan dari luar:

 // Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);

Dalam kode shader simpul, kami menghitung posisi simpul baru dengan menerapkan matriks transformasi pada simpul asli:

 gl_Position = matrixVertex * vec4(coordVertexes, 1.0);

Untuk menghitung matriks transformasi ini, kami menghitung beberapa matriks terpisah: screen scale, translate scene, scale, rotate, dan center. Kami kemudian menemukan produk dari matriks ini untuk menghitung matriks transformasi akhir. Mulailah dengan menerjemahkan pusat model ke asal (0, 0, 0), yang juga merupakan pusat layar. Rotasi ditentukan oleh interaksi pengguna dengan adegan menggunakan beberapa alat penunjuk. Pengguna dapat mengklik adegan dan menyeretnya untuk memutar. Ketika pengguna mengklik, kami menyimpan posisi kursor, dan setelah gerakan kami memiliki posisi kursor kedua. Menggunakan dua koordinat ini, bersama dengan pusat pemandangan, kami membentuk segitiga. Mengikuti beberapa perhitungan sederhana, kita dapat menentukan sudut rotasi, dan kita dapat memperbarui matriks rotasi untuk mencerminkan perubahan ini. Untuk penskalaan, kami hanya mengandalkan roda mouse untuk memodifikasi faktor penskalaan sumbu X dan Y dari widget OpenGL. Model ditranslasikan kembali sebesar 0,5 agar tetap berada di belakang bidang tempat adegan dirender. Terakhir, untuk mempertahankan rasio aspek alami, kita perlu menyesuaikan penurunan perluasan model di sepanjang sisi yang lebih panjang (tidak seperti adegan OpenGL, widget tempat widget dirender mungkin memiliki dimensi fisik yang berbeda di sepanjang kedua sumbu). Menggabungkan semua ini, kami menghitung matriks transformasi akhir sebagai berikut:

 void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }

Kesimpulan

Dalam pengantar rendering 3D OpenGL ini, kami menjelajahi salah satu teknologi yang memungkinkan ud menggunakan kartu video kami untuk merender model 3D. Ini jauh lebih efisien daripada menggunakan siklus CPU untuk tujuan yang sama. Kami menggunakan teknik bayangan yang sangat sederhana, dan membuat adegan menjadi interaktif melalui penanganan input pengguna dari mouse. Kami menghindari penggunaan bus memori video untuk meneruskan data bolak-balik antara memori video dan program. Meskipun kita hanya merender satu baris teks dalam 3D, adegan yang lebih rumit dapat dirender dengan cara yang sangat mirip.

Agar adil, tutorial ini hampir tidak menggores permukaan pemodelan dan rendering 3D. Ini adalah topik yang luas, dan tutorial OpenGL ini tidak dapat mengklaim bahwa hanya ini yang perlu Anda ketahui untuk dapat membuat game 3D atau perangkat lunak pemodelan. Namun, tujuan artikel ini adalah untuk memberi Anda gambaran tentang dunia ini, dan menunjukkan betapa mudahnya Anda dapat memulai dengan OpenGL untuk membangun aplikasi 3D.