Comment interpoler entre deux états de jeu?

24

Quel est le meilleur modèle pour créer un système avec toutes les positions des objets à interpoler entre deux états de mise à jour?

La mise à jour s'exécutera toujours à la même fréquence, mais je veux pouvoir effectuer un rendu à n'importe quel FPS. Ainsi, le rendu sera aussi fluide que possible, peu importe les images par seconde, qu'elles soient inférieures ou supérieures à la fréquence de mise à jour.

Je voudrais mettre à jour 1 image dans l'interpolation future de l'image actuelle vers la future image. Cette réponse a un lien qui parle de faire ceci:

Horodatage semi-fixe ou entièrement fixe?

Edit: Comment pourrais-je également utiliser la dernière vitesse et la vitesse actuelle dans l'interpolation? Par exemple, avec une interpolation uniquement linéaire, il se déplacera à la même vitesse entre les positions. J'ai besoin d'un moyen pour qu'il interpole la position entre les deux points, mais prenne en considération la vitesse à chaque point pour l'interpolation. Il serait utile pour les simulations à faible débit comme les effets de particules.

AttackingHobo
la source
2
les tiques étant des tiques logiques? Donc, votre mise à jour fps <rendu fps?
The Communist Duck
J'ai changé le terme. Mais oui, la logique tourne. Et non, je veux libérer complètement le rendu de la mise à jour, donc le jeu peut rendre à 120HZ ou 22.8HZ et la mise à jour fonctionnera toujours à la même vitesse, à condition que l'utilisateur réponde aux exigences du système.
AttackingHobo
cela peut être vraiment délicat car pendant le rendu, toutes les positions de vos objets doivent rester immobiles (les modifier pendant le processus de rendu peut entraîner un comportement indéterminé)
Ali1S232
L'interpolation calculerait l'état à la fois entre 2 trames de mise à jour déjà calculées. N'est-ce pas une question d'extrapolation, de calcul de l'état pendant un certain temps après la dernière trame de mise à jour? Depuis la prochaine mise à jour n'est même pas encore calculée.
Maik Semder
Je pense que s'il n'a qu'un seul thread de mise à jour / rendu, il ne peut pas arriver de re-mettre à jour juste la position de rendu. Vous envoyez simplement des positions au GPU, puis effectuez une nouvelle mise à jour.
zacharmarz

Réponses:

22

Vous souhaitez séparer les taux de mise à jour (tick logique) et de dessin (tick de rendu).

Vos mises à jour produiront la position de tous les objets dans le monde à dessiner.

Je couvrirai ici deux possibilités différentes, celle que vous avez demandée, l'extrapolation, et aussi une autre méthode, l'interpolation.

1.

L'extrapolation est l'endroit où nous calculerons la position (prédite) de l'objet à l'image suivante, puis interpolerons entre la position actuelle des objets et la position que l'objet sera à l'image suivante.

Pour ce faire, chaque objet à dessiner doit avoir un velocityet associé position. Pour trouver la position que l'objet sera à l'image suivante, nous ajoutons simplement velocity * draw_timestepà la position actuelle de l'objet, pour trouver la position prédite de l'image suivante. draw_timestepest le temps qui s'est écoulé depuis le tick de rendu précédent (alias l'appel de dessin précédent).

Si vous vous en tenez à cela, vous constaterez que les objets "scintillent" lorsque leur position prédite ne correspond pas à la position réelle à l'image suivante. Pour supprimer le scintillement, vous pouvez stocker la position prédite et lerp entre la position prédite précédemment et la nouvelle position prédite à chaque étape de dessin, en utilisant le temps écoulé depuis la dernière mise à jour cocher comme facteur lerp. Cela se traduira toujours par un mauvais comportement lorsque des objets en mouvement rapide changent soudainement d'emplacement, et vous voudrez peut-être gérer ce cas spécial. Tout ce qui est dit dans ce paragraphe est la raison pour laquelle vous ne voulez pas utiliser d'extrapolation.

2.

L'interpolation est l'endroit où nous stockons l'état des deux dernières mises à jour et nous les interpolons en fonction du temps actuel écoulé depuis la dernière mise à jour. Dans cette configuration, chaque objet doit avoir un positionet associé previous_position. Dans ce cas, notre dessin représentera au pire un tick de mise à jour derrière le gamestate actuel, et au mieux, exactement au même état que le tick de mise à jour actuel.


