كتابة تظليل في الوحدة. GrabPass ، PerRendererData

مرحبًا أود أن أشارك تجربتي في كتابة تظليل في الوحدة. لنبدأ مع تظليل الإزاحة / الانكسار في 2D ، والنظر في الوظيفة المستخدمة لكتابته (GrabPass ، PerRendererData) ، وكذلك الانتباه إلى المشاكل التي ستنشأ بالضرورة.

المعلومات مفيدة لأولئك الذين لديهم فكرة عامة عن التظليل وحاولوا صنعها ، ولكنهم ليسوا على دراية بالقدرات التي توفرها Unity ، ولا يعرفون أي جانب يجب الاقتراب منه. ألق نظرة ، ربما ستساعدني تجربتي في معرفة ذلك.



هذه هي النتيجة التي نريد تحقيقها.

الصورة

تحضير


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

رمز تظليل لتقديم العفريت
Shader "Displacement/Displacement_Wave" { Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } Cull Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; fixed4 _Color; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return texColor; } ENDCG } } } 

العفريت للعرض
الخلفية شفافة بالفعل ومعتمة عن قصد.

الصورة

الشغل الناتج.

الصورة

أمسك


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

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

 struct v2f { float4 vertex : SV_POSITION; float2 uv : float4 color : COLOR; float4 grabPos : TEXCOORD1; }; 

وفي قمة الظل تظليلها.

 o.grabPos = ComputeGrabScreenPos (o.vertex); 

للحصول على اللون من _GrabTexture ، يمكننا استخدام الطريقة التالية إذا استخدمنا إحداثيات غير طبيعية

 tex2Dproj(_GrabTexture, i.grabPos) 

لكننا سنستخدم طريقة مختلفة وتطبيع الإحداثيات بأنفسنا ، باستخدام تقسيم المنظور ، أي لتقسيم جميع الآخرين إلى المكون w.

 tex2D(_GrabTexture, i.grabPos.xy/i.grabPos.w) 

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

يمكن أيضًا إجراء تقسيم المنظور في تظليل الرأس ، ويمكن نقل البيانات المعدة بالفعل إلى تظليل الجزء.

 v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); o.grabPos = ComputeScreenPos (o.vertex); o.grabPos /= o.grabPos.w; return o; } 

أضف تظليل جزء ، على التوالي.

 fixed4 frag (v2f i) : SV_Target { fixed4 = grabColor = tex2d(_GrabTexture, i.grabPos.xy); fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return grabColor; } 

إيقاف وضع الخلط المحدد ، لأن الآن نقوم بتنفيذ وضع المزج الخاص بنا داخل جهاز تظليل الأجزاء.

 //Blend SrcAlpha OneMinusSrcAlpha Blend Off 

وإلقاء نظرة على نتيجة GrabPass .

الصورة

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

 Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) _DisplacementPower ("Displacement Power" , Float) = 0 } SubShader { Pass { ... float _DisplacementPower; ... } } 

ومرة أخرى ، قم بإجراء تغييرات على تظليل الجزء.

 fixed4 grabColor = tex2d(_GrabTexture, i.grabPos.xy + _DisplaccementPower); 

المرجع والنتيجة! صورة مع التحول.



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

القوام المستخدم للتشويه



دعنا نبدأ. أضف خاصية جديدة لتخزين النسيج.

 _DisplacementTex ("Displacement Texture", 2D) = "white" {} 

ومتغير.

 sampler2D _DisplacementTex; 

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

 fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); 

الآن ، بتغيير قيم معلمة _DisplacementPower ، لا نقوم فقط بتغيير الصورة الأصلية ، ولكننا نحرفها.



تراكب


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



حيث S هي الصورة الأصلية ، و C تصحيحية ، أي أن العفريت لدينا ، R هي النتيجة.

نقل هذه الصيغة إلى تظليل لدينا.

 fixed4 color = grabColor < 0.5 ? 2*grabColor*texColor : 1-2*(1-texColor)*(1-grabColor); 

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

 c = step ( y, x); r = c * a + (1 - c) * b; 

دالة الخطوة
سترجع الدالة step 1 إذا كانت x أكبر من أو تساوي y . و 0 إذا كانت س أقل من ص .

على سبيل المثال ، إذا كانت x = 1 و y = 0.5 ، فستكون نتيجة c هي 1. وسيظهر التعبير التالي
ص = 1 * أ + 0 * ب
لأن الضرب في 0 يعطي 0 ، النتيجة ستكون فقط قيمة a .
وإلا ، إذا كان c يساوي 0 ،
ص = 0 * أ + 1 * ب
وستكون النتيجة النهائية ب .

أعد كتابة اللون لوضع التراكب .

 fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); 

