في مرحلة ما من مغامرة الرسم ، ستحتاج إلى إخراج النص من خلال OpenGL. على عكس ما قد تتوقعه ، فإن الحصول على سطر بسيط على الشاشة أمر صعب للغاية مع مكتبة منخفضة المستوى مثل OpenGL. إذا كنت لا تحتاج إلى أكثر من 128 حرفًا مختلفًا لرسم النص ، فلن يكون ذلك صعبًا. تنشأ الصعوبات عندما لا تتطابق الأحرف مع الطول والعرض والإزاحة. حسب المكان الذي تعيش فيه ، قد تحتاج إلى أكثر من 128 حرفًا. ولكن ماذا لو كنت تريد شخصيات خاصة ، شخصيات رياضية أو موسيقية؟ بمجرد أن تفهم أن رسم النص ليس أسهل المهام ، فسوف تتوصل إلى إدراك أنه على الأرجح يجب ألا ينتمي إلى واجهة برمجة التطبيقات منخفضة المستوى مثل OpenGL.
نظرًا لأن برنامج OpenGL لا يوفر أي وسيلة لتقديم النص ، فإن جميع الصعوبات التي تواجهها هذه الحالة تقع علينا. نظرًا لعدم وجود "Symbol" للرسوم البيانية البدائية ، سيتعين علينا اختراعها بأنفسنا. هناك بالفعل أمثلة جاهزة: ارسم رمزًا من خلال GL_LINES
، أو أنشئ نماذج ثلاثية الأبعاد للرموز ، أو ارسم رموزًا على الزوايا الرباعية المسطحة في مساحة ثلاثية الأبعاد.
غالبًا ما يكون المطورون كسالى جدًا لشرب القهوة واختيار الخيار الأخير. رسم هذه المربعات الرباعية ليس بالأمر الصعب مثل اختيار الملمس المناسب. في هذا البرنامج التعليمي ، سنتعلم بعض الطرق ونكتب عارض النصوص المتقدم والمرن باستخدام FreeType.
محتوىالجزء 2. الإضاءة الأساسية الجزء 4. ميزات OpenGL المتقدمة الجزء 5. الإضاءة المتقدمة الكلاسيكية: الخطوط النقطية
ذات مرة في وقت الديناصورات ، تضمن تقديم النص تحديد خط (أو إنشائه) للتطبيق ونسخ الأحرف المطلوبة على نسيج كبير يسمى خط صورة نقطية. يحتوي هذا الملمس على جميع الأحرف اللازمة في أجزاء معينة. وتسمى هذه الشخصيات الحروف الرسومية. كل حرف رسومي له مساحة محددة من إحداثيات الملمس المرتبطة به. في كل مرة ترسم فيها حرفًا ، يمكنك تحديد حرف رسومي محدد ورسم الجزء المطلوب فقط على رباعي مسطح.

