جعل سبلاش مع تأثيرات مائية ديناميكية 2D
Arabic (العربية/عربي) translation by Basrah (you can also view the original English article)
Sploosh! في هذا البرنامج التعليمي ، سأوضح لك كيف يمكنك استخدام الرياضيات البسيطة والفيزياء وتأثيرات الجسيمات لمحاكاة أمواج الماء والقطرات ثنائية الأبعاد.
ملاحظة: على الرغم من كتابة هذا البرنامج التعليمي باستخدام C و XNA ، يجب أن تكون قادرًا على استخدام نفس الأساليب والمفاهيم في أي بيئة تطوير ألعاب.
معاينة النتيجة النهائية
إذا كان لديك XNA ، يمكنك تنزيل الملفات المصدر وتجميع العرض التوضيحي بنفسك. بخلاف ذلك ، تحقق من الفيديو التوضيحي أدناه:
هناك نوعان من أجزاء مستقلة في الغالب لمحاكاة المياه. أولاً ، سنجعل الأمواج تستخدم نموذج الربيع. ثانيًا ، سنستخدم تأثيرات الجسيمات لإضافة البقع.
صنع الأمواج
ولصنع الأمواج ، سنقوم بتصوير سطح الماء كسلسلة من الينابيع الرأسية ، كما هو موضح في هذا المخطط:



سيسمح ذلك للأمواج بالتقاط صعودا وهبوطا. سنجعل جزيئات الماء تسحب الجسيمات المجاورة للسماح للأمواج بالانتشار.
الينابيع وقانون هوكي
شيء واحد عظيم حول الينابيع هو أنها سهلة لمحاكاة. الينابيع لها طول طبيعي معين. إذا قمت بتمديد أو ضغط زنبرك ، فستحاول العودة إلى هذا الطول الطبيعي.
القوة التي يوفرها الربيع منصوص عليها في قانون هوك:
\[
F = -kx
\]
F
هي القوة التي ينتجها الربيع ، و k
هي ثابت الربيع ، و x
هي إزاحة الربيع من طولها الطبيعي. تشير العلامة السالبة إلى أن القوة في الاتجاه المعاكس التي ينزح إليها الربيع ؛ إذا دفعت الربيع إلى الأسفل ، فستدفعه للخلف ، والعكس صحيح.
ثابت الربيع ، k
، يحدد صلابة الربيع.
لمحاكاة الينابيع ، يجب علينا معرفة كيفية نقل الجسيمات حولها استنادا إلى قانون هوك. للقيام بذلك ، نحتاج إلى صيغتين إضافيتين من الفيزياء. أولاً ، قانون نيوتن الثاني للحركة:
\[
F = ma
\]
هنا ، F
هي القوة ، m
هي الكتلة و التسارa
. وهذا يعني أن القوة تدفع بقوة إلى الجسم ، وأخف الجسم ، كلما تسارعت.
الجمع بين هذين الصيغتين وإعادة الترتيب يعطينا:
\[
a = -\frac{k}{m} x
\]
هذا يعطينا التسارع لجزيئاتنا. سنفترض أن جميع جسيماتنا ستكون لها نفس الكتلة ، لذا يمكننا دمج k/m
في ثابت واحد.
لتحديد الموقف من التسارع ، نحن بحاجة إلى تحقيق التكامل العددي. سنستخدم أبسط أشكال التكامل العددي - كل إطار نقوم ببساطة بعمل ما يلي:
Position += Velocity; Velocity += Acceleration;
وهذا ما يسمى طريقة أويلر. إنه ليس النوع الأكثر دقة للتكامل العددي ، ولكنه سريع وبسيط وملائم لأغراضنا.
بوضع كل ذلك معًا ، ستقوم جسيمات سطح الماء لدينا بتنفيذ كل إطار:
public float Position, Velocity; public void Update() { const float k = 0.025f; // adjust this value to your liking float x = Height - TargetHeight; float acceleration = -k * x; Position += Velocity; Velocity += acceleration; }
هنا ، TargetHeight
هو الوضع الطبيعي في الجزء العلوي من الربيع عندما لا يكون امتدت ولا مضغوط. يجب عليك تعيين هذه القيمة على المكان الذي تريد أن يكون سطح الماء فيه. بالنسبة إلى العرض التوضيحي ، قمت بإعداده إلى نصف الشاشة أسفل 240 بكسل.
التوتر والتلطيف
ذكرت في وقت سابق أن ثابت الربيع ، k
، يتحكم في صلابة الربيع. يمكنك ضبط هذه القيمة لتغيير خصائص الماء. ثابت الربيع المنخفض سيجعل الينابيع فضفاضة. هذا يعني أن القوة ستسبب موجات كبيرة تتأرجح ببطء. على العكس من ذلك ، سيزيد ثابت الربيع المرتفع من التوتر في الربيع. القوات سوف تخلق موجات صغيرة تتأرجح بسرعة. سوف يجعل ثبات الينبوع المرتفع الماء يشبه جيالو.
كلمة تحذير: لا تقم بتعيين ثابت الربيع عالية جدا. تطبق الينابيع شديدة الانحدار قوى قوية جدًا تتغير كثيرًا في وقت قصير جدًا. هذا لا يلعب بشكل جيد مع التكامل العددي ، الذي يحاكي الينابيع كسلسلة من القفزات المنفصلة على فترات زمنية منتظمة. حتى الربيع القوي يمكن أن يكون لديه فترة تذبذب أقصر من خطوتك الزمنية. والأسوأ من ذلك ، أن طريقة أويلر للتكامل تميل إلى الحصول على الطاقة حيث تصبح المحاكاة أقل دقة ، مما يؤدي إلى انفجار ينابيع قوية.
هناك مشكلة مع نموذج الربيع لدينا حتى الآن. بمجرد أن يبدأ الربيع بالتأرجح ، لن يتوقف أبدًا. لحل هذا يجب علينا تطبيق بعض الملطف. الفكرة هي تطبيق قوة في الاتجاه المعاكس التي يتحرك بها ربيعنا لإبطائها. وهذا يتطلب تعديلًا صغيرًا لصيغة الربيع:
\[
a = -\frac{k}{m} x - dv
\]
هنا ، v
هي السرعة و d
هي عامل التخميد - ثابت آخر يمكنك تعديله لتعديل شكل الماء. يجب أن تكون صغيرة إلى حد ما إذا كنت تريد أن تتذبذب موجاتك. يستخدم العرض التوضيحي عامل مخفف يبلغ 0.025. سوف يجعل العامل المرهف عاليًا المياه تبدو سميكة مثل الدبس ، في حين أن القيمة المنخفضة ستسمح للأمواج بالتأرجح لفترة طويلة.
جعل الأمواج تنتشر
الآن يمكننا أن نجعل من الربيع ، دعونا نستخدمها لنمذجة الماء. كما هو موضح في الرسم التخطيطي الأول ، نحن نمذجة المياه باستخدام سلسلة من الينابيع الرأسية المتوازية. بطبيعة الحال ، إذا كانت جميع الينابيع مستقلة ، فلن تنتشر الموجات مثلما تفعل الموجات الحقيقية.
سأعرض التعليمة البرمجية أولاً ، ثم أطلع عليها:
for (int i = 0; i < springs.Length; i++) springs[i].Update(Dampening, Tension); float[] leftDeltas = new float[springs.Length]; float[] rightDeltas = new float[springs.Length]; // do some passes where springs pull on their neighbours for (int j = 0; j < 8; j++) { for (int i = 0; i < springs.Length; i++) { if (i > 0) { leftDeltas[i] = Spread * (springs[i].Height - springs [i - 1].Height); springs[i - 1].Speed += leftDeltas[i]; } if (i < springs.Length - 1) { rightDeltas[i] = Spread * (springs[i].Height - springs [i + 1].Height); springs[i + 1].Speed += rightDeltas[i]; } } for (int i = 0; i < springs.Length; i++) { if (i > 0) springs[i - 1].Height += leftDeltas[i]; if (i < springs.Length - 1) springs[i + 1].Height += rightDeltas[i]; } }
سوف يطلق على هذا الرمز كل إطار من طريقة التحديث()
الخاصة بك. هنا ، الينابيع
هي مجموعة من الينابيع ، وضعت من اليسار إلى اليمين. leftDeltas
هي مجموعة من العوامات التي تخزن الفرق في الارتفاع بين كل ربيع وجارته اليسرى. rightDeltas
هو ما يعادل الجيران المناسبين. نقوم بتخزين كل هذه الاختلافات في الارتفاع في صفائف لأن آخر اثنين if
العبارات بتعديل مرتفعات الينابيع. يتعين علينا قياس اختلافات الارتفاع قبل تعديل أي من الارتفاعات.
يبدأ الرمز عن طريق تشغيل قانون هوك في كل ربيع كما هو موضح سابقًا. ثم ينظر إلى فرق الارتفاع بين كل ربيع وجيرانه ، وكل ربيع يسحب الينابيع المجاورة نحو نفسه عن طريق تغيير مواقع الجير والسرعات. تتكرر خطوة سحب الجار ثماني مرات للسماح بنشر الموجات بشكل أسرع.
هناك قيمة واحدة قابلة للتعديل هنا تسمى Spread
. يتحكم في مدى سرعة انتشار الموجات. يمكن أن يأخذ القيم بين 0 و 0.5 ، مع قيم أكبر مما يجعل الأمواج تنتشر بشكل أسرع.
لبدء حركة الأمواج ، سنقوم بإضافة طريقة بسيطة تسمى Splash()
.
public void Splash(int index, float speed) { if (index >= 0 && index < springs.Length) springs[i].Speed = speed; }
في أي وقت تريد فيه جعل موجات ، اتصل بـ Splash()
. تحدد معلمة الفهرس
في أي الربيع يجب أن تنشأ البداية ، وتحدد معلمة السرعة
مدى حجم الموجات.
طريقة العزف
سنستخدم فئة XNA PrimitiveBatch
من XNA PrimitivesSample. تساعدنا فئة PrimitiveBatch
على رسم الخطوط والمثلثات مباشرة مع وحدة معالجة الرسوميات GPU. كنت تستخدم ذلك مثل:
// in LoadContent() primitiveBatch = new PrimitiveBatch(GraphicsDevice); // in Draw() primitiveBatch.Begin(PrimitiveType.TriangleList); foreach (Triangle triangle in trianglesToDraw) { primitiveBatch.AddVertex(triangle.Point1, Color.Red); primitiveBatch.AddVertex(triangle.Point2, Color.Red); primitiveBatch.AddVertex(triangle.Point3, Color.Red); } primitiveBatch.End();
شيء واحد هو أن نلاحظ أنه ، بشكل افتراضي ، يجب عليك تحديد رؤوس المثلث في ترتيب عقارب الساعة. إذا قمت بإضافتها بترتيب عكسي في اتجاه عقارب الساعة فسيتم إعدام المثلث ولن ترى ذلك.
ليس من الضروري أن يكون هناك ربيع لكل بكسل للعرض. في العرض التجريبي استخدمت 201 زنبركاً منتشرة عبر نافذة بعرض 800 بكسل. هذا يعطي 4 بيكسلات بالضبط بين كل ربيع ، مع أول ربيع عند 0 والأخير عند 800 بكسل. ربما يمكنك استخدام عدد أقل من الينابيع ولا تزال تبدو المياه على نحو سلس.
ما نريد القيام به هو رسم شبه منحرف رفيع ، يمتد من أسفل الشاشة إلى سطح الماء وربط الينابيع ، كما هو موضح في هذا المخطط:

نظرًا لأن بطاقات الرسومات لا ترسم شبه منحرف بشكل مباشر ، يجب علينا رسم كل شبه منحرف على شكل مثلثين. لجعلها تبدو أجمل قليلاً ، سنجعل الماء أكثر قتامة كلما تعمقنا عن طريق تلوين القمم السفلية باللون الأزرق الغامق. سيقوم GPU باستكمال الألوان تلقائيًا بين الرؤوس.
primitiveBatch.Begin(PrimitiveType.TriangleList); Color midnightBlue = new Color(0, 15, 40) * 0.9f; Color lightBlue = new Color(0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // stretch the springs' x positions to take up the entire window float scale = viewport.Width / (springs.Length - 1f); // be sure to use float division for (int i = 1; i < springs.Length; i++) { // create the four corners of our triangle. Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p2.X, bottom); Vector2 p4 = new Vector2(p1.X, bottom); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p4, midnightBlue); } primitiveBatch.End();
وهنا النتيجة:
صنع البقع
تبدو الأمواج جيدة ، لكنني أرغب في رؤية النبضة عندما تصطدم الصخور بالماء. آثار الجسيمات هي مثالية لهذا.
آثار الجسيمات
يستخدم تأثير الجسيمات عددًا كبيرًا من الجسيمات الصغيرة لإنتاج بعض التأثير البصري. يتم استخدامها أحيانًا لأشياء مثل الدخان أو الشرر. سنستخدم الجسيمات لقطرات الماء في البقع.
أول شيء نحتاجه هو طبقة الجسيمات لدينا:
class Particle { public Vector2 Position; public Vector2 Velocity; public float Orientation; public Particle(Vector2 position, Vector2 velocity, float orientation) { Position = position; Velocity = velocity; Orientation = orientation; } }
هذه الفئة تحمل فقط الخصائص التي يمكن أن تحتويها الجسيمات. بعد ذلك ، نخلق قائمة من الجسيمات.
List<Particle> particles = new List<Particle>();
كل إطار ، يجب علينا تحديث ورسم الجسيمات.
void UpdateParticle(Particle particle) { const float Gravity = 0.3f; particle.Velocity.Y += Gravity; particle.Position += particle.Velocity; particle.Orientation = GetAngle(particle.Velocity); } private float GetAngle(Vector2 vector) { return (float)Math.Atan2(vector.Y, vector.X); } public void Update() { foreach (var particle in particles) UpdateParticle(particle); // delete particles that are off-screen or under water particles = particles.Where(x => x.Position.X >= 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); }
نقوم بتحديث الجسيمات لتقع تحت الجاذبية وتعيين اتجاه الجسيمات لتتناسب مع الاتجاه الذي تسير فيه. ثم نتخلص من أي جسيمات تكون خارج الشاشة أو تحت الماء عن طريق نسخ كل الجسيمات التي نريد الاحتفاظ بها في قائمة جديدة وتعيينها إلى جسيمات. بعد ذلك نرسم الجسيمات.
void DrawParticle(Particle particle) { Vector2 origin = new Vector2(ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw(ParticleImage, particle.Position, null, Color.White, particle.Orientation, origin, 0.6f, 0, 0); } public void Draw() { foreach (var particle in particles) DrawParticle(particle); }
أدناه هو نسيج كنت تستخدم للجسيمات.

الآن ، كلما أنشأنا لطخة ، نجعل حفنة من الجسيمات.
private void CreateSplashParticles(float xPosition, float speed) { float y = GetHeight(xPosition); if (speed > 60) { for (int i = 0; i < speed / 8; i++) { Vector2 pos = new Vector2(xPosition, y) + GetRandomVector2(40); Vector2 vel = FromPolar(MathHelper.ToRadians(GetRandomFloat(-150, -30)), GetRandomFloat(0, 0.5f * (float)Math.Sqrt(speed))); particles.Add(new Particle(pos, velocity, 0)); } } }
يمكنك استدعاء هذه الطريقة من طريقة Splash()
التي نستخدمها لصنع الأمواج. سرعة المعلمة هي مدى سرعة الصخور يضرب الماء. سنجعل البقع أكبر إذا تحركت الصخور بشكل أسرع.
(إرجاع GetRandomVector2 (40
متجه مع اتجاه عشوائي وطول عشوائي بين 0 و 40. نريد إضافة القليل من العشوائية إلى المواضع بحيث لا تظهر كل الجسيمات في نقطة واحدة. FromPolar()
إرجاع Vector2
مع اتجاه محدد وطول.
وهنا النتيجة:
استخدام Metaballs كما الجسيمات
تبدو لطخاتنا كريمة إلى حد ما ، وبعض الألعاب الرائعة ، مثل عالم جوو ، لها رشقات تأثير جسيمات تشبه إلى حد كبير مثلنا. ومع ذلك ، سأريكم تقنية لجعل البقع تبدو أكثر سائلة. هذه التقنية هي استخدام metaballs ، النقط ذات المظهر العضوي التي قمت بكتابة برنامج تعليمي حولها من قبل. إذا كنت مهتمًا بتفاصيل حول metaballs وكيفية عملها ، فاقرأ هذا البرنامج التعليمي. إذا كنت تريد فقط معرفة كيفية تطبيقها على البقع لدينا ، تابع القراءة.
تبدو Metaballs شبيهة بالسوائل بالطريقة التي تندمج فيها مع بعضها البعض ، مما يجعلها مباراة جيدة للبقع السائلة. لجعل metaballs ، سنحتاج إلى إضافة متغيرات فئة جديدة:
RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;
التي نبدأ بها مثل:
var view = GraphicsDevice.Viewport; metaballTarget = new RenderTarget2D(GraphicsDevice, view.Width, view.Height); alphaTest = new AlphaTestEffect(GraphicsDevice); alphaTest.ReferenceAlpha = 175; alphaTest.Projection = Matrix.CreateTranslation(-0.5f, -0.5f, 0) * Matrix.CreateOrthographicOffCenter(0, view.Width, view.Height, 0, 0, 1);
ثم نستخلص metaballs:
GraphicsDevice.SetRenderTarget(metaballTarget); GraphicsDevice.Clear(Color.Transparent); Color lightBlue = new Color(0.2f, 0.5f, 1f); spriteBatch.Begin(0, BlendState.Additive); foreach (var particle in particles) { Vector2 origin = new Vector2(ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw(ParticleImage, particle.Position, null, lightBlue, particle.Orientation, origin, 2f, 0, 0); } spriteBatch.End(); GraphicsDevice.SetRenderTarget(null); device.Clear(Color.CornflowerBlue); spriteBatch.Begin(0, null, null, null, null, alphaTest); spriteBatch.Draw(metaballTarget, Vector2.Zero, Color.White); spriteBatch.End(); // draw waves and other things
يعتمد تأثير metaball على وجود نسيج جسيم يتلاشى كلما ابتعدت عن المركز. إليك ما استخدمته ، تم تعيينه على خلفية سوداء لجعله مرئيًا:

إليك ما يبدو عليه:
قطرات الماء الآن تندمج معًا عندما تكون قريبة. ومع ذلك ، فإنها لا تندمج مع سطح الماء. يمكننا إصلاح هذا عن طريق إضافة تدرج إلى سطح الماء يجعله يتلاشى تدريجياً ، مما يجعله مستهدفًا.
قم بإضافة التعليمة البرمجية التالية إلى الأسلوب أعلاه قبل السطر GraphicsDevice.SetRendertarget (خالية)
:
primitiveBatch.Begin(PrimitiveType.TriangleList); const float thickness = 20; float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); for (int i = 1; i < springs.Length; i++) { Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p1.X, p1.Y - thickness); Vector2 p4 = new Vector2(p2.X, p2.Y - thickness); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p4, Color.Transparent); primitiveBatch.AddVertex(p2, lightBlue); } primitiveBatch.End();
الآن سوف تندمج الجسيمات مع سطح الماء.
مضيفا تأثير الميل
تبدو جسيمات الماء مسطحة قليلاً ، وسيكون من اللطيف إعطاءها بعض التظليل. من الناحية المثالية ، يمكنك القيام بذلك في تظليل. ومع ذلك ، من أجل الحفاظ على هذا البرنامج التعليمي بسيطًا ، سنستخدم خدعة سريعة وسهلة: سنقوم ببساطة بسحب الجسيمات ثلاث مرات مع تباين وتصويبات مختلفة ، كما هو موضح في الرسم البياني أدناه.



للقيام بذلك ، نريد التقاط جسيمات metaball في هدف تقديم جديد. سنقوم بعد ذلك برسم ذلك الهدف مرة واحدة لكل لون.
أولاً ، أعلن عن RenderTarget2D
جديد تمامًا كما فعلنا في metaballs:
particlesTarget = new RenderTarget2D(GraphicsDevice, view.Width, view.Height);
ثم ، بدلاً من رسم metaballsTarget
مباشرةً إلى backbuffer ، نريد أن نرسمه على particlesTarget
. للقيام بذلك ، انتقل إلى الطريقة التي نرسم بها metaballs وقم ببساطة بتغيير هذه السطور:
GraphicsDevice.SetRenderTarget(null); device.Clear(Color.CornflowerBlue);
...إلى:
GraphicsDevice.SetRenderTarget(particlesTarget); device.Clear(Color.Transparent);
ثم استخدم التعليمة البرمجية التالية لرسم الجسيمات ثلاث مرات بصبغات وتغييرات مختلفة:
Color lightBlue = new Color(0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget(null); device.Clear(Color.CornflowerBlue); spriteBatch.Begin(); spriteBatch.Draw(particlesTarget, -Vector2.One, new Color(0.8f, 0.8f, 1f)); spriteBatch.Draw(particlesTarget, Vector2.One, new Color(0f, 0f, 0.2f)); spriteBatch.Draw(particlesTarget, Vector2.Zero, lightBlue); spriteBatch.End(); // draw waves and other stuff
خاتمة
هذا كل شيء للمياه 2D الأساسية. بالنسبة إلى العرض التوضيحي ، أضفت صخرة يمكنك إسقاطها في الماء. أرسم المياه بشفافية على قمة الصخرة لجعلها تبدو وكأنها تحت الماء ، وجعلها بطيئة عندما تكون تحت الماء بسبب مقاومة الماء.
لجعل العرض التجريبي يبدو أجمل قليلاً ، ذهبت إلى opengameart.org وجدت صورة للصخرة وخلفية السماء. يمكنك العثور على الصخور والسماء على http://opengameart.org/content/rocks و opengameart.org/content/sky-backdrop على التوالي.