Cómo crear un motor de física 2D personalizado cuerpos rígidos orientados

Hasta ahora, hemos cubierto la resolución de impulsos, la arquitectura del núcleo y la fricción. En este tutorial final de esta serie, veremos un tema muy interesante: la orientación.

En este artículo discutiremos los siguientes temas:

  • Matemáticas de rotación
  • Formas orientadas
  • Detección de colisiones
  • Resolución de colisiones

Recomendé encarecidamente leer sobre los tres artículos anteriores de la serie antes de intentar abordar este. Gran parte de la información clave de los artículos anteriores son requisitos previos para el resto de este artículo..


Código de muestra

He creado un pequeño motor de muestra en C ++, y le recomiendo que explore y consulte el código fuente durante la lectura de este artículo, ya que muchos detalles prácticos de implementación no caben en el propio artículo..


Este repositorio de GitHub contiene el motor de muestra, junto con un proyecto de Visual Studio 2010. GitHub le permite ver la fuente sin necesidad de descargar la fuente en sí, para su comodidad.

Artículos Relacionados
  • Philip Diffenderfer ha bifurcado el repositorio para crear una versión Java del motor también!

Orientacion matematica

Las matemáticas que implican rotaciones en 2D son bastante simples, aunque se requerirá un dominio del tema para crear cualquier cosa de valor en un motor de física. La segunda ley de Newton dice:

\ [Ecuación \: 1: \\
F = ma \]

Hay una ecuación similar que relaciona específicamente la fuerza angular y la aceleración angular. Sin embargo, antes de poder mostrar estas ecuaciones, se requiere una descripción rápida del producto cruzado en 2D.

Producto cruzado

El producto cruzado en 3D es una operación bien conocida. Sin embargo, el producto cruzado en 2D puede ser bastante confuso, ya que no hay una interpretación geométrica sólida.

El producto cruzado 2D, a diferencia de la versión 3D, no devuelve un vector sino un escalar. Este valor escalar en realidad representa la magnitud del vector ortogonal a lo largo del eje z, si el producto cruzado se realizara realmente en 3D. En cierto modo, el producto cruzado 2D es solo una versión simplificada del producto cruzado 3D, ya que es una extensión de las matemáticas vectoriales 3D..

Si esto es confuso, no se preocupe: una comprensión profunda del producto cruzado 2D no es todo lo necesario. Solo sepa exactamente cómo realizar la operación, y sepa que el orden de las operaciones es importante: \ (a \ times b \) no es lo mismo que \ (b \ times a \). Este artículo hará un uso intensivo del producto cruzado para transformar la velocidad angular en velocidad lineal..

Conocimiento cómo Sin embargo, realizar el producto cruzado en 2D es muy importante. Se pueden cruzar dos vectores, se puede cruzar un escalar con un vector y se puede cruzar un vector con un escalar. Aquí están las operaciones:

 // Dos vectores cruzados devuelven un producto cruzado flotante escalar (const Vec2 & a, const Vec2 & b) return a.x * b.y - a.y * b.x;  // Formas más exóticas (pero necesarias) del producto cruzado // con un vector a y scalar s, ambos devolviendo un vector Vec2 CrossProduct (const Vec2 & a, float s) return Vec2 (s * ay, -s * ax );  Vec2 CrossProduct (float s, const Vec2 & a) return Vec2 (-s * a.y, s * a.x); 

Torsión y velocidad angular

Como todos deberíamos saber de los artículos anteriores, esta ecuación representa una relación entre la fuerza que actúa sobre un cuerpo con la masa y la aceleración de ese cuerpo. Hay un análogo para la rotación:

\ [Ecuación \: 2: \\
T = r \: \ times \: \ omega \]

\ (T \) significa esfuerzo de torsión. El par es la fuerza de rotación.

\ (r \) es un vector desde el centro de masa (COM) a un punto particular en un objeto. Puede pensarse que \ (r \) se refiere a un "radio" desde COM hasta un punto. Cada punto único en un objeto requerirá un valor \ (r \) diferente para ser representado en la Ecuación 2.

\ (\ omega \) se llama "omega", y se refiere a la velocidad de rotación. Esta relación se utilizará para integrar la velocidad angular de un cuerpo rígido..

