شاشة عرض المياه

الصورة

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

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

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

في النهاية ، وجدت عرض Simon Green مع GDC 2010 ، Screen Space Fluid Rendering For Games. يبدأ بنفس الطريقة تمامًا مثل Screen Space Meshes: مع عرض الجسيمات في المخزن المؤقت للعمق وتنعيمه. ولكن بدلاً من إنشاء الشبكة ، يتم استخدام المخزن المؤقت الناتج لتظليل وتكوين السائل في المشهد الرئيسي (من خلال تسجيل العمق بشكل صريح.) قررت تنفيذ مثل هذا النظام.

تحضير


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

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

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

تقديم عازلة السوائل


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

حسابات العمق والعرض بسيطة إلى حد ما:

frag_out o; float3 N; N.xy = i.uv*2.0 - 1.0; float r2 = dot(N.xy, N.xy); if (r2 > 1.0) discard; Nz = sqrt(1.0 - r2); float4 pixel_pos = float4(i.view_pos + N * i.size, 1.0); float4 clip_pos = mul(UNITY_MATRIX_P, pixel_pos); float depth = clip_pos.z / clip_pos.w; o.depth = depth; float thick = Nz * i.size * 2; 

(بالطبع ، يمكن تبسيط حسابات العمق ؛ من موضع المقطع ، نحتاج فقط إلى z و w.)

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

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

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

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

دعنا نترك شرحًا طويلًا ، إليك وظيفة بسيطة إلى حد ما:

 float3 ComputeStretchedVertex(float3 p_world, float3 c_world, float3 vdir_world, float stretch_amount) { float3 center_offset = p_world - c_world; float3 stretch_offset = dot(center_offset, vdir_world) * vdir_world; return p_world + stretch_offset * lerp(0.25f, 3.0f, stretch_amount); } 

لحساب متجه الحركة لمساحة الشاشة ، نحسب مجموعتين من مواضع المتجهات:

 float3 vp1 = ComputeStretchedVertex( vertex_wp, center_wp, velocity_dir_w, rand); float3 vp0 = ComputeStretchedVertex( vertex_wp - velocity_w * unity_DeltaTime.x, center_wp - velocity_w * unity_DeltaTime.x, velocity_dir_w, rand); o.motion_0 = mul(_LastVP, float4(vp0, 1.0)); o.motion_1 = mul(_CurrVP, float4(vp1, 1.0)); 

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

 public class ScreenspaceLiquidRenderer : MonoBehaviour { public Camera LiquidCamera; private ParticleSystemRenderer m_ParticleRenderer; private bool m_First; private Matrix4x4 m_PreviousVP; void Start() { m_ParticleRenderer = GetComponent(); m_First = true; } void OnWillRenderObject() { Matrix4x4 current_vp = LiquidCamera.nonJitteredProjectionMatrix * LiquidCamera.worldToCameraMatrix; if (m_First) { m_PreviousVP = current_vp; m_First = false; } m_ParticleRenderer.material.SetMatrix("_LastVP", GL.GetGPUProjectionMatrix(m_PreviousVP, true)); m_ParticleRenderer.material.SetMatrix("_CurrVP", GL.GetGPUProjectionMatrix(current_vp, true)); m_PreviousVP = current_vp; } } 

أقوم بتخزين المصفوفة السابقة يدويًا لأن Camera.previousViewProjectionMatrix يعطي نتائج غير صحيحة.

¯ \ _ (ツ) _ / ¯

(أيضًا ، تنتهك هذه الطريقة طريقة العرض ، وقد يكون من الحكمة تعيين ثوابت المصفوفة العالمية في الممارسة بدلاً من استخدامها لكل مادة.)

