يمكن رسم مثلث عبر وظائف مكتبة OpenGL لكن يتطلب الإدراك بأن الشاشة التي نعمل عليها هي ثنائية البعد.[1]
وعلى الرغم من أن وظائف OpenGL هي دائمة العمل بالرسم 3D إلا أن ما يحدث هو خداع بصري عن طريق خصائص التحويل والجبر الخطي في البرمجة.[1]
على سبيل المثال عند رسم مثلث 2D فإن خصائص بعض الوظائف تتطلب التحويل من 3D الى 2D.[1] ع
لى عكس مكتبات أخرى مثل DirectX.
إذ تتبنى بعض الإختلافات في تعريف المتغيرات وطرق الإستدعاء.
يتم إدارة عمليات التحويل من 2D إلى 3D عن طريق خط أنابيب يتم تقسيمه إلى قسمين.[1]
وبالتالي يعمل القسم الأول على تحويل وقراءة المشهد إلى مخرجات العرض (الشاشة) .
بينما القسم الثاني يتعلق بإدارة الإحداثيات داخل صندوق العرض مع فتح نطاق العمق للرسم ثلاثي الأبعاد وإجراء حسابات التحويل.[1]
الفرق بين رسومات 2D و 3D
عندما يتم الإشارة إلى رسم ثنائي البعد فإن أبرز مثال عليه هو ساعة تناظرية تتطلب معادلات نصف قطر الدائرة والزوايا الحادة والمنفرجة وغيرها من قيم الدائرة الواحدية.
هناك أمثلة أخرى على الرسومات 2D مثل واجهة المستخدم لأنظمة ويندوز ولينيكس و ماك وجوجل وحتى قائمة الهواتف وشاشة النماذج الأخرى.
مع ذلك فإن أماكن وجود العناصر لا تتطلب الكثير من التعقيدات.
وعلى فرض أن أيقونة الحاسوب تقع أعلى شاشة العرض في سطح المكتب ، فإن ذلك يدل على أنها تعتمد أقرب مكان من الإحداثيات وهو 64×64 بيكسل.
عادة ما تبدأ الإحداثيات (خط الصفر) في رسم الحاسب من أعلى بيكسل موجود على الشاشة أقصى اليسار ، وبعض الحالات من الممكن أن تبدأ الإحداثيات من الوسط.
جميع قيم إحداثيات 2D هي موجبة بالأصل على أجهزة الحاسوب ، وفي حال كانت سالبة فإن ذلك يعود إلى نوع المكتبة وسلوك المطور في المشروع.
بينما يختلف الأمر في معادلات الرسم 3D بوجود عمق للعناصر.
ما يعني أن هناك احداثيات x و y في الواقع ثابتة ، وإنما الإختلاف الذي طرأ عليها هو إضافة العمق والذي يعبر عنه بالرياضيات بالقيمة z.
تعتمد القيمة Z إحداثيات مجال واحدي يتراوح بين -1 و 1 ، ويدل تطبيقها على الحاسوب ببروز بعض الصفات والخصائص المتعلقة بالعناصر.
لكن عادة ما يقود إدخال إحداثيات الرسم إلى الحاسوب إلى انقلاب بعض الحسابات بسبب علوم (المصفوفات) Algebra.
رسم مثلث باستخدام Shaders
هناك مجموعة من العناصر التي تساعد في رسم مثلث ، سواء أكان ملونا أو عادي وذلك من خلال خطوط الرسم pipelines.[1]
وهي عبارة عن أنابيب متصلة ببطاقة العرض تحول قيم بيكسل إلى ألوان حقيقية.[1]
وبسبب الطبيعة الموازية تحتوي غالبية بطاقات الرسم على الآلاف من نوى المعالجة الصغيرة ، والتي تعمل على معالجة البيانات بسرعة كبيرة عن طريق خطوط الأنابيب.[1]
يمكن الإشارة إليها بــ shaders.[1] وبالتالي فإن بعض هذه الظلال قابلة للإدارة من قبل المطورين.
وخاصة أنها تسمح للمبرمجين كتابة وظائف الألوان الخاصة بهم من خلال الأنابيب.[1]
يتم كتابة Shaders باستخدام OpenGL GLSL ، حيث أن الأمر لم يقتصر على رسم مثلث فحسب بل أن إدارة المشروع بالكامل تقع على عاتقها.[1]
أنواع أنابيب pipeline[1]
- إحداثي (Vertex Shader).
- شكلي (Shape Assembly).
- هندسة (Geometry).
- فسيفسائي (Tessellation).
- نقطي (Rasterization).
- جزئي (Fragment Shader).
- مخلوط (Blending).
توفر الأنابيب السابقة مجموعة شاملة جدًا من طرق التلوين تدعمها غالبية بطاقات العرض.[1]
ربما يتم إلقاء الضوء أكثر على Vertex Shader و Fragment Shader.
أنابيب Vertex Shader
يتم الإشارة بــ Vertex shader إلى الظلال الرأس وهي بذلك الجزء الخاص بتعيين الإحداثيات 3D.[1]
على سبيل المثال ، تركز هذه الخصائص على تظليل النقاط الرئيسية لكل إحداثية عند رسم مثلث.[1]
التظليل الجزئي (Fragment Shader)
تستدعي الحاجة لإستخدام Fragment Shader عند احتساب اللون النهائي لكل بيكسل داخل حدود المثلث.[1]
وغالبا ما يستخدم التظليل الجزئي عن كثب أيضًا ,ohwm في المراحل المتقدمة من الأضواء وانعكاساتها.
البدء في رسم مثلث
يمكن رسم مثلث من خلال تخزين احداثيات Vertex في مصفوفة خاصة من لغة c++ أو بداخل متغير من مكتبة GLFW.
GLfloat vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
ولا بد من الإشارة أن إحداثيات الرسم تبدأ من القيمة 0 ، وأول نقطة تقع على الحدود السالبة من مجال الرسم.
وذلك بناء على طريقة vertex التي قمنا باستخدامها ، يمكن أن نتصور النقطة صفر في منتصف المثلث ويتم تشكيل الأضلاع بناء عليها.
لا نعطي أهمية للقيمة الثالثة فهي تساوي 0.0f بكافة الأضلاع ، فقط نريد الإبقاء عليها كما هي.
بالتالي تعمل مكتبة OpenGL وفق نظام 3D لذلك ما نقوم به الآن بنظام الاستدعاء هو تعطيل العمق Z.[1]
إعداد VBO
يتم ترحيل الإحداثيات التي قمنا بحفظها إلى نظام VBO وهو عبارة عن مخزن قادر على حفظ أعداد كبيرة من الإحداثيات.
ويتميّز المخزن VBO بسرعة عالية وإدارة قوية لعتاد البطاقة ، حيث يتسع لأعداد كبيرة من المضلعات.
يمكن فصل قيم أكثر من مخزن VBO عن طريق عنوان ID خاص ، يعمل على تجريد إحداثيات الأجزاء عن بعضها البعض.[1]
بالتالي نقوم بإضافة الكود التالي إلى الكود الخاص بنا:
GLuint VBO; glGenBuffers(1, &VBO);
تمتلك مكتبة OpenGL أكثر من كائن ونوع لتخزين البيانات ، [1] وتتطلب منا الإحداثيات مخزن من نوع مصفوفة.
لذلك نقوم بإضافة الشيفرة التالية إلى المشروع:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
يتم الإتصال بأي مخزن عن طريق دالة glBufferData ، حيث أنها تقوم بتحضير البيانات الخاصة أثناء إحداثيات رسم مثلث التي قمنا بتعريفها.[1]
عادة ما تأخذ المصفوفة دائما صفة GL_ARRAY_BUFFER ، لكي يتم الإشارة إلى نوع البيانات مع تحديد خاصية الرسم GL_STATIC_DRAW.[1]
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
يعتمد رسم مثلث مبدئي على خاصية GL_STATIC_DRAW ، ما يعني أن بيانات المخزن الخاص به ستبقى ثابتة طوال فترة تشغيل البرنامج.
وبالتالي تشير أنواع أخرى من الرسم إلى بيانات متغيرة مثل GL_DYNAMIC_DRAW أو GL_STREAM_DRAW .
وهي حدوث تغيير على مضلعات النموذج في وقت محدد، [1] غالبًا ما يتم دمج هذه الخصائص عند تمكين وتعطيل بعض الأحداث المتغيرة في المشاريع الكبيرة.[1]
ملف أنابيب Vertex Shader
يمكن أن يساعد Vertex Shader في إجراء بعض الظلال على إحداثيات المثلث وفقًا للألوان التي نحددها في Fragment Shader.
على سبيل المثال يتم الإشارة إلى إحداثيات الظلال الرأسية من خلال ملف يتم اعادة الاتصال به عبر أدوات اللغة.[1]
وفي حال ما أردنا قراءة المصادر من متغير فإن ذلك ممكن وبحسب المصادر المتوفرة لدينا.
بالتالي سوف نعمل على وضع بيانات أنابيب الظل والألوان عبر مصفوفة Character من متغيرات لغة سي أو من مكتبة OpenGL.[1]
يتم استخدام Vertex Shader عبر لغة GLSL (OpenGL Shading Language).[1] ومن ثم ترجمته في المشروع الخاص بنا. في هذه الشيفرة الحد الأدنى للأساسيات المطلوبة :
#version 330 core layout (location = 0) in vec3 position; void main() { gl_Position = vec4(position.x, position.y, position.z, 1.0); }
كما نجد فإن رسم مثلث يتطلب 3 إحداثيات للنقاط xyz وقد نجد بأن الشيفرة متشابهة جدا مع لغة c++.
كل ظل يبدأ بإعلان إصدار المكتبة في الأعلى ، ثم يتم الإعلان عن vector .
وذلك نظرا لأن GLSL تتطلب الوصول إلى القيم من خلال المتجهات.[1]
نقوم بتحديد مدخلات كل قيمة عن طريق متغير يعرف باسم position مع إعلان مكان الرسم بقيمة 0.[1]
يمكن تعريف Vertex Shader عن طريق متغير فوق كود main.cpp على نفس الصفحة.
const GLchar* vertexShaderSource = \"#version 330 core \" \"layout (location = 0) in vec3 position; \" \"void main() \" \"{ \" \"gl_Position = vec4(position.x, position.y, position.z, 1.0); \" \"}\";
المتجهات Vectors
يتم الإشارة إلى برمجة الرسومات بالمفاهيم الرياضية التقليدية ومنها المتجهات ، حيث أنها تمثل مواقع واتجاهات الفضاء في رياضيات الرسم.
[1] في لغة GLSL يتم استلام القيم عبر متجهات من قيم XYZ ، لتمثل أماكن وإحداثيات الرسم من الفضاء.
بينما القيمة W تدل على شاشة المخرجات إسقاط الرسم.[1] سيتم مناقشة المتجهات بعمق في الدرس القادم.
إرفاق مصادر Vertex Shader
لقد قمنا بكتابة vertex shader في متغير خاص ولكننا لم نعمل على تجهيز بيئة OpenGL لقراءة المصادر.
ولذلك نقوم بتعريف متغير بعنوان vertex وهو من نوع GLuint تماما مثل الشيفرة التالية ثم نعمل على إضافة الظلال الخاصة به.
GLuint vertex; vertex= glCreateShader(GL_VERTEX_SHADER);
الآن نقوم بإرفاق الملفات الخاصة بــ Vertex Shader عن طريق الكود التالي:
glShaderSource(vertex, 1, &vertexShaderSource, NULL); glCompileShader(vertex);
يتكون الكود السابق من متغير vertex Shader الذي سيحمل القيم القادمة من GLSL.
بينما تشير القيمة 1 إلى إعلان البدء بـالظلال الرأسية ، المتغير الثالث هو نصوص GLSL التي قمنا بتعريفها واستدعائها.
عند رسم مثلث بالطريقة السابقة ، من المستحسن فحص عملية الربط الكود Vertex.
قد لا تعتبر أساسية ولكن فقط للتحقق من أن الأمور تسير على ما يرام.
GLint success; GLchar infoLog[512]; glGetShaderiv(vertex, GL_COMPILE_STATUS, & success); if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << \"ERROR::SHADER::VERTEX::COMPILATION_FAILED \" << infoLog << std::endl; }
مصادر Fragment Shader
تعتبر الظلال الجزئية هي الظلال الثانية والنهائية التي نريد استخدامها في درس رسم مثلث.
فهي تتعلق بإجراء الحسابات على مخرجات البيكسل من ناحية الألوان.
على سبيل المثال ، فإن رسومات الحاسب تتكون من ثلاثة ألوان رئيسية RGBA وهي اختصار للأحمر والأخضر والأزرق ومزيج ألفا.
يصبح لدينا الترتيب Red , Green , Blue , Alpha(RGBA).
ومع دمج وتركيب الألوان نحصل على أكثر من 16 مليون لون.[1]
بالتالي عند تحضير شيفرة Fragment Shader فإنها تصبح على النحو التالي:
#version 330 core out vec4 color; void main() { color = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
يتم إخراج الألوان عبر متجه يحتوي على 4 قيم RGBA ، تمثل درجة 1.0 قيمة عشرية مبهمة.
تتم الموالفة من خلال تركيز وخفت درجات الألوان الأساسية من العرض.[1]
إرفاق مصادر Fragment Shader
يتم تفعيل الظلال الجزئية بطريقة مماثلة لـ Vertex Shader ، ومن خلال الشيفرة التالية يتبين لنا عملية تفعيل وارفاق Fragment Shader:
GLuint fragment; fragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment, 1, &fragmentShaderSource, NULL); glCompileShader(fragment);
إضافة وربط ظلال البرنامج
يشير متغير Shader Program إلى آخر خطوة من ربط ودمج الظلال التي قمنا بإعدادها.[1]
لا بد أن ننوه بأن عملية الربط لا تنطبق حصريًا على رسم مثلث واحد فحسب ، بل يمكن أن تتجاوز المضلعات الأكثر تعقيدًا وخاصة في الرسومات 3D.
عند دمج الظلال سويًا نحصل على مخرجات كل مدخل من مراحل الظل السابق.
وينطبق ذلك أيضا على مراحل GLSL المتعددة مثل النقطية والفسيفساء وغيرها.[1]
نقوم الآن بربط الظلال من خلال الكود التالي:
GLuint program; program= glCreateProgram(); glAttachShader(program, vertex); glAttachShader(program, fragment); gl Link Program(program);
نفعل شرط التحقق فيما لو نجح الربط أم لا ، تمامًا مثل ملفات Shaders.
بالتالي يصبح كود التحقق على النحو التالي:
//Check status of program linking glGetProgramiv(shaderProgram, GL_LINK_STATUS, & success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << \"Failed To Link Shader Program!\"; }
ثم لا ننسى تفعيل البرنامج مع حذف كائنات الظل بعد عملية التفعيل لأننا لم نعد بحاجة لها.
بالتالي تصبح شيفرة التفعيل على النحو التالي:
//GL use program gl UseProgram(program); //Delete buffers glDeleteShader(vertex); glDeleteShader(fragment);
ربط وتفعيل الخصائص والسمات
بعد تعيين جميع خصائص رسم مثلث ، يتعين علينا تفعيل السمات (glVertexAttribPointer ).
والتي تسمح لنا بتحديد الإدخالات التي نريدها بالفعل ، وذلك لأننا نقوم بتعيين السمات بشكل يدوي مع تمرير الإحداثيات.
يتم الوصول إلى السمات عن طريق glVertexAttribPointer تمامًا مثل الشيفرة التالية:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), ( GLvoid*)0); glEnableVertexAttribArray(0);
بالتالي ، تدل القيمة الأولى على البدء من إحداثيات صفر كما أشرنا إليه في Vertex Shader.
فهو بذلك يحجز مساحة البدء بالرسم التي سيبدأ بتمرير المتغيرات لها.
وتدل القيمة الثانية على طبيعة البيانات التي تستقبلها لغة GLSL وهي من نوع vec3.
بينما تعبر القيمة الثالثة عن أن البيانات عشرية من نوع Float.
سيتم استقبال بيانات رسم مثلث من خلال مخزن VBO، حيث أنه يتولى مهام إحداثيات الرسم في المكتبة.
بالتالي يتم تشغيل المخزن قبل الاتصال بدالة glVertexAttribPointer.
الآن تصبح الشيفرة الخاصة بالسمات على النحو التالي بالترتيب:
GLuint VBO; glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), ( GLvoid*)0); glEnableVertexAttribArray(0); //GL use program gl Use Program(shaderProgram);
لا شك بأن رسم مثلث وأي كائن آخر يتطلب تفعيل كافة الخطوات المتعلقة بالصفات.
على سبيل المثال ، عند رسم 5 مثلثات يتطلب منا VBO مستقل لكل واحدة منه ستحدد لنا المكان والإحداثيات وغيرها من القيم.
تفعيل Vertex Array Object(VAO)
يطلق عليها VAO وهي تتشابه قليلا مع VBO ، إلا أنها متطلب لعملية الرسم عبر OpenGL.
على سبيل المثال هي لا تتولى مهام تعيين الإحداثيات ، ربما تشير إلى أماكن توزيعها مع تمكين وتعطيل الخصائص دون الرجوع إلى المخزنات السابقة.
تحتوي VAO على عناصر أهمها تفعيل الإحداثيات أو إلغاؤها.
التعرف على السمات القادمة والارتباط بها. نقوم الآن بتعريف كائن VAO كما في الشيفرة التالية:
GLuint VAO; glGenVertexArrays(1, &VAO);
يتم تمكين VAO قبل الرسم ، وبعد الرسم يتم تعطيلها تمامًا مثل الشيفرة التالية:
//Bind the VAO gl BindVertexArray(VAO); //Your Draw Method inside //Unbind the VAO gl BindVertexArray(0);
رسم مثلث (الوظائف النهائية)
توفر لنا دالة glDrawArrays البدء برسم مثلث وفقا لكافة المعطيات التي تم تفعيلها من مكتبة OpenGL.
بالتالي لا ننسى أن ربط VAO سيعمل قبل دالة الرسم ومن ثم سيتوقف عند إتمام الوظيفة لتصبح الشيفرة بهذا الشكل:
//Draw Triangle gl Use Program(shaderProgram); gl BindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); gl BindVertexArray(0);
شيفرة رسم مثلث
#include<iostream> #include <glew.h> #include <glfw3.h> #define GLEW_STATIC // Function prototypes void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); // Window dimensions const GLuint width = 800, height = 600; // Shaders const GLchar* vertexShaderSource = \"#version 330 core \" \"layout (location = 0) in vec3 position; \" \"void main() \" \"{ \" \"gl_Position = vec4(position.x, position.y, position.z, 1.0); \" \"}\"; const GLchar* fragmentShaderSource = \"#version 330 core \" \"out vec4 color; \" \"void main() \" \"{ \" \"color = vec4(1.0f, 0.5f, 0.7f, 1.0f); \" \"} \"; void initialize(); GLFWwindow* createWindow(const GLuint width , const GLuint height); void compileShaders(const char* shaderName , const GLchar* sources , GLuint& shader); void checkCompileStatus(GLuint shaderObject, const char* shaderName, bool compile); void cleanMemory(GLuint VBO, GLuint VAO); // The MAIN function, from here we start the application and run the game loop int main() { initialize(); GLFWwindow* window = createWindow(1024, 768); // Set the required callback functions glfwSetKeyCallback(window, key_callback); // Build and compile our shader program // Vertex shader GLuint vertex; compileShaders(\"vertex\" , vertexShaderSource , vertex); // Fragment shader GLuint fragmentShader; compileShaders(\"fragmentShader\", fragmentShaderSource, fragmentShader); // Link shaders GLuint program = glCreateProgram(); glAttachShader(program , vertex); glAttachShader(program , fragment); gl Link Program(program); // Check for linking errors checkCompileStatus(shaderProgram, \"Shader Program\", false); glDeleteShader(vertex); glDeleteShader(fragment); // Set up vertex data (and buffer(s)) and attribute pointers GLfloat vertices[] = { -0.5f, -0.5f, 0.0f, // Left 0.5f, -0.5f, 0.0f, // Right 0.0f, 0.5f, 0.0f // Top }; GLuint VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); // Bind the Vertex Array Object first, then bind and set vertex buffer(s) and attribute pointer(s). glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); // Note that this is allowed, the call to glVertexAttribPointer registered VBO as the currently bound vertex buffer object so afterwards we can safely unbind glBindVertex Array(0); // Unbind VAO (it\'s always a good thing to unbind any buffer/array to prevent strange bugs) // Game loop while (!glfwWindowShouldClose(window)) { // Check if any events have been activated (key pressed, mouse moved etc.) and call corresponding response functions glfwPollEvents(); // Render // Clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 0.5f); glClear(GL_COLOR_BUFFER_BIT); // Draw our first triangle gl Use Program(shaderProgram); glBindVertex Array(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertex Array(0); // Swap the screen buffers glfwSwapBuffers(window); } clean Memory(VBO,VAO); return 0; } // Is called whenever a key is pressed/released via GLFW void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) { if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) glfwSetWindowShouldClose(window, GL_TRUE); } void initialize() { // Init GLFW glfwInit(); // Set all the required options for GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); } GLFWwindow* createWindow(const GLuint width , const GLuint height) { // Create a GLFWwindow object that we can use for GLFW\'s functions GLFWwindow* window = glfwCreateWindow(width , height, \"Our First Triangle\", nullptr, nullptr); glfwMakeContextCurrent(window); // Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions glewExperimental = GL_TRUE; // Initialize GLEW to setup the OpenGL Function pointers glewInit(); // Define the viewport dimensions int width, height; glfwGetFramebufferSize(window, &width, &height); glViewport(0, 0, width, height); return window; } void compileShaders(const char* shaderName, const GLchar* sources , GLuint& shader) { if (shaderName == \"vertexShader\") { shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(shader, 1, &vertexShaderSource, NULL); glCompileShader(shader); checkCompileStatus(shader, \"vertex shader\", true); } else { shader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(shader, 1, &fragmentShaderSource, NULL); glCompileShader(shader); checkCompileStatus(shader, \"fragment shader\", true); } } void checkCompileStatus(GLuint shaderObject, const char* shaderName, bool compile) { // Check for compile time errors GLint success; GLchar infoLog[512]; if (compile) { glGetShaderiv(shaderObject, GL_COMPILE_STATUS, & success); if (!success) { glGetShaderInfoLog(shaderObject, 512, NULL, infoLog); std::cout << \"ERROR::SHADER::\" << shaderName << \"::COMPILATION_FAILED \" << infoLog << std::endl; } else { std::cout << shaderName <<\" Compiled Successfully \"; } } else { glGetShaderiv(shaderObject, GL_LINK_STATUS, & success); if (!success) { glGetShaderInfoLog(shaderObject, 512, NULL, infoLog); std::cout << \"ERROR::SHADER::\" << shaderName << \"::COMPILATION_FAILED \" << infoLog << std::endl; } else { std::cout << shaderName << \" Compiled Successfully \"; } } } void cleanMemory(GLuint VBO, GLuint VAO) { // Properly deallocate all resources once they\'ve outlived their purpose glDeleteVertexArrays(1, & VAO); gl Delete Buffers(1, & VBO); // Terminate GLFW, clearing any resources allocated by GLFW. glfwTerminate(); }