WBOIT في برنامج OpenGL: الشفافية دون الفرز

سنتحدث عن "شفافية مستقلة عن النظام المختلط الموزون" (المشار إليها فيما يلي بـ WBOIT) - وهي التقنية الموضحة في JCGT في عام 2013 ( link ).

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

\ تبدأ {matrix} C = C_ {near} \ alpha + C_ {far} (1- \ alpha) && (1) \ end {matrix}


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

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

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

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

\ تبدأ {matrix} C = {{\ \ sum_ {i = 1} ^ {n} C_i \ alpha_i} \ over {\ sum_ {i = 1} ^ {n} \ alpha_i}} (1- \ prod_ {i = 1} ^ {n} (1- \ alpha_i)) + C_0 \ prod_ {i = 1} ^ {n} (1- \ alpha_i) && (2) \ end {matrix}




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

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

C 0 في الصيغة (2) هو لون جزء غير شفاف ، تُعلَّم فوقه الأجزاء الشفافة ، التي لدينا منها قطع n ، يشار إليها بالمؤشرات من 1 إلى n. C i هو لون القطعة i-th الشفافة ، α i هو غموضها.

إذا نظرت عن كثب ، فإن الصيغة (2) تشبه إلى حد ما الصيغة (1). إذا كنت تتخيل ذلك هو C قرب ، C 0 هو C far ، و - هذا هو α ، ثم ستكون هذه هي الصيغة الأولى ، واحدة إلى واحدة. و حقا - هذا هو المتوسط ​​المرجح لألوان الشظايا الشفافة (يتم تحديد مركز الكتلة في الميكانيكا بنفس الصيغة) ، وسيكون لون أقرب جزء C قريب . C 0 هو لون الجزء المعتم الموجود خلف جميع الأجزاء ، التي حسبناها هذا المتوسط ​​المرجح ، وسوف يمر لـ C بعيدًا . بمعنى آخر ، استبدلنا جميع الأجزاء الشفافة بشظية واحدة "متوسطة" وقمنا بتطبيق الصيغة القياسية لخلط الألوان - الصيغة (1). ما هي هذه الصيغة الماكرة لـ α التي يقدمها لنا مؤلفو المقال الأصلي؟

 alpha=1 prodi=1n(1 alphai)


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

إذا كانت عتامة القطعة 0 ، فهي غير مرئية على الإطلاق ، ولا تؤثر على اللون الناتج.

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



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

هذا هو أفضل رؤية هنا:



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

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

مع قيم عتامة منخفضة ، تبدو الصور الموجودة على اليسار واليمين كما هي:



ومع ارتفاع أنها تختلف بشكل ملحوظ:



هذا ما يبدو عليه الشكل المتعدد السطوح الشفاف:




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



WBOIT القائم على العمق


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

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

قانون


الآن حول كيفية تنفيذ الصيغة (2) على OpenGL. رمز المثال موجود على Github ( link ) ، ومعظم الصور في المقالة من هناك. يمكنك جمع واللعب مع مثلثات بلدي. يتم استخدام إطار كيو تي.

بالنسبة لأولئك الذين بدأوا للتو في دراسة تقديم الكائنات الشفافة ، أوصي بهاتين المادتين:

تعلم OpenGL. الدرس 4.3 - خلط الألوان
خوارزمية الشفافية المستقلة للطلب باستخدام القوائم المرتبطة في Direct3D 11 و OpenGL 4

الثاني ، ومع ذلك ، ليست مهمة للغاية لفهم هذه المواد ، ولكن الأول هو يجب أن تقرأ.

لحساب الصيغة (2) ، نحتاج إلى 2 من أدوات إزالة الإطارات ، و 3 أشكال متعددة النماذج ، ومخزن مؤقت لتقديم ، نكتب فيه العمق. في أول نسيج - colorTextureNT (يعني NT غير شفاف) - سنقوم بتقديم كائنات معتمة. لديها نوع GL_RGB10_A2. سيكون الملمس الثاني (colorTexture) من النوع GL_RGBA16F ؛ في المكونات الثلاثة الأولى من هذا الملمس ، سنكتب هذه الصيغة (2): في الرابع - . سوف تحتوي على مادة أخرى من نوع GL_R16 (alphaTexture) .

تحتاج أولاً إلى إنشاء هذه الكائنات للحصول على معرفاتها من OpenGL:

  f->glGenFramebuffers (1, &framebufferNT ); f->glGenTextures (1, &colorTextureNT ); f->glGenRenderbuffers(1, &depthRenderbuffer); f->glGenFramebuffers(1, &framebuffer ); f->glGenTextures (1, &colorTexture); f->glGenTextures (1, &alphaTexture); 

كما قلت ، يتم استخدام إطار عمل Qt هنا ، وجميع مكالمات OpenGL تمر عبر كائن من النوع QOpenGLFunctions_4_5_Core ، والذي يُشار إليه دومًا باسم f بالنسبة لي.

