في عملية تطوير لعبة في فئات أنواع مختلفة تمامًا ، قد يكون من الضروري "إطلاق" أي كائن لعبة على طول منحنى سلس بسرعة ثابتة أو مضبوطة ، سواء كانت شاحنة تتحرك من المدينة A إلى المدينة B ، أو صاروخًا يتم إطلاقه على طول مسار ماكر ، أو طائرة معادية القيام بمناورة على الأرض.
من المحتمل أن كل شخص مرتبط بالموضوع يعرف أو على الأقل سمع عن منحنيات بيزيير ، وخطوط B ، وشرائح هيرمايت وغيرها من شرائح الاستيفاء والتنعيم ، ويقترح بشكل صحيح تمامًا استخدام واحد منهم في الحالة الموصوفة ، ولكن ليس كل شيء بهذه البساطة كما نود.
في حالتنا ، الخدد هو وظيفة تعرض مجموعة من معلمات الإدخال (نقاط التحكم) وقيمة الوسيطة (عادةً ما تأخذ القيم من 0 إلى 1) إلى نقطة على مستوى أو في الفضاء. المنحنى الناتج هو مجموعة قيم وظيفة الشريحة لـ...
على سبيل المثال ، ضع في اعتبارك منحنى Bezier المكعب ، والذي يتم تقديمه بالمعادلة التالية:
منحنى بيزير المكعب
يوضح الشكل منحنيين بيزير مكعّبين ، محددين بأربع نقاط (يمر المنحنى من خلال اثنتين منهم ، والاثنان المتبقيان يحددان الانحناء). رسم متحرك
لعرض المعلمة t إلى نقطة منحنى من
أجل بناء مسار أكثر تعقيدًا ومتغيرًا من أقسام المنحنى ، يمكن توصيلها في سلسلة ، تاركًا نقطة ارتباط واحدة مشتركة:
مخطط تفصيلي متقطع لقد اكتشفنا
المهمة وتحديد معايير المسار ، ننتقل الآن إلى السؤال الرئيسي. لكي يتحرك المستوى الشرطي على طول المسار بسرعة ثابتة ، نحتاج في أي وقت إلى أن نكون قادرين على حساب نقطة على المنحنى ، اعتمادًا على المسافة المقطوعة على طول هذا المنحنى.، مع القدرة فقط على حساب موضع نقطة على المنحنى من قيمة المعلمة (وظيفة ). في هذه المرحلة تبدأ الصعوبات.
كانت فكرتي الأولى هي القيام برسم خرائط خطية واحسب من القيمة الناتجة - ما تحتاجه سهل ، ورخيص حسابيا ، بشكل عام.
مشكلة هذه الطريقة واضحة على الفور - في الواقع ، المسافة المقطوعة يعتمد على غير خطي ، ولكي تكون مقتنعًا بذلك ، يكفي الترتيب على طول المسار الموزع بشكل موحد حسب القيمة النقاط: النقاط
"بالتساوي" موزعة على طول المسار
سوف تتباطأ الطائرة في بعض الأقسام وتتسارع في البعض الآخر ، مما يجعل طريقة تحديد معاملات المنحنى هذه غير قابلة للتطبيق تمامًا لحل المشكلة الموصوفة (تم اختيار الطائرة حصريًا كمثال توضيحي ولم يتم متابعة الهدف من وصف حركتها بطريقة صحيحة ماديًا ):
تصور معلمات غير صحيحة للمنحنى
بعد استشارة محرك البحث ، على stackoverflow و youtube ، وجدت طريقة ثانية لحساب، أي تمثيل المنحنى في شكل دالة متعددة الخطوط (حساب مجموعة من النقاط على مسافة متساوية من بعضها البعض على طول المنحنى):
تمثيل المنحنى على شكل شريحة خطية متعددة الأجزاء
هذا الإجراء تكراري: يتم اتخاذ خطوة صغيرة، نتحرك على طول منحنى معها ، ونلخص المسافة المقطوعة بطول شريحة خطية متعددة الأجزاء حتى يتم تغطية مسافة معينة ، وبعد ذلك يتم تذكر النقطة ، وتستأنف العملية.
عينة من الرموز
مصدر
public Vector2[] CalculateEvenlySpacedPoints(float spacing, float resolution = 1)
{
List<Vector2> evenlySpacedPoints = new List<Vector2>();
evenlySpacedPoints.Add(points[0]);
Vector2 previousPoint = points[0];
float dstSinceLastEvenPoint = 0;
for (int segmentIndex = 0; segmentIndex < NumSegments; segmentIndex++)
{
Vector2[] p = GetPointsInSegment(segmentIndex);
float controlNetLength = Vector2.Distance(p[0], p[1]) + Vector2.Distance(p[1], p[2]) + Vector2.Distance(p[2], p[3]);
float estimatedCurveLength = Vector2.Distance(p[0], p[3]) + controlNetLength / 2f;
int divisions = Mathf.CeilToInt(estimatedCurveLength * resolution * 10);
float t = 0;
while (t <= 1)
{
t += 1f/divisions;
Vector2 pointOnCurve = Bezier.EvaluateCubic(p[0], p[1], p[2], p[3], t);
dstSinceLastEvenPoint += Vector2.Distance(previousPoint, pointOnCurve);
while (dstSinceLastEvenPoint >= spacing)
{
float overshootDst = dstSinceLastEvenPoint - spacing;
Vector2 newEvenlySpacedPoint = pointOnCurve + (previousPoint - pointOnCurve).normalized * overshootDst;
evenlySpacedPoints.Add(newEvenlySpacedPoint);
dstSinceLastEvenPoint = overshootDst;
previousPoint = newEvenlySpacedPoint;
}
previousPoint = pointOnCurve;
}
}
return evenlySpacedPoints.ToArray();
}
ويبدو أن المشكلة قد تم حلها ، لأنه يمكنك تقسيم المسار إلى العديد من المقاطع والاستمتاع بمدى سلاسة وقياس حركة الجسم ، نظرًا لأن حساب نقطة على دالة خطية متعددة التعابير يعد أمرًا بسيطًا وسريعًا. ولكن هذا النهج له أيضًا عيوب واضحة تزعجني - إجراء مكلف إلى حد ما لتغيير خطوة التقسيم أو هندسة المنحنى والحاجة إلى إيجاد توازن بين الدقة (المزيد من استهلاك الذاكرة) وتوفير الذاكرة (يصبح المسار "المكسور" أكثر وضوحًا).
ونتيجة لذلك ، واصلت البحث ووجدت مقالًا ممتازًا بعنوان "التحرك على طول منحنى بسرعة محددة" ، والذي يقوم على أساسه مزيد من التفكير.
يمكن حساب قيمة السرعة على أنهاأين - وظيفة خدد. لحل المشكلة ، تحتاج إلى إيجاد الوظيفةأين - طول القوس من نقطة البداية إلى النقطة المرغوبة ، وهذا التعبير يؤسس العلاقة بينهما و ...
باستخدام تعويض متغير التفاضل ، يمكننا الكتابة
مع الأخذ في الاعتبار أن السرعة كمية غير سالبة نحصل عليها
لان بشرط أن يظل معامل متجه السرعة ثابتًا (بشكل عام ، لا يساوي واحدًا ، بل ثابتًا ، ولكن لتبسيط العمليات الحسابية ، سنأخذ هذا الثابت يساوي واحدًا).
الآن نحصل على النسبة بين و صراحة:
من أين الطول الكلي للمنحنى من الواضح أنه محسوب على أنه ... باستخدام الصيغة الناتجة ، من الممكن أن يكون لها قيمة الوسيطة، احسب طول القوس إلى النقطة التي تكون عندها هذه القيمة معروض.
نحن مهتمون بالعملية العكسية ، أي حساب قيمة الوسيطة على طول قوس معين :
نظرًا لعدم وجود طريقة تحليلية عامة لإيجاد الدالة العكسية ، فسيتعين عليك البحث عن حل رقمي. نشير لاجل منحه ، لا بد من إيجاد مثل هذه القيمة الذي ... وهكذا ، تحولت المهمة إلى مهمة إيجاد جذر المعادلة ، والتي يمكن لطريقة نيوتن التعامل معها تمامًا.
الطريقة تشكل تسلسل تقريبي للنموذج
أين
لكي يحسب بحاجة لحساب ، يتطلب حسابه بدوره تكاملًا عدديًا.
خياركتقريب أولي يبدو الآن معقولاً (تذكر الطريقة الأولى لحل المشكلة).
بعد ذلك ، نكرر استخدام طريقة نيوتن حتى يصبح خطأ الحل مقبولًا ، أو أن عدد التكرارات التي تم إجراؤها كبير للغاية.
هناك مشكلة محتملة في استخدام طريقة نيوتن القياسية. وظيفة - غير متناقص منذ مشتقه غير سالب. إذا كان المشتق الثاني، ثم تسمى الوظيفة محدبة وطريقة نيوتن مضمونة لتتقارب مع الجذر. ومع ذلك ، في حالتنا ، قد تكون سلبية ، بسبب طريقة نيوتن التي قد تتقارب خارج نطاق التعريف ... يقترح مؤلف المقال استخدام تعديل لطريقة نيوتن ، والتي ، إذا كانت نتيجة التكرار التالية بواسطة طريقة نيوتن لا تقع في الفاصل الزمني الحالي الذي يحيط بالجذر ، فإنه يختار بدلاً من ذلك منتصف الفترة ( طريقة الانقسام ). بغض النظر عن نتيجة الحساب عند التكرار الحالي ، يتم تضييق النطاق ، مما يعني أن الطريقة تتقارب مع الجذر.
يبقى اختيار طريقة التكامل العددي - لقد استخدمت تربيعات طريقة غاوس ، لأنها توفر نتيجة دقيقة إلى حد ما بتكلفة منخفضة.
رمز الوظيفة لحساب t (s)
public float ArcLength(float t) => Integrate(x => TangentMagnitude(x), 0, t);
private float Parameter(float length)
{
float t = 0 + length / ArcLength(1);
float lowerBound = 0f;
float upperBound = 1f;
for (int i = 0; i < 100; ++i)
{
float f = ArcLength(t) - length;
if (Mathf.Abs(f) < 0.01f)
break;
float derivative = TangentMagnitude(t);
float candidateT = t - f / derivative;
if (f > 0)
{
upperBound = t;
if (candidateT <= 0)
t = (upperBound + lowerBound) / 2;
else
t = candidateT;
}
else
{
lowerBound = t;
if (candidateT >= 1)
t = (upperBound + lowerBound) / 2;
else
t = candidateT;
}
}
return t;
}
كود دالة التكامل العددي
private static readonly (float, float)[] CubicQuadrature =
{(-0.7745966F, 0.5555556F), (0, 0.8888889F), (0.7745966F, 0.5555556F)};
public static float Integrate(Func<float, float> f, in float lowerBound, in float uppedBound)
{
var sum = 0f;
foreach (var (arg, weight) in CubicQuadrature)
{
var t = Mathf.Lerp(lowerBound, uppedBound, Mathf.InverseLerp(-1, 1, arg));
sum += weight * f(t);
}
return sum * (upperBound - lowerBound) / 2;
}
بعد ذلك ، يمكنك ضبط خطأ طريقة نيوتن ، واختيار طريقة أكثر دقة للتكامل العددي ، ولكن المشكلة ، في الواقع ، قد تم حلها - تم إنشاء خوارزمية لحساب الوسيطة شريحة لطول قوس معين. لدمج عدة أقسام من المنحنيات في قسم واحد وكتابة إجراء حساب معمميمكنك تخزين أطوال جميع المقاطع والحساب المسبق لفهرس المقطع حيث تريد تنفيذ إجراء تكراري باستخدام طريقة نيوتن المعدلة.
النقاط موزعة بالتساوي على طول المسار
الآن تتحرك الطائرة بشكل موحد
وهكذا ، فقد درسنا عدة طرق لتحديد معلمات الشريحة فيما يتعلق بالمسافة المقطوعة ، في المقالة التي استخدمتها كمصدر ، كخيار ، يقترح المؤلف حل المعادلة التفاضلية عدديًا ، ولكن وفقًا لملاحظته الخاصة ، يفضل الطريقة المعدلة نيوتن:
أنا أفضل طريقة نيوتن لأنه يمكن حساب t بشكل عام أسرع من حل المعادلات التفاضلية.
في الختام ، سأقدم جدولًا زمنيًا لحساب موضع نقطة على المنحنى الموضح في لقطات الشاشة في مؤشر ترابط واحد على معالج i5-9600K:
عدد العمليات الحسابية | متوسط الوقت ، مللي |
---|---|
100 | 0.62 |
1000 | 6.24 |
10000 | 69.01 |
100،000 | 672.81 |