Spostare le navi tra due pianeti lungo un più bezier, mancando alcune equazioni per l'accelerazione


48

OK, l'ho già pubblicato su math.stackechange.com ma non ho ricevuto alcuna risposta :(

Prima di tutto ecco un'immagine del mio problema, la descrizione segue in seguito:

testo alternativo

Quindi ho impostato tutti i punti e i valori.

La nave inizia a muoversi intorno al pianeta sinistro P1con S=0.27 Degreesogni gametick, quando raggiunge Point Ainizia a seguire la curva più bezier fino a raggiungere Point D, quindi viaggia intorno al pianeta destro P2con S=0.42 Degreestick per partita. La differenza Ssta nel fatto che il viaggio con la stessa velocità di movimento attorno ai pianeti.

Fin qui tutto bene, l'ho messo in funzione, ora il mio problema.

Quando S P1e S P2differiscono da molto, la nave salta tra le due velocità quando raggiunge la sua destinazione, che sembra piuttosto male. Quindi ho bisogno di accelerare la nave tra Point Ae Point Dda S P1a S P2.

Quello che mi manca sono in viola, quelli sono:

  • Un modo per calcolare le zecche impiega la nave a muoversi lungo il bezier considerando l'accelerazione.

  • E un modo per trovare una posizione sulla curva di Bezier in base a T, considerando ancora l'accelerazione.

ATM I calcola la lunghezza del bezier calcolando la distanza tra i Nsuoi punti. Quindi quello che penso di aver bisogno è un modo per ridimensionare la Tnecessità di inserire il mio calcolo più bezier in base all'accelerazione.


2
Ottimo lavoro per capirlo. Ti suggerisco di pubblicare i risultati come risposta alla tua domanda.
Bummzack,

Risposte:


83

OK, ho tutto funzionato, ci ho messo un'eternità, quindi pubblicherò la mia soluzione dettagliata qui.
Nota: tutti gli esempi di codice sono in JavaScript.

Quindi suddividiamo il problema nelle parti di base:

  1. È necessario calcolare la lunghezza di, nonché i punti tra 0..1sulla curva di Bezier

  2. Ora devi regolare il ridimensionamento del tuo Tper accelerare la nave da una velocità all'altra

Ottenere il Bezier giusto

Trovare un po 'di codice per disegnare una curva di Bezier è facile, tuttavia ci sono molti approcci diversi, uno di questi è l' algoritmo DeCasteljau , ma puoi anche usare l' equazione per le curve cubiche di Bézier:

// Part of a class, a, b, c, d are the four control points of the curve
x: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
           + 3 * ((1 - t) * (1 - t)) * t * this.b.x
           + 3 * (1 - t) * (t * t) * this.c.x
           + (t * t * t) * this.d.x;
},

y: function (t) {
    return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
           + 3 * ((1 - t) * (1 - t)) * t * this.b.y
           + 3 * (1 - t) * (t * t) * this.c.y
           + (t * t * t) * this.d.y;
}

Con questo, ora si può disegnare una curva più bezier chiamando xe ycon tquali intervalli da 0 to 1, diamo un'occhiata:

testo alternativo

Uh ... non è proprio una distribuzione uniforme dei punti, vero?
A causa della natura della curva di Bézier, i punti su 0...1hanno diversi arc lenghts, quindi i segmenti vicino all'inizio e alla fine sono più lunghi di quelli che si trovano vicino al centro della curva.

Mappatura T uniformemente sulla parametrizzazione della lunghezza dell'arco AKA della curva

Quindi che si fa? Bene in termini semplici abbiamo bisogno di una funzione per mappare il nostro Tsul tdella curva, in modo che i nostri T 0.25risultati nella tquella a 25%della lunghezza della curva.

Come lo facciamo? Bene, noi Google ... ma si scopre che il termine non è così googleable , e ad un certo punto colpirai questo PDF . Il che è sicuramente un'ottima lettura, ma nel caso in cui hai già dimenticato tutte le cose matematiche che hai imparato a scuola (o semplicemente non ti piacciono quei simboli matematici) è piuttosto inutile.

E adesso? Bene, andate ancora su Google (leggete: 6 ore) e finalmente trovate un ottimo articolo sull'argomento (incluse belle foto! ^ _ ^ "):
Http://www.planetclegg.com/projects/WarpingTextToSplines.html

Fare il codice attuale

