Como interpolar entre dois estados do jogo?

24

Qual é o melhor padrão para criar um sistema em que todas as posições dos objetos sejam interpoladas entre dois estados de atualização?

A atualização sempre será executada na mesma frequência, mas eu quero poder renderizar em qualquer FPS. Portanto, a renderização será a mais suave possível, independentemente dos quadros por segundo, seja menor ou maior que a frequência de atualização.

Gostaria de atualizar 1 quadro para o futuro interpolar do quadro atual para o quadro futuro. Esta resposta tem um link que fala sobre fazer isso:

Timestep semi-fixo ou totalmente fixo?

Edit: Como eu também poderia usar a última e atual velocidade na interpolação? Por exemplo, com interpolação apenas linear, ele se moverá na mesma velocidade entre as posições. Preciso de uma maneira de interpolar a posição entre os dois pontos, mas leve em consideração a velocidade em cada ponto para a interpolação. Seria útil para simulações de baixa taxa, como efeitos de partículas.

AttackingHobo
fonte
2
carrapatos sendo lógica carrapatos? Então, sua atualização fps <rendering fps?
The Duck comunista
Eu mudei o termo. Mas sim, a lógica funciona. E não, eu quero liberar completamente a renderização da atualização, para que o jogo seja renderizado em 120HZ ou 22,8HZ e a atualização ainda funcionará na mesma velocidade, desde que o usuário atenda aos requisitos do sistema.
AttackingHobo
isso pode ser realmente complicado uma vez que durante a prestação de todas as suas posições objeto deve ficar ainda (alterá-los durante o processo de renderização pode causar algum comportamento undifined)
Ali1S232
A interpolação calcularia o estado em um momento entre 2 quadros de atualização já calculados. Esta pergunta não é sobre extrapolação, calculando o estado por um tempo após o último quadro de atualização? Uma vez que a próxima atualização ainda nem foi calculada.
Maik Semder
Eu acho que se ele tiver apenas um thread atualizando / renderizando, não poderá atualizar novamente apenas a posição de renderização. Você acabou de enviar posições para a GPU e, em seguida, atualiza novamente.
Zacharmarz 26/05

Respostas:

22

Você deseja separar as taxas de atualização (lógica) e desenhar (renderizar).

Suas atualizações produzirão a posição de todos os objetos no mundo a serem desenhados.

Vou abordar duas possibilidades diferentes aqui, a que você solicitou, extrapolação e também outro método, a interpolação.

1

Extrapolação é onde calcularemos a posição (prevista) do objeto no próximo quadro e, em seguida, interpolaremos entre a posição atual dos objetos e a posição em que o objeto estará no próximo quadro.

Para fazer isso, cada objeto a ser desenhado deve ter um velocitye associado position. Para encontrar a posição em que o objeto estará no próximo quadro, basta adicionar velocity * draw_timestepa posição atual do objeto, para encontrar a posição prevista do próximo quadro. draw_timestepé a quantidade de tempo que passou desde o tick de renderização anterior (também conhecido como call de draw anterior).

Se você deixar assim, verá que os objetos "piscam" quando a posição prevista não corresponde à posição real no próximo quadro. Para remover a tremulação, você pode armazenar a posição prevista e o lerp entre a posição prevista anteriormente e a nova posição prevista em cada etapa do sorteio, usando o tempo decorrido desde a atualização anterior como o fator lerp. Isso ainda resultará em mau comportamento quando objetos em movimento rápido mudarem repentinamente de local, e você pode querer lidar com esse caso especial. Tudo o que foi dito neste parágrafo são as razões pelas quais você não deseja usar a extrapolação.

2)

Interpolação é onde armazenamos o estado das duas últimas atualizações e interpolamos entre elas com base na quantidade de tempo atual que se passou desde a atualização antes da última. Nesta configuração, cada objeto deve ter um positione previous_position. Nesse caso, nosso desenho representará, na pior das hipóteses, uma marca de atualização atrás do estado do jogo atual e, na melhor das hipóteses, exatamente no mesmo estado que a marca de atualização atual.


Na minha opinião, você provavelmente deseja interpolação como a descrevi, pois é o mais fácil de implementar e desenhar uma pequena fração de segundo (por exemplo, 1/60 segundo) atrás do seu estado atualizado atual é bom.


Editar:

Caso o exposto acima não seja suficiente para permitir a execução de uma implementação, aqui está um exemplo de como executar o método de interpolação que descrevi. Não cobrirei extrapolação, porque não consigo pensar em nenhum cenário do mundo real em que você deva preferir.

Quando você cria um objeto desenhável, ele armazena as propriedades necessárias para serem desenhadas (ou seja, as informações de estado necessárias para desenhá-lo).

Neste exemplo, armazenaremos a posição e a rotação. Você também pode armazenar outras propriedades, como cor ou posição da coordenada da textura (ou seja, se uma textura rolar).

Para impedir que os dados sejam modificados enquanto o thread de renderização o desenha (ou seja, a localização de um objeto é alterada enquanto o thread de renderização é desenhado, mas todos os outros ainda não foram atualizados), precisamos implementar algum tipo de buffer duplo.

Um objeto armazena duas cópias dele previous_state. Vou colocá-los em uma matriz e me referir a eles como previous_state[0]e previous_state[1]. Da mesma forma, precisa de duas cópias current_state.

Para rastrear qual cópia do buffer duplo é usada, armazenamos uma variável state_index, que está disponível para os threads de atualização e de desenho.

O thread de atualização primeiro calcula todas as propriedades de um objeto usando seus próprios dados (qualquer estrutura de dados que você desejar). Em seguida, ele copia current_state[state_index]a previous_state[state_index], e copia os novos dados relevantes para o desenho, positione rotationem current_state[state_index]. Para fazer isso state_index = 1 - state_index, inverter a cópia atualmente usada do buffer duplo.

