La dynamique des corps mous consiste à simuler des objets déformables réalistes. Nous l'utilisons ici pour simuler un rideau en tissu déchirable et un ensemble de tablettes avec lesquelles vous pouvez interagir et vous déplacer autour de l'écran. Ça va être rapide, stable et assez simple à faire avec les mathématiques de niveau secondaire.
Remarque: Bien que ce tutoriel soit écrit en traitement et compilé avec Java, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..
Dans cette démo, vous pouvez voir un grand rideau (montrant la simulation de la structure) et un certain nombre de petits bonhommes (montrant la simulation de ragdoll):
Vous pouvez aussi essayer la démo. Cliquez et faites glisser pour interagir, appuyez sur «R» pour réinitialiser, et appuyez sur «G» pour basculer la gravité.
Les blocs de construction de notre jeu seront le Point. Pour éviter toute ambiguïté, nous appellerons cela la PointMass
. Les détails sont dans le nom: c'est un point dans l'espace, et cela représente une quantité de masse.
Le moyen le plus simple d’appliquer la physique sur ce point est de "transmettre" sa vitesse d’une manière ou d’une autre.
x = x + velX y = y + velY
Nous ne pouvons pas supposer que notre jeu fonctionnera à la même vitesse tout le temps. Il peut fonctionner à 15 images par seconde pour certains utilisateurs, mais à 60 pour d'autres. Il est préférable de prendre en compte les fréquences d'images de toutes les plages, ce qui peut être fait à l'aide d'un timestep.
x = x + velX * timeElapsed y = y + velY * timeElapsed
De cette façon, si un cadre mettait plus de temps à s'écouler pour une personne que pour une autre, le jeu fonctionnerait toujours à la même vitesse. Pour un moteur physique, cependant, c'est incroyablement instable.
Imaginez si votre jeu se fige pendant une seconde ou deux. Le moteur compenserait excessivement cela et déplacerait le PointMass
passé plusieurs murs et objets avec lesquels il aurait sinon détecté une collision. Ainsi, non seulement la détection de collision serait affectée, mais également la méthode de résolution de contraintes que nous allons utiliser..
Comment pouvons-nous avoir la stabilité de la première équation, x = x + velX
, avec la consistance de la deuxième équation, x = x + velX * timeElapsed
? Et si, peut-être, nous pourrions combiner les deux?
C'est exactement ce que nous ferons. Imaginez notre temps écoulé
était 30
. Nous pourrions faire exactement la même chose que la dernière équation, mais avec une précision et une résolution supérieures, en appelant x = x + (velX * 5)
six fois.
elapsedTime = lastTime - currentTime lastTime = currentTime // réinitialiser lastTime // ajouter du temps qui n'a pas pu être utilisé la dernière image elapsedTime + = leftOverTime // le diviser en morceaux de 16 ms timesteps = floor (elapsedTime / 16) // stocker le temps nous ne pouvions pas utiliser pour la prochaine image. leftOverTime = elapsedTime - timesteps * 16 pour (i = 0; i < timesteps; i++) x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc.
L'algorithme utilise ici un pas de temps fixe supérieur à un. Il trouve le temps écoulé, le divise en "morceaux" de taille fixe et reporte le temps restant à l'image suivante. Nous exécutons la simulation petit à petit pour chaque morceau de notre temps écoulé est divisé en.
J'ai choisi 16 pour la taille du pas de temps, afin de simuler la physique comme si elle tournait à environ 60 images par seconde. Conversion de temps écoulé
en images par seconde peut être fait avec quelques maths: 1 seconde / elapsedTimeInSeconds
.
1s / (16ms / 1000s) = 62.5fps
, donc un pas de 16ms équivaut à 62,5 images par seconde.
Les contraintes sont des restrictions et des règles ajoutées à la simulation, indiquant où PointMasses peut et ne peut pas aller.
Elles peuvent être simples comme cette contrainte de limite, afin d’empêcher les PointMasses de s’écarter du bord gauche de l’écran:
si (x < 0) x = 0 if (velX < 0) velX = velX * -1
L'ajout de la contrainte pour le bord droit de l'écran se fait de la même manière:
si (x> largeur) x = largeur si (velX> 0) velX = velX * -1
Faire cela pour l’axe des y consiste à changer tous les x en a.
Avoir le bon type de contraintes peut entraîner des interactions très belles et captivantes. Les contraintes peuvent également devenir extrêmement complexes. Essayez d'imaginer la simulation d'un panier de grains vibrant sans aucun intersection des grains, ou d'un bras robotique à 100 joints, ou même de quelque chose de simple comme une pile de boîtes. Le processus typique consiste à trouver des points de collision, à trouver l'heure exacte de la collision, puis à trouver la force ou l'impulsion appropriée à appliquer à chaque corps pour éviter cette collision..
Comprendre la complexité d’un ensemble de contraintes peut être difficile, puis résoudre ces contraintes, en temps réel est encore plus difficile. Nous allons simplifier considérablement la résolution de contraintes.
Thomas Jakobsen, un mathématicien et programmeur, a exploré différentes manières de simuler la physique des personnages pour les jeux. Il a suggéré que la précision n’était pas aussi importante que la crédibilité et la performance. Le cœur de tout son algorithme était une méthode utilisée depuis les années 60 pour modéliser la dynamique moléculaire, appelée Intégration Verlet. Vous connaissez peut-être le jeu Hitman: Codename 47. Ce fut l'un des premiers jeux à utiliser la physique de ragdoll et utilise les algorithmes développés par Jakobsen..
Verlet Integration est la méthode que nous allons utiliser pour transmettre la position de notre PointMass. Ce que nous avons fait avant, x = x + velX
, est une méthode appelée intégration d'Euler (que j'ai également utilisée dans le codage de terrain de pixel destructible).
La principale différence entre l'intégration d'Euler et Verlet réside dans la mise en œuvre de la vélocité. En utilisant Euler, une vélocité est enregistrée avec l'objet et est ajoutée à la position de l'objet à chaque image. Cependant, l'utilisation de Verlet applique l'inertie en utilisant la position précédente et la position actuelle. Prenez la différence entre les deux positions et ajoutez-la à la dernière position pour appliquer l'inertie.
// Inertia: les objets en mouvement restent en mouvement. velX = x - lastX velY = y - lastY nextX = x + velX + accX * timestepSq nextY = y + velY + accY * timestepSS lastX = x lastY = y x = suivantX y = suivantY
Nous avons ajouté une accélération pour la gravité. Autre que ça, accX
et accy
ne sera pas nécessaire pour résoudre les collisions. Grâce à Verlet Integration, nous n’avons plus besoin de résoudre les impulsions ni de forcer les collisions. Changer la position suffira pour avoir une simulation stable, réaliste et rapide. Ce que Jakobsen a développé est un substitut linéaire à quelque chose qui serait autrement non linéaire.
Les avantages de l'intégration de Verlet peuvent être mieux illustrés par un exemple. Dans un moteur de structure, nous aurons non seulement PointMasses, mais également des liens entre eux. Nos "liens" seront une contrainte de distance entre deux PointMasses. Idéalement, nous voulons que deux PointMasses avec cette contrainte soient toujours à une certaine distance.
Chaque fois que nous résolvons cette contrainte, Verlet Integration doit conserver ces masses de points en mouvement. Par exemple, si une extrémité devait être déplacée rapidement vers le bas, l’autre extrémité devrait la suivre comme un fouet par inertie.
Nous n'aurons besoin que d'un seul lien pour chaque paire de PointMasses attachés l'un à l'autre. Toutes les données dont vous avez besoin dans le lien sont les PointMasses et les distances de repos. Vous pouvez éventuellement avoir la rigidité, pour plus d'une contrainte de printemps. Dans notre démo, nous avons également une "sensibilité à la déchirure", qui est la distance à laquelle le lien sera supprimé.
Je vais seulement expliquer distance de repos
ici, mais la distance de déchirure et la rigidité sont toutes deux implémentées dans la démo et le code source.
Lien reposingDistance tearDistance raideur PointMass A PointMass B resol () math pour résoudre la distance
Vous pouvez utiliser l'algèbre linéaire pour résoudre la contrainte. Trouvez les distances entre les deux, déterminez à quelle distance le distance de repos
ils sont, puis les traduire en fonction de cela et de leurs différences.
// calcule la distance diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // difference scalar difference = (reposingDistance - d) / d // translation pour chaque PointMass. Ils seront poussés à la moitié de la distance requise pour correspondre à leurs distances de repos. translateX = diffX * 0.5 * différence translateY = diffY * 0.5 * différence p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY
Dans la démo, nous tenons également compte de la masse et de la rigidité. La résolution de cette contrainte pose quelques problèmes. Lorsqu'il y a plus de deux ou trois PointMasses liés l'un à l'autre, la résolution de certaines de ces contraintes peut enfreindre d'autres contraintes précédemment résolues..
Thomas Jakobsen a également rencontré ce problème. Au début, on pourrait créer un système d'équations et résoudre toutes les contraintes en même temps. Cela augmenterait toutefois rapidement la complexité et il serait difficile d’ajouter plus que quelques liens dans le système..
Jakobsen a mis au point une méthode qui peut paraître bête et naïve au début. Il a créé une méthode appelée "relaxation", où au lieu de résoudre une fois la contrainte, nous la résolvons plusieurs fois. Chaque fois que nous répétons et résolvons les liens, l'ensemble des liens devient de plus en plus proche de tous ceux qui sont résolus.
Pour récapituler, voici comment notre moteur fonctionne en pseudocode. Pour un exemple plus spécifique, consultez le code source de la démo.
animationLoop numPhysicsUpdates = combien de fois nous pouvons tenir dans le temps écoulé pour (chaque numPhysicsUpdates) // (avec constraintSolve étant n'importe quel nombre supérieur ou égal à 1. J'utilise généralement 3 pour (chaque constraintSolve) pour (chaque contrainte de lien) pour résoudre la contrainte // fin résout la contrainte de lien // fin met à jour la physique // (use verlet!) // fin met à jour la physique dessine des points et des liens
Maintenant, nous pouvons construire le tissu lui-même. La création des liens devrait être assez simple: lien vers la gauche lorsque PointMass n'est pas le premier de sa rangée, et lien lorsque ce n'est pas le premier dans sa colonne..
La démo utilise une liste unidimensionnelle pour stocker PointMasses, et trouve les points sur lesquels établir un lien. x + y * largeur
.
// nous voulons que la boucle y soit à l'extérieur, donc elle analyse ligne par ligne plutôt que colonne par colonne pour (chaque y de 0 à la hauteur) pour (chaque x de 0 à la largeur) new PointMass en x, y // attache à gauche si (x! = 0) attache PM à dernier PM de la liste // attache à droite si (y! = 0) attache PM à PM @ ((y - 1) * ( width + 1) + x) dans la liste si (y == 0) NIP PM ajouter PM à la liste
Vous remarquerez peut-être dans le code que nous avons également "pin PM". Si nous ne voulons pas que notre rideau tombe, nous pouvons verrouiller la rangée supérieure de PointMasses sur leurs positions de départ. Pour programmer une contrainte d'épingle, ajoutez quelques variables pour garder une trace de l'emplacement de l'épingle, puis déplacez le PointMass à cette position après la résolution de chaque contrainte..
Les Ragdolls étaient les intentions initiales de Jakobsen derrière son utilisation de Verlet Integration. Nous allons commencer par les têtes. Nous allons créer une contrainte de cercle qui n'interagira qu'avec la limite.
Cercle PointMass radius resolve () if (y < radius) y = 2*(radius) - y; if (y > hauteur-rayon) y = 2 * (hauteur - rayon) - y; si (x> largeur-rayon) x = 2 * (largeur - rayon) - x; si (x < radius) x = 2*radius - x;
Ensuite, nous pouvons créer le corps. J'ai ajouté chaque partie du corps pour qu'elle corresponde assez précisément aux proportions de masse et de longueur d'un corps humain normal. Check-out Body.pde
dans les fichiers source pour plus de détails. Cela nous mènera à un autre problème: le corps se contorsionnera facilement en formes inconfortables et semblera très irréaliste..
Il y a plusieurs façons de résoudre ce problème. Dans la démo, nous utilisons des liens invisibles et très instables des pieds à l’épaule et du bassin à la tête afin de pousser naturellement le corps dans une position de repos moins gênante..
Vous pouvez également créer des contraintes de faux angle en utilisant des liens. Disons que nous avons trois PointMasses, dont deux sont liés à un au milieu. Vous pouvez trouver une longueur entre les extrémités afin de satisfaire l’angle choisi. Pour trouver cette longueur, vous pouvez utiliser la loi des cosinus.
A = distance de repos de PointMass au centre PointMass B = distance de repos d'un autre PointMass au centre PointMass longueur = sqrt (A * A + B * B - 2 * A * B * cos (angle)) créer un lien entre PointMass final en utilisant la longueur comme distance de repos
Modifiez le lien afin que cette contrainte ne s'applique que lorsque la distance est inférieure à la distance de repos ou, si elle est supérieure à. Cela évitera que l'angle au centre ne soit trop proche ou trop éloigné, selon vos besoins.
L'un des grands avantages d'un moteur physique complètement linéaire est le fait qu'il peut s'agir de n'importe quelle dimension. Tout ce qui a été fait pour x a également été créé avec une valeur y, et peut donc être étendu à trois voire quatre dimensions (je ne suis pas sûr de savoir comment rendre cela, cependant!)
Par exemple, voici une contrainte de lien pour la simulation en 3D:
// calcule la distance diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) // différence différence scalaire = (reposingDistance - d) / d // traduction pour chaque PointMass. Ils seront poussés à la moitié de la distance requise pour correspondre à leurs distances de repos. translateX = diffX * 0.5 * différence translateY = diffY * 0.5 * différence translateZ = diffZ * 0.5 * différence p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = traduireX p2.y - = translateY p2.z - = translateZ
Merci d'avoir lu! Une grande partie de la simulation est largement basée sur l'article de Thomas Jakobsen intitulé Advanced Character Physics, publié dans GDC 2001. J'ai fait de mon mieux pour éliminer la plupart des éléments complexes et simplifier au point que la plupart des programmeurs le comprendront. Si vous avez besoin d'aide ou avez des commentaires, n'hésitez pas à poster ci-dessous.