Ottimizzazione dei calcoli di gravità


23

Ho un sacco di oggetti di varie dimensioni e velocità che gravitano l'uno verso l'altro. Ad ogni aggiornamento, devo esaminare ogni oggetto e sommare le forze dovute alla gravità di ogni altro oggetto. Non si adatta molto bene, è uno dei due grandi colli di bottiglia che ho trovato nel mio gioco e non sono sicuro di cosa fare per migliorare le prestazioni.

Si sente come dovrei essere in grado di migliorare le prestazioni. In qualsiasi momento, probabilmente il 99% degli oggetti nel sistema avrà solo un'influenza trascurabile su un oggetto. Ovviamente non posso ordinare gli oggetti in base alla massa e prendere in considerazione solo i primi 10 oggetti più grandi o qualcosa del genere, perché la forza varia con la distanza più che con la massa (l'equazione è lungo le linee di force = mass1 * mass2 / distance^2). Penso che una buona approssimazione sarebbe quella di considerare gli oggetti più grandi e gli oggetti più vicini, ignorando le centinaia di minuscoli frammenti di roccia dall'altra parte del mondo che non possono influenzare nulla, ma per scoprire quali oggetti sono più vicino devo iterare su tutti gli oggetti e le loro posizioni cambiano costantemente, quindi non è che posso farlo una volta sola.

Attualmente sto facendo qualcosa del genere:

private void UpdateBodies(List<GravitatingObject> bodies, GameTime gameTime)
{
    for (int i = 0; i < bodies.Count; i++)
    {
        bodies[i].Update(i);
    }
}

//...

public virtual void Update(int systemIndex)
{
    for (int i = systemIndex + 1; i < system.MassiveBodies.Count; i++)
    {
        GravitatingObject body = system.MassiveBodies[i];

        Vector2 force = Gravity.ForceUnderGravity(body, this);
        ForceOfGravity += force;
        body.ForceOfGravity += -force;
    }

    Vector2 acceleration = Motion.Acceleration(ForceOfGravity, Mass);
    ForceOfGravity = Vector2.Zero;

    Velocity += Motion.Velocity(acceleration, elapsedTime);
    Position += Motion.Position(Velocity, elapsedTime);
}

(si noti che ho rimosso molto codice, ad esempio i test di collisione, non riesco a scorrere gli oggetti una seconda volta per rilevare le collisioni).

Quindi non sto sempre ripetendo l'intera lista - lo faccio solo per il primo oggetto, e ogni volta che l'oggetto trova la forza che prova verso un altro oggetto, quell'altro oggetto sente la stessa forza, quindi aggiorna entrambi loro - e quindi quel primo oggetto non deve essere considerato di nuovo per il resto dell'aggiornamento.

Le funzioni Gravity.ForceUnderGravity(...)e Motion.Velocity(...), ecc. Usano solo un po 'di matematica vettoriale integrata di XNA.

Quando due oggetti si scontrano, creano detriti senza massa. È tenuto in un elenco separato e gli oggetti massicci non iterano sui detriti come parte del loro calcolo della velocità, ma ogni pezzo di detriti deve iterare sulle particelle massicce.

Questo non deve scalare a limiti incredibili. Il mondo non è illimitato, contiene un bordo che distrugge gli oggetti che lo attraversano - mi piacerebbe essere in grado di gestire forse un migliaio di oggetti, attualmente il gioco inizia a soffocare intorno ai 200.

Qualche idea su come potrei migliorare questo? Qualche euristica che posso usare per radere la lunghezza del loop da centinaia a pochi? Qualche codice che posso eseguire meno spesso di ogni aggiornamento? Devo solo multithread fino a quando non è abbastanza veloce da consentire un mondo di dimensioni decenti? Dovrei provare a scaricare i calcoli di velocità sulla GPU? Se è così, come lo progetterei? Posso conservare dati statici e condivisi sulla GPU? Posso creare funzioni HLSL sulla GPU e chiamarle arbitrariamente (usando XNA) o devono far parte del processo di disegno?