Tudo no parágrafo acima deve ser feito com uma trava retirada current_state. A atualização e os threads de desenho eliminam esse bloqueio. O bloqueio é retirado apenas durante a cópia das informações do estado, o que é rápido.

No encadeamento de renderização, você faz uma interpolação linear na posição e rotação da seguinte maneira:

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

Onde elapsedé a quantidade de tempo que passou no encadeamento de renderização, desde o último tick de atualização, e update_tick_lengtho tempo que sua taxa fixa de atualização leva por tick (por exemplo, em atualizações de 20FPS update_tick_length = 0.05).

Se você não sabe qual é a Lerpfunção acima, consulte o artigo da wikipedia sobre o assunto: Interpolação linear . No entanto, se você não sabe o que é leitura, provavelmente não está pronto para implementar atualização / desenho desacoplado com desenho interpolado.

Olhovsky
fonte
11
+1 o mesmo deve ser feito para orientações / rotações e todos os outros estados que a mudança ao longo do tempo, ou seja, como animações materiais em sistemas de partículas etc.
Maik Semder
11
Bom ponto Maik, eu apenas usei a posição como exemplo. Você precisa armazenar a "velocidade" de qualquer propriedade que deseja extrapolar (ou seja, a taxa de alteração ao longo do tempo dessa propriedade), se desejar usar a extrapolação. No final, eu realmente não consigo pensar em uma situação em que a extrapolação é melhor do que a interpolação, eu a incluí apenas porque a pergunta do solicitante a solicitou. Eu uso interpolação. Com a interpolação, precisamos armazenar os resultados da atualização atual e anterior de qualquer propriedade para interpolar, como você disse.
Olhovsky 27/05
Esta é uma reafirmação do problema e a diferença entre interpolação e extrapolação; não é uma resposta.
11
No meu exemplo, eu armazenei posição e rotação no estado. Você também pode armazenar a velocidade (ou velocidade) no estado também. Então você lê a velocidade exatamente da mesma maneira ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Você pode fazer isso com qualquer número que desejar armazenar no estado. A leitura apenas fornece um valor entre dois valores, considerando um fator de leiturap.
Olhovsky 27/05
11
Para a interpolação do movimento angular, é recomendável usar slerp em vez de lerp. O mais fácil seria armazenar os quaternions de ambos os estados e slerp entre eles. Caso contrário, as mesmas regras se aplicam à velocidade angular e aceleração angular. Você tem um caso de teste para animação esquelética?
Maik Semder 28/05
-2

Esse problema exige que você pense sobre suas definições de início e término de maneira um pouco diferente. Os programadores iniciantes costumam pensar em mudanças de posição por quadro e esse é um bom caminho a percorrer no início. Para o bem da minha resposta, vamos considerar uma resposta unidimensional.

Digamos que você tenha um macaco na posição x. Agora você também tem um "addX" ao qual você adiciona a posição do macaco por quadro, com base no teclado ou em algum outro controle. Isso funcionará desde que você tenha uma taxa de quadros garantida. Digamos que seu x é 100 e seu addX é 10. Após 10 quadros, seu x + = addX deve acumular para 200.

Agora, em vez de addX, quando você tem uma taxa de quadros variável, pense em termos de velocidade e aceleração. Vou guiar você por toda essa aritmética, mas é super simples. O que queremos saber é até onde você quer viajar por milissegundo (1/1000 de segundo)

Se você estiver gravando a 30 FPS, seu velX deve ser 1/3 de segundo (10 quadros do último exemplo a 30 FPS) e você sabe que deseja viajar 100 'x' nesse tempo, então configure seu velX para 100 distância / 10 FPS ou 10 distância por quadro. Em milissegundos, isso resulta em 1 distância x por 3,3 milissegundos ou 0,3 'x' por milissegundo.

Agora, toda vez que você atualiza, tudo o que você precisa fazer é descobrir o tempo decorrido. Se 33 ms passaram (1/30 de segundo) ou o que for, basta multiplicar a distância 0,3 pelo número de milissegundos passados. Isso significa que você precisa de um cronômetro que ofereça precisão em ms (milissegundos), mas a maioria dos cronômetros fornece isso. Simplesmente faça algo assim:

var beginTime = getTimeInMillisecond ()

... mais tarde ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = hora

... agora use esse tempo decorrido para calcular todas as suas distâncias.

Mickey
fonte
11
Ele não tem uma taxa de atualização variável. Ele tem uma taxa fixa de atualização. Para ser honesto, eu realmente não sei o que ponto você está tentando fazer aqui: /
Olhovsky
11
??? -1. Esse é o ponto, estou tendo uma taxa de atualização garantida, mas uma taxa de renderização variável, e quero que ela seja suave sem gagueira.
AttackingHobo
As taxas variáveis ​​de atualização não funcionam bem com jogos em rede, jogos competitivos, sistemas de repetição ou qualquer outra coisa que dependa da determinação do jogo.
AttackingHobo
11
A atualização fixa também permite fácil integração de pseudo-atrito. Por exemplo, se você deseja multiplicar sua velocidade por 0,9 cada quadro, como você calcula quanto multiplicar se tiver um quadro rápido ou lento? Às vezes, a atualização fixa é bastante preferida - praticamente todas as simulações de física usam uma taxa de atualização fixa.
Olhovsky 27/05
2
Se eu usei uma taxa de quadros variável e configurei um estado inicial complexo com muitos objetos refletidos um no outro, não há garantia de que ele simulará exatamente o mesmo. De fato, provavelmente simulará um pouco diferente a cada vez, com pequenas diferenças no início, compondo durante um curto período de tempo em estados completamente diferentes entre cada execução da simulação.
AttackingHobo