دعنا نعود إلى تظليل الجزء: نستخدم المواضع المتوقعة لحساب متجهات الحركة لمساحة الشاشة:

 float3 hp0 = i.motion_0.xyz / i.motion_0.w; float3 hp1 = i.motion_1.xyz / i.motion_1.w; float2 vp0 = (hp0.xy + 1) / 2; float2 vp1 = (hp1.xy + 1) / 2; #if UNITY_UV_STARTS_AT_TOP vp0.y = 1.0 - vp0.y; vp1.y = 1.0 - vp1.y; #endif float2 vel = vp1 - vp0; 

(لم يتم تغيير حساب ناقلات الحركة تقريبًا من https://github.com/keijiro/ParticleMotionVector/blob/master/Assets/ParticleMotionVector/Shaders/Motion.cginc )

وأخيرًا ، تكون القيمة الأخيرة في المخزن المؤقت للسوائل هي الضوضاء. أستخدم رقمًا عشوائيًا ثابتًا لكل جسيم لتحديد واحد من أربعة أصوات (معبأة في نسيج واحد). ثم يتم قياسها بالسرعة والوحدة مطروحًا منها حجم الجسيمات (وبالتالي ، تكون الجسيمات السريعة والصغيرة أكثر ضوضاء). يتم استخدام قيمة الضوضاء هذه في ممر التظليل لتشويه المعايير الطبيعية وإضافة طبقة من الرغوة. يستخدم عمل Green الضوضاء البيضاء بثلاث قنوات ، ولكن العمل الأحدث (Screen Space Fluid Rendering with Curvature Flow) يقترح استخدام ضجيج Perlin. أستخدم ضوضاء Voronoi / ضوضاء الخلية بمقاييس مختلفة:


مشاكل المزج (والحلول)


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

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

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

تنعيم العمق


وأخيرًا ، أهم شيء في التقنية الخضراء. قدمنا ​​مجموعة من الكرات الكروية في المخزن المؤقت للعمق ، ولكن في الواقع ، لا تتكون المياه من "الكرات". لذا الآن نأخذ هذا التقريب وطمسه لجعله أشبه بسطح السائل.

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

تنشأ مشكلة واحدة فقط هنا: مثل هذه التغييرات تجعل التمويه لا يمكن تمييزه. يمكن تنفيذ التمويه المشترك في مرورين: التمويه أفقيًا ، ثم رأسيًا. يتم التمويه الذي لا يمكن تمييزه بتمريرة واحدة. هذا الاختلاف مهم لأن مقاييس التمويه المشتركة خطية (O (w) + O (h)) ، ومقاييس التمويه غير المشتركة بشكل مباشر (O (w * h)). أصبح الضبابية غير المشتركة على نطاق واسع غير قابلة للتطبيق بسرعة في الممارسة العملية.

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

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

التظليل


انتهينا أخيرًا من العمل مع المخزن المؤقت للسوائل. انتقل الآن إلى الجزء الثاني من التأثير: تظليل الصورة الرئيسية وتركيبها.

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

يتم التحكم في المرحلة الأخيرة من التظليل من خلال برنامج نصي ، وتستخدم المخزن المؤقت للأوامر لإرسال مكالمات السحب. يعد ذلك ضروريًا لأن مادة متجه الحركة (المستخدمة للتشويش المؤقت (TAA) وتمويه الحركة) لا يمكن استخدامها للعرض المباشر باستخدام Graphics.SetRenderTarget (). في النص المرفق بالكاميرا الرئيسية نكتب ما يلي:

 void Start() { //... m_QuadMesh = new Mesh(); m_QuadMesh.subMeshCount = 1; m_QuadMesh.vertices = new Vector3[] { new Vector3(0, 0, 0.1f), new Vector3(1, 0, 0.1f), new Vector3(1, 1, 0.1f), new Vector3(0, 1, 0.1f), }; m_QuadMesh.uv = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1), }; m_QuadMesh.triangles = new int[] { 0, 1, 2, 0, 2, 3, }; m_QuadMesh.UploadMeshData(false); m_CommandBuffer = new CommandBuffer(); m_CommandBuffer.Clear(); m_CommandBuffer.SetProjectionMatrix( GL.GetGPUProjectionMatrix( Matrix4x4.Ortho(0, 1, 0, 1, -1, 100), false)); m_CommandBuffer.SetRenderTarget( BuiltinRenderTextureType.CameraTarget, BuiltinRenderTextureType.CameraTarget); m_CommandBuffer.DrawMesh( m_QuadMesh, Matrix4x4.identity, m_Mat, 0, m_Mat.FindPass("LIQUIDCOMPOSITE")); m_CommandBuffer.SetRenderTarget( BuiltinRenderTextureType.MotionVectors, BuiltinRenderTextureType.Depth); m_CommandBuffer.DrawMesh( m_QuadMesh, Matrix4x4.identity, m_Mat, 0, m_Mat.FindPass("MOTION")); } 

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

في كل إطار ، نتخلص من عرض مركب من OnPostRender ():

 RenderTexture GenerateRefractionTexture() { RenderTexture result = RenderTexture.GetTemporary(m_MainCamera.activeTexture.descriptor); Graphics.Blit(m_MainCamera.activeTexture, result); return result; } void OnPostRender() { if (ScreenspaceLiquidCamera && ScreenspaceLiquidCamera.IsReady()) { RenderTexture refraction_texture = GenerateRefractionTexture(); m_Mat.SetTexture("_MainTex", ScreenspaceLiquidCamera.GetColorBuffer()); m_Mat.SetVector("_MainTex_TexelSize", ScreenspaceLiquidCamera.GetTexelSize()); m_Mat.SetTexture("_LiquidRefractTexture", refraction_texture); m_Mat.SetTexture("_MainDepth", ScreenspaceLiquidCamera.GetDepthBuffer()); m_Mat.SetMatrix("_DepthViewFromClip", ScreenspaceLiquidCamera.GetProjection().inverse); if (SunLight) { m_Mat.SetVector("_SunDir", transform.InverseTransformVector(-SunLight.transform.forward)); m_Mat.SetColor("_SunColor", SunLight.color * SunLight.intensity); } else { m_Mat.SetVector("_SunDir", transform.InverseTransformVector(new Vector3(0, 1, 0))); m_Mat.SetColor("_SunColor", Color.white); } m_Mat.SetTexture("_ReflectionProbe", ReflectionProbe.defaultTexture); m_Mat.SetVector("_ReflectionProbe_HDR", ReflectionProbe.defaultTextureHDRDecodeValues); Graphics.ExecuteCommandBuffer(m_CommandBuffer); RenderTexture.ReleaseTemporary(refraction_texture); } } 

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

لنبدأ بمرور ناقلات الحركة. إليك ما يبدو عليه تظليل كامل:

 #include "UnityCG.cginc" sampler2D _MainDepth; sampler2D _MainTex; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_P, v.vertex); o.uv = v.uv; return o; } struct frag_out { float4 color : SV_Target; float depth : SV_Depth; }; frag_out frag(v2f i) { frag_out o; float4 fluid = tex2D(_MainTex, i.uv); if (fluid.a == 0) discard; o.depth = tex2D(_MainDepth, i.uv).r; float2 vel = fluid.gb / fluid.a; o.color = float4(vel, 0, 1); return o; } 

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

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

أكثر إثارة للاهتمام هو ممر التظليل الرئيسي. أولويتنا الأولى بعد التقنيع بسماكة السائل هي إعادة بناء موضع ومساحة العرض الطبيعية (مساحة العرض).

 float3 ViewPosition(float2 uv) { float clip_z = tex2D(_MainDepth, uv).r; float clip_x = uv.x * 2.0 - 1.0; float clip_y = 1.0 - uv.y * 2.0; float4 clip_p = float4(clip_x, clip_y, clip_z, 1.0); float4 view_p = mul(_DepthViewFromClip, clip_p); return (view_p.xyz / view_p.w); } float3 ReconstructNormal(float2 uv, float3 vp11) { float3 vp12 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(0, 1)); float3 vp10 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(0, -1)); float3 vp21 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(1, 0)); float3 vp01 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(-1, 0)); float3 dvpdx0 = vp11 - vp12; float3 dvpdx1 = vp10 - vp11; float3 dvpdy0 = vp11 - vp21; float3 dvpdy1 = vp01 - vp11; // Pick the closest float3 dvpdx = dot(dvpdx0, dvpdx0) > dot(dvpdx1, dvpdx1) ? dvpdx1 : dvpdx0; float3 dvpdy = dot(dvpdy0, dvpdy0) > dot(dvpdy1, dvpdy1) ? dvpdy1 : dvpdy0; return normalize(cross(dvpdy, dvpdx)); } 

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

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

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

