ملف GLSL يزداد حجمه عند تخصيص بعض المتجهات بحسب ما يستدعي من وظائف uniforms.
حيث أن ذلك من شأنه أن يعيق فهم وقراءة الشيفرة كلما ازداد استخدام ظلال GLSL.
وبالتالي يتعين علينا بناء ملف GLSL لكل من Vertex Shader و Fragment Shader لتنظيم قراءة الشيفرة.
على سبيل المثال ، فقد قمنا بتخصيص بضعًا من الدوال التي جردنا عملياتها لتسهيل عملية التنظيم.
حيث أن في فصل ملف GLSL العديد من المزايا ومنها حصر الأخطاء مع التخلي عن الرموز التي يتم فرض استخدامها من قبل متغيرات char.[1]
إضافة ملف GLSL
نعمل على إضافة header خاص يعمل على قراءة كلا الملفين من القرص الصلب. نعطي الآن العنوان GLSL.h تمام مثل الصورة التالية:
بعد القيام بتلك المهمة نجد أن هنالك ملف جديد سوف يتم تضمينه في شيفرة c++.
ولكن قبل ذلك لا بد لنا من ملئ بعض الخصائص الفريدة في لغة C++ والتي تمكننا من قراءة الملفات من الخارج.
بداية نقوم بتعريف Class جديد بعنوان OurShaders تمامًا مثل الشيفرة التالية:
#pragma once #ifndef SHADER_H #define SHADER_H #include <string> #include <fstream> #include <sstream> #include <iostream> using namespace std; #include <glew.h>; // we call glew to activate OpenGL class OurShaders { public: // This is our program ID GLuint Program; // Constructors OurShaders(const GLchar* vertexSourcePath, const GLchar* fragmentSourcePath); // To use our program void Use(); }; #endif
سوف نجري تطويرًا على ملف GLSL ليحقق أهدافنا من هذا الدرس.
على سبيل المثال ، الآن نعمل على إرفاق كافة العناوين القادمة من برنامج الظلال.
بالتالي يتطلب الـ constructor أماكن وجود ملف GLSL وهما vertex shader و fragment shader.[1]
يتم الحصول على المصادر وقراءتها من القرص الصلب وذلك بتزويد الكلاس بعناوين كل منهما.
سيشكل هذا الملف دافع قوي في تطوير الشيفرة لاحقًا كما سيعمق لدينا بعض الأساسيات في لغة c++.
وخاصة بعد أن يتم الرجوع لبعض المكتبات القياسية.[1]
بناء الــ Constructor
يتطلب بناء الــ Constructor إدراك بعض الأساسيات حولها قبل الإستمرار في هذا الدرس.[2] ويمكن أخذ لمحة سريعة عن دور هذه الدالة الأساسية في بناء وتكوين القيم الإفتراضية للكائنات.[2]
// 1. Receive vertex & fragment source from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensures ifstream objects can throw exceptions: vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
بعد آخر إجراءات قمنا بها على ملف GLSL أضفنا القيم السابقة في الــ constructor التابع للكلاس. وهي متغيرات من نوع string في مكتبة c++.
حيث يدل الــ Class ifstream على كل مدخل يتم قراءته من ملف مرفق أو خارجي.[3]
تمكننا أدوات لغة سي القياسية من حصر شيفرة فتح الملفات try & catch ليتم اقتناص الأخطاء وإظهارها بطريقة أو بأخرى.
وأما في حال ما قمنا بالتخلي عن هذا النمط من فحص وقراءة الملفات ، من المحتمل ظهور بعض الأخطاء في القراءة.
إعداد قراءة ملف GLSL
تكشف لنا الشيفرة التالية بعض الطرق التقليدية في جلب الملفات:
try { // Open files from path vShaderFile.open(vertexSourcePath); fShaderFile.open(fragmentSourcePath); std::stringstream vShaderStream, fShaderStream; // Read file’s by buffers & streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close handlers vShaderFile.close(); fShaderFile.close(); // Convert stream to GLchar array vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch (std::ifstream::failure e) { std::cout << \"ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ\" << std:: endl; } const GLchar* vShaderCode = vertexCode.c_str(); const GLchar* fShaderCode = fragmentCode.c_str();
تتكون الشيفرة من أربعة مهام رئيسية وهي فتح الملفات من قرص التخزين.
ومن ثم قراءتها والخروج من المساعد وبعد ذلك تحويل القيم إلى أحرف ليتم ارفاقها في متغيرات GLchar.
ترجمة وربط الظلال
بعد تشكيل شيفرة قراءة ملف GLSL من الــ constructor سنتمكن الآن من تحضير شيفرة الترجمة ووصل برنامج الظلال.
مع ذلك لا تختلف عن أي شروحات سابقة إنما فقط تعمل على تنفيذ قراءة كلا الملفين من خارج البرنامج.
الآن نعمل على إضافة شيفرة vertex Shader لتصبح على النحو التالي:
// 2. Compile shaders GLuint vertex, fragment; GLint success; GLchar infoLog[512]; // Vertex Shader as the same in original code vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); // Compile and catch errors if found 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 على النحو التالي:
// Same this for Fragment Shader // Shader Program this->Program = glCreateProgram(); glAttach Shader(this->Program, vertex); glAttach Shader(this->Program, fragment); glLinkProgram(this->Program); // Compile and catch errors if found glGetProgramiv(this->Program, GL_LINK_STATUS, & success); if (!success) { glGetProgramInfoLog(this->Program, 512, NULL, infoLog); std::cout << \"ERROR::SHADER::PROGRAM::LINKING_FAILED \" << infoLog <<std::endl; }
نتطرق الآن إلى آخر وحدة من مهام الــ Constructor وهي التخلص من ملفات shaders.
//Delete shaders glDeleteShader(vertex); glDeleteShader(fragment);
توصيل البرنامج
بعد الإنتهاء من بناء الــ constructor يتعين علينا استكمال الدالة Use ليتم وصل ملف GLSL.
وبالتالي تصبح شيفرة Use على النحو التالي:
void OurShaders::Use() { glUseProgram(this->Program); }
نداء ملف GLSL
الآن وبعد أن انتهينا من إعداد ملف GLSL بكافة مكوناته أصبح لدينا قدرة على الاتصال بــ OurShaders Class.
لكن قبل ذلك يجب نقل شيفرة كل من ملف Vertex Shader و Fragment Shader إلى مكان آمن في القرص الصلب.
يمكننا إنشاء هذه الملفات داخل دليل المشروع ومن ثم نسخ الروابط عند عملية الإتصال.
سيتم إنشاء ملفين الأول يحمل الإسم vert.vs والثاني بعنوان frag.fs. ومن ثم نقوم بنقل كل من الشيفرة الخاصة لدى ملف GLSL.
يتطلب نداء ملف GLSL تضمين الصفحة أولاً ومن ثم استخدام الــ Class الذي قمنا بانشائه.
ولذلك بعد عملية التضمين سوف نعمل على إنشاء مؤشر من نوع OurShaders.
وبالتالي فإن السبب في ذلك هو فصل الشيفرة ببعض الوظائف (functions) ما يعيق عملية الإتصال دون متغيرات من نوع Pointer.
حيث نعين متغير أعلى صفحة main.cpp ونقوم بتسميته بــ ourShaders تمامًا مثل الشيفرة التالية:
OurShaders* ourShaders;
يتطلب إرفاق الملفات تعيين أماكنها الصحيحة في القرص الصلب.
وقد قمنا بتخزينها بملفات المشروع ليتم الإتصال بها على النحو التالي:
ourShaders = new OurShaders(\"C:/Users/Mmutawe/source/repos/Test OpenGL/Test OpenGL/shaders/vert.vs\", \"C:/Users/Mmutawe/source/repos/Test OpenGL/Test OpenGL/shaders/frag.fs\");
بعد نجاح عملية الاتصال يجب ربط دالة Use في منطقة العرض والإستدعاء الخاصة بمكتبة OpenGL.
وقد قمنا بوضع دالة منفصلة تحقق عملية الإتصال. تصبح نتيجة العرض كما في الصورة التالية:
تبدو النتائج قليلة بعض الشيء ، بل ربما مشابهة للدرس السابق.
لكننا بالفعل حققنا عملية ربط GLSL من ملفات خارجية ، وبذلك سيسهل العمل عند بناء نماذج ثلاثية الأبعاد واستدعاء الكاميرات.