ملف GLSL سيزداد حجمه عند تخصيص بعض المتجهات بحسب ما قمنا بالحديث عنه في درس uniforms. وذلك من شأنه أن يعيق فهم وقراءة الشيفرة التي سنقوم بوضعها في كل جزء من دورتنا.[1]
وبالتالي يتعين علينا بناء ملف GLSL لكل من Vertex Shader و Fragment Shader لإضفاء مزيدًا من التنظيم على المصادر.[1]
على سبيل المثال , لقد قمنا بتخصيص بضعًا من الدوال التي جردنا عملياتها لتسهيل التنظيم وفي فصل ملف GLSL العديد من المزايا التي تمكننا من تحليل شيفرة البرنامج أيا كانت.[1]
وبالتالي يسهل حصر الأخطاء مع التخلي عن رموز التي يتم فرض استخدامها من قبل متغيرات char.[1]
سنعمل على إضافة 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 إدراك بعض الأساسيات حولها قبل الإستمرار في هذا الدرس.[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 ليتم اقتناص الأخطاء وإظهارها بطريقة أو بأخرى.
وأما في حال قمت بالتخلي عن هذا النمط من فحص وقراءة الملفات , من المحتمل ظهور بعض الأخطاء في القراءة.
ستكشف لك الشيفرة التالية بعض الطرق التقليدية في جلب الملفات:
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 بكافة مكوناته أصبح لدينا قدرة على الاتصال بــ 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 من ملفات خارجية , وبذلك سيسهل العمل عند بناء نماذج ثلاثية الأبعاد واستدعاء الكاميرات.