1
Solo una nota, hai detto "gli oggetti massicci non iterano sui detriti come parte del loro calcolo della velocità, ma ogni pezzo di detriti deve iterare sulle particelle massicce". Ne ho avuto l'impressione che tu stia assumendo che sia più efficiente. Tuttavia, iterando 100 oggetti di detriti 10 volte ciascuno, è sempre lo stesso di iterare 10 oggetti enormi 100 volte. Forse, sarebbe una buona idea iterare ogni oggetto di detriti nel loop di oggetti enormi in modo da non farlo una seconda volta.
Richard Marskell - Drackir,

Quanto è necessaria una simulazione accurata? Hai davvero bisogno che tutto acceleri l'uno verso l'altro? E hai davvero bisogno di usare un vero calcolo di gravitazione? O puoi discostarti da quelle convenzioni per quello che stai cercando di realizzare?
caos Tecnico

@Drackir Penso che tu abbia ragione. Parte del motivo per cui sono separati è perché la matematica è diversa per gli oggetti senza massa, e in parte perché in origine non obbedivano affatto alla gravità, quindi era più efficiente non includerli. Quindi è rudimentale
Carson Myers il

@chaosTechnician non deve essere molto preciso - infatti se si tratta solo delle poche forze più dominanti, un sistema sarebbe più stabile, il che è l'ideale. Ma sta scoprendo quali delle forze sono più dominanti in un modo efficiente con cui ho problemi. Anche il calcolo della gravitazione è già approssimato, è solo G * m1 * m2 / r^2dove G è solo per modificare il comportamento. (anche se non posso semplicemente farli seguire un percorso, perché l'utente può disturbare il sistema)
Carson Myers,

Perché ogni frammento di detriti deve scorrere sulle particelle massicce se è privo di massa? Collisioni?
Sam Hocevar,

Risposte:


26

Sembra un lavoro per una griglia. Dividi il tuo spazio di gioco in una griglia e per ogni cella della griglia conserva un elenco degli oggetti attualmente presenti. Quando gli oggetti si spostano attraverso un limite di cella, aggiorna l'elenco in cui si trovano. Quando aggiorni un oggetto e cerchi altri con cui interagire, puoi guardare solo la cella della griglia corrente e alcune adiacenti. È possibile modificare le dimensioni della griglia per le migliori prestazioni (bilanciando il costo dell'aggiornamento delle celle della griglia - che è maggiore quando le celle della griglia sono troppo piccole - con il costo di fare le ricerche, che è maggiore quando le celle della griglia sono troppo grande).

Ciò, naturalmente, farà sì che gli oggetti più distanti rispetto a un paio di celle della griglia non interagiscano affatto, il che è probabilmente un problema perché un grande accumulo di massa (o un grande oggetto o un gruppo di molti piccoli oggetti) dovrebbe , come hai detto, hanno una più ampia regione di influenza.

Una cosa che potresti fare è tenere traccia della massa totale all'interno di ciascuna cella della griglia e trattare l'intera cella come un singolo oggetto ai fini delle interazioni più lontane. Cioè: quando calcoli la forza su un oggetto, calcola l'accelerazione diretta da oggetto a oggetto per gli oggetti in alcune celle della griglia vicine, quindi aggiungi un'accelerazione da cella a cella per ciascuna cella della griglia più lontana (o forse solo quelli con una quantità non trascurabile di massa). Per accelerazione da cella a cellula, intendo un vettore calcolato utilizzando le masse totali delle due celle e la distanza tra i loro centri. Ciò dovrebbe fornire una ragionevole approssimazione della gravità sommata da tutti gli oggetti in quella cella della griglia, ma molto più a buon mercato.

Se il mondo di gioco è molto grande, potresti persino usare una griglia gerarchica , come un quadrifoglio (2D) o un ottetto (3D), e applicare principi simili. Le interazioni a distanza maggiore corrisponderebbero a livelli più alti della gerarchia.


1
+1 per l'idea della griglia. Suggerirei anche di tracciare il centro di massa per la griglia e di mantenere i calcoli un po 'più puri (se necessario).
caos Tecnico

Mi piace molto questa idea. Avevo preso in considerazione la possibilità di contenere gli oggetti nelle celle, ma l'ho abbandonato considerando due oggetti vicini che erano tecnicamente in celle diverse, ma non ho fatto il salto mentale per considerare alcune celle adiacenti, oltre a considerare la massa combinata di altre le cellule. Penso che dovrebbe funzionare davvero bene se lo faccio bene.
Carson Myers,