Es importante entender que la velocidad lineal es la velocidad de la COM de un cuerpo rígido. En el artículo anterior, todos los objetos no tenían componentes de rotación, por lo que la velocidad lineal de la COM era la misma velocidad para todos los puntos de un cuerpo. Cuando se introduce la orientación, los puntos más alejados del COM giran más rápido que los que están cerca del COM. Esto significa que necesitamos una nueva ecuación para encontrar la velocidad de un punto en un cuerpo, ya que los cuerpos ahora pueden girar y traducir al mismo tiempo.

Use la siguiente ecuación para entender la relación entre un punto en un cuerpo y la velocidad de ese punto:

\ [Ecuación \: 3: \\
\ omega = r \: \ times v \]

\ (v \) representa la velocidad lineal. Para transformar la velocidad lineal en velocidad angular, cruce el radio \ (r \) con \ (v \).

De manera similar, podemos reorganizar la Ecuación 3 para formar otra versión:

\ [Ecuación \: 4: \\
v = \ omega \: \ times r \]

Las ecuaciones de la última sección son bastante poderosas solo si los cuerpos rígidos tienen una densidad uniforme. La densidad no uniforme hace que las matemáticas involucradas en el cálculo de cualquier cosa que requiera la rotación y el comportamiento de un cuerpo rígido sean demasiado complicadas. Además, si el punto que representa un cuerpo rígido no está en el COM, entonces los cálculos relativos a \ (r \) serán completamente erróneos.

Inercia

En dos dimensiones, un objeto gira alrededor del eje z imaginario. Esta rotación puede ser bastante difícil dependiendo de cuánta masa tenga un objeto y a qué distancia de la COM esté la masa del objeto. Un círculo con una masa igual a una barra larga y delgada será más fácil de girar que la barra. Este factor de "dificultad para girar" se puede considerar como el momento de inercia de un objeto..

En cierto sentido, la inercia es la masa rotacional de un objeto. Cuanta más inercia tiene algo, más difícil es hacerlo girar..

Sabiendo esto, uno podría almacenar la inercia de un objeto dentro del cuerpo con el mismo formato que la masa. También sería aconsejable almacenar el inverso de este valor de inercia, teniendo cuidado de no realizar una división por cero. Consulte los artículos anteriores de esta serie para obtener más información sobre la masa y la masa inversa..

Integración

Cada cuerpo rígido requerirá algunos campos más para almacenar información de rotación. Aquí hay un ejemplo rápido de una estructura para contener algunos datos adicionales:

 struct RigidBody Forma * forma // Componentes lineales Vec2 posición Vec2 velocidad aceleración de flotación // Componentes angulares orientación de flotación // radianes flotan angularVelocity float torque;

La integración de la velocidad angular y la orientación de un cuerpo son muy similares a la integración de la velocidad y la aceleración. Aquí hay un ejemplo de código rápido para mostrar cómo se hace (nota: los detalles sobre la integración se trataron en un artículo anterior):

 const Vec2 gravedad (0, -10.0f) velocidad + = fuerza * (1.0f / masa + gravedad) * dt angularVelocity + = par * (1.0f / momentOfInertia) * posición dt + = velocidad * dt orient + = angularVelocity * dt

Con la pequeña cantidad de información presentada hasta el momento, debería poder comenzar a rotar varias cosas en la pantalla sin ningún problema. Con solo unas pocas líneas de código, se puede construir algo bastante impresionante, tal vez lanzando una forma al aire mientras gira alrededor de la COM cuando la gravedad la empuja hacia abajo para formar una trayectoria de recorrido en arco..

Mat22

La orientación debe almacenarse como un solo valor de radianes, como se ve arriba, aunque muchas veces el uso de una pequeña matriz de rotación puede ser una opción mucho mejor para ciertas formas.

Un gran ejemplo es el cuadro de límites orientados (OBB). El OBB consiste en una extensión de ancho y alto, los cuales pueden ser representados por vectores. Estos dos vectores de extensión pueden ser rotados por una matriz de rotación de dos por dos para representar los ejes de un OBB.

Sugiero la creación de un Mat22 clase matricial que se agregará a la biblioteca matemática que esté utilizando. Yo mismo uso una pequeña biblioteca de matemáticas personalizada que está empaquetada en la demostración de código abierto. Aquí hay un ejemplo de cómo puede ser un objeto de este tipo:

 struct Mat22 union struct float m00, m01 float m10, m11; ; struct Vec2 xCol; Vec2 yCol; ; ; ;