Nel caso in cui non potessi resistere al download di quel PDF, anche se hai già perso le tue conoscenze matematiche molto, molto, molto tempo fa (e sei riuscito a saltare l' ottimo link dell'articolo), potresti ora pensare: "Dio, ci vorrà centinaia di righe di codice e tonnellate di CPU "

No, non lo farà. Perché facciamo ciò che fanno tutti i programmatori, quando si tratta di cose matematiche:
semplicemente imbrogliamo.

Parametrizzazione della lunghezza dell'arco, il modo pigro

Ammettiamolo, non abbiamo bisogno di una precisione infinita nel nostro gioco, vero? Quindi, a meno che tu non stia lavorando alla Nasa e non preveda di inviare persone su Marte, non avrai bisogno di una 0.000001 pixelsoluzione perfetta.

Quindi, come possiamo mappare Tsu t? È semplice e comprende solo 3 passaggi:

  1. Calcola i Npunti sulla curva usando te archivia arc-length(ovvero la lunghezza della curva) in quella posizione in un array

  2. Per mappare Tsu t, prima moltiplica Tper la lunghezza totale della curva per ottenere, uquindi cerca nell'array di lunghezze l'indice del valore più grande che è più piccolo diu

  3. Se abbiamo avuto un risultato esatto, restituiamo il valore di matrice in quell'indice diviso per N, se non interpolare un po 'tra il punto che abbiamo trovato e quello successivo, dividere nuovamente la cosa per Ne restituire.

È tutto! Quindi ora diamo un'occhiata al codice completo:

function Bezier(a, b, c, d) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;

    this.len = 100;
    this.arcLengths = new Array(this.len + 1);
    this.arcLengths[0] = 0;

    var ox = this.x(0), oy = this.y(0), clen = 0;
    for(var i = 1; i <= this.len; i += 1) {
        var x = this.x(i * 0.05), y = this.y(i * 0.05);
        var dx = ox - x, dy = oy - y;        
        clen += Math.sqrt(dx * dx + dy * dy);
        this.arcLengths[i] = clen;
        ox = x, oy = y;
    }
    this.length = clen;    
}

Questo inizializza la nostra nuova curva e calcola la arg-lenghts, memorizza anche l'ultima delle lunghezze come total lengthla curva, il fattore chiave qui è this.lenquale è la nostra N. Più è alta, più precisa sarà la mappatura, poiché una curva delle dimensioni nell'immagine sopra 100 pointssembra essere sufficiente, se hai solo bisogno di una buona stima della lunghezza, qualcosa come 25farebbe già il lavoro con solo 1 pixel di sconto nel nostro esempio, ma avrai una mappatura meno precisa che comporterà una distribuzione non uniforme di Tquando mappata t.

Bezier.prototype = {
    map: function(u) {
        var targetLength = u * this.arcLengths[this.len];
        var low = 0, high = this.len, index = 0;
        while (low < high) {
            index = low + (((high - low) / 2) | 0);
            if (this.arcLengths[index] < targetLength) {
                low = index + 1;

            } else {
                high = index;
            }
        }
        if (this.arcLengths[index] > targetLength) {
            index--;
        }

        var lengthBefore = this.arcLengths[index];
        if (lengthBefore === targetLength) {
            return index / this.len;

        } else {
            return (index + (targetLength - lengthBefore) / (this.arcLengths[index + 1] - lengthBefore)) / this.len;
        }
    },

    mx: function (u) {
        return this.x(this.map(u));
    },

    my: function (u) {
        return this.y(this.map(u));
    },

Il codice di mappatura effettivo, prima facciamo un semplice binary searchsulle nostre lunghezze memorizzate per trovare la lunghezza più grande che è più piccola targetLength, quindi restituiamo o facciamo l'interpolazione e restituiamo.

    x: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.x
               + 3 * ((1 - t) * (1 - t)) * t * this.b.x
               + 3 * (1 - t) * (t * t) * this.c.x
               + (t * t * t) * this.d.x;
    },

    y: function (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.a.y
               + 3 * ((1 - t) * (1 - t)) * t * this.b.y
               + 3 * (1 - t) * (t * t) * this.c.y
               + (t * t * t) * this.d.y;
    }
};

Ancora una volta questo calcola tsulla curva.

Tempo per i risultati

testo alternativo

Ormai usando mxe myottieni una distribuzione uniforme Tsulla curva :)

Non è stato difficile, vero? Ancora una volta, si scopre che una soluzione semplice (anche se non perfetta) sarà sufficiente per un gioco.

Se vuoi vedere il codice completo, c'è un Gist disponibile:
https://gist.github.com/670236