8
Questo è essenzialmente l'algoritmo Barnes-Hut: en.wikipedia.org/wiki/Barnes –Hut_simulation
Russell Borogove

Sembra persino simile a come dovrebbe funzionare la gravità in fisica - la flessione dello spazio-tempo.
Zan Lynx,

Mi piace l'idea - ma sinceramente penso che abbia bisogno di un po 'più di raffinatezza - cosa succede se due oggetti sono molto vicini tra loro ma in celle separate? Ho potuto vedere come il tuo terzo paragrafo potrebbe essere d'aiuto - ma perché non fare semplicemente un abbattimento circolare sugli oggetti interagenti?
Jonathan Dickinson,

16

L'algoritmo Barnes-Hut è la strada da percorrere con questo. È stato utilizzato nelle simulazioni di supercomputer per risolvere il problema esatto. Non è troppo difficile da codificare ed è molto efficiente. In realtà ho scritto un'applet Java non molto tempo fa per risolvere questo problema.

Visita http://mathandcode.com/programs/javagrav/ e premi "start" e "show quadtree".

Nella scheda Opzioni, puoi vedere che il conteggio delle particelle può arrivare fino a 200.000. Sul mio computer, il calcolo termina in circa 2 secondi (il disegno di 200.000 punti richiede circa 1 secondo, ma il calcolo viene eseguito su un thread separato).

Ecco come funziona la mia applet:

  1. Crea un elenco di particelle casuali, con masse, posizioni e velocità iniziali casuali.
  2. Costruisci un quadrifoglio da queste particelle. Ogni nodo quadrifoglio contiene il centro di massa di ciascun nodo secondario. Fondamentalmente, per ogni nodo hai tre valori: massx, massy e mass. Ogni volta che aggiungi una particella a un dato nodo, aumenti massx e massy rispettivamente di particle.x * particle.mass e particle.y * particle.mass. La posizione (massx / mass, massy / mass) finirà come centro di massa del nodo.
  3. Per ogni particella, calcola le forze (completamente descritte qui ). Questo viene fatto iniziando dal nodo superiore e ricorrendo attraverso ciascun nodo secondario del quadrifoglio fino a quando il nodo secondario dato non è abbastanza piccolo. Una volta che smetti di ricorrere, puoi calcolare la distanza dalla particella al centro di massa del nodo, quindi puoi calcolare la forza usando la massa del nodo e la massa della particella.

Il tuo gioco dovrebbe essere in grado di gestire facilmente mille oggetti che si attraggono a vicenda. Se ogni oggetto è "stupido" (come le particelle di ossa nude nel mio applet), dovresti essere in grado di ottenere da 8000 a 9000 particelle, forse di più. E questo presuppone il single threading. Con le applicazioni di elaborazione multi-thread o parallele, puoi ottenere molte più particelle di quelle che si aggiornano in tempo reale.

Vedi anche: http://www.youtube.com/watch?v=XAlzniN6L94 per un ampio rendering di questo


Il primo link è morto. L'applet è ospitata altrove?
Anko,

1
Fisso! scusa, ho dimenticato di pagare l'affitto su quel dominio e qualcuno lo ha acquistato automaticamente: \ Inoltre, 3 minuti è un tempo di risposta piuttosto buono su un post 8D di 1,3 anni

E dovrei aggiungere: non ho il codice sorgente. Se stai cercando del codice sorgente, controlla part-nd (scritto in c). Sono sicuro che ci sono anche altri là fuori.

3

Nathan Reed ha un'ottima risposta. La sua versione breve prevede l'uso di una tecnica a fase larga adatta alla topologia della simulazione e l'esecuzione dei calcoli di gravità solo su coppie di oggetti che avranno un effetto evidente l'uno sull'altro. In realtà non è diverso da quello che faresti per la normale fase di rilevamento delle collisioni.

