Simule telas desechables y ragdolls con una integración simple de Verlet

La dinámica del cuerpo suave consiste en simular objetos deformables realistas. Lo utilizaremos aquí para simular una cortina de tela desgarrable y un conjunto de ragdolls con los que puede interactuar y arrojar alrededor de la pantalla. Será rápido, estable y lo suficientemente simple como para hacerlo con las matemáticas del nivel secundario..

Nota: Aunque este tutorial está escrito en Processing y compilado con Java, debería poder usar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos.


Vista previa del resultado final

En esta demostración, puedes ver una gran cortina (mostrando la simulación de la tela), y una serie de pequeños stickmen (mostrando la simulación de ragdoll):

También puedes probar la demo. Haz clic y arrastra para interactuar, presiona 'R' para reiniciar, y presiona 'G' para alternar la gravedad.


Paso 1: Un punto y su movimiento

Los bloques de construcción de nuestro juego con ser el punto. Para evitar la ambigüedad, lo llamaremos el PointMass. Los detalles están en el nombre: es un punto en el espacio y representa una cantidad de masa.

La forma más básica de implementar la física para este punto es "reenviar" su velocidad de alguna manera..

 x = x + velX y = y + velY

Paso 2: Pasos de tiempo

No podemos asumir que nuestro juego se ejecutará a la misma velocidad todo el tiempo. Puede funcionar a 15 cuadros por segundo para algunos usuarios, pero a 60 para otros. Es mejor tener en cuenta las tasas de fotogramas de todos los rangos, lo que se puede hacer usando un paso de tiempo.

 x = x + velX * timeElapsed y = y + velY * timeElapsed

De esta manera, si un cuadro tardara más en transcurrir para una persona que para otra, el juego seguiría funcionando a la misma velocidad. Para un motor de física, sin embargo, esto es increíblemente inestable..

Imagina si tu juego se congela por un segundo o dos. El motor compensaría en exceso eso y movería el PointMass más allá de varios muros y objetos con los que de otro modo habría detectado una colisión. Por lo tanto, no solo se vería afectada la detección de colisiones, sino también el método de resolución de restricciones que utilizaremos.

¿Cómo podemos tener la estabilidad de la primera ecuación?, x = x + velX, Con la consistencia de la segunda ecuación., x = x + velX * timeElapsed? ¿Y si, tal vez, pudiéramos combinar los dos??

Eso es exactamente lo que haremos. Imagina nuestra tiempo transcurrido estaba 30. Podríamos hacer exactamente lo mismo que la última ecuación, pero con una mayor precisión y resolución, llamando a x = x + (velX * 5) seis veces.

 elapsedTime = lastTime - currentTime lastTime = currentTime // reset lastTime // agregar tiempo que no se pudo usar el último fotograma elapsedTime + = leftOverTime // dividirlo en trozos de 16 ms timesteps = floor (tiempo transcurrido / 16) // almacenar tiempo No podíamos usar para el siguiente cuadro. leftOverTime = elapsedTime - timesteps * 16 para (i = 0; i < timesteps; i++)  x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc. 

El algoritmo aquí utiliza un paso de tiempo fijo mayor que uno. Encuentra el tiempo transcurrido, lo divide en "trozos" de tamaño fijo y empuja la cantidad restante de tiempo al siguiente fotograma. Ejecutamos la simulación poco a poco para cada fragmento en el que nuestro tiempo transcurrido se divide en.

Escogí 16 para el tamaño del paso del tiempo, para simular la física como si estuviera funcionando a aproximadamente 60 cuadros por segundo. Conversión de tiempo transcurrido Los cuadros por segundo se pueden hacer con algunas matemáticas: 1 segundo / elapsedTimeInSeconds.

1s / (16ms / 1000s) = 62.5fps, así que un paso de tiempo de 16 ms es equivalente a 62.5 cuadros por segundo.


Paso 3: Restricciones

Las restricciones son restricciones y reglas agregadas a la simulación, que guían dónde PointMasses puede y no puede ir.

Pueden ser simples como esta restricción de límite, para evitar que las PointMass se muevan fuera del borde izquierdo de la pantalla:

 si (x < 0)  x = 0 if (velX < 0)  velX = velX * -1  

La adición de la restricción para el borde derecho de la pantalla se realiza de manera similar:

 if (x> ancho) x = ancho if (velX> 0) velX = velX * -1

Hacer esto para el eje y es una cuestión de cambiar cada x a una y.

Tener el tipo correcto de restricciones puede resultar en interacciones muy hermosas y cautivadoras. Las restricciones también pueden llegar a ser extremadamente complejas. Intenta imaginarte simulando una canasta vibrante de granos con ninguno de los granos que se intersecan, o un brazo robótico de 100 articulaciones, o incluso algo tan simple como una pila de cajas. El proceso típico consiste en encontrar puntos de colisión, encontrar la hora exacta de la colisión y luego encontrar la fuerza o el impulso correctos para aplicar a cada cuerpo para evitar esa colisión..