المعايير الناتجة هي:


أقوم بتشويه هذا المعدل المحسوب باستخدام مشتقات قيمة الضوضاء من المخزن المؤقت للسوائل ، مقيسة بمعامل القوة وتم تطبيعه بقسمة سمك النفاثة (لنفس السبب مثل السرعة):

 N.xy += NoiseDerivatives(i.uv, fluid.r) * (_NoiseStrength / fluid.a); N = normalize(N); 

يمكننا أخيرا المضي قدما في التظليل نفسه. يتكون تظليل الماء من ثلاثة أجزاء رئيسية: الانعكاس المرآوي والانكسار المرآوي والرغوة.

الانعكاس هو معيار GGX مأخوذ بالكامل من جهاز تظليل الوحدة القياسي. (مع تصحيح واحد ، يتم استخدام F0 الصحيح لـ 2٪ في الماء.)

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

 float aspect = _MainTex_TexelSize.y * _MainTex_TexelSize.z; float2 refract_uv = (i.grab_pos.xy + N.xy * float2(1, -aspect) * fluid.a * _RefractionMultiplier) / i.grab_pos.w; float4 refract_color = tex2D(_LiquidRefractTexture, refract_uv); 

(لاحظ أنه يتم استخدام تصحيح الارتباط ؛ إنه اختياري - بعد كل شيء ، إنه مجرد تقريب ، ولكن إضافته بسيطة للغاية.)

