Pindahkan objek secara merata di sepanjang kurva





Dalam proses mengembangkan game dalam kategori genre yang sama sekali berbeda, mungkin perlu "meluncurkan" objek game apa pun di sepanjang kurva yang mulus dengan kecepatan konstan atau terkontrol, baik itu truk yang bergerak dari kota A ke kota B, rudal yang ditembakkan di sepanjang lintasan yang licik, atau pesawat musuh melakukan manuver yang sudah ditentukan.



Mungkin, semua orang yang terkait dengan topik tersebut mengetahui atau setidaknya mendengar tentang kurva BΓ©zier, B-splines, Hermite splines, dan interpolasi lainnya serta splines smoothing dan akan benar-benar menyarankan penggunaan salah satu dari mereka dalam situasi yang dijelaskan, tetapi tidak semuanya sesederhana itu seperti yang kami inginkan.



Dalam kasus kami, spline adalah fungsi yang menampilkan sekumpulan parameter input (titik kontrol) dan nilai argumen t(biasanya mengambil nilai dari 0 hingga 1) ke suatu titik di bidang atau di luar angkasa. Kurva yang dihasilkan adalah himpunan nilai dari fungsi spline untukt∈[0,1]...



Sebagai contoh, perhatikan kurva Bezier kubik , yang diberikan oleh persamaan berikut:

gambar




gambar

Kurva Kubik Bezier



Gambar menunjukkan dua kurva Bezier kubik, yang ditentukan oleh empat titik (kurva melewati dua di antaranya, dua sisanya menentukan kelengkungan).



gambar

Animasi menampilkan parameter t ke titik kurva



Untuk membangun rute yang lebih kompleks dan variabel dari bagian kurva, mereka dapat dihubungkan dalam sebuah rantai, menyisakan satu titik-link yang sama:





Sedikit spline Kita telah



menemukan tugas dan parameterisasi rute, sekarang kita beralih ke pertanyaan utama. Agar bidang konvensional kita bergerak di sepanjang rute dengan kecepatan konstan, setiap saat kita perlu menghitung titik pada kurva yang bergantung pada jarak yang ditempuh sepanjang kurva ini.s(len), sementara hanya memiliki kemampuan untuk menghitung posisi titik pada kurva dari nilai parameter t (fungsi y(t)). Pada tahap inilah kesulitan dimulai.



Pikiran pertama saya adalah melakukan pemetaan linierlen∈[0,Length]β‡’t∈[0,1] dan menghitung y(t)dari nilai yang dihasilkan - mudah, murah secara komputasi, secara umum, apa yang Anda butuhkan.



gambar



Masalah dengan metode ini segera terlihat - pada kenyataannya, jarak yang ditempuhs tergantung pada t secara nonlinier, dan untuk diyakinkan akan hal ini, cukup mengatur sepanjang rute yang didistribusikan secara seragam berdasarkan nilai tpoin: titik yang





"merata" didistribusikan di sepanjang jalur



Bidang akan melambat di beberapa bagian dan dipercepat di bagian lain, yang membuat metode parameterisasi kurva ini sama sekali tidak dapat diterapkan untuk menyelesaikan masalah yang dijelaskan (bidang dipilih secara eksklusif sebagai contoh ilustratif dan tujuan untuk menjelaskan pergerakannya dengan cara yang benar secara fisik tidak dikejar ):





Visualisasi parameterisasi kurva yang salah



Setelah berkonsultasi dengan mesin pencari, di stackoverflow dan youtube , saya menemukan cara kedua untuk menghitungs(len)=g(t), yaitu, representasi kurva dalam bentuk fungsi linier-sepotong (menghitung sekumpulan titik yang berjarak sama satu sama lain di sepanjang kurva):





Mewakili kurva dalam bentuk spline linier sepotong-sepotong



Prosedur ini berulang: sebuah langkah kecil dilakukant, kita bergerak sepanjang kurva dengannya, menjumlahkan jarak yang ditempuh sebagai panjang spline linier sepotong-sepotong sampai jarak yang ditentukan tercapai, setelah itu titik diingat dan proses dilanjutkan.



Kode sampel
Sumber



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();
  }


Dan, tampaknya, masalahnya terpecahkan, karena Anda dapat membagi rute menjadi banyak segmen dan menikmati betapa mulus dan mantapnya objek bergerak, karena menghitung titik pada fungsi linier sepotong-sepotong adalah hal yang sederhana dan cepat. Namun pendekatan ini juga memiliki kelemahan yang cukup jelas yang menghantui saya - prosedur yang agak mahal untuk mengubah langkah partisi atau geometri kurva dan kebutuhan untuk menemukan keseimbangan antara akurasi (konsumsi memori lebih banyak) dan penghematan memori (rute "rusak" menjadi lebih terlihat).



Hasilnya, saya terus mencari dan menemukan artikel yang sangat bagus "Bergerak Sepanjang Kurva dengan Kecepatan Tertentu" , yang menjadi dasar penalaran lebih lanjut.



Nilai kecepatan dapat dihitung sebagaiσ(t)=|V(t)|=|dXdt|dimana X(t)- fungsi spline. Untuk mengatasi masalah tersebut, Anda perlu mencari fungsinyaY(t)=X(s)dimana s - panjang busur dari titik awal ke titik yang diinginkan, dan ekspresi ini menetapkan hubungan antara t dan s...