Comprender la cantidad de complejidad que puede tener un conjunto de restricciones puede ser difícil, y luego resolver esas restricciones, en tiempo real Es aún más difícil. Lo que haremos es simplificar la resolución de restricciones significativamente..


Paso 4: Integración de Verlet

Un matemático y programador llamado Thomas Jakobsen exploró algunas formas de simular la física de los personajes para los juegos. Él propuso que la precisión no es tan importante como la credibilidad y el rendimiento. El corazón de todo su algoritmo fue un método utilizado desde los años 60 para modelar dinámicas moleculares, llamado Integración de Verlet. Puede que estés familiarizado con el juego Hitman: Codename 47. Fue uno de los primeros juegos en usar la física de ragdoll y usa los algoritmos desarrollados por Jakobsen..

La integración de Verlet es el método que utilizaremos para reenviar la posición de nuestro PointMass. Lo que hicimos antes, x = x + velX, es un método llamado Integración de Euler (que también usé para Codificar Terreno de Pixel Destructible).

La principal diferencia entre Euler y Verlet Integration es cómo se implementa la velocidad. Usando Euler, se almacena una velocidad con el objeto y se agrega a la posición del objeto en cada cuadro. Sin embargo, el uso de Verlet aplica la inercia utilizando la posición anterior y actual. Tome la diferencia en las dos posiciones y agréguela a la última posición para aplicar la inercia.

 // Inercia: los objetos en movimiento permanecen en movimiento. velX = x - lastX velY = y - lastY nextX = x + velX + accX * timestepSq nextY = y + velY + accy * timestepSq lastX = x lastY = y x = nextX y = nextY

Agregamos aceleración allí para la gravedad. Aparte de eso, accX y accy No será necesario para resolver colisiones. Con la integración de Verlet, ya no necesitamos hacer ningún tipo de impulso o fuerza para resolver colisiones. Cambiar la posición solo será suficiente para tener una simulación estable, realista y rápida. Lo que Jakobsen desarrolló es un sustituto lineal de algo que de otra manera no sería lineal..


Paso 5: Restricciones de enlace

Los beneficios de la integración de Verlet se pueden mostrar mejor a través del ejemplo. En un motor de tela, no solo tendremos PointMasses, sino también enlaces entre ellos. Nuestros "enlaces" serán una restricción de distancia entre dos PointMasses. Idealmente, queremos que dos PointMasses con esta restricción estén siempre a cierta distancia de distancia.

Cada vez que resolvemos esta restricción, la Integración de Verlet debería mantener a estos puntos en movimiento. Por ejemplo, si un extremo se moviera rápidamente hacia abajo, el otro extremo debería seguirlo como un látigo a través de la inercia.

Solo necesitaremos un enlace para cada par de PointMasses que se adjunten entre sí. Todos los datos que necesitará en el enlace son las PointMasses y las distancias de descanso. Opcionalmente, puede tener rigidez, por más de una restricción de resorte. En nuestra demostración también tenemos una "sensibilidad al desgarro", que es la distancia a la que se eliminará el enlace.

Sólo explicaré descansoDistancia Aquí, pero la distancia al rasgado y la rigidez se implementan en la demo y en el código fuente..

 Enlace restingDistance tearDistance rigidez PointMass A PointMass B resolver () matemáticas para resolver distancias

Puedes usar el álgebra lineal para resolver la restricción. Encuentra las distancias entre los dos, determina a qué distancia a lo largo del descansoDistancia Ellos son, luego los traducen en base a eso y sus diferencias..

 // calcular la distancia diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // diferencia escalar diferencia = (restingDistance - d) / d // traducción para cada PointMass. Serán empujados 1/2 la distancia requerida para que coincidan con sus distancias de descanso. translateX = diffX * 0.5 * diferencia translateY = diffY * 0.5 * diferencia p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY

En la demostración, también tenemos en cuenta la masa y la rigidez. Hay algunos problemas para resolver esta restricción. Cuando hay más de dos o tres PointMasses vinculadas entre sí, resolver algunas de estas restricciones puede violar otras restricciones previamente resueltas.

Thomas Jakobsen también tuvo este problema. Al principio, uno podría crear un sistema de ecuaciones y resolver todas las restricciones a la vez. Sin embargo, esta complejidad aumentaría rápidamente y sería difícil agregar más que solo unos pocos enlaces al sistema.

Jakobsen desarrolló un método que podría parecer tonto e ingenuo al principio. Él creó un método llamado "relajación", donde en lugar de resolver la restricción una vez, lo resolvemos varias veces. Cada vez que reiteramos y resolvemos los enlaces, el conjunto de enlaces se acerca cada vez más a todos los que se resuelven..

Paso 6: Juntarlo

