¿Cómo interpolar entre dos estados de juego?

24

¿Cuál es el mejor patrón para crear un sistema en el que todos los objetos se posicionen para ser interpolados entre dos estados de actualización?

La actualización siempre se ejecutará con la misma frecuencia, pero quiero poder procesar en cualquier FPS. Por lo tanto, el renderizado será lo más suave posible sin importar los cuadros por segundo, ya sea menor o mayor que la frecuencia de actualización.

Me gustaría actualizar 1 cuadro en el futuro interpolar del cuadro actual al cuadro futuro. Esta respuesta tiene un enlace que habla sobre hacer esto:

Paso de tiempo semi-fijo o totalmente fijo?

Editar: ¿Cómo podría usar también la última y la velocidad actual en la interpolación? Por ejemplo, con solo interpolación lineal, se moverá a la misma velocidad entre las posiciones. Necesito una forma de interpolar la posición entre los dos puntos, pero tenga en cuenta la velocidad en cada punto para la interpolación. Sería útil para simulaciones de baja velocidad como efectos de partículas.

AttackingHobo
fuente
2
¿Son las garrapatas lógicas? Entonces, ¿su actualización fps <renderizado fps?
El pato comunista
Cambié el término Pero sí, la lógica funciona. Y no, quiero liberar completamente el renderizado de la actualización, por lo que el juego puede renderizar a 120HZ o 22.8HZ y la actualización seguirá funcionando a la misma velocidad, siempre que el usuario cumpla con los requisitos del sistema.
AttackingHobo
esto puede ser realmente complicado ya que mientras se procesan todas las posiciones de los objetos debe permanecer quieto (cambiarlos durante el proceso de renderizado puede causar un comportamiento indefinido)
Ali1S232
La interpolación calcularía el estado a la vez entre 2 marcos de actualización ya calculados. ¿No es esta pregunta sobre extrapolación, calcular el estado por un tiempo después del último marco de actualización? Dado que la próxima actualización aún no se ha calculado aún.
Maik Semder
Creo que si solo tiene una actualización / representación de subprocesos, no puede volver a actualizar solo la posición de representación. Simplemente envía posiciones a la GPU y luego vuelve a actualizar.
zacharmarz

Respuestas:

22

Desea separar las tasas de actualización (tic lógico) y de dibujo (render tick).

Sus actualizaciones producirán la posición de todos los objetos en el mundo a dibujar.

Cubriré dos posibilidades diferentes aquí, la que solicitó, la extrapolación, y también otro método, la interpolación.

1)

La extrapolación es donde calcularemos la posición (predicha) del objeto en el siguiente cuadro, y luego interpolaremos entre la posición actual de los objetos y la posición en la que estará el objeto en el siguiente cuadro.

Para hacer esto, cada objeto a dibujar debe tener una velocityy asociada position. Para encontrar la posición en la que estará el objeto en el siguiente cuadro, simplemente agregamos velocity * draw_timestepa la posición actual del objeto, para encontrar la posición pronosticada del siguiente cuadro. draw_timestepes la cantidad de tiempo que ha pasado desde la marca de renderización anterior (también conocida como la llamada de extracción anterior).

Si lo deja así, encontrará que los objetos "parpadean" cuando su posición prevista no coincidía con la posición real en el siguiente cuadro. Para eliminar el parpadeo, puede almacenar la posición pronosticada y alternar entre la posición predicha previamente y la nueva posición predicha en cada paso del sorteo, utilizando el tiempo transcurrido desde la marca de actualización anterior como el factor lerp. Esto seguirá dando como resultado un mal comportamiento cuando los objetos que se mueven rápidamente cambien de ubicación de repente y es posible que desee manejar ese caso especial. Todo lo dicho en este párrafo son las razones por las que no desea utilizar la extrapolación.

2)