Algunas operaciones útiles incluyen: construcción desde ángulo, construcción desde vectores de columna, transposición, multiplicación con Vec2, multiplicar con otro Mat22, valor absoluto.

La última función útil es poder recuperar la X o y columna de un vector. La función de columna se vería algo así como:

 Mat22 m (PI / 2.0f); Vec2 r = m.ColX (); // recuperar la columna del eje x

Esta técnica es útil para recuperar un vector unitario a lo largo de un eje de rotación, ya sea el X o y eje. Además, se puede construir una matriz de dos por dos a partir de dos vectores de unidades ortogonales, ya que cada vector se puede insertar directamente en las filas. Aunque este método de construcción es un poco raro para los motores de física 2D, todavía puede ser muy útil para entender cómo funcionan las rotaciones y matrices en general.

Este constructor podría verse algo como:

 Mat22 :: Mat22 (const Vec2 & x, const Vec2 & y) m00 = x.x; m01 = x.y; m01 = y.x; m11 = y.y;  // o Mat22 :: Mat22 (const Vec2 & x, const Vec2 & y) xCol = x; yCol = y; 

Dado que la operación más importante de una matriz de rotación es realizar rotaciones basadas en un ángulo, es importante poder construir una matriz desde un ángulo y multiplicar un vector por esta matriz (para rotar el vector en sentido contrario a las agujas del reloj según el ángulo del matriz fue construida con):

 Mat2 (radianes reales) real c = std :: cos (radianes); real s = std :: pecado (radianes); m00 = c; m01 = -s; m10 = s; m11 = c;  // Gire un operador de const const2 de vector * (const Vec2 & rhs) const return Vec2 (m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y); 

Por razones de brevedad, no deduciré por qué la matriz de rotación en sentido contrario a las agujas del reloj tiene la siguiente forma:

 a = ángulo cos (a), -sin (a) sin (a), cos (a)

Sin embargo, es importante al menos saber que esta es la forma de la matriz de rotación. Para obtener más información acerca de las matrices de rotación, consulte la página de Wikipedia.

Artículos Relacionados
  • Construyamos un motor de gráficos 3D: transformaciones lineales

Transformando a una base

Es importante comprender la diferencia entre el modelo y el espacio del mundo. El espacio modelo es el sistema de coordenadas local de una forma física. El origen está en el COM, y la orientación del sistema de coordenadas se alinea con los ejes de la propia forma..

Para transformar una forma en el espacio del mundo se debe rotar y traducir. La rotación debe ocurrir primero, ya que la rotación siempre se realiza sobre el origen. Dado que el objeto está en el espacio modelo (origen en COM), la rotación girará alrededor del COM de la forma. La rotación se produciría con una Mat22 matriz. En el código de ejemplo, las matrices de orientación son del nombre. tu.

Después de realizar la rotación, el objeto se puede traducir a su posición en el mundo mediante la adición de vectores.

Una vez que un objeto se encuentra en el espacio del mundo, se puede traducir al espacio modelo de un objeto completamente diferente mediante el uso de transformaciones inversas. La rotación inversa seguida de la traducción inversa se utiliza para hacerlo. Así se simplifican las matemáticas durante la detección de colisiones!

Transformación inversa (de izquierda a derecha) del espacio mundial al espacio modelo del polígono rojo.

Como se ve en la imagen anterior, si la transformación inversa del objeto rojo se aplica a los polígonos rojo y azul, entonces la prueba de detección de colisión puede reducirse a la forma de una prueba AABB vs OBB, en lugar de calcular matemáticas complejas entre dos formas orientadas.

En gran parte del código fuente de muestra, los vértices se transforman constantemente de modelo a mundo y de nuevo a modelo, por todo tipo de razones. Debe comprender claramente lo que esto significa para comprender el código de detección de colisión de muestra.


Detección de colisiones y generación de colectores.

En esta sección, presentaré esquemas rápidos de polígonos y colisiones circulares. Consulte el código fuente de ejemplo para obtener detalles más detallados de la implementación.

Polígono a polígono

Comencemos con la rutina de detección de colisiones más compleja de toda esta serie de artículos. La idea de verificar la colisión entre dos polígonos se realiza mejor (en mi opinión) con el Teorema del eje de separación (SAT).