تأكد من النظر في شفافية العفريت. للقيام بذلك ، سوف نستخدم الاستيفاء الخطي بين اللونين.

 color = lerp(grabColor, color ,texColor.a); 

كود تظليل جزء كامل.

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

ونتيجة لعملنا.



ميزة GrabPass


ذكر أعلاه أن تمرير GrabPass {} يلتقط محتويات الشاشة في نسيج _GrabTexture . في نفس الوقت ، في كل مرة يتم استدعاء هذا المقطع ، سيتم تحديث محتويات النسيج.
يمكن تجنب التحديث المستمر من خلال تحديد اسم النسيج الذي سيتم فيه التقاط محتويات الشاشة.
 GrabPass{"_DisplacementGrabTexture"} 

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

باستخدام GrabPass {"_ DisplacementGrabTexture"}.



باستخدام GrabPass {}.



الرسوم المتحركة


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

البرنامج النصي للرسوم المتحركة
 public class Wave : MonoBehaviour { private float _elapsedTime; private SpriteRenderer _renderer; public float Duration; [Space] public AnimationCurve ScaleProgress; public Vector3 ScalePower; [Space] public AnimationCurve PropertyProgress; public float PropertyPower; [Space] public AnimationCurve AlphaProgress; private void Start() { _renderer = GetComponent<SpriteRenderer>(); } private void OnEnable() { _elapsedTime = 0f; } void Update() { if (_elapsedTime < Duration) { var progress = _elapsedTime / Duration; var scale = ScaleProgress.Evaluate(progress) * ScalePower; var property = PropertyProgress.Evaluate(progress) * PropertyPower; var alpha = AlphaProgress.Evaluate(progress); transform.localScale = scale; _renderer.material.SetFloat("_DisplacementPower", property); var color = _renderer.color; color.a = alpha; _renderer.color = color; _elapsedTime += Time.deltaTime; } else { _elapsedTime = 0; } } } 

وإعداداته


نتيجة الرسوم المتحركة.



Perrendererdata


انتبه إلى الخط أدناه.

 _renderer.material.SetFloat("_DisplacementPower", property); 

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

للقيام بذلك ، قم بإضافة سمة إلى خاصية _DisplacementPower في تظليل.

 [PerRendererData] _DisplacementPower ("Displacement Power" , Range(-.1,.1)) = 0 

بعد ذلك ، لن يتم عرض الخاصية في المفتش ، لأنه الآن هو فردي لكل كائن ، والذي سيحدد القيم.



نعود إلى البرنامج النصي ونجري تغييرات عليه.

 private MaterialPropertyBlock _propertyBlock; private void Start() { _renderer = GetComponent<SpriteRenderer>(); _propertyBlock = new MaterialPropertyBlock(); } void Update() { ... //_renderer.material.SetFloat("_DisplacementPower", property); _renderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat("_DisplacementPower", property); _renderer.SetPropertyBlock(_propertyBlock); ... } 

الآن ، لتغيير الخاصية ، سنقوم بتحديث MaterialPropertyBlock للكائن الخاص بنا دون إنشاء نسخ من المادة.

حول SpriteRenderer
دعونا نلقي نظرة على هذا الخط في التظليل.

 [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} 

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

ميزة PerRendererData


يمكنك الحصول على MaterialPropertyBlock من جميع المكونات المتعلقة بالعرض تقريبًا. على سبيل المثال ، SpriteRenderer و ParticleRenderer و MeshRenderer ومكونات Renderer الأخرى. ولكن هناك دائمًا استثناء ، هذا هو CanvasRenderer . من المستحيل الحصول على الخصائص وتغييرها باستخدام هذه الطريقة. لذلك ، إذا كتبت لعبة ثنائية الأبعاد باستخدام مكونات واجهة المستخدم ، فستواجه هذه المشكلة عند كتابة تظليل.

الدوران


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

الموجة اليمنى عند الدوران (90 درجة) تعطي تشوهًا آخر.



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

لحل هذه المشكلة ، سوف نستخدم مصفوفة تحويل وحدة_المشروع . سيساعد في إعادة سرد ناقلنا من الإحداثيات المحلية إلى الإحداثيات العالمية.

 float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld, offset); 

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

 _propertyBlock.SetFloat("_DisplacementPower", property/transform.localScale.x); 

يتم أيضًا تدوير الموجة اليمنى 90 درجة ، ولكن يتم حساب التشويه الآن بشكل صحيح.



مقطع


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

كود تظليل جزء كامل.

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy * 2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld,offset); fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; clip(texColor.a - 0.01); fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * 2 * grabColor * texColor + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

ملاحظة: الكود المصدري للتظليل والبرنامج النصي هو رابط إلى git . يحتوي المشروع أيضًا على مولد نسيج صغير للتشويه. تم أخذ الكريستال مع القاعدة من الأصل - 2D Game Kit.

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


All Articles