La interpolación es donde almacenamos el estado de las dos últimas actualizaciones e interpolamos entre ellas en función de la cantidad de tiempo actual que ha pasado desde la última actualización. En esta configuración, cada objeto debe tener un asociadoposition y previous_position. En este caso, nuestro dibujo representará, en el peor de los casos, un tick de actualización detrás del estado de juego actual, y en el mejor de los casos, exactamente en el mismo estado que el tick de actualización actual.


En mi opinión, es probable que desee la interpolación como la describí, ya que es la más fácil de implementar y dibujar una pequeña fracción de segundo (por ejemplo, 1/60 de segundo) detrás de su estado actual actualizado está bien.


Editar:

En caso de que lo anterior no sea suficiente para permitirle realizar una implementación, aquí hay un ejemplo de cómo hacer el método de interpolación que he descrito. No cubriré la extrapolación, porque no puedo pensar en ningún escenario del mundo real en el que deba preferirlo.

Cuando crea un objeto dibujable, almacenará las propiedades necesarias para dibujar (es decir, el estado información de necesaria para dibujarlo).

Para este ejemplo, almacenaremos la posición y la rotación. También es posible que desee almacenar otras propiedades como la posición de coordenadas de color o textura (es decir, si se desplaza una textura).

Para evitar que los datos se modifiquen mientras el hilo de renderizado lo dibuja (es decir, la ubicación de un objeto se cambia mientras el hilo de renderizado se dibuja, pero todos los demás aún no se han actualizado), necesitamos implementar algún tipo de búfer doble.

Un objeto almacena dos copias de él previous_state. Los pondré en una matriz y me referiré a ellos como previous_state[0]y previous_state[1]. De manera similar, necesita dos copias de él current_state.

Para realizar un seguimiento de qué copia del búfer doble se utiliza, almacenamos una variable state_index, que está disponible tanto para la actualización como para el subproceso de dibujo.

El hilo de actualización primero calcula todas las propiedades de un objeto utilizando sus propios datos (cualquier estructura de datos que desee). Entonces, se copia current_state[state_index]a previous_state[state_index], y copia los nuevos datos relevantes para el dibujo, positiony rotationen current_state[state_index]. Luego lo hace state_index = 1 - state_index, para voltear la copia actualmente usada del doble buffer.

Todo en el párrafo anterior debe hacerse con un candado quitado current_state . Los hilos de actualización y sorteo eliminan este bloqueo. El bloqueo solo se retira mientras dura la copia de la información del estado, que es rápido.

En el hilo de renderizado, entonces haces una interpolación lineal en la posición y la rotación de la siguiente manera:

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

¿Dónde elapsedestá la cantidad de tiempo que ha pasado en el subproceso de renderizado, desde la última actualización tick, y update_tick_lengthes la cantidad de tiempo que tarda su velocidad de actualización fija por tick (por ejemplo, en 20FPS actualizaciones update_tick_length = 0.05).

Si no sabe cuál es la Lerpfunción anterior, consulte el artículo de Wikipedia sobre el tema: Interpolación lineal . Sin embargo, si no sabe qué es lerping, entonces probablemente no esté listo para implementar actualizaciones / dibujos desacoplados con dibujos interpolados.

Olhovsky
fuente
1
+1 lo mismo debe hacerse para las orientaciones / rotaciones y todos los demás estados que cambian con el tiempo, es decir, como animaciones de materiales en sistemas de partículas, etc.
Maik Semder
1
Buen punto Maik, acabo de utilizar la posición como ejemplo. Debe almacenar la "velocidad" de cualquier propiedad que desee extrapolar (es decir, la tasa de cambio a lo largo del tiempo de esa propiedad), si desea utilizar la extrapolación. Al final, realmente no puedo pensar en una situación en la que la extrapolación sea mejor que la interpolación, solo la incluí porque la pregunta del autor la solicitó. Yo uso la interpolación. Con la interpolación, necesitamos almacenar los resultados de actualización actuales y anteriores de cualquier propiedad para interpolar, como usted dijo.
Olhovsky
Esta es una reformulación del problema y la diferencia entre interpolación y extrapolación; No es una respuesta.
1
En mi ejemplo, almacené la posición y la rotación en el estado. También puede almacenar la velocidad (o velocidad) en el estado también. Luego saltas entre la velocidad exactamente de la misma manera ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Puede hacerlo con cualquier número que desee almacenar en el estado. Lerping solo te da un valor entre dos valores, dado un factor lerp.
Olhovsky
1
Para la interpolación del movimiento angular se recomienda usar slerp en lugar de lerp. Lo más fácil sería almacenar los cuaterniones de ambos estados y slerp entre ellos. De lo contrario, se aplican las mismas reglas para la velocidad angular y la aceleración angular. ¿Tienes un caso de prueba para la animación esquelética?
Maik Semder
-2