هنا يمكنك أن ترى كيف يمكننا تقديم النص "OpenGL". نأخذ الخط النقطي ونختبر الحروف الرسومية اللازمة من النسيج ، ونختار بعناية إحداثيات النسيج ، والتي سنرسمها على عدة رباعيات. عند تشغيل المزج والحفاظ على خلفية شفافة ، نحصل على سلسلة من الأحرف على الشاشة. تم إنشاء هذا الخط الصورة النقطية باستخدام منشئ الخط صورة نقطية Codehead .
هذا النهج له إيجابيات وسلبيات. يحتوي هذا الأسلوب على تطبيق بسيط ، نظرًا لأن خطوط الصورة النقطية بالفعل منقطة. ومع ذلك ، هذا ليس دائما مريحة. إذا كنت بحاجة إلى خط مختلف ، فأنت بحاجة إلى إنشاء خط صورة نقطية جديد. علاوة على ذلك ، ستؤدي زيادة حجم الأحرف إلى عرض حواف منقطة بسرعة. علاوة على ذلك ، غالبًا ما يتم ربط خطوط الصورة النقطية بمجموعة صغيرة من الأحرف ، لذلك لن يتم عرض أحرف Unicode على الأرجح.
كانت هذه التقنية شائعة منذ وقت ليس ببعيد (وما زالت تحتفظ بشعبية) ، لأنها سريعة جدًا وتعمل على أي منصة. ولكن حتى الآن ، هناك طرق أخرى لتقديم النص. واحد منهم هو تقديم خطوط تروتايب باستخدام FreeType.
الحداثة: FreeType
FreeType هي مكتبة تقوم بتنزيل الخطوط ، وتعرضها على الصور النقطية ، وتوفر الدعم لبعض العمليات المتعلقة بالخط. تُستخدم هذه المكتبة الشائعة في أنظمة التشغيل Mac OS X و Java و Qt و PlayStation و Linux و Android. القدرة على تحميل خطوط تروتايب تجعل هذه المكتبة جذابة بما فيه الكفاية.
خط تروتايب عبارة عن مجموعة من الحروف الرسومية المحددة ليس بالبكسل ، ولكن بواسطة الصيغ الرياضية. كما هو الحال مع الصور المتجهة ، يمكن إنشاء صورة خطوط منقطة بناءً على حجم الخط المفضل. باستخدام خطوط تروتايب ، يمكنك بسهولة تقديم الحروف الرسومية بأحجام مختلفة دون فقدان الجودة.
يمكن تنزيل FreeType من الموقع الرسمي . يمكنك إما ترجمة FreeType بنفسك ، أو استخدام الإصدارات المترجمة مسبقًا ، إن وجدت ، على الموقع. تذكر ربط البرنامج الخاص بك بـ freetype.lib
وتأكد من أن المترجم يعرف مكان البحث عن ملفات الرأس.
ثم قم بإرفاق ملفات الرأس الصحيحة:
#include <ft2build.h> #include FT_FREETYPE_H
منذ أن تم تصميم FreeType بطريقة غريبة بعض الشيء (في وقت كتابة النص الأصلي ، واسمحوا لي أن أعرف إذا كان هناك شيء قد تغير) ، يمكنك وضع ملفات رأسه فقط في جذر المجلدات مع ملفات الرأس. يمكن أن يؤدي توصيل FreeType بطريقة أخرى (على سبيل المثال ، #include <3rdParty/FreeType/ft2build.h>
) إلى إثارة تعارض في ملف الرأس.
ماذا تفعل FreeType؟ يقوم بتحميل خطوط TrueType ويقوم بإنشاء صورة نقطية لكل حرف رسومي ويحسب بعض مقاييس الصورة الرمزية. يمكننا الحصول على صور نقطية لتوليد مواد ووضع كل حرف رسومي بناءً على المقاييس المستلمة.
لتنزيل خط ما ، نحتاج إلى تهيئة FreeType وتحميل الخط كوجه (كما يستدعي FreeType الخط). في هذا المثال ، نقوم بتحميل خط تروتايب arial.ttf
، تم نسخه من مجلد C: / Windows / Fonts.
FT_Library ft; if (FT_Init_FreeType(&ft)) std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; FT_Face face; if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face)) std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
إرجاع كل من هذه الوظائف FreeType قيمة غير صفرية في حالة الفشل.
بمجرد قيامنا بتحميل وجه الخط للوجه ، نحتاج إلى تحديد حجم الخط المرغوب ، والذي سنستخلصه:
FT_Set_Pixel_Sizes(face, 0, 48);
هذه الوظيفة تحدد عرض وارتفاع الصورة الرمزية. من خلال تعيين العرض على 0 (صفر) ، نسمح لـ FreeType بحساب العرض اعتمادًا على ارتفاع المجموعة.
وجه FreeType يحتوي على مجموعة من الحروف الرسومية. يمكننا FT_Load_Char
بعض الحروف الرسومية عن طريق الاتصال بـ FT_Load_Char
. هنا نحاول تحميل الصورة الرمزية X
:
if (FT_Load_Char(face, 'X', FT_LOAD_RENDER)) std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
من خلال تعيين FT_LOAD_RENDER
كواحدة من علامات التنزيل ، نطلب من FreeType إنشاء صورة نقطية بتدرجات رمادية 8 بت ، والتي يمكننا الحصول عليها مثل هذا:
face->glyph->bitmap;
الحروف الرسومية المحملة بـ FreeType ليست بنفس حجم الخطوط النقطية. الصورة النقطية التي تم إنشاؤها باستخدام FreeType هي الحجم الأدنى لحجم الخط المحدد ولا تكفي إلا لاستيعاب حرف واحد. على سبيل المثال ، صورة نقطية للصورة الرمزية .
أصغر بكثير من الصورة الرمزية X
الرمزية X
لهذا السبب ، يقوم FreeType أيضًا بتنزيل بعض المقاييس التي توضح الحجم ومكان وجود حرف واحد. يوجد أدناه صورة توضح المقاييس التي يحسبها FreeType لكل حرف رسومي.