Proseguendo da ciò, tuttavia, un'altra possibilità è quella di aggiornare gli oggetti solo in modo intermittente. Fondamentalmente, ogni passaggio temporale (frame) aggiorna solo una frazione di tutti gli oggetti e lascia la velocità (o l'accelerazione, a seconda delle preferenze) uguale per gli altri oggetti. È improbabile che l'utente noti alcun ritardo negli aggiornamenti da questo a condizione che gli intervalli non siano troppo lunghi. Questo ti darà una velocità lineare dell'algoritmo, quindi dai un'occhiata alle tecniche a larga fase suggerite da Nathan, che possono dare accelerazioni molto più significative se hai una tonnellata di oggetti. Anche se non è affatto modellato allo stesso modo, è un po 'come avere "onde di gravità". :)

Inoltre, è possibile generare un campo di gravità in un passaggio, quindi aggiornare gli oggetti in un secondo passaggio. Il primo passaggio consiste sostanzialmente nel riempire una griglia (o una struttura di dati spaziali più complessa) con le influenze di gravità di ciascun oggetto. Il risultato ora è un campo gravitazionale che puoi persino renderizzare (sembra piuttosto bello) per vedere quale accelerazione verrà applicata a un oggetto in una determinata posizione. Quindi si esegue l'iterazione sugli oggetti e si applicano semplicemente gli effetti del campo di gravità a quell'oggetto. Ancora più interessante, puoi farlo su una GPU eseguendo il rendering degli oggetti come cerchi / sfere su una trama, quindi leggendo la trama (o usando un altro passaggio di feedback di trasformazione sulla GPU) per modificare le velocità dell'oggetto.


Separare il processo in passaggi è un'ottima idea, poiché l'intervallo di aggiornamento è (per quanto ne so) una frazione molto piccola di secondo. La trama del campo di gravità è FANTASTICA ma forse un po 'oltre la mia portata in questo momento.
Carson Myers,

1
Non dimenticare di moltiplicare le forze applicate per quante fasce orarie sono state saltate dall'ultimo aggiornamento.
Zan Lynx,

@seanmiddlemitch: potresti approfondire un po 'di più la trama del campo di gravità? Mi dispiace, non sono un programmatore di grafica, ma sembra davvero interessante; Non capisco come dovrebbe funzionare. E / o forse hai un link a una descrizione della tecnica?
Felix Dombek,