À mon avis, vous voulez probablement une interpolation telle que je l'ai décrite, car c'est la plus facile des deux à implémenter, et dessiner une infime fraction de seconde (par exemple 1/60 seconde) derrière votre état actuel mis à jour est très bien.


Modifier:

Dans le cas où ce qui précède ne suffit pas pour vous permettre d'effectuer une implémentation, voici un exemple de la façon de faire la méthode d'interpolation que j'ai décrite. Je ne couvrirai pas l'extrapolation, car je ne pense à aucun scénario du monde réel dans lequel vous devriez le préférer.

Lorsque vous créez un objet dessinable, il stockera les propriétés nécessaires à dessiner (c'est-à-dire les informations d' état nécessaires pour le dessiner).

Pour cet exemple, nous allons stocker la position et la rotation. Vous pouvez également vouloir stocker d'autres propriétés comme la position des coordonnées de couleur ou de texture (c'est-à-dire si une texture défile).

Pour éviter que les données ne soient modifiées pendant que le thread de rendu les dessine (c'est-à-dire que l'emplacement d'un objet est modifié pendant que le thread de rendu dessine, mais tous les autres n'ont pas encore été mis à jour), nous devons implémenter un certain type de double tampon.

Un objet en stocke deux copies previous_state. Je vais les mettre dans un tableau et les désigner comme previous_state[0]et previous_state[1]. De même, il en a besoin de deux copies current_state.

Pour garder une trace de la copie du double tampon utilisée, nous stockons une variable state_index, qui est disponible à la fois pour le fil de mise à jour et de dessin.

Le thread de mise à jour calcule d'abord toutes les propriétés d'un objet en utilisant ses propres données (toutes les structures de données que vous souhaitez). Ensuite, il copie current_state[state_index]à previous_state[state_index], et copie les nouvelles données pertinentes pour le dessin, positionet rotationen current_state[state_index]. Ensuite, il state_index = 1 - state_indexretourne la copie actuellement utilisée du double tampon.

Tout dans le paragraphe ci-dessus doit être fait avec un verrou retiré current_state. Les fils de mise à jour et de dessin retirent tous les deux ce verrou. Le verrou n'est retiré que pendant la durée de la copie des informations d'état, ce qui est rapide.

Dans le fil de rendu, vous effectuez ensuite une interpolation linéaire sur la position et la rotation comme suit:

current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)

elapsedest le temps qui s'est écoulé dans le fil de rendu, depuis la dernière mise à jour, et update_tick_lengthle temps que met votre taux de mise à jour fixe par tick (par exemple, lors des mises à jour 20FPS, update_tick_length = 0.05).

Si vous ne savez pas quelle est la Lerpfonction ci-dessus, consultez l'article de wikipedia sur le sujet: Interpolation linéaire . Cependant, si vous ne savez pas ce qu'est le lerping, vous n'êtes probablement pas prêt à implémenter la mise à jour / dessin découplé avec le dessin interpolé.

Olhovsky
la source
1
+1 la même chose doit être faite pour les orientations / rotations et tous les autres états qui changent au fil du temps, c'est-à-dire comme les animations de matériaux dans les systèmes de particules, etc.
Maik Semder
1
Bon point Maik, j'ai juste utilisé position comme exemple. Vous devez stocker la "vitesse" de toute propriété que vous souhaitez extrapoler (c'est-à-dire le taux de changement dans le temps de cette propriété), si vous souhaitez utiliser l'extrapolation. En fin de compte, je ne peux vraiment pas penser à une situation où l'extrapolation est meilleure que l'interpolation, je ne l'ai incluse que parce que la question du demandeur l'a demandé. J'utilise l'interpolation. Avec l'interpolation, nous devons stocker les résultats de mise à jour actuels et précédents de toutes les propriétés à interpoler, comme vous l'avez dit.
Olhovsky
Il s'agit d'une reformulation du problème et de la différence entre interpolation et extrapolation; ce n'est pas une réponse.
1
Dans mon exemple, j'ai enregistré la position et la rotation dans l'état. Vous pouvez également stocker la vitesse (ou la vitesse) dans l'état. Ensuite, vous lerp entre la vitesse de la même manière exacte ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Vous pouvez le faire avec n'importe quel numéro que vous souhaitez stocker dans l'état. Lerping vous donne juste une valeur entre deux valeurs, étant donné un facteur lerp.
Olhovsky
1
Pour l'interpolation du mouvement angulaire, il est recommandé d'utiliser slerp au lieu de lerp. Le plus simple serait de stocker les quaternions des deux états et de les séparer. Sinon, les mêmes règles s'appliquent pour la vitesse angulaire et l'accélération angulaire. Avez-vous un cas de test pour l'animation squelettique?
Maik Semder
-2