Sin embargo, en lugar de proyectar las extensiones de cada polígono entre sí, hay un método ligeramente más nuevo y más eficiente, como lo describe Dirk Gregorius en su Conferencia de GDC de 2013 (diapositivas disponibles aquí de forma gratuita).

Lo primero que hay que aprender es el concepto de puntos de apoyo..

Puntos de apoyo

El punto de apoyo de un polígono es el vértice que está más alejado en una dirección dada. Si dos vértices tienen distancias iguales a lo largo de la dirección dada, cualquiera de los dos es aceptable.

Para calcular un punto de apoyo, el producto de puntos debe usarse para encontrar una distancia firmada a lo largo de una dirección determinada. Como esto es muy simple, mostraré un ejemplo rápido dentro de este artículo:

 // El punto extremo a lo largo de una dirección dentro de un polígono Vec2 GetSupport (const Vec2 & dir) real bestProjection = -FLT_MAX; Vec2 bestVertex; para (uint32 i = 0; i < m_vertexCount; ++i)  Vec2 v = m_vertices[i]; real projection = Dot( v, dir ); if(projection > bestProjection) bestVertex = v; bestProjection = proyección;  devolver bestVertex; 

El producto punto se utiliza en cada vértice. El producto punto representa una distancia firmada en una dirección dada, por lo que el vértice con la mayor distancia proyectada sería el vértice a devolver. Esta operación se realiza en el espacio modelo del polígono dado dentro del motor de muestra.

Encontrando eje de separación

Al utilizar el concepto de puntos de apoyo, se puede realizar una búsqueda del eje de separación entre dos polígonos (polígono A y polígono B). La idea de esta búsqueda es recorrer todas las caras del polígono A y encontrar el punto de apoyo en la normal negativa a esa cara..

En la imagen anterior, se muestran dos puntos de soporte: uno en cada objeto. La normal azul correspondería al punto de apoyo en el otro polígono como el vértice más alejado en la dirección opuesta a la normal azul. De manera similar, la normal roja se usaría para encontrar el punto de soporte ubicado al final de la flecha roja.

La distancia desde cada punto de apoyo a la cara actual sería la penetración firmada. Al almacenar la mayor distancia se puede registrar un posible eje mínimo de penetración.

Aquí hay una función de ejemplo del código fuente de muestra que encuentra el posible eje de penetración mínima usando el Obtener apoyo función:

 FindAxisLeastPenetration real (uint32 * faceIndex, PolygonShape * A, PolygonShape * B) real bestDistance = -FLT_MAX; uint32 bestIndex; para (uint32 i = 0; i < A->m_vertexCount; ++ i) // Recuperar una cara normal de A Vec2 n = A-> m_normals [i]; // Recuperar el punto de soporte de B a lo largo de -n Vec2 s = B-> GetSupport (-n); // Recuperar el vértice en la cara de A, transformarlo en el espacio modelo de B de Vec2 v = A-> m_vertices [i]; // Calcular la distancia de penetración (en el espacio modelo de B) real d = Punto (n, s - v); // Almacena la mayor distancia si (d> bestDistance) bestDistance = d; bestIndex = i;  * faceIndex = bestIndex; devuelve bestDistance; 

Dado que esta función devuelve la mayor penetración, si esta penetración es positiva, significa que las dos formas no se superponen (la penetración negativa significaría que no hay eje de separación).

Esta función deberá llamarse dos veces, volteando los objetos A y B en cada llamada.

Clipping Incidente y Cara de Referencia

Desde aquí, es necesario identificar el incidente y la cara de referencia, y la cara del incidente debe recortarse contra los planos laterales de la cara de referencia. Esta es una operación bastante no trivial, aunque Erin Catto (creadora de Box2D, y toda la física utilizada actualmente por Blizzard) ha creado algunas grandes diapositivas que cubren este tema en detalle..

Este recorte generará dos puntos de contacto potenciales. Todos los puntos de contacto detrás de la cara de referencia pueden considerarse puntos de contacto..

Más allá de las diapositivas de Erin Catto, el motor de muestra también tiene implementadas las rutinas de recorte como ejemplo.

Círculo a polígono

La rutina de colisión círculo frente a polígono es bastante más simple que la detección de colisión polígono frente a polígono. Primero, la cara más cercana en el polígono al centro del círculo se calcula de manera similar al uso de los puntos de apoyo de la sección anterior: haciendo un bucle sobre cada cara normal del polígono y encontrando la distancia desde el centro del círculo hasta la cara.

