Como modelar movimento com física: o que funciona e o que é perda de tempo
Gravidade, atrito, colisão. São os três pilares que qualquer simulação decente precisa ter. O resto é detalhe. Quando você começa a trabalhar com física movimento no dia a dia, logo descobre que a teoria da sala de aula não se aplica igual quando o código precisa rodar em tempo real. Vou mostrar como eu lido com isso depois de passar anos consertando simulações que quebravam nos piores momentos.
Física movimento na prática: o básico sem enrolação
A base é simples. Posição, velocidade, aceleração. Você atualiza a cada frame usando um integrador. A maioria dos iniciantes começa com Euler explícito porque é a primeira coisa que aparece em qualquer tutorial. Eu recomendo começar com Euler também, mas saiba exatamente quando ele falha. Euler acumula erro de energia a cada frame. Em simulações curtas de teste, você nem nota. Em projetos de longa duração, seu objeto vai ganhar energia do nada e sair voando para fora do mundo. O passo seguinte é migrar para Verlet ou semi-implícito Euler. Semi-implícito é a transição mais barata que existe. A diferença é mínima no código, mas a estabilidade é drasticamente melhor. Você atualiza a velocidade primeiro, depois usa a velocidade atualizada para avançar a posição. Pronto. Isso resolve 80% dos problemas de instabilidade que eu vejo em projetos novos.
O coeficiente de restituição define o quanto um objeto quica. Um valor de 0 significa colisão perfeitamente inelástica. Valor próximo de 1 é elástico. Na prática, raramente usei valores maiores que 0,7 para objetos do cotidiano. anything above that looks fake and is harder to stabilize.
Atrito: o problema que ninguém avisa antes
Atrito cinético e estático são tratamentos completamente diferentes no motor. O erro mais comum é aplicar o mesmo coeficiente para ambos. Se você não separar, seu objeto ora fica travado para sempre, ora desliza como se estivesse no gelo. Eu uso 0,6 para atrito estático e 0,4 para cinético na maioria dos cenários. Ajuste conforme a superfície. Madeira sobre madeira é diferente de metal sobre concreto. Um detalhe importante que pouca gente menciona: o atrito precisa ser normalizado pela massa do objeto. Um cubo leve com atrito alto vai parar quase instantaneamente. Um caminhão com o mesmo coeficiente continua derrapando. Divida a força de atrito pela massa antes de aplicar à aceleração. Isso evita que objetos leves se comportem de forma irreais só porque o coeficiente numérico é alto.
Colisões: o ponto onde tudo dá errado
Detecção de colisão e resposta são coisas separadas. Detectar que dois objetos se tocam é uma etapa. Resolver a colisão, ou seja, impedir que eles se sobreponham e calcular as novas velocidades, é outra. A falha mais comum é resolver as duas em uma única etapa mal otimizada. Eu divido o processo em três etapas claras. Primeira, broad phase: uma varredura rápida usando bounds AABB para eliminar pares que claramente não colidem. Segunda, narrow phase: detecção precisa entre os pares que sobraram. Terceira, resolução: aplicação do impulso de colisão com correção de posição. Quando eu pulei essa divisão num projeto anterior, o desempenho caiu de 60fps para 12fps sozinha. A separação restaurou a performance rapidamente.
Outro problema frequente é o tunneling. Objetos rápidos atravessam paredes porque o passo do simulador é muito grande em relação à velocidade. A solução direta é usar swept collision detection, que verifica a trajetória inteira entre frames em vez de apenas as posições finais. Se isso não for viável por limitações de performance, reduza o timestep. Sub-stepping resolve o problema sem precisar de detecção swept.
Uma situação real que encontrei e como resolvi
Num projeto de simulação de física movimento para um prototype de jogo, os objetos começavam a vibrar violentamente quando empilhados em mais de quatro camadas. O problema era a tolerância do solver de restrições. O motor padrão aceitava margem de erro de 0,001 metros por passo. Para pilhas altas, esse erro se acumulava e gerava oscilações visíveis. A correção foi ajustar dois parâmetros: aumentar a taxa de convergência do solver de 4 iterações para 12, e reduzir a tolerância de posição para 0,0001. O custo foi um aumento de 15% no uso de CPU, mas a vibração desapareceu completamente. Outra alternativa que testei foi adicionar damping de velocidade proporcional à altura da pilha, mas isso introduzia comportamento artificial que os jogadores notavam. A primeira solução foi mais limpa.
Implementação passo a passo para quem está começando
Configurando o loop principal
O loop de simulação precisa de um timestep fixo ou semi-fixo. Timestep variável é a causa número um de simulações inconsistentes. Se o frame rate cai, a física acelera. Se sobe, a física desacelera. Use um accumulator para separar a taxa de renderização da taxa de atualização física. A cada frame, calcule o delta de tempo real, adicione ao acumulador e processe todas as iterações físicas completas que o acumulador permitir. Renderize apenas uma vez por frame, independente de quantas iterações físicas foram executadas. Um timestep de 1/60 segundos (cerca de 16,67ms) funciona para a maioria dos casos. Simulações que exigem mais precisão, como jogos de física competiva, usam 1/120 ou 1/240. Quanto menor o timestep, mais preciso o resultado, mas mais custo de processamento. O equilíbrio ideal depende do público-alvo e da plataforma alvo.
Estrutura básica de dados
Cada corpo dinâmico precisa armazenar pelo menos: posição (vetor 2D ou 3D), velocidade (vetor), massa, inversa da massa, inércia e inversa da inércia. Corpos estáticos têm massa zero e inversa da massa zero. A inversa da massa é pré-calculada uma vez e reutilizada durante o cálculo de impulsos, evitando divisões custosas no loop principal. Para rotações em 2D, armazene o ângulo como escalar e a velocidade angular também como escalar. Em 3D, use quaternions para evitar gimbal lock. A maioria dos iniciantes tenta usar ângulos de Euler em 3D e acaba com problemas de rotação imprevisíveis. Quaternion é mais simples do que parece e resolve isso sem esforço.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Integração e atualização
A cada passo físico, o processo segue esta ordem: aplicar forças, integrar velocidade, integrar posição, detectar colisões, resolver colisões, repetir resolução por algumas iterações. As forças aplicadas incluem gravidade, atrito, forças de mola e forças externas como vento ou empurrões do jogador. A gravidade é tipicamente um vetor constante apontando para baixo, como (0, -9.81, 0) m/s² em unidades reais. O integrador semi-implícito Euler segue esta lógica: velocidade += aceleração * dt; posição += velocidade * dt. Note que a posição usa a velocidade já atualizada, não a anterior. Essa pequena diferença é o que garante estabilidade. O integrador de Verlet é ainda mais estável para simulações com restrições, mas requer armazenar a posição anterior de cada corpo. Escolha o integrador com base no que seu projeto precisa.
Resolução de colisões com impulsos
Quando dois corpos colidem, você precisa calcular o impulso que deve ser aplicado a cada um para respeitar a conservação de momento. A equação básica do impulso escalado depende da normal de colisão, das velocidades relativas, dos raios de rotação, das massas e dos coeficientes de restituição. Se você não está lidando com rotações, a equação para algo bem mais direto. Mas assim que objetos podem girar, a parte da inércia entra e a coisa fica um pouco mais complexa. O separação de posição é crucial. Após resolver o impulso, verifique se os objetos ainda estão sobrepostos. Se sim, mova-os para fora um do outro proporcionalmente à sua massa. Isso evita que objetos fiquem preso visualmente mesmo após a colisão ser processada. Sem correção de posição, sua simulação vai parecer fluida no primeiro segundo e completamente quebrada no terceiro.
Erros comuns que custaram horas dos meus projetos
O primeiro erro que cometi e que continua aparecendo em fóruns é aplicar gravidade diretamente na posição em vez de na aceleração. O resultado é que a gravidade age de forma inconsistente dependendo do timestep. Sempre passe pela cadeia força -> aceleração -> velocidade -> posição. Outro erro clássico é esquelecer a normalização de vetores de força. Uma força aplicada com magnitude errada por causa de vetor não normalizado gera acelerações desproporcionais. Normalize vetores de direção antes de multiplicar pela magnitude da força.
Terceiro erro frequente: não considerar a massa na aplicação de forças externas. Empurrar um objeto leve e um pesado com a mesma força results em comportamentos irreais. Force = massa * aceleração. Se a massa do objeto é 10x maior, a aceleração resultante deve ser 10x menor para a mesma força aplicada.
Limitações que você precisa saber antes de começar
Simulações de física movimento baseadas em impulso têm limitações intrínsecas. Elas funcionam bem para objetos rígidos com colisões pontuais. Não funcionam bem para objetos macios, tecidos ou fluidos. Se seu projeto exige deformação de materiais, você precisa de um approach diferente, como finite element method ou position-based dynamics. Nada que eu explique aqui vai substituir isso. Outra limitação séria é a precisão numérica em escalas muito grandes ou muito pequenas. Float de 32 bits perde precisão rapidamente quando as coordenadas ultrapassam algumas centenas de metros. Simulações astronômicas ou microscópicas exigem double precision ou coordendas relativas. O custo de performance pode dobrar.
Colisões com geometria complexa também são um desafio. Meshes triangulares geram milhares de pares potenciais de colisão. Sem uma broad phase eficiente, seu simulador vai travar com mais de 20 objetos. Spatial partitioning como spatial hash, quadtree ou BVH é obrigatório acima de dez objetos em cena.
Alternativas e when to use them
Se você não quer implementar tudo do zero, bibliotecas como Box2D, Chipmunk e Bullet resolvem a maior parte do problema. Box2D é a escolha padrão para jogos 2D. Bullet para 3D. Ambas são estáveis, amplamente testadas e têm documentação razoável. O problema é que elas impõem suas próprias estruturas de dados e padrões de uso. Se seu projeto precisa de comportamento não padrão, você acaba gastando mais tempo adaptando a biblioteca do que implementando do início. Para projetos educacionais ou prototypes rápidos, implemente o básico sozinho primeiro. Entender o integrador, o solver de impulsos e a detecção de colisão em nível conceitual faz diferença quando algo dá errado. Terceiras versões resolvem problemas conhecidos, mas não vão explicar por que o seu caso específico quebrou.
Se o foco do seu projeto é física movimento pura sem necessidade de interatividade em tempo real, considere usar integração numérica com passos menores e validação com cenários analíticos conhecidos. Compare o resultado da simulação com a solução teórica de queda livre, projéteis com resistência do ar e pêndulos. A divergência entre simulação e teoria mede diretamente a qualidade da sua implementação.
Métricas para validar sua simulação
Monitore energia total do sistema em cada frame. Em um sistema isolado sem atrito, a energia mecânica deve permanecer constante. Se ela varia significativamente, seu integrador ou solver tem problemas. Monitore também o momento linear e angular. Ambos devem se conservar em colisões perfeitamente elásticas sem forças externas. Um gráfico de energia versus tempo é a ferramenta de diagnóstico mais útil. Linha reta horizontal significa simulação saudável. Linhas com tendência ascendente indicam ganho de energia, comum com Euler explícito. Tendência descendente pode significar damping excessivo ou erro de resolução de colisão. Identificar o padrão no gráfico economiza horas de debugging cego.
Teste cenários específicos: queda livre de diferentes massas, colisão frontal entre massas iguais, colisão oblíqua, rotação de um bastão livre. Cada cenário tem solução conhecida. Se sua simulação produz resultados diferentes sem motivo aparente, há um bug na implementação. Ajuste, teste, repita. É o único caminho que funciona consistentemente.