مرحبًا أود أن أشارك تجربتي في كتابة تظليل في الوحدة. لنبدأ مع تظليل الإزاحة / الانكسار في 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; }
إيقاف وضع الخلط المحدد ، لأن الآن نقوم بتنفيذ وضع المزج الخاص بنا داخل جهاز تظليل الأجزاء.
وإلقاء نظرة على نتيجة
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)
الآن ، بتغيير قيم معلمة
_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() { ...
الآن ، لتغيير الخاصية ، سنقوم بتحديث
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.