كل حرف رسومي موجود على خط الأساس (خط أفقي بسهم). بعضها بالضبط على خط الأساس ( X
) ، وبعضها أدناه ( g
، p
). تحدد هذه المقاييس بدقة الإزاحات لتحديد موضع الحروف الرسومية بدقة على الأساس ، وتعديل حجم الحروف الرسومية ومعرفة عدد البكسلات التي تحتاج إلى تركها لرسم الحروف الرسومية التالية. فيما يلي قائمة بالمقاييس التي سنستخدمها:
- العرض : عرض الصورة الرمزية بالبكسل ، الوصول عن طريق
face->glyph->bitmap.width
- الارتفاع : ارتفاع الصورة الرمزية بالبكسل ، الوصول عن طريق
face->glyph->bitmap.rows
الصورة الرمزية face->glyph->bitmap.rows
- bearingX : الإزاحة الأفقية للنقطة اليسرى العليا للصورة الرمزية بالنسبة إلى الأصل ، والوصول عن طريق
face->glyph->bitmap_left
- المحامل Y : الإزاحة الرأسية للنقطة اليسرى العليا للرسمة نسبة إلى الأصل ، والوصول عن طريق
face->glyph->bitmap_top
- مقدما : الإزاحة الأفقية لبداية الصورة الرمزية التالية بـ 1/64 بكسل بالنسبة إلى الأصل ، والوصول عن طريق
face->glyph->advance.x
يمكننا تحميل صورة رمزية لرمز ما ، والحصول على قياساته وإنشاء نسيج في كل مرة نريد أن نرسمها على الشاشة ، ولكن إنشاء مواد لكل رمز على كل إطار ليست طريقة جيدة. من الأفضل أن نحفظ البيانات التي تم إنشاؤها في مكان ما ونطلبها عندما نحتاج إليها. نحدد هيكلًا مناسبًا std::map
في std::map
:
struct Character { GLuint TextureID;
في هذه المقالة ، سنعمل على تبسيط حياتنا وسوف نستخدم فقط الـ 128 حرفًا الأولى. لكل حرف ، سنقوم بإنشاء نسيج وحفظ البيانات اللازمة في بنية من النوع Character
، والتي سنضيفها إلى Characters
type std::map
. وبالتالي ، يتم حفظ جميع البيانات اللازمة لرسم شخصية للاستخدام في المستقبل.
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
داخل الحلقة ، لكل من أول 128 حرفًا ، نحصل على حرف رسومي ، وننشئ نسيجًا ، ونضبط إعداداته ونحفظ المقاييس. من المثير للاهتمام أن نلاحظ أننا نستخدم GL_RED
internalFormat
GL_RED
القوام. الصورة النقطية التي تم إنشاؤها عن طريق الصورة الرمزية هي صورة ذات تدرج رمادي 8 بت ، يشغل كل بكسل منها بايت واحد. لهذا السبب ، سنقوم بتخزين مخزن الصور النقطية كقيمة لون النسيج. يتم تحقيق ذلك عن طريق إنشاء نسيج يتوافق فيه كل بايت مع المكون الأحمر للون. إذا استخدمنا بايت واحد لتمثيل ألوان النسيج ، فلا تنسَ قيود OpenGL:
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
يتطلب برنامج OpenGL أن تحتوي جميع القوام على إزاحة مكونة من 4 بايت ، أي يجب أن يكون حجمها مضاعفات 4 بايت (على سبيل المثال 8 بايت أو 4000 بايت أو 2048 بايت) أو (و) يجب أن تستخدم 4 بايت لكل بكسل (مثل بتنسيق RGBA) ، ولكن نظرًا لاستخدامنا بايت واحد لكل بكسل ، يمكن أن يكون لها مختلف على نطاق واسع. عن طريق تعيين إزاحة محاذاة فك الحزم (هل هناك ترجمة أفضل؟) إلى 1 ، نقوم بإزالة أخطاء الإزاحة التي قد تسبب أخطاء.
أيضًا ، عندما ننتهي من العمل مع الخط نفسه ، يجب أن نحذف موارد FreeType:
FT_Done_Face(face);
تظليل
لرسم الحروف الرسومية ، استخدم تظليل قمة الرأس التالي:
#version 330 core layout (location = 0) in vec4 vertex;
نحن نجمع بين موقف رمز والإحداثيات في vec4
واحد. يحسب تظليل vertex ناتج الإحداثيات باستخدام مصفوفة الإسقاط وينقل إحداثيات الملمس إلى تظليل الأجزاء:
#version 330 core in vec2 TexCoords; out vec4 color; uniform sampler2D text; uniform vec3 textColor; void main() { vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); color = vec4(textColor, 1.0) * sampled; }
يقبل شادر الشظتان متغيرين عالميين - صورة أحادية اللون من الصورة الرمزية ولون الصورة الرمزية نفسها. أولا ، نحن عينة قيمة اللون من الصورة الرمزية. نظرًا لأن بيانات المادة مخزّنة في المكون الأحمر من المادة ، فنحن نعتبر فقط المكون r
كقيمة شفافية. عن طريق تغيير شفافية اللون ، سيكون اللون الناتج شفافًا على خلفية الصورة الرمزية ومعتمًا إلى وحدات البكسل الحقيقية للصورة الرمزية. نقوم أيضًا بضرب ألوان RGB مع متغير textColor لتغيير لون النص.
ولكن لكي تعمل آليتنا ، تحتاج إلى تمكين الخلط:
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
كما مصفوفة الإسقاط ، سيكون لدينا مصفوفة إسقاط الهجاء. لرسم نص ، في الواقع ، مصفوفة المنظور غير مطلوبة ، ويسمح لنا استخدام الإسقاط الهجائي أيضًا بتعيين جميع إحداثيات الرأس في إحداثيات الشاشة إذا قمنا بتعيين المصفوفة مثل هذا:
glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f);
وضعنا أسفل المصفوفة على 0.0f
، من أعلى إلى ارتفاع النافذة. نتيجة لذلك ، يأخذ الإحداثي y
القيم من أسفل الشاشة ( y = 0
) إلى أعلى الشاشة ( y = 600
). هذا يعني أن النقطة (0, 0)
تشير إلى الركن الأيسر السفلي من الشاشة.
في الختام ، قم بإنشاء VBO و VAO لرسم الرباعي. نحن هنا نحتفظ بذاكرة كافية في VBO حتى نتمكن من تحديث البيانات لرسم الأحرف.
GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
يتطلب رباعي الزوايا 6 رؤوس من 4 أرقام للفاصلة العائمة ، لذلك نحتفظ بـ 6 * 4 = 24
عائم ذاكرات. نظرًا لأننا سنقوم بتغيير بيانات قمة الرأس في كثير من الأحيان ، فإننا نخصص الذاكرة باستخدام GL_DYNAMIC_DRAW
.
عرض سطر النص
لعرض سطر من النص ، نقوم باستخراج بنية Character
المطابق للرمز وحساب أبعاد الرباعي من مقاييس الرمز. من الأبعاد المحسوبة للرباعي ، على الطاير نقوم بإنشاء مجموعة من 6 رؤوس وتحديث بيانات قمة الرأس باستخدام glBufferSubData
.
للراحة ، RenderText
وظيفة RenderText
التي ترسم سلسلة من الأحرف:
void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color) {
محتويات الوظيفة واضحة نسبيًا: حساب أصل وأحجام ورؤوس رباعي الزوايا. لاحظ أننا ضربنا كل مقياس حسب scale
. بعد ذلك ، قم بتحديث VBO ورسم رباعية.
يتطلب هذا السطر من التعليمات البرمجية بعض الاهتمام:
GLfloat ypos = y - (ch.Size.y - ch.Bearing.y);
يتم رسم بعض الأحرف ، مثل p
و g
، بشكل ملحوظ أسفل الخط الأساسي ، مما يعني أن RenderText
يجب أن يكون أقل بشكل ملحوظ من المعلمة y
لوظيفة RenderText
. يمكن التعبير عن الإزاحة الدقيقة y_offset
من مقاييس الصورة الرمزية:

لحساب الإزاحة ، نحتاج إلى أذرع مستقيمة لمعرفة المسافة التي يقع بها الرمز أسفل الخط الأساسي. تظهر هذه المسافة بالسهم الأحمر. من الواضح ، y_offset = bearingY - height
و ypos = y + y_offset
.
إذا تم كل شيء بشكل صحيح ، يمكنك عرض النص على الشاشة مثل هذا:
RenderText(shader, "This is sample text", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f)); RenderText(shader, "(C) LearnOpenGL.com", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f));
يجب أن تبدو النتيجة كما يلي:

رمز المثال هنا (رابط إلى موقع المؤلف الأصلي).
لفهم المربعات الرباعية ، قم بإيقاف المزج:

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