@FelixDombek: Rendi i tuoi oggetti come cerchi che rappresentano l'area di influenza. Frammento shader scrive un vettore che punta al centro dell'oggetto e con la grandezza appropriata (in base alla distanza dal centro e dalla massa dell'oggetto). La fusione hardware può gestire la somma di questi vettori in modalità additiva. Il risultato non saranno campi di gravità precisi, ma quasi sicuramente saranno abbastanza buoni per le esigenze di un gioco. Come ancora un altro approccio che utilizza la GPU, vedi questa tecnica di simulazione della gravità del corpo N basata su CUDA: http.developer.nvidia.com/GPUGems3/gpugems3_ch31.html
Sean Middleditch

3

Consiglierei di usare un Quad Tree. Ti consentono di cercare in modo rapido ed efficiente tutti gli oggetti in un'area rettangolare arbitraria. Ecco l'articolo wiki su di loro: http://en.wikipedia.org/wiki/Quadtree

E un link spudorato al mio progetto XNA Quad Tree su SourceForge: http://sourceforge.net/projects/quadtree/

Vorrei anche mantenere un elenco di tutti gli oggetti di grandi dimensioni in modo che possano interagire con tutto indipendentemente dalla distanza.


0

Solo un piccolo input (possibilmente ingenuo). Non faccio programmi di gioco, ma quello che sento è che il tuo collo di bottiglia fondamentale è il calcolo gravità-a-gravità. Invece di iterare su ogni oggetto X e quindi trovare l'effetto gravitazionale da ciascun oggetto Y e aggiungerlo, puoi prendere ogni coppia X, Y e trovare la forza tra di loro. Ciò dovrebbe ridurre il numero di calcoli di gravità da O (n ^ 2). Quindi eseguirai molte aggiunte (O (n ^ 2)), ma di solito è meno costoso.

Inoltre a questo punto è possibile implementare regole come "se la forza gravitazionale sarà inferiore a \ epsilon perché questi corpi sono troppo piccoli, impostare la forza su zero". Può essere vantaggioso disporre di questa struttura anche per altri scopi (incluso il rilevamento delle collisioni).


Questo è fondamentalmente quello che sto facendo. Dopo che ottengo tutte le coppie che coinvolgono X, non eseguo più iterazioni su X. Trovo la forza tra X e Y, X e Z, ecc., E applico quella forza su entrambi gli oggetti nella coppia. Una volta completato il ciclo, il ForceOfGravityvettore è la somma di tutte le forze, che viene quindi convertita in velocità e nuova posizione. Non sono sicuro che il calcolo della gravità sia particolarmente costoso e controllare se supera prima una soglia non risparmierebbe un notevole lasso di tempo, non credo
Carson Myers il

0

Estendendo la risposta di seanmiddleditch, ho pensato di poter far luce (ironia?) Sull'idea del campo di gravità.

In primo luogo, non pensarlo come una trama, ma un campo discreto di valori che può essere modificato (una matrice bidimensionale, per così dire); e la successiva precisione della simulazione potrebbe essere la risoluzione di quel campo.

Quando si introduce un oggetto nel campo, il suo potenziale di gravitazione può essere calcolato per tutti i valori circostanti; creando così un pozzo di gravità nel campo.

Ma quanti di questi punti dovresti calcolare prima che diventi più o meno efficace di prima? Probabilmente non molti, anche 32x32 è un campo sostanziale da iterare per ogni oggetto. Pertanto suddividere l'intero processo in più passaggi; ciascuno con risoluzioni (o accuratezza) variabili.

Vale a dire, il primo passaggio può calcolare la gravità degli oggetti rappresentata in una griglia 4x4, con ogni valore di cella che rappresenta una coordinata 2D nello spazio. Dare una complessità sub-totale O (n * 4 * 4).

Il secondo passaggio può essere più preciso, con un campo di gravità con risoluzione 64x64, con ogni valore di cella che rappresenta una coordinata 2D nello spazio. Tuttavia, poiché la complessità è molto elevata, è possibile limitare il raggio delle cellule circostanti interessate (forse, vengono aggiornate solo le celle 5x5 circostanti).

Un terzo passaggio aggiuntivo potrebbe essere utilizzato per calcoli ad alta precisione, con forse una risoluzione di 1024x1024. Ricordando in nessun momento si eseguono calcoli separati 1024x1024, ma si opera solo su porzioni di questo campo (forse 6x6 sottosezioni).

In questo modo, la complessità complessiva per l'aggiornamento è O (n * (4 * 4 + 5 * 5 + 6 * 6)).

Per calcolare quindi le variazioni di velocità su ciascuno dei tuoi oggetti, per ogni campo di gravità (4x4, 64x64, 1024x1024) basta mappare la posizione delle masse punti su una cella della griglia, applicare quel vettore potenziale potenziale gravitazionale delle celle della griglia su un nuovo vettore; ripetere per ogni "livello" o "passaggio"; quindi aggiungerli insieme. Questo dovrebbe darti un buon vettore di forza gravitazionale risultante.

Pertanto, la complessità complessiva è: O (n * (4 * 4 + 5 * 5 + 6 * 6) + n). Ciò che conta davvero (per complessità) è quante celle circostanti aggiorni quando calcoli il potenziale gravitazionale nei passaggi, non la risoluzione complessiva dei campi di gravità.

Il motivo dei campi a bassa risoluzione (primi passi) è ovviamente quello di abbracciare l'universo nel suo insieme e garantire che le masse periferiche siano attratte da aree più dense nonostante la distanza. Quindi utilizzare i campi a risoluzione più elevata come livelli separati per aumentare la precisione per i pianeti vicini.

Spero che abbia senso.


0

Che ne dici di un altro approccio:

Assegna un'area di influenza agli oggetti in base alla loro massa: sono semplicemente troppo piccoli per avere un effetto misurabile oltre quell'intervallo.

Ora dividi il tuo mondo in una griglia e metti ciascun oggetto in un elenco di tutte le celle su cui ha influenza.

Esegui i tuoi calcoli di gravità solo sugli oggetti nell'elenco allegato alla cella in cui si trova un oggetto.

Devi aggiornare gli elenchi solo quando un oggetto si sposta in una nuova cella della griglia.

Più piccole sono le celle della griglia, minore sarà il calcolo che eseguirai per aggiornamento, ma maggiore sarà il lavoro che eseguirai negli elenchi.

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.