يمر هذا الضوء المنكسر عبر السائل ، لذا يتم امتصاص جزء منه:

 float3 water_color = _AbsorptionColor.rgb * _AbsorptionIntensity; refract_color.rgb *= exp(-water_color * fluid.a); 

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

يتم خلط الانعكاس والانكسار باستخدام معاملات فريسنل:

 float spec_blend = lerp(0.02, 1.0, pow(1.0 - ldoth, 5)); float4 clear_color = lerp(refract_color, spec, spec_blend); 

حتى تلك اللحظة ، لعبنا بالقواعد (في الغالب) واستخدمنا التظليل المادي.

إنه جيد جدًا ، لكن لديه مشكلة في الماء. من الصعب رؤيته:


لإصلاحها ، دعنا نضيف بعض الرغوة.

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

 float3 foam_color = _SunColor * saturate((dot(N, L)*0.25f + 0.25f)); 

تتم إضافته إلى اللون النهائي باستخدام عامل خاص ، اعتمادًا على ضوضاء السوائل ومعامل فريسنل الناعم:

 float foam_blend = saturate(fluid.r * _NoiseStrength) * lerp(0.05f, 0.5f, pow(1.0f - ndotv, 3)); clear_color.rgb += foam_color * saturate(foam_blend); 

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

ولكن بشكل عام ، يبدو كل شيء جيدًا ويجعل البث أكثر وضوحًا:


مزيد من العمل والتحسينات


في النظام الذي تم إنشاؤه ، يمكن تحسين الكثير.

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

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

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


All Articles