Si el centro del círculo está detrás de la cara más cercana, se puede generar información de contacto específica y la rutina puede finalizar de inmediato..

Una vez que se identifica la cara más cercana, la prueba se convierte en una prueba de segmento de línea vs. círculo. Un segmento de línea tiene tres regiones interesantes llamadas Regiones de voronoi. Examina el siguiente diagrama:

Voronoi regiones de un segmento de línea.

Intuitivamente, dependiendo de dónde se ubica el centro del círculo, se puede derivar información de contacto diferente. Imagina que el centro del círculo está ubicado en cualquier región del vértice. Esto significa que el punto más cercano al centro del círculo será un vértice del borde, y la colisión normal adecuada será un vector desde este vértice hasta el centro del círculo..

Si el círculo está dentro de la región de la cara, el punto más cercano del segmento al centro del círculo será el proyecto del centro del círculo en el segmento. La colisión normal solo será la cara normal..

Para calcular en qué región Voronoi se encuentra el círculo, usamos el producto de punto entre un par de vértices. La idea es crear un triángulo imaginario y probar si el ángulo de la esquina construida con el vértice del segmento está por encima o por debajo de 90 grados. Se crea un triángulo para cada vértice del segmento de línea.

Proyectando un vector desde el vértice del borde al centro del círculo sobre el borde.

Un valor de más de 90 grados significará que se ha identificado una región de borde. Si ninguno de los ángulos del vértice del borde de un triángulo está por encima de los 90 grados, entonces el centro del círculo debe proyectarse en el propio segmento para generar información múltiple. Como se ve en la imagen de arriba, si el vector desde el vértice del borde hasta el centro del círculo salpicado con el vector del borde es negativo, entonces se conoce la región Voronoi en la que se encuentra el círculo..

Afortunadamente, el producto punto se puede usar para calcular una proyección firmada, y este signo será negativo si está por encima de 90 grados y positivo si está por debajo.


Resolución de colisión

Es ese momento otra vez: volveremos a nuestro código de resolución de impulsos por tercera y última vez. A estas alturas, ya debería estar completamente cómodo escribiendo su propio código de resolución que calcula los impulsos de resolución, junto con los impulsos de fricción, y también puede realizar una proyección lineal para resolver la penetración restante..

Los componentes rotacionales deben agregarse tanto a la fricción como a la resolución de penetración. Alguna energía será colocada en velocidad angular..

Aquí está nuestro impulso de resolución como lo dejamos en el artículo anterior sobre fricción:

\ [Ecuación 5: \\
j = \ frac - (1 + e) ​​((V ^ A - V ^ B) * t) \ frac 1 masa ^ A + \ frac 1 masa ^ B
\]

Si lanzamos componentes rotativos, la ecuación final se ve así:

\ [Ecuación 6: \\
j = \ frac - (1 + e) ​​((V ^ A - V ^ B) * t) \ frac 1 masa ^ A + \ frac 1 masa ^ B + \ frac (r ^ A \ times t) ^ 2 I ^ A + \ frac (r ^ B \ times t) ^ 2 I ^ B
\]

En la ecuación anterior, \ (r \) es nuevamente un "radio", como en un vector desde el COM de un objeto hasta el punto de contacto. Una derivación más profunda de esta ecuación se puede encontrar en el sitio de Chris Hecker.

Es importante darse cuenta de que la velocidad de un punto dado en un objeto es:

\ [Ecuación 7: \\
V '= V + \ omega \ veces r
\]

La aplicación de los impulsos cambia ligeramente para dar cuenta de los términos de rotación:

 void Body :: ApplyImpulse (const Vec2 & impulse, const Vec2 & contactVector) speed + = 1.0f / mass * impulse; angularVelocity + = 1.0f / inercia * Cruz (contactVector, impulso); 

Conclusión

Con esto concluye el artículo final de esta serie. Hasta ahora, se han cubierto bastantes temas, que incluyen resolución basada en impulsos, generación múltiple, fricción y orientación, todo en dos dimensiones..

Si has llegado hasta aquí, ¡debo felicitarte! La programación de motores de física para juegos es un área de estudio extremadamente difícil. Deseo suerte a todos los lectores, y nuevamente, siéntase libre de comentar o hacer preguntas a continuación..