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

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

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

الميزة الرئيسية هي أن المعلومات المخزنة في المخزن المؤقت G تنتمي إلى أقرب الشظايا التي لا يحجبها أي شيء: اختبار العمق يتركها فقط. بفضل هذا ، نحسب الإضاءة لكل بكسل مرة واحدة فقط ، دون القيام بالكثير من العمل. علاوة على ذلك ، تتيح لنا الإضاءة المؤجلة فرصًا لمزيد من التحسينات ، مما يسمح لنا باستخدام مصادر إضاءة أكثر بكثير من الإضاءة المباشرة.
ومع ذلك ، هناك بعض العوائق: يخزن المخزن المؤقت G كمية كبيرة من المعلومات حول المشهد. بالإضافة إلى ذلك ، يجب تخزين بيانات نوع الموضع بدقة عالية ، ونتيجة لذلك ، يشغل المخزن المؤقت G مساحة كبيرة من الذاكرة. عيب آخر هو أننا لن نكون قادرين على استخدام الأشياء الشفافة (لأن المخزن المؤقت يخزن المعلومات فقط للسطح الأقرب) وأن مكافحة التعرج مثل MSAA لن تعمل أيضًا. هناك العديد من الحلول لحل هذه المشاكل ، تتم مناقشتها في نهاية المقالة.
(ممر ملاحظة. - يستهلك المخزن المؤقت G مساحة كبيرة من الذاكرة. على سبيل المثال ، بالنسبة لشاشة بدقة 1920 * 1080 واستخدام 128 بت لكل بكسل ، سيستغرق المخزن المؤقت 33 ميجابايت. هناك متطلبات متزايدة لعرض النطاق الترددي للذاكرة - تتم كتابة وقراءة المزيد من البيانات)
عازلة G
يشير G-buffer إلى المواد المستخدمة لتخزين المعلومات المتعلقة بالإضاءة المستخدمة في آخر تمرير العرض. دعنا نرى المعلومات التي نحتاجها لحساب الإضاءة للعرض المباشر:
- متجه الموضع ثلاثي الأبعاد: يستخدم لمعرفة موضع الجزء المتعلق بالكاميرا ومصادر الضوء.
- لون منتشر للجزء (انعكاسية للون الأحمر والأخضر والأزرق - بشكل عام ، اللون).
- ناقل عادي ثلاثي الأبعاد (لتحديد أي زاوية تسقط الضوء على السطح)
- تعويم لتخزين مكون المرآة
- موقف مصدر الضوء ولونه.
- موضع الكاميرا.
باستخدام هذه المتغيرات ، يمكننا حساب التغطية باستخدام نموذج Blinn-Fong الذي نعرفه بالفعل. يمكن أن يكون لون ومصدر مصدر الضوء ، وكذلك موضع الكاميرا متغيرات شائعة ، لكن بقية القيم ستكون مختلفة لكل جزء من الصورة. إذا قمنا بتمرير نفس البيانات تمامًا إلى الممر النهائي للإضاءة المؤجلة ، والذي سنستخدمه للتمرير المباشر ، فسوف نحصل على نفس النتيجة ، على الرغم من حقيقة أننا سوف نرسم شظايا على مستطيل عادي ثنائي الأبعاد.
لا توجد قيود على OpenGL على ما يمكننا تخزينه في النسيج ، لذلك من المنطقي تخزين جميع المعلومات في نسيج واحد أو أكثر بحجم الشاشة (يسمى المخزن المؤقت G) واستخدامها جميعًا في مرور الإضاءة. نظرًا لأن حجم القوام والشاشة هو نفسه ، نحصل على نفس بيانات الإدخال مثل الإضاءة المباشرة.
في الكود الزائف ، تبدو الصورة العامة شيئًا مثل هذا:
while(...) // render loop { // 1. : / g- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); gBufferShader.use(); for(Object obj : Objects) { ConfigureShaderTransformsAndUniforms(); obj.Draw(); } // 2. : g- glBindFramebuffer(GL_FRAMEBUFFER, 0); glClear(GL_COLOR_BUFFER_BIT); lightingPassShader.use(); BindAllGBufferTextures(); SetLightingUniforms(); RenderQuad(); }
المعلومات المطلوبة لكل بكسل: متجه الموضع والمتجه العادي ومتجه اللون والقيمة لمكون المرآة . في الممر الهندسي ، نرسم كل الكائنات في المشهد ونحفظ كل هذه البيانات في المخزن المؤقت G. يمكننا استخدام أهداف تجسيد متعددة لملء جميع المخازن المؤقتة في سحب واحد ، تمت مناقشة هذا النهج في المقالة السابقة حول تنفيذ التوهج: Bloom ، الترجمة على المحور
بالنسبة للممر الهندسي ، قم بإنشاء حاجز إطار يحمل الاسم الواضح gBuffer ، والذي سنقوم بإرفاق العديد من مخازن الألوان ومخزن عمق واحد. لتخزين المواضع والعادات ، يفضل استخدام مادة ذات دقة عالية (قيم تعويم 16 أو 32 بت لكل مكون) ، سنقوم بتخزين اللون المنتشر والقيم المرآوية في النسيج بشكل افتراضي (8 بت لكل دقة مكون).
unsigned int gBuffer; glGenFramebuffers(1, &gBuffer); glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); unsigned int gPosition, gNormal, gColorSpec; // 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); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0); // glGenTextures(1, &gNormal); glBindTexture(GL_TEXTURE_2D, gNormal); 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); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0); // + glGenTextures(1, &gAlbedoSpec); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 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_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0); // OpenGL, unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 }; glDrawBuffers(3, attachments); // . [...]
نظرًا لأننا نستخدم العديد من أهداف العرض ، يجب أن نوضح برنامج OpenGL بشكل صريح إلى أي مخازن من GBuffer المرفقة التي glDrawBuffers()
في glDrawBuffers()
. من الجدير بالذكر أيضًا أننا نقوم بتخزين المواضع والعناصر الطبيعية تحتوي على 3 مكونات لكل منها ، ونقوم بتخزينها في مواد RGB. ولكن في الوقت نفسه ، وضعنا على الفور في نفس نسيج RGBA كل من اللون ومعامل الانعكاس المرآوي - وبفضل هذا نستخدم مخزنًا مؤقتًا أقل. إذا أصبح تنفيذ العرض المؤجل أكثر تعقيدًا واستخدم المزيد من البيانات ، يمكنك بسهولة العثور على طرق جديدة لدمج البيانات وترتيبها في الأنسجة.
في المستقبل ، يجب علينا تقديم البيانات إلى المخزن المؤقت G. إذا كان لكل كائن اللون ، ومعامل الانعكاس العادي ، فإنه يمكننا كتابة شيء ما مثل التظليل التالي:
#version 330 core layout (location = 0) out vec3 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; uniform sampler2D texture_diffuse1; uniform sampler2D texture_specular1; void main() { // G- gPosition = FragPos; // G- gNormal = normalize(Normal); // gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb; // gAlbedoSpec.a = texture(texture_specular1, TexCoords).r; }
نظرًا لأننا نستخدم العديد من أهداف العرض ، layout
إلى ماذا وفي أي مخزن مؤقت لإطار الإطارات الحالي نقدمه. يرجى ملاحظة أننا لا نخزن معامل المرآة في مخزن مؤقت منفصل ، حيث يمكننا تخزين القيمة العائمة في قناة ألفا لأحد المخازن المؤقتة.
ضع في اعتبارك أنه عند حساب الإضاءة ، من المهم للغاية تخزين جميع المتغيرات في نفس مساحة الإحداثيات ، وفي هذه الحالة نقوم بتخزين (وإجراء العمليات الحسابية) في مساحة العالم.
إذا قدمنا الآن العديد من النانو في المخزن المؤقت G ورسمنا محتوياته عن طريق عرض كل مخزن مؤقت على ربع الشاشة ، فسوف نرى شيئًا مثل هذا:

حاول تصور الموضع والمتجهات العادية وتأكد من صحتها. على سبيل المثال ، تكون المتجهات العادية التي تشير إلى اليمين حمراء. وبالمثل مع الأشياء الموجودة على يمين وسط المشهد. بعد أن تكون راضيًا عن محتويات المخزن المؤقت G ، دعنا ننتقل إلى الجزء التالي: مرور الإضاءة.
مرور الإضاءة
الآن لدينا كمية كبيرة من المعلومات في المخزن المؤقت G ، ونحن قادرون على حساب الإضاءة والألوان النهائية لكل بكسل من المخزن المؤقت G بشكل كامل ، باستخدام محتوياته كمدخل لخوارزميات حساب الإضاءة. نظرًا لأن قيم المخزن المؤقت G لا تمثل سوى الأجزاء المرئية ، فسنجري حسابات إضاءة معقدة مرة واحدة لكل بكسل. ونتيجة لذلك ، تكون الإضاءة المؤجلة فعالة للغاية ، خاصة في المشاهد المعقدة ، حيث ، عند العرض المباشر لكل بكسل ، غالبًا ما يكون من الضروري حساب الإضاءة عدة مرات.
من أجل مرور الإضاءة ، سنقوم بتقديم مستطيل ملء الشاشة (يشبه إلى حد ما تأثير ما بعد المعالجة) وإجراء حساب بطيء للإضاءة لكل بكسل.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_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, gAlbedoSpec); // shaderLightingPass.use(); SendAllLightUniformsToShader(shaderLightingPass); shaderLightingPass.setVec3("viewPos", camera.Position); RenderQuad();
نحن نربط جميع مواد G-buffer الضرورية قبل التقديم ، بالإضافة إلى ذلك ، نعين القيم المتغيرة المتعلقة بالإضاءة في التظليل.
يشبه مظلل المقطع جزءًا كبيرًا من الذي استخدمناه في دروس الاجتماع. الجديد بشكل أساسي هو الطريقة التي نحصل بها على مدخل للإضاءة مباشرة من G-buffer.
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedoSpec; struct Light { vec3 Position; vec3 Color; }; const int NR_LIGHTS = 32; uniform Light lights[NR_LIGHTS]; uniform vec3 viewPos; void main() { // G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb; float Specular = texture(gAlbedoSpec, TexCoords).a; // vec3 lighting = Albedo * 0.1; // vec3 viewDir = normalize(viewPos - FragPos); for(int i = 0; i < NR_LIGHTS; ++i) { // vec3 lightDir = normalize(lights[i].Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Albedo * lights[i].Color; lighting += diffuse; } FragColor = vec4(lighting, 1.0); }
يقبل جهاز تظليل الإضاءة 3 مواد تحتوي على جميع المعلومات المسجلة في الممر الهندسي والتي يتكون منها العازلة G. إذا أخذنا الإدخال للإضاءة من القوام ، نحصل على نفس القيم تمامًا كما هو الحال مع العرض المباشر العادي. في بداية تظليل الجزء ، نحصل على القيم المتعلقة بمتغيرات الإضاءة بمجرد القراءة من الملمس. لاحظ أننا نحصل على كل من اللون ومعامل الانعكاس المرآوي من نسيج واحد - gAlbedoSpec
.
نظرًا لأن لكل جزء قيمًا (بالإضافة إلى متغيرات تظليل موحدة) ضرورية لحساب الإضاءة وفقًا لنموذج Blinn-Fong ، لا نحتاج إلى تغيير رمز حساب الإضاءة. الشيء الوحيد الذي تم تغييره هو الطريقة للحصول على قيم الإدخال.
إن بدء عرض توضيحي بسيط مع 32 مصدرًا صغيرًا للضوء يبدو شيئًا مثل هذا:

أحد عيوب الإضاءة المؤجلة هو استحالة المزج ، لأن جميع المخازن المؤقتة لكل بكسل تحتوي على معلومات حول سطح واحد فقط ، بينما يستخدم المزج مجموعات من عدة أجزاء. (مزج) ، ترجمة . عيب آخر للإضاءة المؤجلة هو أنه يجبرك على استخدام طريقة واحدة مشتركة لحساب الإضاءة لجميع الكائنات ؛ على الرغم من أن هذا القيد يمكن التحايل عليه بطريقة أو بأخرى عن طريق إضافة معلومات جوهرية إلى المخزن المؤقت g.
للتغلب على هذه العيوب (خاصة نقص الخلط) ، غالبًا ما يقسم العرض إلى جزأين: العرض مع الإضاءة المؤجلة ، والجزء الثاني مع العرض المباشر مخصص لتطبيق شيء على المشهد أو استخدام تظليل غير متوافق مع الإضاءة المؤجلة. (ملاحظة. من الأمثلة: إضافة دخان نصف شفاف ، نار ، زجاج) لتوضيح العمل ، سنرسم مصادر الضوء كمكعبات صغيرة باستخدام التقديم المباشر ، لأن مكعبات الإضاءة تتطلب تظليلًا خاصًا (تتوهج بشكل موحد بنفس اللون).
الجمع بين العرض المؤجل مع المباشر.
افترض أننا نريد رسم كل مصدر للضوء على شكل مكعب ثلاثي الأبعاد مع مركز يتزامن مع موضع مصدر الضوء وانبعاث الضوء بلون المصدر. الفكرة الأولى التي تتبادر إلى الذهن هي تقديم مكعبات مباشرة لكل مصدر ضوء أعلى نتائج العرض المؤجلة. أي أننا نرسم مكعبات كالمعتاد ، ولكن فقط بعد العرض المؤجل. سيبدو الرمز على النحو التالي:
// [...] RenderQuad(); // shaderLightBox.use(); shaderLightBox.setMat4("projection", projection); shaderLightBox.setMat4("view", view); for (unsigned int i = 0; i < lightPositions.size(); i++) { model = glm::mat4(); model = glm::translate(model, lightPositions[i]); model = glm::scale(model, glm::vec3(0.25f)); shaderLightBox.setMat4("model", model); shaderLightBox.setVec3("lightColor", lightColors[i]); RenderCube(); }
هذه المكعبات المقدمة لا تأخذ في الاعتبار قيم العمق من العرض المؤجل ونتيجة لذلك يتم رسمها دائمًا فوق الكائنات المقدمة بالفعل: هذا ليس ما نهدف إليه.

نحتاج أولاً إلى نسخ معلومات العمق من الممر الهندسي إلى المخزن المؤقت للعمق ، وبعد ذلك فقط نرسم المكعبات المضيئة. وبالتالي ، سيتم رسم شظايا المكعبات المضيئة فقط إذا كانت أقرب من الأشياء المرسومة بالفعل.
يمكننا نسخ محتويات الإطار إلى ملف آخر باستخدام وظيفة glBlitFramebuffer
. لقد استخدمنا بالفعل هذه الوظيفة في مثال مكافحة التعرج : ( مكافحة التعرج ) ، الترجمة . تقوم وظيفة glBlitFramebuffer
بنسخ الجزء المحدد من قبل المستخدم من الإطار Framebuffer إلى الجزء المحدد من آخر من Framebuffer.
بالنسبة للكائنات المرسومة في ممر الإضاءة المؤجل ، قمنا بحفظ العمق في المخزن g للكائن Framebuffer. إذا قمنا ببساطة بنسخ محتويات المخزن المؤقت لعمق المخزن المؤقت g إلى المخزن المؤقت العمق الافتراضي ، فسيتم رسم المكعبات المضيئة كما لو تم رسم هندسة المشهد بالكامل باستخدام تمرير عرض مباشر. كما تم شرحه بإيجاز في مثال مكافحة التعرج ، نحتاج إلى ضبط إطارات الإطارات للقراءة والكتابة:
glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // - glBlitFramebuffer( 0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST ); glBindFramebuffer(GL_FRAMEBUFFER, 0); // [...]
نقوم هنا بنسخ محتويات المخزن المؤقت لعمق إطار الإطار المؤقت إلى المخزن المؤقت العمق الافتراضي (إذا لزم الأمر ، يمكنك نسخ المخازن المؤقتة للألوان أو المخزن المؤقت للشد بالطريقة نفسها). إذا قدمنا الآن المكعبات المتوهجة ، فسيتم رسمها كما لو كانت هندسة المشهد حقيقية (على الرغم من أنها مرسومة ببساطة).

يمكن العثور على رمز المصدر للعرض هنا .
باستخدام هذا النهج ، يمكننا بسهولة الجمع بين العرض المؤجل والعرض المباشر. هذا ممتاز ، حيث يمكننا تطبيق المزج ورسم الأشياء التي تتطلب تظليلًا خاصًا لا ينطبق على العرض المؤجل.
المزيد من مصادر الضوء
غالبًا ما يتم الإشادة بالإضاءة المؤجلة لكونها قادرة على رسم عدد كبير من مصادر الإضاءة دون انخفاض كبير في الأداء. لا تسمح لنا الإضاءة المتأخرة وحدها برسم عدد كبير جدًا من مصادر الضوء ، حيث لا يزال يتعين علينا حساب مساهمة جميع مصادر الضوء لكل بكسل. لرسم عدد كبير من مصادر الضوء ، يتم استخدام تحسين جميل جدًا ، ينطبق على العرض المؤجل - منطقة عمل مصادر الضوء. (كميات صغيرة)
عادة ، عندما نرسم شظايا في مشهد شديد الإضاءة ، نأخذ في الاعتبار مساهمة كل مصدر ضوئي في المشهد ، بغض النظر عن بعده إلى الجزء. إذا كانت معظم مصادر الضوء لن تؤثر أبدًا على الجزء ، فلماذا نضيع الوقت في الحوسبة لهم؟
تتمثل فكرة نطاق مصدر الضوء في إيجاد نصف قطر (أو حجم) مصدر الضوء - أي المنطقة التي يستطيع الضوء من خلالها الوصول إلى السطح. نظرًا لأن معظم مصادر الضوء تستخدم نوعًا من التوهين ، يمكننا العثور على أقصى مسافة (نصف قطر) يمكن للضوء الوصول إليها. بعد ذلك ، نقوم بإجراء حسابات الإضاءة المعقدة فقط لمصادر الضوء التي تؤثر على هذا الجزء. وهذا يوفر علينا عددًا كبيرًا من الحسابات ، نظرًا لأننا نحسب الإضاءة فقط عند الحاجة إليها.
مع هذا النهج ، فإن الحيلة الرئيسية هي تحديد حجم منطقة عمل مصدر الضوء.
حساب نطاق مصدر الضوء (نصف القطر)
للحصول على نصف قطر مصدر الضوء ، يجب علينا حل معادلة التخميد للسطوع ، والتي نعتبرها مظلمة - يمكن أن تكون 0.0 أو شيء أكثر إضاءة قليلاً ، ولكنها لا تزال مظلمة: على سبيل المثال ، 0.03. لتوضيح كيفية حساب نصف القطر ، سنستخدم إحدى أكثر وظائف التوهين تعقيدًا وأكثرها شيوعًا من مثال المذرة الخفيفة
نريد حل هذه المعادلة للحالة عندما أي عندما يكون مصدر الضوء معتمًا تمامًا. ومع ذلك ، فإن هذه المعادلة لن تصل أبدًا إلى القيمة الدقيقة 0.0 ، لذلك لا يوجد حل. ومع ذلك ، يمكننا بدلاً من ذلك حل معادلة السطوع لقيمة قريبة من 0.0 ، والتي يمكن اعتبارها مظلمة عمليًا. في هذا المثال ، نعتبر قيمة السطوع مقبولة - مقسومًا على 256 ، حيث يمكن أن يحتوي المخزن 8 بت على 256 قيمة سطوع مختلفة.
تصبح وظيفة التوهين المحددة مظلمة تقريبًا على مسافة من النطاق ، إذا قصرناها على سطوع أقل من 5/256 ، فسيصبح نطاق مصدر الضوء كبيرًا جدًا - وهذا ليس فعالًا جدًا. من الناحية المثالية ، يجب ألا يرى الشخص حدًا حادًا مفاجئًا للضوء من مصدر ضوئي. بالطبع ، يعتمد هذا على نوع المشهد ، فالقيمة الأكبر من الحد الأدنى من السطوع تعطي مناطق أصغر للعمل من مصادر الضوء وتزيد من كفاءة العمليات الحسابية ، ولكنها يمكن أن تؤدي إلى قطع أثرية ملحوظة في الصورة: الإضاءة ستنقطع فجأة عند حدود منطقة عمل مصدر الضوء.
تصبح معادلة التوهين التي يجب أن نحلها:
هنا - ألمع مكون للضوء (من قنوات r و g و b). سوف نستخدم المكون الأكثر سطوعًا ، لأن المكونات الأخرى ستعطي قيودًا أضعف على نطاق مصدر الضوء.
نواصل حل المعادلة:
المعادلة الأخيرة هي معادلة تربيعية في الشكل مع الحل التالي:
حصلنا على معادلة عامة تسمح لنا باستبدال المعلمات (التوهين المستمر والمعاملات الخطية والتربيعية) لإيجاد x - نصف قطر مصدر الضوء.
float constant = 1.0; float linear = 0.7; float quadratic = 1.8; float lightMax = std::fmaxf(std::fmaxf(lightColor.r, lightColor.g), lightColor.b); float radius = (-linear + std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax))) / (2 * quadratic);
ترجع الصيغة نصف قطر يتراوح بين 1.0 و 5.0 تقريبًا اعتمادًا على أقصى سطوع لمصدر الضوء.
نجد هذا نصف القطر لكل مصدر ضوء على المسرح ونستخدمه من أجل أن نأخذ في الاعتبار فقط مصادر الضوء التي تقع ضمن نطاق كل جزء. فيما يلي ممر إضاءة يعيد النظر في مجالات عمل مصادر الضوء. يرجى ملاحظة أن هذا النهج يتم تطبيقه فقط للأغراض التعليمية وليس مناسبًا للاستخدام العملي (سنناقش قريبًا السبب).
struct Light { [...] float Radius; }; void main() { [...] for(int i = 0; i < NR_LIGHTS; ++i) { // float distance = length(lights[i].Position - FragPos); if(distance < lights[i].Radius) { // [...] } } }
والنتيجة هي نفسها تمامًا كما كانت من قبل ، ولكن الآن لكل تأثير ضوئي يتم أخذ تأثيره في الاعتبار فقط داخل منطقة عمله.
الرمز النهائي هو تجريبي. .
التطبيق الحقيقي لنطاق مصدر الضوء.
لن يعمل جهاز تظليل الأجزاء الموضح أعلاه عمليًا ويعمل فقط لتوضيح كيف يمكننا التخلص من حسابات الإضاءة غير الضرورية. في الواقع ، تعمل بطاقة الفيديو ولغة تظليل GLSL على تحسين الحلقات والفروع بشكل سيئ جدًا. والسبب في ذلك هو أن تنفيذ التظليل على بطاقة الفيديو يتم بالتوازي مع وحدات البكسل المختلفة ، وتفرض العديد من الهياكل القيد أنه في التنفيذ المتوازي يجب أن تحسب سلاسل مختلفة نفس التظليل. غالبًا ما يؤدي هذا إلى حقيقة أن جهاز تظليل الجري دائمًا يحسب جميع الفروع بحيث تعمل جميع أجهزة تظليل في نفس الوقت. (حارة الملاحظة. هذا لا يؤثر على نتيجة الحسابات ، لكنه يمكن أن يقلل من أداء جهاز التظليل). وبسبب ذلك ، قد يتبين أن فحص نصف القطر عديم الفائدة: سنظل نحسب الإضاءة لجميع المصادر!
الطريقة المناسبة لاستخدام نطاق الضوء هو جعل المجالات ذات نصف قطر مثل مصدر الضوء. يتطابق مركز الكرة مع موضع مصدر الضوء ، بحيث تحتوي الكرة داخلها على نطاق عمل مصدر الضوء. هناك خدعة صغيرة هنا - نستخدم بشكل أساسي نفس أداة تظليل الأجزاء المؤجلة لرسم كرة. عند رسم كرة ، يُطلق على تظليل الأجزاء تحديدًا لوحدات البكسل التي تتأثر بمصدر الضوء ، فنحن نقدم وحدات البكسل الضرورية فقط ونتخطى جميع وحدات البكسل الأخرى. :

, . , , . _*__
_ + __ , .
: ( ) , , , - ( ). stenil .
, , , . ( ) : c (deferred lighting) (tile-based deferred shading) . MSAA. .
vs
( ) - , , . , — , MSAA, .
( ), ( g- ..) . , .
: , , , . , , , . . parallax mapping, , . , .
روابط أقسام الموقع
PS - . , !