Infine, accelerando le navi

Quindi tutto ciò che rimane ora è accelerare le navi lungo il loro percorso, mappando la posizione su Tcui poi utilizziamo per trovare la tcurva.

Per prima cosa abbiamo bisogno di due delle equazioni del moto , vale a dire ut + 1/2at²e(v - u) / t

Nel codice effettivo sarebbe simile al seguente:

startSpeed = getStartingSpeedInPixels() // Note: pixels
endSpeed = getFinalSpeedInPixels() // Note: pixels
acceleration = (endSpeed - startSpeed) // since we scale to 0...1 we can leave out the division by 1 here
position = 0.5 * acceleration * t * t + startSpeed * t;

Quindi lo ridimensioniamo 0...1facendo:

maxPosition = 0.5 * acceleration + startSpeed;
newT = 1 / maxPosition * position;

Ed ecco qua, le navi ora si muovono senza intoppi lungo il percorso.

Nel caso in cui non funzioni ...

Quando stai leggendo questo, tutto funziona benissimo e dandy, ma inizialmente ho avuto alcuni problemi con la parte dell'accelerazione, quando ho spiegato il problema a qualcuno nella chat di Gamedev ho trovato l'ultimo errore nel mio pensiero.

Nel caso in cui non ti sia già dimenticato dell'immagine nella domanda originale, menziono slì, risulta che sè la velocità in gradi , ma le navi si muovono lungo il percorso in pixel e mi ero dimenticato di questo fatto. Quindi quello che dovevo fare in questo caso era convertire lo spostamento in gradi in uno spostamento in pixel, risulta che questo è piuttosto facile:

function rotationToMovement(planetSize, rotationSpeed) {
    var r = shipAngle * Math.PI / 180;
    var rr = (shipAngle + rotationSpeed) * Math.PI / 180;
    var orbit = planetSize + shipOrbit;
    var dx = Math.cos(r) * orbit - Math.cos(rr) * orbit;
    var dy = Math.sin(r) * orbit - Math.sin(rr) * orbit;
    return Math.sqrt(dx * dx + dy * dy);
};

Quindi e questo è tutto! Grazie per aver letto ;)


7
Questo richiederà del tempo per essere digerito. Ma wow, incredibile risposta alla tua domanda.
Attaccando

7
Ho fatto un account solo per votare questa risposta
Nessuno il

Qualche punto amico mio. Ha funzionato come un fascino. Domanda e risposta sia Upvoted.
Jace,

2
'i' viene moltiplicato per 0,05, mentre 'len' era impostato su 100. questo sarebbe 't' da mappare su '0-5' invece di '0-1'.
Evil Activity

1
@EvilActivity Sì, l'ho visto anche io, la sua lunghezza originale deve essere stata 20, quindi ho dimenticato di cambiare da 0,05 a 0,01. Quindi meglio avere un 'len' dinamico (adattivo alla lunghezza dell'arco reale, o forse anche esattamente uguale ad esso), e calcolare il "passo" di 1 / 'len'. Lo trovo così strano che nessun altro lo ha sollevato in tutti questi anni !!!
Bill Kotsias,

4

Il problema è che una nave non prenderebbe naturalmente quella traiettoria. Quindi, anche se funziona perfettamente, non sembrerà ancora giusto.

Se vuoi simulare la transizione graduale tra i pianeti, suggerirei di modellarlo realmente. Le equazioni sono molto semplici in quanto hai solo due forze significative: la gravità e la spinta.

Devi solo impostare le tue costanti: massa di P1, P2, nave

Con ogni tick di gioco (time: t) stai facendo 3 cose

  1. Calcola la gravità di p1 sulla nave e p2 sulla nave, aggiungi i vettori risultanti al vettore di spinta.

  2. Calcola la tua nuova velocità in base alla nuova accelerazione dal passaggio 1

  3. Sposta la nave in base alla tua nuova velocità

Potrebbe sembrare molto lavoro, ma può essere fatto in una dozzina di righe di codice e sembrerà molto naturale.

Se hai bisogno di aiuto con la fisica fammi sapere.


Potrei considerare di provare che se riesci a fornire un modo per farlo all'interno di una funzione che richiede t:)
Ivo Wetzel,

-ma nella programmazione del gioco non usi t come variabile. Praticamente sei già in una situazione parametrica, perché stai semplicemente calcolando il nuovo dx e il dy per la nave. Ecco un esempio di orbita attorno a due pianeti (in Flash) aharrisbooks.net/flash/fg2r12/twoPlanets.html - ed ecco la stessa cosa in Python: aharrisbooks.net/pythonGame/ch09/twoPlanets.py
Due pi