Este problema requiere que piense en sus definiciones de inicio y finalización de manera un poco diferente. Los programadores principiantes a menudo piensan en el cambio de posición por cuadro y esa es una buena manera de comenzar al principio. Por el bien de mi respuesta, consideremos una respuesta unidimensional.

Digamos que tienes un mono en la posición x. Ahora también tiene un "addX" al que agrega a la posición del mono por cuadro basado en el teclado o algún otro control. Esto funcionará siempre que tenga una velocidad de fotogramas garantizada. Digamos que su x es 100 y su addX es 10. Después de 10 cuadros, su x + = addX debería acumularse a 200.

Ahora, en lugar de addX, cuando tiene una velocidad de cuadro variable, debe pensar en términos de velocidad y aceleración. Te guiaré a través de toda esta aritmética, pero es muy simple. Lo que queremos saber es qué tan lejos quiere viajar por milisegundo (1/1000 de segundo)

Si está disparando a 30 FPS, entonces su velX debe ser 1/3 de segundo (10 cuadros del último ejemplo a 30 FPS) y sabe que quiere viajar 100 'x' en ese tiempo, así que configure su velX en 100 distancias / 10 FPS o 10 distancias por cuadro. En milisegundos, eso equivale a 1 distancia x por 3.3 milisegundos o 0.3 'x' por milisegundo.

Ahora, cada vez que actualice, todo lo que necesita hacer es calcular el tiempo transcurrido. Ya sea que hayan pasado 33 ms (1/30 de segundo) o lo que sea, simplemente multiplique la distancia 0.3 por el número de milisegundos pasados. Esto significa que necesita un temporizador que le proporcione una precisión de ms (milisegundos), pero la mayoría de los temporizadores le proporcionan esto. Simplemente haz algo como esto:

var beginTime = getTimeInMillisecond ()

... más tarde ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = time

... ahora usa este tiempo transcurrido para calcular todas tus distancias.

Mickey
fuente
1
No tiene una tasa de actualización variable. Tiene una tasa de actualización fija. Para ser honesto, realmente no sé qué punto estás tratando de hacer aquí: /
Olhovsky
1
??? -1. Ese es el punto, tengo una tasa de actualización garantizada, pero una tasa de renderización variable, y quiero que sea suave y sin tartamudeos.
AttackingHobo
Las tasas de actualización variables no funcionan bien con juegos en red, juegos competitivos, sistemas de repetición o cualquier otra cosa que dependa de que el juego sea determinista.
AttackingHobo
1
La actualización fija también permite una fácil integración de pseudo fricción. Por ejemplo, si desea multiplicar su velocidad por 0.9 en cada cuadro, ¿cómo calcula cuánto multiplicar si tiene un cuadro rápido o lento? La actualización fija a veces es muy preferible: prácticamente todas las simulaciones físicas usan una tasa de actualización fija.
Olhovsky
2
Si utilicé una velocidad de fotogramas variable y configuré un estado inicial complejo con muchos objetos rebotando entre sí, no hay garantía de que simule exactamente lo mismo. De hecho, lo más probable es que se simule de forma ligeramente diferente cada vez, con pequeñas diferencias al comienzo, que se combinan en un tiempo corto en estados completamente diferentes entre cada ejecución de simulación.
AttackingHobo