تعلم برنامج OpenGL. الدرس 5.10 - انسداد مساحة الشاشة المحيطة

OGL3

SSAO


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

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

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


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


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

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


هنا ، تقع كل نقطة رمادية داخل كائن هندسي معين ، وبالتالي تسهم في قيمة معامل التظليل. كلما زاد عدد العينات داخل هندسة الكائنات المحيطة ، كلما قلت الكثافة المتبقية لتظليل الخلفية في هذه المنطقة.

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


كما يتبين ، تتم إزالة النطاقات الملحوظة بسبب العدد الصغير للعينات بشكل جيد عن طريق إدخال التوزيع العشوائي لاتجاه العينات.

كان لدى تطبيق Crytek الخاص لـ SSAO أسلوب مرئي يمكن التعرف عليه. نظرًا لأن متخصصي Crytek استخدموا قلبًا كرويًا للعينة ، فقد أثر ذلك على الأسطح المستوية مثل الجدران ، مما جعلها مظللة - لأن نصف حجم قلب العينة تم غمره تحت الهندسة. فيما يلي لقطة شاشة لمشهد من Crysis معروض بالتدرج الرمادي بناءً على قيمة عامل التظليل. هنا يظهر تأثير "الرمادي" بوضوح:


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


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

مخزن البيانات الخام


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

  • متجه الموضع لكل جزء ؛
  • ناقل عادي لكل جزء.
  • لون منتشر لكل جزء ؛
  • لب العينة
  • متجه دوران عشوائي لكل جزء يستخدم في إعادة توجيه قلب العينة.

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


نظرًا لأن SSAO هو تأثير محقق في مساحة الشاشة ، فمن الممكن إجراء حساب مباشر عن طريق عرض رباعي بملء الشاشة. ولكن بعد ذلك لن يكون لدينا بيانات عن هندسة المشهد. للتغلب على هذا التقييد ، سنقوم بتقديم جميع المعلومات الضرورية في النسيج ، والتي سيتم استخدامها لاحقًا في تظليل SSAO للوصول إلى المعلومات الهندسية وغيرها حول المشهد. إذا اتبعت هذه الدروس بعناية ، فيجب أن تعرف بالفعل في النهج الموصوف ظهور خوارزمية التظليل المتأخرة. هذا هو إلى حد كبير سبب ظهور تأثير SSAO كمواطن في العرض مع تظليل مؤجل - بعد كل شيء ، تتوفر المواد التي تخزن الإحداثيات والمعايير في G-buffer.

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

نظرًا لأن الوصول إلى معلومات الجزء حول الإحداثيات والمعايير يجب أن يكون متاحًا بالفعل بسبب المخزن المؤقت G ، فإن تظليل الجزء لمرحلة المعالجة الهندسية بسيط للغاية:

#version 330 core layout (location = 0) out vec4 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; void main() { //        gPosition = FragPos; //       gNormal = normalize(Normal); //    -   gAlbedoSpec.rgb = vec3(0.95); } 

نظرًا لأن خوارزمية SSAO لها تأثير في مساحة الشاشة ، ويتم حساب عامل التظليل بناءً على المنطقة المرئية من المشهد ، فمن المنطقي إجراء الحسابات في مساحة العرض. في هذه الحالة ، يقوم متغير FragPos الذي تم الحصول عليه من shader vertex بتخزين الموضع تمامًا في إطار العرض. يجدر التأكد من تخزين الإحداثيات والمعايير في المخزن المؤقت G في مساحة العرض ، حيث سيتم إجراء جميع الحسابات الإضافية فيه.

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

يتم تكوين نسيج المخزن المؤقت للألوان gPosition على النحو التالي:

 glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 

يخزن هذا النسيج إحداثيات الشظايا ويمكن استخدامه للحصول على بيانات العمق لكل نقطة من قلب العينات. ألاحظ أن الملمس يستخدم تنسيق بيانات الفاصلة العائمة - وهذا سيسمح بإحداثيات الشظايا لا يتم تقليلها إلى الفاصل الزمني [0. ، 1.]. انتبه أيضًا إلى وضع التكرار - تم تعيين GL_CLAMP_TO_EDGE . هذا ضروري للقضاء على إمكانية عدم المبالغة في مساحة الشاشة عن قصد. تجاوز الفاصل الزمني الرئيسي لإحداثيات النسيج سيعطينا بيانات غير صحيحة عن الموضع والعمق.