2

Ho trovato un eccellente articolo che spiega una possibile soluzione a questo problema con un esempio di codice scritto in javascript. Funziona "spingendo il valore t" nella giusta direzione.

Invece, possiamo usare il fatto che la lunghezza media delle gambe d_avg per qualsiasi distribuzione di punti è quasi identica alle lunghezze delle gambe prodotte da punti equidistanti (questa somiglianza aumenta all'aumentare di n). Se calcoliamo la differenza d_err tra la lunghezza effettiva delle gambe d e la lunghezza media delle gambe d_avg, il parametro temporale t corrispondente a ciascun punto può essere spostato per ridurre questa differenza.

Questa domanda ha già molte risposte interessanti, ma ho trovato degna di nota questa soluzione.


1

Grazie per la tua eccellente pagina che descrive come hai risolto questo problema. Ho fatto qualcosa di leggermente diverso da te in un dettaglio, dato che ero profondamente limitato dalla memoria: non costruisco un array, o devo cercare il "segmento" giusto con una ricerca binaria. Questo perché so sempre che mi sto spostando da un'estremità della mia curva di Bezier a un'altra: pertanto, ricordo semplicemente il segmento "corrente" e se vedo che uscirò dai limiti di quel segmento per calcolare il mio prossimo posizione, calcolo il segmento successivo (o precedente) (in base alla direzione di marcia.) Funziona abbastanza bene per la mia applicazione. L'unico inconveniente che ho dovuto capire era che, in alcune curve, la dimensione dei segmenti era così piccola che il mio prossimo diagramma da puntare era - in rari momenti - più di un segmento davanti a quello attuale, quindi invece di andare semplicemente al '

Non so se questo abbia un senso, ma questo certamente mi ha aiutato.


0

Questo tipo di modellistica è strano e può produrre strani risultati illogici. Soprattutto se la velocità dei pianeti di partenza è molto lenta.

Modella le navi con una potenza di spinta.

Quando le navi sono sulla loro ultima orbita sul pianeta iniziale, accelerare con la massima spinta.

Quando la nave raggiunge una certa distanza, usa la spinta inversa per rallentare la nave fino alla velocità dell'orbita del pianeta bersaglio.

Modifica: esegui l'intera simulazione in una sola volta quando un nodo sta per lasciare l'orbita. inviare tutti i dati o inviare solo pochi vettori di movimento a intervalli e interpolare tra di essi.


Il problema è che questo è tutto basato su tick, non c'è posizione intermedia. È un gioco multiplayer in rete e l'invio di tutte le posizioni di oltre 600 navi in ​​un gioco completo ucciderà tutte le reti. Ci sono solo eventi che trasmettono un tickOffset, il resto viene calcolato in base al tick del mondo corrente e all'offset.
Ivo Wetzel,

Ho modificato la mia risposta.
Attaccando

0

Se lo capisco correttamente, il tuo problema è troppo limitato.

Credo che tu voglia che l'astronave viaggi lungo un percorso specificato tra le orbite in un certo tempo t , e vuoi anche che acceleri dalla velocità s1 alla velocità s2 nello stesso tempo t . Sfortunatamente, non è possibile (in generale) trovare un'accelerazione che soddisfi entrambi questi vincoli contemporaneamente.

Dovrai rilassare un po 'il tuo problema per renderlo risolvibile.


2
Allora come rilassarlo? Quello che potrei immaginare è modificare la T che inserisco nella roba del percorso più bezier. Avrei bisogno di ridimensionarlo in qualche modo per crescere prima più lentamente a 0,5 e poi più velocemente a 1. Quindi la nave decelera dalla sua velocità originale a una fissa al centro della curva e quindi accelera nuovamente da questa velocità alla velocità alla fine della curva?
Ivo Wetzel,

1
Penso che sembrerà più realistico se l'astronave accelera dalla sua velocità originale a qualche punto attorno al punto medio del trasferimento e poi decelera verso la nuova orbita.
Gareth Rees,

Sono ancora bloccato su come collegare l'accelerazione a tutto, ho bisogno di modificare la T in qualche modo: /
Ivo Wetzel

0

Ho trovato questa risposta perché sto cercando di distribuire i punti in modo uniforme lungo un percorso svg che utilizza una curva più bezier.

Nonostante MDN affermi che è obsoleto, è possibile utilizzare il path.getPointAtLengthper ottenere il risultato corretto. https://developer.mozilla.org/en-US/docs/Web/API/SVGPathElement/getPointAtLength

Attualmente funziona in Chrome / Safari / Firefox e dovrebbe funzionare anche in IE / Edge ma non ho verificato quelli 2.


-1

Il problema con la soluzione accettata

Poiché Bezier è una funzione esponenziale , prevediamo tassi di avanzamento diversi in diverse aree della curva.

Poiché la soluzione di Ivo si interpola linearmente tra questi campioni esponenziali iniziali , le imprecisioni saranno pesantemente distorte verso le estremità / metà della curva (tipicamente cubica) dove quei delta sono maggiori; quindi, a meno che la frequenza di campionamento non Nsia aumentata notevolmente come suggerisce, gli errori sono evidenti e ad un certo livello di zoom, sarà sempre evidente per un dato dato N, vale a dire che il bias è intrinseco per quell'algoritmo. Non va bene, ad esempio, per la grafica vettoriale in cui lo zoom può essere illimitato.

Contrastare la distorsione attraverso il campionamento guidato

Una soluzione alternativa è quella di rimappatura linearmente distancea tdopo contrastare la tendenza naturale che la funzione Bezier produce.

Supponendo che questo è ciò che idealmente vogliamo:

curve length = 10

t      distance
0.2    2
0.4    4
0.6    6
0.8    8
1.0    10

ma questo è ciò che otteniamo dalla funzione di posizione di Bezier:

t      distance
0.2    0.12
0.4    1.22
0.6    2.45
0.8    5.81
1.0    10.00

Osservando i Ncampioni prelevati, possiamo vedere dove sono più grandi i delta di distanza e ricampionare ("dividere") a metà strada tra le due distanze adiacenti, aumentando Ndi 1. Ad esempio, dividendo a t=0.9(che è a metà strada nel delta più grande), potremmo ottenere:

0.8    5.81
0.9    7.39
1.0    10.00

Ripetiamo questo processo per il prossimo intervallo di distanza più grande fino a quando il delta massimo tra due distanze qualsiasi nell'intero set è inferiore ad alcuni minDistanceDelta, e più specificamente, meno che epsilonlontano da distanze specifiche che vogliamo mappare a passi di t; possiamo quindi mappare linearmente i nostri tpassi desiderati sui corrispondenti distances. Ciò produce una tabella / hash alla quale è possibile accedere a buon mercato e di cui è possibile scorrere i valori, in fase di esecuzione, senza errori.

Man mano che il set cresce N, il costo per ripetere ciò aumenta, quindi idealmente fatelo come pre-processo. Ogni volta che Naumenta, aggiungi i due nuovi intervalli risultanti a una intervalsraccolta rimuovendo il vecchio singolo intervallo che hanno sostituito. Questa è la struttura su cui lavori per trovare il prossimo intervallo più grande da dividere in due. Mantenere l' intervalsordinamento per distanza semplifica le cose, in quanto puoi semplicemente estrarre l'elemento di lavoro successivo dalla fine e dividerlo ecc.

Finiamo con qualcosa di simile a ciò che volevamo idealmente:

epsilon: 0.01

t            distance
0.200417     2.00417
0.3998132    3.9998132
0.600703     6.00703
0.800001     8.00001
0.9995309    9.995309

Dal momento che stiamo facendo ipotesi ad ogni passo, non otterremo esattamente le distanze esatte 2, 4ecc. Che volevamo, ma ripetendo l'iterazione questi si avvicinano abbastanza ai valori di distanza desiderati in modo da poter mappare i tuoi tpassi con la giusta precisione, eliminando la distorsione dovuta campionamento quasi equidistante.

È quindi possibile recuperare ad esempio t=0.5, come fa Ivo nella sua risposta, ovvero interpolando tra i due valori più vicini sopra ( 3.9998132e 6.00703).

Conclusione

Nella maggior parte dei casi, la soluzione di Ivo funzionerà bene, ma per i casi in cui è necessario evitare la distorsione a tutti i costi, assicurarsi che distancei messaggi siano distribuiti il ​​più uniformemente possibile e quindi mappati in modo lineare t.

Si noti che la suddivisione potrebbe essere eseguita stocasticamente anziché dividere ogni volta il centro, ad esempio potremmo aver diviso il primo intervallo di esempio in t=0.827anziché in t=0.9.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.