Dengan menggunakan substitusi variabel diferensiasi, kita dapat menulis

dYdt=dXdsdsdt.

Dengan mempertimbangkan bahwa kecepatan adalah kuantitas non-negatif, kami memperolehnya

|dYdt|=|dXds||dsdt|=dsdt,

karena |dXds|=1 dengan kondisi bahwa modulus vektor kecepatan tetap tidak berubah (secara umum, |dXds|tidak sama dengan satu, tetapi dengan konstanta, tetapi untuk kesederhanaan perhitungan, kita akan menganggap konstanta ini sama dengan satu).



Sekarang kita mendapatkan rasio antarat dan s secara eksplisit:

s=g(t)=∫0t|dY(Ο„)dt|dΟ„,

darimana total panjang kurva Ljelas dihitung sebagai g(1)... Menggunakan rumus yang dihasilkan, dimungkinkan, memiliki nilai argument, hitung panjang busur ke titik di mana nilai ini berada tditampilkan.



Kami tertarik dengan operasi kebalikannya, yaitu menghitung nilai argument sepanjang busur tertentu s:

t=gβˆ’1(s).

Karena tidak ada cara analitik umum untuk mencari fungsi invers, Anda harus mencari solusi numerik. Kami menunjukkanF(t)=g(t)βˆ’s. Untuk diberikan s, diperlukan untuk menemukan nilai seperti itu tdi mana F(t)=0... Jadi, tugas berubah menjadi tugas menemukan akar persamaan, yang dapat diatasi dengan sempurna oleh metode Newton.



Metode tersebut membentuk urutan perkiraan bentuk

ti+1=tiβˆ’F(ti)Fβ€²(ti),

Dimana

Fβ€²(t)=dFdt=dgdt=|dYdt|.

Menghitung F(t) perlu menghitung g(t), yang perhitungannya, pada gilirannya, membutuhkan integrasi numerik.



Pilihant0=sLsebagai perkiraan awal sekarang terlihat masuk akal (ingat pendekatan pertama untuk memecahkan masalah).



Selanjutnya, kita mengulangi menggunakan metode Newton sampai kesalahan solusi diterima, atau jumlah iterasi yang dilakukan sangat besar.



Ada masalah potensial dengan menggunakan metode Newton standar. FungsiF(t) - tidak menurun, karena turunannya Fβ€²(t)=|dY/dt|tidak negatif. Jika turunan keduanyaFβ€³(t)>0, maka fungsinya disebut konveks dan metode Newton di atasnya dijamin konvergen ke akar. Namun, dalam kasus kami,Fβ€³(t) dapat berubah menjadi negatif, karena metode Newton dapat bertemu di luar rentang definisi t∈[0,1]... Penulis artikel mengusulkan untuk menggunakan modifikasi metode Newton, yang, jika hasil iterasi berikutnya dengan metode Newton tidak termasuk dalam interval saat ini yang membatasi akar, maka akan memilih tengah interval ( metode dikotomi ). Terlepas dari hasil kalkulasi pada iterasi saat ini, kisarannya menyempit, yang berarti metode konvergen ke akar.



Tetap memilih metode integrasi numerik - Saya menggunakan kuadrat dari metode Gauss , karena memberikan hasil yang cukup akurat dengan biaya rendah.



Kode fungsi untuk menghitung 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;
}




Kode Fungsi Integrasi Numerik
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;
}


Selanjutnya, Anda dapat menyesuaikan kesalahan metode Newton, memilih metode integrasi numerik yang lebih akurat, tetapi masalahnya, pada kenyataannya, telah terpecahkan - sebuah algoritma untuk menghitung argumen dibangun tspline untuk panjang busur tertentu. Untuk menggabungkan beberapa bagian kurva menjadi satu dan menulis prosedur penghitungan umums(t)Anda dapat menyimpan panjang semua segmen dan menghitung sebelumnya indeks segmen tempat Anda ingin melakukan prosedur iteratif menggunakan metode Newton yang dimodifikasi.





Titik-titik terdistribusi merata di sepanjang jalur





Sekarang bidang bergerak secara seragam



Dengan demikian, kami telah mempertimbangkan beberapa cara untuk membuat parameter spline relatif terhadap jarak yang ditempuh, dalam artikel yang saya gunakan sebagai sumber, penulis menyarankan penyelesaian persamaan diferensial secara numerik sebagai opsi, tetapi, menurut komentarnya sendiri, lebih memilih metode yang dimodifikasi Newton:
Saya lebih suka pendekatan metode Newton karena umumnya t dapat dihitung lebih cepat daripada dengan pemecah persamaan diferensial.


Sebagai kesimpulan, saya akan memberikan tabel waktu untuk menghitung posisi titik pada kurva yang ditunjukkan pada tangkapan layar dalam satu utas pada prosesor i5-9600K:

Jumlah perhitungan Waktu rata-rata, ms
100 0.62
1000 6.24
10000 69.01
100.000 672.81
Saya percaya bahwa kecepatan komputasi seperti itu memungkinkan untuk menggunakannya dalam berbagai game dan simulasi. Saya akan dengan senang hati mengklarifikasi dan mengkritik pada intinya di komentar.



All Articles