بعد ذلك ، سننخرط في تكوين قلب نصف كروي للعينات وإنشاء طريقة للتوجيه العشوائي.

خلق نصف الكرة ذات الاتجاه الطبيعي


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


بافتراض أن نصف قطر نصف الكرة الأرضية يكون عملية واحدة ، فإن تكوين قلب عينة من 64 نقطة يبدو كما يلي:

 //      0.0 - 1.0 std::uniform_real_distribution<float> randomFloats(0.0, 1.0); std::default_random_engine generator; std::vector<glm::vec3> ssaoKernel; for (unsigned int i = 0; i < 64; ++i) { glm::vec3 sample( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) ); sample = glm::normalize(sample); sample *= randomFloats(generator); float scale = (float)i / 64.0; ssaoKernel.push_back(sample); } 

هنا نختار عشوائيًا إحداثيات x و y في الفاصل الزمني [-1.، 1.] ، وإحداثيات z في الفاصل الزمني [0. ، 1.] (إذا كان الفاصل الزمني هو نفسه لـ x و y ، فسوف نحصل على قلب كروي أخذ العينات). ستقتصر نواقل العينة الناتجة على نصفي الكرة الأرضية ، حيث سيتم توجيه قلب العينة في النهاية على طول الوضع الطبيعي إلى السطح.

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

 scale = lerp(0.1f, 1.0f, scale * scale); sample *= scale; ssaoKernel.push_back(sample); } 

يتم تعريف دالة lerp () على النحو التالي:

 float lerp(float a, float b, float f) { return a + f * (b - a); } 

تعطينا مثل هذه الخدعة توزيعاً معدلاً ، حيث تقع معظم نقاط العينة بالقرب من أصل النواة.


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

دوران عشوائي للعينة الأساسية


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

أنشئ مصفوفة 4 × 4 واملأها بنواقل دوران عشوائية موجهة على طول الناقل العادي في الفضاء المماس:

 std::vector<glm::vec3> ssaoNoise; for (unsigned int i = 0; i < 16; i++) { glm::vec3 noise( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); ssaoNoise.push_back(noise); } 

نظرًا لأن المحاذاة الأساسية على طول semiaxis Z الموجب في الفضاء المماس ، نترك المكون z مساويًا للصفر - وهذا سيضمن الدوران فقط حول المحور Z.

بعد ذلك ، قم بإنشاء نسيج 4x4 واملأه بمجموعة المتجهات الدوارة لدينا. تأكد من استخدام وضع إعادة GL_REPEAT لتجانب الملمس:

 unsigned int noiseTexture; glGenTextures(1, &noiseTexture); glBindTexture(GL_TEXTURE_2D, noiseTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 

حسنًا ، لدينا الآن جميع البيانات اللازمة للتنفيذ المباشر لخوارزمية SSAO!

شادر SSAO


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

 unsigned int ssaoFBO; glGenFramebuffers(1, &ssaoFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); unsigned int ssaoColorBuffer; glGenTextures(1, &ssaoColorBuffer); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0); 

نظرًا لأن نتيجة الخوارزمية هي الرقم الحقيقي الوحيد داخل [0. ، 1.] ، للتخزين سيكون كافياً لإنشاء نسيج بالمكون الوحيد المتاح. هذا هو السبب في تعيين GL_RED كتنسيق داخلي لمخزن الألوان المؤقت.

بشكل عام ، تبدو عملية تقديم مرحلة SSAO كالتالي:

 //  :  G- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); [...] glBindFramebuffer(GL_FRAMEBUFFER, 0); //  G-      SSAO glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, noiseTexture); shaderSSAO.use(); SendKernelSamplesToShader(); shaderSSAO.setMat4("projection", projection); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0); //  :    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shaderLightingPass.use(); [...] glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); [...] RenderQuad(); 

يقبل shaderSSAO shader مواد G-buffer التي يحتاجها كمدخل ، بالإضافة إلى نسيج الضجيج ونواة العينة:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D texNoise; uniform vec3 samples[64]; uniform mat4 projection; //             //      1280x720 const vec2 noiseScale = vec2(1280.0/4.0, 720.0/4.0); void main() { [...] } 

