هذا المنشور يدور حول شفافية مستقلة عن النظام المختلط (WBOIT) - الخدعة التي تمت
تغطيتها في JCGT في عام 2013.
عندما تظهر كائنات شفافة متعددة على الشاشة ، يعتمد لون البكسل على أي منها أقرب إلى العارض. فيما يلي مشغل مزج معروف يستخدم في هذه الحالة:
\ تبدأ {matrix} C = C_ {near} \ alpha + C_ {far} (1- \ alpha) && (1) \ end {matrix}
طلب شظايا أمر مهم. يحتوي المشغل على لون (
قرب C) وعتامة (
α )
لشظية قريبة ولون شامل (C
بعيد ) لجميع الأجزاء التي خلفه. قد يتراوح العتامة بين 0 و 1 ؛ 0 تعني أن الكائن شفاف تمامًا (غير مرئي) والرقم 1 يعني أنه غير شفاف تمامًا.
من أجل استخدام هذا المشغل ، تحتاج إلى فرز الأجزاء حسب العمق. تخيل ما هي لعنة. بشكل عام ، تحتاج إلى إجراء فرز واحد لكل إطار. إذا قمت بفرز الكائنات ، فقد تضطر إلى التعامل مع الأسطح غير المنتظمة التي يجب تقطيعها إلى أقسام ، ومن ثم يجب فرز الأجزاء PARTS من تلك الأسطح (تحتاج بالتأكيد إلى القيام بذلك من أجل السطوح المتقاطعة). إذا قمت بفرز الأجزاء ، فستضع الفرز الفعلي في تظليلك. تُعرف هذه الطريقة باسم "شفافية مستقلة عن الطلب" (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);
).
الآن ، دعونا ننظر إلى ما يحدث في مرحلة ما مع coords مساحة الشاشة (س ، ص). الشظايا الشفافة - التي تكون أقرب من اختبار العمق غير الشفاف - تمر بغض النظر عن كيفية وضعها بالنسبة إلى الأجزاء الشفافة المقدمة بالفعل. تلك الأجزاء الشفافة التي تكمن خلف الجزء غير الشفاف - حسنًا ، لا تجتاز اختبار العمق ويتم التخلص منها بشكل طبيعي.
C
0 في الصيغة (2) هو لون القطعة غير الشفافة المقدمة في هذه النقطة (x ، y). لدينا شظايا شفافة في المجموع اجتازت اختبار العمق ، ولديها مؤشرات i ∈ [1، n]. C
i هو لون الجزء الشفاف ith و
α i هو غموضه.
الصيغة (2) تشبه قليلاً الصيغة (1) ، رغم أنها ليست واضحة جدًا. استبدل

مع C
قرب ، C
0 مع C
بعيد و

مع
α والصيغة (1) سيكون بالضبط ما ستحصل عليه. في الواقع،

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

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

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

وتختلف بشكل ملحوظ مع عتامة عالية:

وهنا متعدد السطوح شفافة:


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

حقن عمق في مشغل المزج
من أجل التعويض عن عدم وجود فرز للعمق ، توصل مؤلفو ورقة JCGT المذكورة أعلاه إلى عدة طرق لحقن العمق في الصيغة (2). إنه يعقد التنفيذ ويجعل النتيجة أقل قابلية للتوقع. لجعلها تعمل ، يجب أن تكون معلمات المزج متوافقة مع مشهد ثلاثي الأبعاد معين. لم أتعمق في هذا الموضوع ، لذا إذا أردت معرفة المزيد ، فاقرأ الورقة.
يدعي المؤلفون أنه في بعض الأحيان ، يمكن لـ WBOIT أن تفعل شيئًا لا يستطيع CODB. على سبيل المثال ، ضع في اعتبارك رسم الدخان كنظام جزيئي به جسيمان: الدخان الداكن والدخان الخفيف. عندما تتحرك الجسيمات ويمر أحد الجسيمات عبر آخر ، يتحول لونها المخلوط على الفور من الظلام إلى النور ، وهو أمر غير جيد. يوفر مشغل WBOIT ذو العمق نتائج أفضل مع انتقال سلس للألوان. الشعر أو الفراء على غرار مجموعة من أنابيب رقيقة لديه نفس الخاصية.
الكود
الآن لتطبيق OpenGL الصيغة (2). يمكنك أن
ترى التنفيذ على جيثب. إنه تطبيق يستند إلى كيو تي ، ومعظم الصور التي تراها هنا جاءت منه.
إذا كنت جديدًا في العرض التقديمي الشفاف ، فإليك مادة جيدة للمبتدئين:
تعلم OpenGL. مزجأوصي بقراءته قبل متابعة هذا المنشور.
من أجل تقييم المعادلة (2) ، نحتاج إلى 2 من أدوات إزالة الأطر الإضافية ، و 3 من القوام متعدد الأوجه وجهاز تقديم للعمق. سيتم تقديم كائنات غير شفافة إلى النسيج الأول ، colorTextureNT. نوعه هو GL_RGB10_A2. سيكون الملمس الثاني (colorTexture) من النوع GL_RGBA16F. تحتوي المكونات الثلاثة الأولى من colorTexture على هذا الجزء من الصيغة (2):

و

سيتم كتابة إلى المكون الرابع. ستحتوي آخر مادة ، alphaTexture ، من النوع GL_R16

.
أولاً ، نحتاج إلى إنشاء كل هذه الكائنات والحصول على معرفاتها من 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 framewok ، كما تتذكر ، وجميع المكالمات إلى 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 );
إعداد Framebuffer:
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. قبل استخدام نفس renderbuffer في تمرير التقديم التالي ، نحتاج إلى التأكد من انتهاء جميع عمليات الكتابة في renderbuffer العميقة من الكائنات غير الشفافة. يتم تحقيق ذلك باستخدام GL_FRAMEBUFFER_BARRIER_BIT. بعد تقديم الكائنات الشفافة ، سنقوم باستدعاء وظيفة ApplyTextures () التي ستؤدي تمرير التقديم النهائي حيث سيتم تظليل الجزء المظلل من عينات القوام colorTextureNT و colorTexture و alphaTexture من أجل تطبيق الصيغة (2). يجب أن تكون القوام جاهزة بحلول تلك اللحظة ، لذلك نستخدم GL_TEXTURE_FETCH_BARRIER_BIT قبل استدعاء ApplyTextures ().
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(); {
defaultFBO هو أداة تحميل إطارات تستخدمها لإظهار الصورة على الشاشة. في معظم الحالات ، يكون 0 ، ولكن في Qt يكون QOpenGLWidget :: defaultFramebufferObject ().
في كل احتجاج لتظليل شظايا ، سنتمكن من الوصول إلى لون وشتمة الجزء الحالي. ولكن في 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 نفسه - عن معلمة الإخراج الأولى ، و 1 - w سوف ينتقل إلى معلمة الإخراج الثاني. يجب تعيين مؤهل تخطيط لكل معلمة مخرجات في شكل "location = X" ، حيث X هي فهرس لعنصر في مجموعة المرفقات - العامل الذي قدمناه لوظيفة glDrawBuffers. للتأكد من دقتها ، تنتقل معلمة الإخراج ذات الموقع = 0 إلى النسيج المرتبط بـ GL_COLOR_ATTACHMENT1 ، والمعلمة ذات الموقع = 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);