عرض رسومات ثلاثية الأبعاد باستخدام OpenGL

مقدمة


لا يمثل تقديم الرسومات ثلاثية الأبعاد مهمة سهلة ، ولكنه مثير للاهتمام ومثير للغاية. هذه المقالة مخصصة لأولئك الذين بدأوا للتو بالتعرف على OpenGL أو لأولئك المهتمين بكيفية عمل خطوط الأنابيب الرسومية وما هي عليه. لن تقدم هذه المقالة إرشادات دقيقة حول كيفية إنشاء سياق وإطار OpenGL ، أو كيفية كتابة أول تطبيق لإطار OpenGL. ويرجع ذلك إلى ميزات كل لغة برمجة واختيار مكتبة أو إطار عمل للعمل مع OpenGL ( سأستخدم C ++ و GLFW ) ، خاصة أنه من السهل العثور على برنامج تعليمي على الشبكة للغة التي تهتم بها. ستعمل جميع الأمثلة الواردة في المقالة بلغات أخرى مع دلالات تغيير بعض الشيء من الأوامر ، لماذا هذا هو الحال ، سأقول قليلا في وقت لاحق.

ما هو برنامج OpenGL؟


يعد OpenGL أحد المواصفات التي تحدد واجهة برنامج مستقلة عن النظام الأساسي لكتابة التطبيقات باستخدام رسومات الكمبيوتر ثنائية الأبعاد وثلاثية الأبعاد. OpenGL ليس تطبيقًا ، لكنه يصف فقط مجموعات التعليمات التي يجب تنفيذها ، أي هو API.


كل إصدار من OpenGL له مواصفاته الخاصة ، سنعمل من الإصدار 3.3 إلى الإصدار 4.6 ، لأنه جميع الابتكارات من الإصدار 3.3 تؤثر على جوانب ذات أهمية ضئيلة بالنسبة لنا. قبل أن تبدأ في كتابة أول تطبيق OpenGL الخاص بك ، أوصي بمعرفة الإصدارات التي يدعمها برنامج التشغيل الخاص بك (يمكنك القيام بذلك على موقع البائع الخاص ببطاقة الفيديو الخاصة بك) وتحديث برنامج التشغيل إلى أحدث إصدار.


جهاز OpenGL


يمكن مقارنة OpenGL بجهاز الحالة الكبير ، الذي يحتوي على العديد من الحالات والوظائف لتغييرها. تشير حالة OpenGL بشكل أساسي إلى سياق OpenGL. أثناء العمل مع OpenGL ، سنستعرض العديد من وظائف تغيير الحالة التي ستغير السياق ، وننفذ إجراءات وفقًا للحالة الحالية لـ OpenGL.


على سبيل المثال ، إذا أعطنا الأمر OpenGL لاستخدام الخطوط بدلاً من المثلثات قبل عرضها ، فسيستخدم OpenGL الأسطر لجميع العروض اللاحقة حتى نغير هذا الخيار أو نغير السياق.


الكائنات في OpenGL


تتم كتابة مكتبات OpenGL بلغة C ولديها العديد من واجهات برمجة التطبيقات (API) لها بلغات مختلفة ، لكنها مع ذلك مكتبات C. لا يتم ترجمة العديد من الإنشاءات من C إلى لغات عالية المستوى ، لذلك تم تطوير OpenGL باستخدام عدد كبير من التجريدات ، أحد هذه التجريدات عبارة عن كائنات.


كائن في OpenGL هو مجموعة من الخيارات التي تحدد حالته. يمكن وصف أي كائن في OpenGL بمعرفه ومجموعة الخيارات التي يكون مسؤولاً عنها. بالطبع ، لكل نوع من الكائنات خياراته الخاصة ، وستؤدي محاولة تكوين خيارات غير موجودة للكائن إلى حدوث خطأ. وهنا تكمن إزعاج استخدام OpenGL: يتم وصف مجموعة من الخيارات من خلال بنية مشابهة C غالباً ما يكون معرفها عبارة عن رقم ، والذي لا يسمح للمبرمج بالعثور على خطأ في مرحلة الترجمة ، لأن رمز خاطئ وصحيح لا يمكن تمييزها بشكل واضح.


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