الآن يجب عليك تخصيص الذاكرة:

  f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGB16F, w, h, GL_TRUE ); f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples, GL_DEPTH_COMPONENT, w, h ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGBA16F, w, h, GL_TRUE ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_R16F, w, h, GL_TRUE ); 

وتكوين framebuffers:

  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0 ); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0 ); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0 ); GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; f->glDrawBuffers(2, attachments); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); 

على مرور التقديم الثاني ، سينتقل الإخراج من تظليل الأجزاء إلى نسختين في وقت واحد ، ويجب تحديد ذلك بشكل صريح باستخدام glDrawBuffers.

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

  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ...   ... 

لقد قمنا فقط برسم كل الكائنات غير الشفافة على نسيج colorTextureNT ، وكتبنا الأعماق في المخزن المؤقت للتجسيد. قبل استخدام نفس renderbuffer في المرحلة التالية من الرسم ، يجب عليك التأكد من أن جميع أعماق الكائنات غير الشفافة مكتوبة بالفعل هناك. لهذا الغرض ، يتم استخدام GL_FRAMEBUFFER_BARRIER_BIT. بعد تقديم كائنات شفافة ، ندعو الدالة ApplyTextures () ، والتي ستطلق المرحلة النهائية من التقديم ، حيث سيقرأ تظليل القطعة البيانات من مواد colorTextureNT و colorTexture و alphaTexture لتطبيق الصيغة (2). يجب أن تكون القوام تمت كتابتها تمامًا بحلول ذلك الوقت ، لذا قبل الاتصال بـ ApplyTextures () نستخدم GL_TEXTURE_FETCH_BARRIER_BIT.

  static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; static constexpr GLfloat clearAlpha = 1.0f; f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glClearBufferfv(GL_COLOR, 0, clearColor); f->glClearBufferfv(GL_COLOR, 1, &clearAlpha); f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); PrepareToTransparentRendering(); { // ...   ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO هو أداة تحميل الإطار من خلالها نعرض الصورة. في معظم الحالات ، يكون 0 ، ولكن في Qt يكون QOpenGLWidget :: defaultFramebufferObject ().

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

فيما يلي محتويات دالة PrepareToTransparentRendering ():

  f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glDepthFunc(GL_LEQUAL); f->glDisable(GL_CULL_FACE); f->glEnable(GL_MULTISAMPLE); f->glEnable(GL_BLEND); f->glBlendFunci(0, GL_ONE, GL_ONE); f->glBlendEquationi(0, GL_FUNC_ADD); f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO); f->glBlendEquationi(1, GL_FUNC_ADD); 

ومحتويات الدالة CleanupAfterTransparentRendering ():

  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

في شظتي الشظية ، يشار إلى العتامة بالحرف w. ناتج اللون بواسطة w و w نفسه نخرجه إلى معلمة مخرجات واحدة ، و 1 - w إلى آخر. لكل معلمة مخرجات ، يتم تعيين مؤهل تخطيط في النموذج "location = X" ، حيث X هو فهرس العنصر في صفيف المرفقات ، والذي تم تمريره إلى glDrawBuffers في القائمة الثالثة (على وجه التحديد ، يتم إرسال معلمة الإخراج ذات الموقع = 0 إلى النسيج المرتبط بـ GL_COLOR_ATTACHMENT0 ، والمعلمة ذات الموقع = 1 - في النسيج المرفق بـ GL_COLOR_ATTACHMENT1). يتم استخدام نفس الأرقام في دالات glBlendFunci و glBlendEquationi للإشارة إلى رقم المرفق الذي قمنا بتعيين معلمات المزج له.

شظية شادر:

 #version 450 core in vec3 color; layout (location = 0) out vec4 outData; layout (location = 1) out float alpha; layout (location = 2) uniform float w; void main() { outData = vec4(w * color, w); alpha = 1 - w; } 

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

لا يوجد شيء ملحوظ في تظليل قمة الرأس:

 #version 450 core const vec2 p[4] = vec2[4]( vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1) ); void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); } 

شظية شادر:

 #version 450 core out vec4 outColor; layout (location = 0) uniform sampler2DMS colorTextureNT; layout (location = 1) uniform sampler2DMS colorTexture; layout (location = 2) uniform sampler2DMS alphaTexture; void main() { ivec2 upos = ivec2(gl_FragCoord.xy); vec4 cc = texelFetch(colorTexture, upos, gl_SampleID); vec3 sumOfColors = cc.rgb; float sumOfWeights = cc.a; vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb; if (sumOfWeights == 0) { outColor = vec4(colorNT, 1.0); return; } float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r; colorNT = sumOfColors / sumOfWeights * alpha + colorNT * (1 - alpha); outColor = vec4(colorNT, 1.0); } 

وأخيراً ، محتويات الدالة ApplyTextures ():

  f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glUniform1i(0, 0); f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glUniform1i(1, 1); f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glUniform1i(2, 2); f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST); f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 

حسنًا ، سيكون من الجيد تحرير موارد OpenGL بعد انتهائها. لقد تم استدعاء هذا الرمز في أداة إتلاف أداة OpenGL:

  f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture); 

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


All Articles