Ce problème vous oblige à réfléchir un peu différemment à vos définitions de début et de fin. Les programmeurs débutants pensent souvent au changement de position par image et c'est une bonne façon de procéder au début. Pour le bien de ma réponse, considérons une réponse unidimensionnelle.

Disons que vous avez un singe en position x. Maintenant, vous avez également un "addX" auquel vous ajoutez à la position du singe par image en fonction du clavier ou d'un autre contrôle. Cela fonctionnera tant que vous aurez une fréquence d'images garantie. Disons que votre x est 100 et votre addX est 10. Après 10 images, votre x + = addX devrait s'accumuler jusqu'à 200.

Maintenant, au lieu d'addX, lorsque vous avez une fréquence d'images variable, vous devriez penser en termes de vitesse et d'accélération. Je vais vous guider à travers toute cette arithmétique mais c'est super simple. Ce que nous voulons savoir, c'est la distance que vous souhaitez parcourir par milliseconde (1 / 1000e de seconde)

Si vous photographiez à 30 FPS, votre velX devrait être au 1/3 de seconde (10 images du dernier exemple à 30 FPS) et vous savez que vous voulez parcourir 100 'x' pendant ce temps, alors réglez votre velX sur 100 distance / 10 FPS ou 10 distance par image. En millisecondes, cela équivaut à 1 distance x par 3,3 millisecondes ou 0,3 'x' par milliseconde.

Maintenant, à chaque mise à jour, il vous suffit de déterminer le temps écoulé. Que 33 ms se soient écoulées (1 / 30ème de seconde) ou autre, vous multipliez simplement la distance 0,3 par le nombre de millisecondes passées. Cela signifie que vous avez besoin d'un temporisateur qui vous donne une précision en ms (millisecondes), mais la plupart des temporisateurs vous en donnent. Faites simplement quelque chose comme ceci:

var beginTime = getTimeInMillisecond ()

... plus tard ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = heure

... utilisez maintenant ce temps écoulé pour calculer toutes vos distances.

Mickey
la source
1
Il n'a pas de taux de mise à jour variable. Il a un taux de mise à jour fixe. Pour être honnête, je ne sais vraiment pas quel point vous essayez de faire ici: /
Olhovsky
1
??? -1. C'est tout le point, j'ai un taux de mise à jour garanti, mais un taux de rendu variable, et je veux qu'il soit fluide sans bégaiement.
AttackingHobo
Les taux de mise à jour variables ne fonctionnent pas bien avec les jeux en réseau, les jeux compétitifs, les systèmes de rejeu ou tout autre élément qui repose sur le déterminisme du jeu.
AttackingHobo
1
La mise à jour fixe permet également une intégration facile de la pseudo-friction. Par exemple, si vous souhaitez multiplier votre vitesse par 0,9 chaque image, comment déterminez-vous combien multiplier par si vous avez une image rapide ou lente? La mise à jour fixe est parfois grandement préférée - pratiquement toutes les simulations physiques utilisent un taux de mise à jour fixe.
Olhovsky
2
Si j'utilise une fréquence d'images variable et que je configure un état initial complexe avec beaucoup d'objets rebondissant les uns sur les autres, rien ne garantit qu'il simulera exactement la même chose. En fait, il sera très probablement simulé légèrement différemment à chaque fois, avec de petites différences au début, se composant sur une courte période dans des états complètement différents entre chaque exécution de simulation.
AttackingHobo