Incrémentation Vs. fonction

Julbert4567 - Modifié le 1 janv. 2024 à 22:00
mamiemando Messages postés 33333 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 31 octobre 2024 - 2 janv. 2024 à 18:16

Bonjour,

Je souhaite réaliser une fonction qui reproduit un déplacement basé sur la fonction t^2 + 2*t. Cela étant dans un moteur de jeu, il est possible d'utiliser le delta t et voici la première méthode:

var quad_speed = 2
var acceleration = 2
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	position.x += quad_speed*delta
	quad_speed += acceleration*delta

Au lieu d'incrémenter, une autre façon serait de directement l'écrire:

var quad_t = 0
func quad(t):
	return t**2 + 2*t
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	position.x = quad(quad_t)
	quad_t += delta

Quels sont les avantages d'une technique par rapport à l'autre? La deuxième semble mieux s'adapter à des fonctions plus complexes mais à part ça, je suis un peu perdu.

2 réponses

yg_be Messages postés 23309 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 2 novembre 2024 Ambassadeur 1 550
2 janv. 2024 à 07:27

bonjour,

La première méthode fait moins de calculs, et donne un résultat moins précis.

0
mamiemando Messages postés 33333 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 31 octobre 2024 7 801
Modifié le 2 janv. 2024 à 18:34

Bonjour,

Réponse courte : l'approche 2 est de loin meilleure (mais améliorable)

Performance

  • Du point de vue de la complexité algorithmique, le coût est le même : O(1).
  • Du point de vue de l'implémentation, il y a quelques nuances
    • La première approche coûte 2 additions et 2 multiplications,
    • La seconde coûte 2 multiplications et une addition et un appel de fonction. On pourrait éviter (à moins que quad serve ailleurs) se passer de cette fonction pour économiser un appel de fonction.
  • En terme de performance pure, la différence est donc négligeable.

Précision

  • La première approche peut propager des incertitudes. En particulier si delta est très petit, tu vas avoir des erreurs d'arrondis. C'est le même problème que si tu avais codé quad avec une fonction récursive. Au bout d'un grand nombre d'exécutions, tu peux, comme l'indique yg_be, les incertitudes deviennent non négligeables.
  • La seconde approche est une forme close. Par construction, on ne propage donc pas d'erreur liées aux incertitudes.

Utilisation

  • La première méthode permet de définir une vitesse et une accélération initiale.
  • Dans la seconde méthode, ça n'est pas possible, tout est encodé dans les coefficients de ton polynôme. On pourrait cependant imaginer les passer en paramètres.

Lisibilité

  • La seconde version est clairement plus lisible.

Je pense que quelle que soit la méthode adoptée, tu devrais revoir la signature et le nommage de tes variables, de sorte à ce qu'elle représente des grandeurs physiques.

  • x : position, x0 : position intiale ;
  • v : vitesse, v0 : vitesse initiale ;
  • a : accélération (constante ?) ;

Toutes ces grandeurs peuvent être vues comme des scalaire (si on travaille uniquement le long de l'axe vertical du référentiel géocentrique) ou plus généralement comme des vecteurs.

Par exemple, à accélération constante, la position, la vitesse et l'accélération sont ainsi liées : x(t) = (1/2)  * a * t^2 + v0 * t + x0. Il serait donc logique que ces grandeurs soient visible dans ton code.

Explication de l'équation :

  • Bilan des forces : considérons un système soumis à son poids P = m.g sans frottement (chute libre sans frottement)
  • Loi de Newton : m.a(t) = m.g
  • En simplifiant, on en déduit l'accélération : a(t) = g
  • En intégrant, on en déduit la vitesse : v(t) = g * t + v0 = a * t + v0
  • En intégrant, on en déduit la position : x(t) = (1/2)  * a * t^2 + v0 * t + x0

Ce qui est gênant dans ton code, c'est que tu manipules des grandeurs (dont certaines au carrées) et scalaire (dont la nature n'est pas précisée), ce qui ne permet pas toujours de faire une analyse dimensionnelle, c'est-à-dire de s'assurer que les calculs effectuées manipulent des grandeurs compatibles entre elles en terme d'unité .

Lien entre le modèle physique, la forme close et la version récursive

En particulier, l'équation que tu donnes semble suggérer que dans ton cas :

  • a = g = 2 (on n'est donc pas sur Terre, plutôt sur la Lune :p)
  • v0 = 2
  • x0 = 0

En effet, par substitution, on obtient bien x(t) = t^2 + 2 * t

Cela veut dire que sous sa forme récursive :

  • x(t) = x(t - delta_t) + delta_t * v(t)
  • v(t) = v(t - delta_t) + delta_t * a
  • a(t) = g

Ces équations impose que delta_t soit très petit (on parle en mathématiques de développement limité à l'ordre 1).

  • L'intuition est que si delta_t est infinitésimal, une parabole se comporte au voisinage du point x = t comme sa tangente en x.
  • Comme notre cas, comme la tangente est en dessous, sous la parabole, on va donc sous-estimer la position.
  • Mais comme on va utiliser cette valeur sous-estimée pour calculer les prochaines valeur de x, on va sous-estimer une valeur... déjà sous-estimée !
  • Pire, plus t est important, plus la parabole croit rapidement. Et donc plus il faudrait considérer un delta_t petit pour avoir une bonne approximation. Cela veut dire que plus t est important, plus on va sous-estimer à delta_t constant !

Pour s'en convaincre, voici un petit programme en python qui va confronter le l'approche 2 (la forme close) et l'approche 1 (la version récursive). Sans rentrer dans les détails de la syntaxe, voici l'intuition de ce que fait ce programme :

  • On simule 10000 pas de temps (num_epochs) séparés d'un intervalle delta_t = 0.001.
  • On calcule pour chaque pas de temps la vitesse actuelle (à l'aide des équations vues auparavant) et on stocke le tout dans une liste v.
  • On calcule pour chaque pas de temps la vitesse actuelle (à l'aide des équations vues auparavant) et on stocke le tout dans une liste x en s'appuyant sur les valeurs actuellement calculées dans v.
  • On affiche la dernière position calculée (obtenue après num_epochs itérations)
  • Puis on fait le calcul avec l'approche 2 (forme close) et on compare les résultats.
a = 2
v0 = 2
x0 = 0
delta_t = 0.001
num_epochs = 10000

# Approche 1
v = list()
for i in range(0, num_epochs + 1):
    v.append(
        v0 if i == 0
        else v[i - 1] + delta_t * a
    )

x = list()
for i in range(0, num_epochs + 1):
    x.append(
        x0 if i == 0
        else x[i - 1] + delta_t * v[i - 1]
    )
print("Approche 1:", x[num_epochs])

# Approche 2
def position(epoch):
    t = epoch * delta_t
    x = 0.5 * a * (t ** 2) + v0 * t + x0
    return x

print("Approche 2:", position(num_epochs))
Approche 1: 119.99000000000277
Approche 2: 120.0

On voit que le résultat n'est pas le même et que manifestement, l'approche 1 est approximative et dérive de plus en plus rapidement.

  • Après seulement 10000 itérations (soit 10000 * 0.001 = 10s de temps), on a déjà dérivé de ~0.01m soit 1cm !
  • Et ça ne va pas aller en s'arrangeant, car si on passe num_epochs à 1000000 (soit une simulation de 1000s), l'écart est de près d'1m donc la dérive est de plus en plus forte.

Bonne chance

0