تعلم برنامج OpenGL. الدرس 6.2 - العرض على أساس جسدي. مصادر الضوء التحليلية

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


أولاً ، دعنا نقوم بتحديث التعبير لحساب الانعكاسية من الدرس السابق:

Lo(p، omegao)= int limits Omega(kd fracc pi+ fracDFG4( omegao cdotn)( omegai cdotn))Li(p، omegai)n cdot omegaid omegai


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

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

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

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

لإعادة كتابة ما سبق: بالنسبة لمصدر الضوء المباشر ، وظيفة الإشعاع Lيحدد لون الضوء الساقط ، مع مراعاة التوهين على مسافة معينة من النقطة pومراعاة القياس بعامل n cdotwiولكن فقط لشعاع واحد من الضوء wiللوصول إلى النقطة p- في الأساس المتجه الوحيد الذي يربط المصدر والنقطة. في شكل شفرة المصدر ، يتم تفسير ذلك على النحو التالي:

vec3 lightColor = vec3(23.47, 21.31, 20.79); vec3 wi = normalize(lightPos - fragPos); float cosTheta = max(dot(N, Wi), 0.0); float attenuation = calculateAttenuation(fragPos, lightPos); vec3 radiance = lightColor * attenuation * cosTheta; 

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

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

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

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

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

نموذج سطح PBR


لنبدأ بتظليل الأجزاء الذي يطبق نموذج PBR الموصوف أعلاه. أولاً ، نقوم بتعيين بيانات الإدخال اللازمة لتظليل السطح:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; uniform vec3 camPos; uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; 

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

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

 void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); [...] } 

إضاءة مباشرة


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

لذا ، نحسب أولاً القيم المحسوبة لكل مصدر:

 vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; [...] 

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

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

علاوة على ذلك ، لكل مصدر ، نحسب أيضًا قيمة المرآة Cook-Torrance BRDF:

 fracDFG4( omegao cdotn)( omegai cdotn)


تتمثل الخطوة الأولى في حساب النسبة بين الانعكاس المرآوي والانتشار ، أو بعبارة أخرى ، النسبة بين مقدار الضوء المنعكس وكمية الضوء المنكسر من السطح. من الدرس السابق ، نعرف كيف يبدو حساب معامل فريسنل:

 vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } 

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

 vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); 

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

بعد أن تلقت Fنحتاج إلى حساب قيمة دالة التوزيع العادية Dووظائف الهندسة G:

كود الوظيفة للحالة مع الإضاءة التحليلية:

 float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float num = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return num / denom; } float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r*r) / 8.0; float num = NdotV; float denom = NdotV * (1.0 - k) + k; return num / denom; } float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; } 

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

بعد تعيين جميع الوظائف ، يمكن للمرء الحصول مباشرة على قيم NDF و G:

 float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); 

المجموع لدينا جميع القيم لحساب Cook-Torrance BRDF بالكامل:

 vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator; 

يرجى ملاحظة أننا نقصر المقام على قيمة الحد الأدنى 0.001 لمنع القسمة على صفر في حالات التصفير للمنتج العادي.

ننتقل الآن إلى حساب مساهمة كل مصدر في معادلة الانعكاسية. بما أن معامل فريسنل متغير مباشرة Ks، ثم يمكننا استخدام قيمة F للإشارة إلى مساهمة المصدر في الانعكاس المرآوي للسطح. من الكمية Ksيمكن الحصول عليها ومؤشر الانكسار Kd:

 vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; 

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

 const float PI = 3.14159265359; float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } 

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

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

 vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; 

التقديم الخطي و HDR


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

لتجنب فقدان قيم HDR ، قبل تصحيح غاما ، من الضروري إجراء ضغط النغمة:

 color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); 

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


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

تظليل PBR للإضاءة التحليلية


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

 #version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; //   uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; //   uniform vec3 lightPositions[4]; uniform vec3 lightColors[4]; uniform vec3 camPos; const float PI = 3.14159265359; float DistributionGGX(vec3 N, vec3 H, float roughness); float GeometrySchlickGGX(float NdotV, float roughness); float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness); vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness); void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); //    vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { //        vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; // Cook-Torrance BRDF float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); vec3 specular = numerator / max(denominator, 0.001); //       Lo float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); FragColor = vec4(color, 1.0); } 

آمل أنه بعد قراءة الجزء النظري ومع تحليل اليوم للتعبير عن القدرة الانعكاسية ، ستتوقف تلك القائمة عن التخويف.

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


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

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

PBR والقوام


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

 [...] uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; void main() { vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2); vec3 normal = getNormalFromNormalMap(); float metallic = texture(metallicMap, TexCoords).r; float roughness = texture(roughnessMap, TexCoords).r; float ao = texture(aoMap, TexCoords).r; [...] } 

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

استخدام القوام بدلاً من معلمات السطح الثابت بالاشتراك مع خوارزمية PBR يعطي زيادة كبيرة في الموثوقية البصرية مقارنة بخوارزميات الإضاءة المستخدمة سابقًا:


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

في الوقت الحالي ، قد لا تكون النتيجة مثيرة للإعجاب مثل بعض عروض PBR - ولكننا لم ننفذ بعد نظام إضاءة قائم على الصور ( IBL ). ومع ذلك ، يتم اعتبار تقديمنا الآن بناءً على المبادئ الفيزيائية ، وحتى بدون IBL ، فإنه يظهر صورة أكثر موثوقية من ذي قبل.

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


All Articles