إنشاء تأثير توزيع اللون في الوحدة


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

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



تأثير مرحلة ما بعد المعالجة في التدرج الرمادي


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


لهذا الغرض ، استخدمت حزمة Unity 2018 الجديدة لما بعد المعالجة ، والتي يمكن تنزيلها من مدير الحزم. إذا كنت لا تعرف كيفية استخدامه ، فأنا أوصي بهذا البرنامج التعليمي .

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

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

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

لماذا نستخدم المنتج القياسي؟ لا تنس أن المنتجات العددية يتم حسابها على النحو التالي:

dot(a, b) = a x * b x + a y * b y + a z * b z

في هذه الحالة ، نقوم بضرب كل قناة من قيمة اللون بالوزن . ثم نضيف هذه المنتجات لتقليلها إلى قيمة عددية واحدة. عندما يكون للون RGB نفس القيم في قنوات R و G و B ، يصبح اللون رمادي.

إليك ما يبدو عليه رمز التظليل:

 float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0); 

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




تقديم تأثير اللون في الفضاء العالمي


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

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

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

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

الحصول على قيمة المخزن المؤقت عمق


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

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

إنشاء عينة _CameraDepthTexture في ملف hlsl

 TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture); 

الآن دعنا نكتب وظيفة GetWorldFromViewPosition وسنستخدمها الآن للتحقق من المخزن المؤقت للعمق . (سنقوم لاحقًا بتوسيعه للحصول على موقع في العالم.)

 float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; } 

في تظليل الأجزاء ، ارسم قيمة عينة عمق النسيج.

 float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0); 

هذا ما تبدو عليه نتائجي عندما يكون هناك مجرد تلة واحدة في المشهد (أوقفت كل الأشجار من أجل تبسيط اختبار قيم الفضاء العالمي). يجب أن تبدو نتيجتك متشابهة. تصف القيم بالأبيض والأسود المسافة بين الشكل الهندسي والكاميرا.


فيما يلي بعض الخطوات التي يمكنك اتخاذها إذا واجهت مشاكل:

  • تأكد من أن الكاميرا مزودة بتقويم عمق.
  • تأكد من تمكين الكاميرا لـ MSAA.
  • حاول تغيير الطائرة القريبة والبعيدة من الكاميرا.
  • تأكد من أن الكائنات التي تتوقع رؤيتها في المخزن المؤقت للعمق تستخدم تظليلًا بتمرير عمق. هذا يضمن أن الكائن يرسم إلى المخزن المؤقت عمق. جميع تظليل القياسية في LWRP القيام بذلك.

الحصول على قيمة في الفضاء العالمي


والآن بعد أن أصبح لدينا جميع المعلومات اللازمة لمساحة الإحداثيات المقطوعة ، فلنتحول إلى مساحة الأنواع ، ثم إلى الفضاء العالمي .

لاحظ أن مصفوفات التحويل المطلوبة لهذه العمليات موجودة بالفعل في مكتبة SRP. ومع ذلك ، فهي موجودة في مكتبة C # الخاصة بمحرك Unity ، لذلك قمت بإدراجها في التظليل في وظيفة Render في البرنامج النصي ColorSpreadRenderer :

 sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse); 

الآن دعنا نمدّد وظيفة GetWorldFromViewPosition الخاصة بنا.

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

أخيرًا ، يمكننا مضاعفة الموقف في إطار العرض بواسطة ViewToWorldMatrix للحصول على المركز في مساحة العالم .

 float3 GetWorldFromViewPosition (VertexOutput i) { //    float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; //      float4 result = mul(unity_InverseProjectionMatrix, float4(2*i.screenPos-1.0, z, 1.0)); float3 viewPos = result.xyz / result.w; //      float3 worldPos = mul(unity_ViewToWorldMatrix, float4(viewPos, 1.0)); return worldPos; } 

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


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


هذا مشابه تمامًا للتظليل الاختباري الذي كتبته ، أي أن حسابات مساحة العالم هي على الأرجح صحيحة!

رسم دائرة في الفضاء العالمي


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

 float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i); // ,      .  //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= _MaxSize? 0 : 1; //   float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); //   float luminance = dot(fullColor.rgb, float3(0.2126729, 0.7151522, 0.0721750)); float3 greyscale = luminance.xxx; // ,       float3 color = (1-blend)*fullColor + blend*greyscale; return float4(color, 1.0); } 

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




مضيفا المؤثرات الخاصة


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

دائرة تكبير الرسوم المتحركة


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

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

_GrowthSpeed ​​يحدد سرعة زيادة الدائرة.

 //           float timeElapsed = _Time.y - _StartTime; float effectRadius = min(timeElapsed * _GrowthSpeed, _MaxSize); //  ,      effectRadius = clamp(effectRadius, 0, _MaxSize); 

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

 // ,         //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= effectRadius? 0 : 1; //     ... 

إليك الشكل الذي يجب أن تبدو عليه النتيجة:


إضافة إلى دائرة نصف قطرها الضوضاء


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

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

 //          float2 worldUV = worldPos.xz; worldUV *= _NoiseTexScale; 

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

 //     float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r; effectRadius -= noise * _NoiseSize; 

إليك ما تبدو عليه النتائج بعد التغيير والتبديل:




في الختام


يمكنك متابعة تحديثات البرامج التعليمية على Twitter ، وفي Twitch أقضي تدفقات الترميز! (أيضًا ، أنا أقوم بدفق الألعاب من وقت لآخر ، لذلك لا تتفاجأ إذا رأيتني جالسًا في بيجاما ولعب Kingdom Hearts 3.)

شكر وتقدير

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


All Articles