سوف تصادف هذه الشفرة في كثير من الأحيان ، لذلك عندما تعتاد على ما يشبه إعداد جهاز دولة ، سوف يصبح الأمر أسهل بالنسبة لك. يعرض هذا الرمز فقط مثالًا عن كيفية عمل OpenGL. بعد ذلك ، سيتم تقديم أمثلة حقيقية.


ولكن هناك إيجابيات. الميزة الرئيسية لهذه الكائنات هي أنه يمكننا إعلان العديد من الكائنات في تطبيقنا ، وتعيين خياراتها ، وكلما بدأنا العمليات باستخدام حالة OpenGL ، يمكننا ببساطة ربط الكائن بالإعدادات المفضلة لدينا. على سبيل المثال ، قد يكون هذا كائنات تحتوي على بيانات نموذج ثلاثي الأبعاد أو شيء نريد أن نرسمه على هذا النموذج. امتلاك كائنات متعددة يجعل من السهل التبديل بينها أثناء عملية العرض. مع هذا النهج ، يمكننا تكوين العديد من الكائنات اللازمة لتقديم واستخدام حالاتهم دون إضاعة الوقت الثمين بين الإطارات.


لبدء العمل مع OpenGL ، يلزمك التعرف على العديد من الكائنات الأساسية التي بدونها لا يمكننا عرض أي شيء. باستخدام هذه الكائنات كمثال ، سوف نفهم كيفية ربط البيانات والتعليمات القابلة للتنفيذ في برنامج OpenGL.


الكائنات الأساسية: برامج تظليل وتظليل


Shader هو برنامج صغير يتم تشغيله على مسرّع رسومات (GPU) عند نقطة معينة في خط أنابيب الرسومات. إذا أخذنا في الاعتبار التظليلات بشكل تجريدي ، فيمكننا القول إن هذه هي مراحل خط أنابيب الرسومات ، وهي:

  1. معرفة مكان الحصول على البيانات للمعالجة.
  2. معرفة كيفية معالجة بيانات الإدخال.
  3. وهم يعرفون مكان كتابة البيانات للمعالجة الإضافية.

ولكن كيف يبدو خط أنابيب الرسومات؟ بسيط جدا ، مثل هذا:



حتى الآن ، في هذا المخطط ، نحن مهتمون فقط بالرأسي الرئيسي ، الذي يبدأ بمواصفات Vertex وينتهي بـ Frame Buffer. كما ذكرنا سابقًا ، لكل تظليل مدخلات ومخرجات خاصة به ، والتي تختلف في نوع وعدد المعلمات.
وصفنا باختصار كل مرحلة من مراحل خط الأنابيب لفهم ما يفعله:

  1. Vertex Shader - ضروري لمعالجة بيانات الإحداثيات ثلاثية الأبعاد وجميع معلمات الإدخال الأخرى. في معظم الأحيان ، يحسب تظليل قمة الرأس موضع قمة الرأس بالنسبة إلى الشاشة ، ويحسب الحالات الطبيعية (إذا لزم الأمر) ويولد بيانات الإدخال إلى تظليلات أخرى.
  2. تظليل التغطية بالفسيفساء وتظليل التحكم في التغطية بالفسيفساء - هاتان التظليلتان مسؤولتان عن تفصيل العناصر الأولية الواردة من تظليل الرأس وإعداد البيانات للمعالجة في التظليل الهندسي. من الصعب وصف قدرة هذين التظليلين على جملتين ، لكن بالنسبة للقراء لديهم فكرة بسيطة ، سأقدم بعض الصور بمستويات منخفضة وعالية من التداخل:

    أنصحك بقراءة هذا المقال إذا كنت تريد معرفة المزيد عن التغطية بالفسيفساء. في هذه السلسلة من المقالات ، سنغطي التغطية بالفسيفساء ، لكن لن يتم ذلك قريبًا.
  3. تظليل هندسي - هو المسؤول عن تكوين بدائل هندسية من إخراج تظليل التغطية بالفسيفساء. باستخدام التظليل الهندسي ، يمكنك إنشاء بدائل جديدة من بدائل OpenGL الأساسية (GL_LINES ، GL_POINT ، GL_TRIANGLES ، إلخ) ، على سبيل المثال ، باستخدام التظليل الهندسي ، يمكنك إنشاء تأثير جسيم عن طريق وصف الجسيمات فقط عن طريق اللون ، مركز الكتلة ، نصف القطر والكثافة.
  4. تظليل التنقيط هو أحد التظليل غير القابلة للبرمجة. عند التحدث بلغة مفهومة ، فإنه يترجم كل أشكال الرسومات الأولية إلى أجزاء (بكسل) ، أي يحدد موقفهم على الشاشة.
  5. تظليل الجزء هو المرحلة الأخيرة من خط أنابيب الرسومات. يحسب تظليل الجزء لون الجزء (البكسل) الذي سيتم تعيينه في المخزن المؤقت للإطار الحالي. غالبًا ما يتم احتساب التظليل والإضاءة في الجزء المظلل ، وتظليل القوام والخرائط العادية - في ظل التظليل الجزئي - تتيح لك كل هذه التقنيات تحقيق نتائج رائعة بشكل لا يصدق.