لاحظ مقياس الضوضاء المتغير. يجب تجانب قوامنا الصغير مع الضجيج على كامل سطح الشاشة ، ولكن نظرًا لأن إحداثيات نسيج TexCoords تقع ضمن [0. ، 1.] فإن هذا لن يحدث بدون تدخلنا. لهذه الأغراض ، نحسب عامل إحداثيات النسيج ، والذي يوجد كنسبة من حجم الشاشة إلى حجم نسيج الضوضاء:

 vec3 fragPos = texture(gPosition, TexCoords).xyz; vec3 normal = texture(gNormal, TexCoords).rgb; vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz; 

منذ إنشاء نسيج ضوضاء texNoise ، قمنا بتعيين وضع التكرار على GL_REPEAT ، والآن سيتكرر عدة مرات على سطح الشاشة. مع وجود randomVec و fragPos والقيم العادية في متناول اليد ، يمكننا إنشاء مصفوفة تحويل TBN من الظل إلى مساحة الأنواع:

 vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal); 

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

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

بينما يبدو الأمر مربكًا ، دعنا نذهب عبر الخطوات:

 float occlusion = 0.0; for(int i = 0; i < kernelSize; ++i) { //     vec3 sample = TBN * samples[i]; //      - sample = fragPos + sample * radius; [...] } 

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

بعد هذه الخطوات ، يجب علينا تحويل ناقل العينة الناتج إلى مساحة شاشة ، حتى نتمكن من التحديد من نسيج G-buffer الذي يخزن مواضع وأعماق الأجزاء باستخدام القيمة المتوقعة التي تم الحصول عليها. نظرًا لأن العينة في إطار العرض ، فنحن بحاجة إلى مصفوفة إسقاط الإسقاط :

 vec4 offset = vec4(sample, 1.0); offset = projection * offset; //     offset.xyz /= offset.w; //   offset.xyz = offset.xyz * 0.5 + 0.5; //    [0., 1.] 

بعد التحويل إلى مساحة المقطع ، نقوم بإجراء تقسيم المنظور يدويًا عن طريق قسمة مكونات xyz على المكون w . يتم ترجمة المتجه الناتج في إحداثيات الجهاز المعياري ( NDC ) إلى فاصل القيم [0. ، 1.] بحيث يمكن استخدامه كم إحداثيات نسيج:

 float sampleDepth = texture(gPosition, offset.xy).z; 

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

 occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0); 

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

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


سيحد الاختيار من المساهمة في معامل التظليل فقط لقيم العمق الموجودة داخل نصف قطر العينة:

 float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth)); occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0) * rangeCheck; 

نستخدم أيضًا دالة GLSL smoothstep () ، التي تنفذ استيفاءًا سلسًا للمعلمة الثالثة بين الأولى والثانية. في نفس الوقت ، يتم إرجاع 0 إذا كانت المعلمة الثالثة أقل من أو تساوي الأولى ، أو 1 إذا كانت المعلمة الثالثة أكبر من أو تساوي الثانية. إذا كان فرق العمق داخل نصف القطر ، فسيتم تنعيم قيمته بسلاسة في الفاصل الزمني [0. ، 1.] وفقًا لهذا المنحنى:


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

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

 } occlusion = 1.0 - (occlusion / kernelSize); FragColor = occlusion; 

بالنسبة للمشهد الذي يرتدي بدلة نانوية كاذبة مألوفة لنا ، فإن أداء تظليل SSAO يعطي النسيج التالي:


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

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

تظليل الخلفية طمس


بعد بناء نتيجة SSAO وقبل الخلط النهائي للإضاءة ، من الضروري طمس النسيج الذي يخزن البيانات في معامل التظليل. للقيام بذلك ، سيكون لدينا آخر Framebuffer:

 unsigned int ssaoBlurFBO, ssaoColorBufferBlur; glGenFramebuffers(1, &ssaoBlurFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO); glGenTextures(1, &ssaoColorBufferBlur); glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0); 