Para resumir, aquí está cómo funciona nuestro motor en pseudocódigo. Para un ejemplo más específico, echa un vistazo al código fuente de la demostración.

 animationLoop numPhysicsUpdates = sin embargo, muchos podemos ajustar en el tiempo transcurrido para (cada numPhysicsUpdates) // (con restrictionSolve es cualquier número 1 o superior. Por lo general, uso 3 para (cada restricantSolve) para (cada restricción de enlace) resolver restricción  // terminar la restricción de enlace resuelve // terminar restricciones actualización física // (¡usa verlet!) // finalizar física actualización dibuja puntos y enlaces

Paso 7: Añadir una tela

Ahora podemos construir el propio tejido. Crear los enlaces debe ser bastante simple: enlace a la izquierda cuando PointMass no es el primero en su fila, y enlace cuando no es el primero en su columna.

La demostración utiliza una lista unidimensional para almacenar PointMasses, y encuentra puntos para vincular usando x + y * ancho.

 // queremos que el bucle y esté en el exterior, de modo que escanee fila por fila en lugar de columna por columna para (cada y desde 0 hasta la altura) para (cada x desde 0 hasta el ancho) nuevo PointMass en x, y // adjuntar a la izquierda si (x! = 0) adjunte PM al último PM de la lista // adjuntar a la derecha si (y! = 0) adjunte PM a PM @ ((y - 1) * ( ancho + 1) + x) en la lista si (y == 0) el pin PM agrega PM a la lista

Puede notar en el código que también tenemos "pin PM". Si no queremos que caiga nuestra cortina, podemos bloquear la fila superior de PointMasses en sus posiciones iniciales. Para programar una restricción de pines, agregue algunas variables para realizar un seguimiento de la ubicación de los pines, y luego mueva la PointMass a esa posición después de cada resolución de restricción.


Paso 8: Añadir algunos Ragdolls

Los Ragdolls fueron las intenciones originales de Jakobsen detrás de su uso de Verlet Integration. Primero empezaremos con las cabezas. Crearemos una restricción de círculo que solo interactuará con el límite..

 Círculo PointMass radio resolver () si (y < radius) y = 2*(radius) - y; if (y > altura-radio) y = 2 * (altura-radio) - y; si (x> ancho-radio) x = 2 * (ancho - radio) - x; si (x < radius) x = 2*radius - x;  

A continuación podemos crear el cuerpo. Agregué cada parte del cuerpo para que coincida con cierta precisión con las proporciones de masa y longitud de un cuerpo humano normal. Revisa Body.pde en los archivos de origen para más detalles. Hacer esto nos llevará a otro tema: el cuerpo se contorsionará fácilmente en formas incómodas y parecerá muy poco realista..

Hay varias maneras de solucionar esto. En la demostración, utilizamos enlaces invisibles y muy poco rígidos desde los pies hasta el hombro y la pelvis hasta la cabeza para empujar naturalmente el cuerpo a una posición de descanso menos incómoda..

También puede crear restricciones de ángulo falso utilizando enlaces. Digamos que tenemos tres PointMasses, con dos vinculados a uno en el medio. Puede encontrar una longitud entre los extremos para satisfacer cualquier ángulo elegido. Para encontrar esa longitud, puedes usar la Ley de los cosenos..

 A = distancia de reposo de PointMass final al punto PointMass B = distancia de reposo de PointMass al punto PointMass central = sqrt (A * A + B * B - 2 * A * B * cos (ángulo)) crea un enlace entre PointMasses finales utilizando la longitud como distancia de descanso

Modifique el enlace para que esta restricción solo se aplique cuando la distancia sea menor que la distancia de reposo o, si es mayor que. Esto evitará que el ángulo en el punto central esté demasiado cerca o demasiado lejos, dependiendo de lo que necesite.


Paso 9: Más dimensiones!

Una de las grandes cosas de tener un motor de física completamente lineal es el hecho de que puede ser de cualquier dimensión que desee. Todo lo que se hizo con x también se hizo con un valor de y, y la fuerza de trabajo puede extenderse a tres o incluso cuatro dimensiones (aunque no estoy seguro de cómo se representaría eso).

Por ejemplo, aquí hay una restricción de enlace para la simulación en 3D:

 // calcular la distancia diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) // diferencia diferencia escalar = (reposoDistancia - d) / d // traducción para cada PointMass. Serán empujados 1/2 la distancia requerida para que coincidan con sus distancias de descanso. translateX = diffX * 0.5 * diferencia translateY = diffY * 0.5 * diferencia translateZ = diffZ * 0.5 * diferencia p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = translateX p2.y - = translateY p2.z - = translateZ

Conclusión

¡Gracias por leer! Gran parte de la simulación se basa en gran medida en el artículo de Thomas Jakobsen Advanced Character Physics de GDC 2001. Hice todo lo posible para eliminar la mayoría de las cosas complicadas y simplificar hasta el punto que la mayoría de los programadores entenderán. Si necesita ayuda o tiene algún comentario, no dude en publicar a continuación.