تتم كتابة تظليل OpenGL بلغة GLSL تشبه C الخاصة والتي يتم تجميعها وترتبطها في برنامج تظليل. بالفعل في هذه المرحلة ، يبدو أن كتابة برنامج تظليل مهمة تستغرق وقتًا كبيرًا للغاية ، لأنه تحتاج إلى تحديد الخطوات الخمس لخط أنابيب الرسومات وربطها معًا. لحسن الحظ ، لم يكن الأمر كذلك: يتم تعريف تظليل التغطية بالفسيفساء والهندسة في خط أنابيب الرسومات افتراضيًا ، مما يسمح لنا بتحديد تظليلين فقط - الرأس وتلك المظلمة (تسمى أحيانًا تظليل البكسل). من الأفضل مراعاة هذين التظليلين بمثال كلاسيكي:


فيرتكس شادر
 #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; } 


شظية شظية
 #version 450 in vec3 Color; out vec4 out_fragColor; void main(){ out_fragColor = Color; } 


مثال التجمع شادر
 unsigned int vShader = glCreateShader(GL_SHADER_VERTEX); //    glShaderSource(vShader,&vShaderSource); //  glCompileShader(vShader); //  //        unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vShader); //    glAttachShader(shaderProgram, fShader); //    glLinkProgram(shaderProgram); //  


هذان التظليلان البسيطان لا يحسبان أي شيء ، إنهما يمرران البيانات أسفل خط الأنابيب. دعنا ننتبه إلى كيفية توصيل تظليل الرأس والجزء: في تظليل الرأس ، يتم الإعلان عن متغير اللون الذي سيتم كتابة اللون فيه بعد تنفيذ الوظيفة الرئيسية ، بينما في تظليل الجزء يتم إعلان نفس المتغير الدقيق مع مؤهل in ، أي كما هو موضح سابقًا ، يتلقى تظليل الأجزاء جزءًا من قمة الرأس عن طريق الدفع البسيط للبيانات إلى أبعد من خلال خط الأنابيب (ولكن في الواقع ليس بهذه البساطة).

ملاحظة: إذا لم تعلن عن بدء تهيئة متغير نوع vec4 في شظايا التجزئة ، فلن يتم عرض أي شيء على الشاشة.

لقد لاحظ القراء المهتمون بالفعل إعلان متغيرات الإدخال من النوع vec3 مع تصفيات تخطيط غريبة في بداية تظليل vertex ، فمن المنطقي افتراض أن هذا إدخال ، لكن من أين نحصل عليه؟

الكائنات الأساسية: المخازن المؤقتة وصفائف Vertex


أعتقد أنه لا يستحق شرح ماهية الكائنات العازلة ، وسننظر بشكل أفضل في كيفية إنشاء وتخزين عازلة في 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); 

لا يوجد شيء صعب في هذا ، فنحن نعلق المخزن المؤقت الذي تم إنشاؤه على الهدف المطلوب (سنكتشف لاحقًا أيًا منها) ونقوم بتحميل البيانات التي تشير إلى حجمها ونوع استخدامها.


GL_STATIC_DRAW - لن يتم تغيير البيانات في المخزن المؤقت.
GL_DYNAMIC_DRAW - ستتغير البيانات الموجودة في المخزن المؤقت ، لكن ليس غالبًا.
GL_STREAM_DRAW - ستتغير البيانات الموجودة في المخزن المؤقت مع كل مكالمة سحب.

