مقدمة إلى OpenGL: برنامج تعليمي لعرض النص ثلاثي الأبعاد
نشرت: 2022-03-11مع توفر أدوات مثل DirectX و OpenGL ، فإن كتابة تطبيق سطح مكتب يعرض عناصر ثلاثية الأبعاد ليس بالأمر الصعب في الوقت الحاضر. ومع ذلك ، مثل العديد من التقنيات ، توجد أحيانًا عقبات تجعل من الصعب على المطورين الذين يحاولون الدخول في هذا المجال. بمرور الوقت ، تسبب السباق بين DirectX و OpenGL في زيادة إمكانية وصول المطورين إلى هذه التقنيات ، إلى جانب توثيق أفضل وعملية أسهل لتصبح مطورًا ماهرًا لـ DirectX أو OpenGL.
DirectX ، التي قدمتها Microsoft وصيانتها ، هي تقنية خاصة بمنصة Windows. من ناحية أخرى ، OpenGL عبارة عن واجهة برمجة تطبيقات متعددة المنصات لساحة الرسومات ثلاثية الأبعاد التي تحتفظ مجموعة Khronos بمواصفاتها.
في هذه المقدمة لبرنامج OpenGL ، سأشرح كيفية كتابة تطبيق بسيط للغاية لعرض نماذج نصية ثلاثية الأبعاد. سنستخدم Qt / Qt Creator لتنفيذ واجهة المستخدم ، مما يسهل تجميع هذا التطبيق وتشغيله على منصات متعددة. الكود المصدري للنموذج الأولي المصمم لهذه المقالة متاح على GitHub.
الهدف من هذا التطبيق البسيط هو إنشاء نماذج ثلاثية الأبعاد وحفظها في ملف بتنسيق بسيط وفتحها وعرضها على الشاشة. سيكون النموذج ثلاثي الأبعاد في المشهد المعروض قابلاً للدوران والتكبير ، لإعطاء إحساس أفضل بالعمق والأبعاد.
المتطلبات الأساسية
قبل البدء ، سنحتاج إلى تجهيز بيئة التطوير الخاصة بنا ببعض الأدوات المفيدة لهذا المشروع. أول شيء نحتاجه هو إطار عمل Qt والأدوات المساعدة ذات الصلة ، والتي يمكن تنزيلها من www.qt.io. قد يكون متاحًا أيضًا من خلال مدير الحزم القياسي لنظام التشغيل الخاص بك ؛ إذا كانت هذه هي الحالة ، فقد ترغب في تجربتها أولاً. تتطلب هذه المقالة بعض الإلمام بإطار عمل Qt. ومع ذلك ، إذا لم تكن معتادًا على إطار العمل ، فالرجاء ألا تشعر بالإحباط من المتابعة ، حيث يعتمد النموذج الأولي على بعض الميزات البسيطة للإطار.
يمكنك أيضًا استخدام Microsoft Visual Studio 2013 على نظام Windows. في هذه الحالة ، يرجى التأكد من استخدام Qt Addin المناسب لبرنامج Visual Studio.
في هذه المرحلة ، قد ترغب في استنساخ المستودع من GitHub ومتابعته أثناء قراءة هذه المقالة.
نظرة عامة على برنامج OpenGL
سنبدأ بإنشاء مشروع تطبيق Qt بسيط باستخدام عنصر واجهة مستخدم مستند واحد. نظرًا لأنها أداة بسيطة ، فلن ينتج عن تجميعها وتشغيلها أي شيء مفيد. مع Qt designer ، سنضيف قائمة "ملف" بأربعة عناصر: "جديد ..." ، "فتح ..." ، "إغلاق" ، و "خروج". يمكنك العثور على الكود الذي يربط عناصر القائمة هذه بالإجراءات المقابلة لها في المستودع.
سيؤدي النقر فوق "جديد ..." إلى ظهور مربع حوار منبثق سيبدو كالتالي:
هنا ، يمكن للمستخدم إدخال بعض النصوص واختيار خط وتعديل ارتفاع النموذج الناتج وإنشاء نموذج ثلاثي الأبعاد. سيؤدي النقر فوق "إنشاء" إلى حفظ النموذج ، كما يجب فتحه إذا اختار المستخدم الخيار المناسب من الزاوية اليسرى السفلية. كما يمكنك أن تقول ، الهدف هنا هو تحويل بعض النص الذي أدخله المستخدم إلى نموذج ثلاثي الأبعاد وعرضه على الشاشة.
سيكون للمشروع هيكل بسيط ، وسيتم تقسيم المكونات إلى عدد قليل من ملفات C ++ والملفات الرأسية:
كريتشرمودلدلج.ه / كب
تحتوي الملفات على كائن مشتق من QDialog. يقوم هذا بتنفيذ عنصر واجهة الحوار الذي يسمح للمستخدم بكتابة النص وتحديد الخط واختيار ما إذا كان سيحفظ النتيجة في ملف و / أو عرضها في شكل ثلاثي الأبعاد.
gl_widget.h / كب
يحتوي على تطبيق كائن مشتق QOpenGLWidget. تُستخدم هذه الأداة لعرض المشهد ثلاثي الأبعاد.
mainwindow.h / cpp
يحتوي على تطبيق القطعة الرئيسية للتطبيق. تم ترك هذه الملفات دون تغيير منذ إنشائها بواسطة معالج Qt Creator.
main.cpp
يحتوي على الوظيفة الرئيسية (...) ، التي تنشئ أداة التطبيق الرئيسية وتعرضها على الشاشة.
model2d_processing.h / cpp
يحتوي على وظيفة إنشاء مشهد ثنائي الأبعاد.
model3d.h / كب
يحتوي على هياكل تخزن كائنات النماذج ثلاثية الأبعاد وتسمح للعمليات بالعمل عليها (حفظ ، تحميل ، إلخ).
model_creator.h / cpp
يحتوي على تطبيق للفئة التي تسمح بإنشاء كائن نموذج مشهد ثلاثي الأبعاد.
تنفيذ OpenGL
للإيجاز ، سنتخطى التفاصيل الواضحة لتنفيذ واجهة المستخدم باستخدام Qt Designer ، والكود الذي يحدد سلوكيات العناصر التفاعلية. هناك بالتأكيد بعض الجوانب الأكثر إثارة للاهتمام في تطبيق النموذج الأولي هذا ، تلك الجوانب التي ليست مهمة فقط ولكنها ذات صلة أيضًا بتشفير النماذج ثلاثية الأبعاد وتقديمها التي نريد تغطيتها. على سبيل المثال ، تتضمن الخطوة الأولى لتحويل النص إلى نموذج ثلاثي الأبعاد في هذا النموذج الأولي تحويل النص إلى صورة أحادية اللون ثنائية الأبعاد. بمجرد إنشاء هذه الصورة ، من الممكن معرفة أي بكسل من الصورة يشكل النص ، وأي بكسل هو مجرد مساحة "فارغة". هناك بعض الطرق الأبسط لعرض النص الأساسي باستخدام OpenGL ، لكننا نتخذ هذا الأسلوب لتغطية بعض التفاصيل الدقيقة للعرض ثلاثي الأبعاد باستخدام OpenGL.
لإنشاء هذه الصورة ، نقوم بإنشاء مثيل لكائن QImage بعلامة QImage :: Format_Mono. نظرًا لأن كل ما نحتاج إلى معرفته هو وحدات البكسل التي تشكل جزءًا من النص وأيها ليست كذلك ، يجب أن تعمل الصورة أحادية اللون بشكل جيد. عندما يقوم المستخدم بإدخال بعض النصوص ، نقوم بتحديث كائن QImage هذا بشكل متزامن. استنادًا إلى حجم الخط وعرض الصورة ، نبذل قصارى جهدنا لملاءمة النص ضمن الارتفاع الذي يحدده المستخدم.
بعد ذلك ، نقوم بتعداد جميع وحدات البكسل التي تشكل جزءًا من النص - في هذه الحالة ، البكسل الأسود. يتم التعامل مع كل بكسل هنا كوحدات مربعة منفصلة. بناءً على ذلك ، يمكننا إنشاء قائمة بالمثلثات ، وحساب إحداثيات رؤوسها ، وتخزينها في ملف النموذج ثلاثي الأبعاد الخاص بنا.
الآن بعد أن أصبح لدينا تنسيق ملف نموذج ثلاثي الأبعاد بسيط خاص بنا ، يمكننا البدء في التركيز على تقديمه. بالنسبة للعرض ثلاثي الأبعاد المستند إلى OpenGL ، يوفر Qt عنصر واجهة مستخدم يسمى QOpenGLWidget. لاستخدام هذه الأداة ، قد يتم تجاوز ثلاث وظائف:
- initializeGl () - هذا هو المكان الذي يذهب إليه رمز التهيئة
- paintGl () - تسمى هذه الطريقة في كل مرة يتم فيها إعادة رسم عنصر واجهة المستخدم
- resizeGl (int w، int h) - تسمى هذه الطريقة بعرض عنصر واجهة المستخدم وارتفاعه في كل مرة يتم تغيير حجمها
سنقوم بتهيئة عنصر واجهة المستخدم من خلال ضبط تكوين التظليل المناسب في طريقة initializeGl.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
يجعل السطر الأول البرنامج يعرض فقط وحدات البكسل المعروضة الأقرب إلينا ، بدلاً من تلك الموجودة خلف وحدات البكسل الأخرى وبعيدًا عن الأنظار. يحدد السطر الثاني تقنية التظليل المسطح. السطر الثالث يجعل البرنامج يعرض مثلثات بغض النظر عن الاتجاه الذي تشير إليه الأعراف.
بمجرد التهيئة ، نقوم بعرض النموذج على الشاشة في كل مرة يتم فيها استدعاء paintGl. قبل تجاوز طريقة paintGl ، يجب أن نجهز المخزن المؤقت. للقيام بذلك ، نقوم أولاً بإنشاء مقبض مخزن مؤقت. ثم نربط المقبض بإحدى نقاط الربط ، وننسخ بيانات المصدر في المخزن المؤقت ، وأخبرنا البرنامج بفك ربط المخزن المؤقت:
// 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);
داخل طريقة paintGl المهيمنة ، نستخدم مصفوفة من الرؤوس ومجموعة من البيانات العادية لرسم المثلثات لكل إطار:

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);
لتحسين الأداء ، استخدمنا Vertex Buffer Object (VBO) في تطبيق النموذج الأولي الخاص بنا. يتيح لنا ذلك تخزين البيانات في ذاكرة الفيديو واستخدامها مباشرة للعرض. تتضمن الطريقة البديلة لذلك توفير البيانات (إحداثيات الرأس والقواعد والألوان) من رمز العرض:
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();
قد يبدو هذا وكأنه حل أبسط ؛ ومع ذلك ، فإن لها آثارًا خطيرة على الأداء ، حيث يتطلب ذلك انتقال البيانات عبر ناقل ذاكرة الفيديو - وهي عملية أبطأ نسبيًا. بعد تطبيق طريقة paintGl ، يجب الانتباه إلى التظليل:
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"));
باستخدام OpenGL ، يتم تنفيذ التظليل باستخدام لغة تعرف باسم GLSL. تم تصميم اللغة لتسهيل معالجة البيانات ثلاثية الأبعاد قبل عرضها. هنا ، سنحتاج إلى اثنين من التظليل: تظليل الرأس وتظليل الشظايا. في ظل الرأس ، سنقوم بتحويل الإحداثيات باستخدام مصفوفة التحويل لتطبيق التدوير والتكبير ولحساب اللون. في التظليل الجزئي ، سنخصص لونًا للجزء. يجب بعد ذلك تجميع برامج التظليل هذه وربطها بالسياق. يوفر OpenGL طرقًا بسيطة لربط البيئتين بحيث يمكن الوصول إلى المعلمات الموجودة داخل البرنامج أو تعيينها من الخارج:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
في كود تظليل الرأس ، نحسب موضع الرأس الجديد من خلال تطبيق مصفوفة التحويل على القمم الأصلية:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
لحساب مصفوفة التحويل هذه ، نحسب بعض المصفوفات المنفصلة: مقياس الشاشة ، وترجمة المشهد ، والقياس ، والتدوير ، والوسط. ثم نجد حاصل ضرب هذه المصفوفات لحساب مصفوفة التحويل النهائية. ابدأ بترجمة مركز النموذج إلى الأصل (0 ، 0 ، 0) ، وهو مركز الشاشة أيضًا. يتم تحديد الدوران من خلال تفاعل المستخدم مع المشهد باستخدام بعض أجهزة التأشير. يمكن للمستخدم النقر فوق المشهد والسحب حوله للتدوير. عندما ينقر المستخدم ، نقوم بتخزين موضع المؤشر ، وبعد الحركة يكون لدينا موضع المؤشر الثاني. باستخدام هذين الإحداثيين ، جنبًا إلى جنب مع مركز المشهد ، نشكل مثلثًا. باتباع بعض الحسابات البسيطة ، يمكننا تحديد زاوية الدوران ، ويمكننا تحديث مصفوفة الدوران لدينا لتعكس هذا التغيير. للقياس ، نعتمد ببساطة على عجلة الماوس لتعديل عامل التحجيم لمحور X و Y لعنصر واجهة OpenGL. تمت ترجمة النموذج مرة أخرى بمقدار 0.5 لإبقائه خلف المستوى الذي تم عرض المشهد منه. أخيرًا ، للحفاظ على نسبة العرض إلى الارتفاع الطبيعية ، نحتاج إلى ضبط تقليل تمدد النموذج على طول الجانب الأطول (على عكس مشهد OpenGL ، قد يكون للأداة التي يتم عرضها أبعاد مادية مختلفة على طول أي من المحورين). بدمج كل ذلك ، نحسب مصفوفة التحويل النهائية على النحو التالي:
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; }
خاتمة
في هذه المقدمة لعرض OpenGL 3D ، اكتشفنا إحدى التقنيات التي تسمح لـ ud باستخدام بطاقة الفيديو الخاصة بنا لتقديم نموذج ثلاثي الأبعاد. هذا أكثر كفاءة من استخدام دورات وحدة المعالجة المركزية لنفس الغرض. استخدمنا تقنية تظليل بسيطة للغاية ، وجعلنا المشهد تفاعليًا من خلال معالجة مدخلات المستخدم من الماوس. لقد تجنبنا استخدام ناقل ذاكرة الفيديو لتمرير البيانات ذهابًا وإيابًا بين ذاكرة الفيديو والبرنامج. على الرغم من أننا قدمنا للتو سطرًا واحدًا من النص ثلاثي الأبعاد ، إلا أنه يمكن عرض المشاهد الأكثر تعقيدًا بطرق متشابهة جدًا.
لكي نكون منصفين ، فإن هذا البرنامج التعليمي بالكاد خدش سطح النمذجة والعرض ثلاثي الأبعاد. هذا موضوع واسع ، ولا يمكن لبرنامج OpenGL التعليمي هذا أن يدعي أن هذا هو كل ما تحتاج إلى معرفته لتكون قادرًا على إنشاء ألعاب ثلاثية الأبعاد أو برامج تصميم. ومع ذلك ، فإن الغرض من هذه المقالة هو منحك نظرة خاطفة على هذا المجال ، وإظهار مدى سهولة بدء استخدام OpenGL لإنشاء تطبيقات ثلاثية الأبعاد.