يوفر تجانب نسيج الضجيج في مساحة الشاشة خصائص عشوائية محددة جيدًا يمكنك استخدامها لصالحك عند إنشاء مرشح ضبابي:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D ssaoInput; void main() { vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0)); float result = 0.0; for (int x = -2; x < 2; ++x) { for (int y = -2; y < 2; ++y) { vec2 offset = vec2(float(x), float(y)) * texelSize; result += texture(ssaoInput, TexCoords + offset).r; } } FragColor = result / (4.0 * 4.0); } 

يقوم جهاز التظليل ببساطة بتحويل texels من نسيج SSAO مع إزاحة من -2 إلى +2 ، وهو ما يتوافق مع الحجم الفعلي لنسيج الضجيج. الإزاحة تساوي الحجم الدقيق لتكسل واحد: يتم استخدام وظيفة نسيج () للحساب ، والتي ترجع vec2 مع أبعاد النسيج المحدد. T.O. يقوم جهاز التظليل ببساطة بحساب متوسط ​​النتائج المخزنة في النسيج ، مما يعطي تمويهًا سريعًا وفعالًا إلى حد ما:


في المجموع ، لدينا نسيج مع بيانات تظليل الخلفية لكل جزء على الشاشة - كل شيء جاهز لمرحلة تقليل الصورة النهائية!

تطبيق تظليل الخلفية


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

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; uniform sampler2D ssao; struct Light { vec3 Position; vec3 Color; float Linear; float Quadratic; float Radius; }; uniform Light light; void main() { //    G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Diffuse = texture(gAlbedo, TexCoords).rgb; float AmbientOcclusion = texture(ssao, TexCoords).r; //   -    //   :   -  vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion); vec3 lighting = ambient; //    (0, 0, 0)   - vec3 viewDir = normalize(-FragPos); //   vec3 lightDir = normalize(light.Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color; //   vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0); vec3 specular = light.Color * spec; //   float dist = length(light.Position - FragPos); float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist); diffuse *= attenuation; specular *= attenuation; lighting += diffuse + specular; FragColor = vec4(lighting, 1.0); } 

هناك تغييران رئيسيان فقط: الانتقال إلى الحسابات في إطار العرض وضرب عنصر إضاءة الخلفية بقيمة AmbientOcclusion . مثال على مشهد بضوء أزرق واحد:


كود المصدر الكامل هنا .

يعتمد مظهر تأثير SSAO بشدة على معلمات مثل kernelSize والقطر والتحيز ، وغالبًا ما يكون ضبطها أمرًا طبيعيًا للفنان ليحدد موقعًا / مشهدًا معينًا. لا توجد مجموعات "أفضل" وعالمية من المعلمات: بالنسبة لبعض المشاهد ، يكون نصف القطر الصغير من قلب العينة جيدًا ، بينما يستفيد البعض الآخر من زيادة نصف القطر وعدد العينات. يستخدم المثال 64 نقطة عينة ، وهي ، بصراحة ، زائدة عن الحاجة ، ولكن يمكنك دائمًا تعديل الرمز ومعرفة ما يحدث مع عدد أقل من العينات.

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

 occlusion = 1.0 - (occlusion / kernelSize); FragColor = pow(occlusion, power); 

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

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

موارد إضافية


  1. دروس SSAO : مقالة درس ممتازة من جون تشابمان ، تم على أساسها بناء كود هذا الدرس.
  2. تعرف على القطع الأثرية لدى SSAO : مقالة قيمة للغاية تظهر بوضوح ليس فقط المشاكل الأكثر إلحاحًا في جودة SSAO ، ولكن أيضًا طرق حلها. القراءة الموصى بها.
  3. SSAO مع إعادة بناء العمق : إضافة إلى درس SSAO الرئيسي من قبل OGLDev فيما يتعلق بالتقنية شائعة الاستخدام لاستعادة إحداثيات القطع بناءً على العمق. ترجع أهمية هذا النهج إلى وفورات كبيرة في الذاكرة بسبب عدم الحاجة إلى تخزين المواقف في المخزن المؤقت G. النهج هو عالمي جدا ، وينطبق على SSAO بقدر ما.

ملاحظة : لدينا برقية أسيوط لتنسيق التحويلات. إذا كانت لديك رغبة جادة في المساعدة في الترجمة ، فأنت مرحب بك!

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


All Articles