إنه لأمر رائع ، الآن توجد بياناتنا في ذاكرة GPU ، يتم تجميع برنامج التظليل وربطه ، ولكن هناك تحذير واحد: كيف يعرف البرنامج مكان الحصول على بيانات الإدخال الخاصة بتظليل vertex؟ قمنا بتنزيل البيانات ، لكننا لم نوضح من أين سيحصل عليها برنامج shader. يتم حل هذه المشكلة عن طريق نوع منفصل من كائنات OpenGL - صفائف قمة الرأس.


صورة
الصورة مأخوذة من هذا البرنامج التعليمي .

كما هو الحال مع المخازن المؤقتة ، من الأفضل عرض صفائف قمة الرأس باستخدام مثال التكوين الخاص بها.


  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); 

لا يختلف إنشاء صفائف الرأس عن إنشاء كائنات OpenGL الأخرى ، حيث تبدأ الأكثر إثارة للاهتمام بعد السطر:

 glBindVertexArray(VAO); 
يتذكر صفيف vertex (VAO) كافة الارتباطات والتكوينات التي يتم إجراؤها باستخدامها ، بما في ذلك ربط الكائنات العازلة لتفريغ البيانات. في هذا المثال ، يوجد كائن واحد فقط ، لكن في الممارسة العملية يمكن أن يكون هناك العديد من الأشياء. بعد ذلك ، يتم تكوين سمة قمة الرأس برقم محدد:


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

من أين حصلنا على هذا الرقم؟ تذكر مؤهلات التصميم لمتغيرات إدخال تظليل قمة الرأس؟ هم الذين يحددون سمة السمة التي سيتم ربط متغير الإدخال بها. انتقل الآن باختصار إلى وسيطات الوظيفة حتى لا تكون هناك أسئلة غير ضرورية:
  1. رقم السمة الذي نريد تكوينه.
  2. عدد العناصر التي نريد أن نأخذها. (نظرًا لأن متغير الإدخال الخاص بتظليل vertex ذي الشكل = 0 هو من النوع vec3 ، فإننا نأخذ 3 عناصر من النوع float)
  3. نوع العناصر.
  4. هل من الضروري تطبيع العناصر ، إذا كانت متجهة.
  5. الإزاحة للرأس التالي (بما أن لدينا الإحداثيات والألوان الموجودة بالتسلسل ولكل منها نوع vec3 ، ثم ننتقل بمقدار 6 * sizeof (تعويم) = 24 بايت).
  6. توضح الحجة الأخيرة ما يجب تعويضه عن القمة الأولى. (للإحداثيات ، تكون هذه الوسيطة 0 بايت للألوان 12 بايت)

كلنا الآن على استعداد لتقديم صورتنا الأولى


تذكر أن تربط برنامج VAO وبرنامج التظليل قبل استدعاء العرض.
 { // your render loop glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES,0,3); //        } 


إذا فعلت كل شيء بشكل صحيح ، فيجب أن تحصل على هذه النتيجة:



كانت النتيجة مثيرة للإعجاب ، ولكن من أين جاءت تعبئة التدرج في المثلث ، لأننا أشرنا إلى ثلاثة ألوان فقط: الأحمر والأزرق والأخضر لكل رأس فردي؟ هذا هو سحر تظليل التنقيط: الحقيقة هي أن قيمة اللون التي وضعناها في الرأس لا تدخل في تظليل التجزئة. ننقل 3 رؤوس فقط ، ولكن يتم إنشاء أجزاء أكثر بكثير (هناك بالضبط عدد الأجزاء التي توجد بها وحدات بكسل مملوءة). لذلك ، يتم أخذ متوسط ​​قيم الألوان الثلاثة لكل جزء ، اعتمادًا على مدى قربه من كل من القمم. يظهر هذا جيدًا في زوايا المثلث ، حيث تأخذ الأجزاء قيمة اللون التي أشرنا إليها في بيانات الرأس.

بالنظر إلى المستقبل ، سأقول أن إحداثيات الملمس تنتقل بنفس الطريقة ، مما يجعل من السهل تراكب القوام على بداياتنا.

أعتقد أن هذا يستحق الانتهاء من هذا المقال ، والأكثر صعوبة هو وراءنا ، ولكن الأكثر إثارة للاهتمام هو مجرد بداية. إذا كانت لديك أسئلة أو رأيت خطأً في المقال ، فاكتب عنه في التعليقات ، سأكون ممتنًا للغاية.


في المقالة التالية ، سننظر في التحولات ، ونتعرف على المتغيرات unifrom ، ونتعرف على كيفية فرض القوام على البدائل.

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


All Articles