O contato roda-solo virou uma fronteira de API.
Refiz o rover em Godot trocando o VehicleBody3D pronto por um modelo força-baseado por raycast: um corpo rígido, quatro raios, e cada roda calculando as próprias forças. O ganho real não é o carro andar — é que todo o contato roda-solo agora mora numa única função, uma fronteira de API pensada pra ser trocada pela ExoPhysics sem mexer no resto. Estas notas registram o que foi construído, o bug que ensinou uma lição de simulação discreta e como isso se encaixa (ou não) no modelo multicorpo do Bloco 5.
Conceitos-base
Antes do "como resolvemos", três peças que o resto do dossiê assume conhecidas. Nada aprofundado — só o suficiente pra acompanhar quem não vive de Godot nem de física de simulação.
RigidBody3D: resolvido sozinho
Um RigidBody3D é um corpo que a Godot move segundo a física: ela cuida sozinha da gravidade, da integração do movimento e da detecção de colisão. O rover inteiro é um único corpo desses.
As forças de contato
O que a Godot não sabe é como a roda toca o chão. Toda força de suspensão, grip e tração é nossa pra calcular e aplicar — é justamente o assunto deste dossiê.
RigidBody3D é 1 kg) — leve demais, ele reagiria a qualquer força como uma bola de pingue-pongue, sem a inércia que assenta o corpo. Segundo, e mais importante: a física não roda contínua, e sim em fatias discretas de tempo — o método _physics_process é chamado ~60×/s, cada chamada avançando um delta ≈ 1/60 s. Esse passo discreto é o que abre a porta pro bug da seção 06.# chamado a cada fatia de física (~1/60 s); delta = duração da fatia
func _physics_process(delta):Um sensor, não um atuador
Cada roda é representada por um RayCast3D: uma "régua invisível" presa ao chassi, apontando pra baixo. Ele não aplica força nenhuma — só informa. Tem uma origem (fixa no chassi), um alcance (target_position, que faz o papel do curso da suspensão) e, quando encosta em algo, devolve o ponto de contato e a normal — a inclinação do chão ali. Transformar isso em força é trabalho nosso, na seção 03.
Por que precisa de mola E amortecedor
A mola sozinha devolve toda a energia que recebe: comprime, empurra de volta, e o chassi balança pra sempre — nada tira energia do sistema. O amortecedor é a peça que dissipa essa energia a cada ciclo, fazendo a oscilação decair até o corpo assentar. Um sem o outro não estabiliza — e é por isso que a força de suspensão (seção 03) soma as duas.
O que foi construído
O rover é um único RigidBody3D com quatro RayCast3D — um por roda — apontando pra baixo. Nada de VehicleBody3D nem VehicleWheel3D: as rodas não existem como corpos, são apenas pontos onde um raio toca o chão e uma força é aplicada. A cada passo de física, rover.gd percorre os quatro raios, monta o estado de cada roda e aplica no ponto de contato a força que a fronteira devolve.
VehicleBody3D (arcade)
Motor de veículo pronto do Godot. Anda de imediato, mas a física do pneu é uma caixa-preta — sem lugar limpo pra plugar outro modelo de contato depois.
Raycast força-baseado
Corpo rígido + 4 raios. Cada roda calcula três forças nomeadas e as soma no ponto de contato. Mais trabalho manual, mas cada força fica visível e isolada — e o contato vira uma fronteira trocável.
prototipo-veiculo/, fora do Docusaurus. O laço de física e a montagem do estado ficam em prototipo-veiculo/scripts/rover.gd; a cena, em scenes/rover.tscn.E aqui está ele rodando — as três forças já somadas em cada roda, em primeira e em terceira pessoa:
Três forças por roda
Toda a física de contato vive em prototipo-veiculo/scripts/ground_contact.gd. Para cada roda em contato, três forças são calculadas e somadas.
Suspensão (mola–amortecedor)
Ao longo da normal do contato. A mola é proporcional à compressão do raio; o amortecedor, à velocidade de compressão. Juntas, seguram o rover em pé sobre os quatro raios.
Grip lateral + tração
No plano do solo: o grip mata a velocidade lateral (evita derrapar) e a tração/freio empurra na direção de rolagem. Ambas limitadas pelo círculo de atrito.

A carga normal N sai da própria suspensão daquele frame, e tanto o grip quanto a tração entram num clamp dentro de ±μN. Esse teto não é cosmético — é o que impede a divergência descrita na seção 06.
A fronteira: WheelState → ContactResult
O ponto central do refactor: rover.gd não lê raycast nem calcula física. Ele só monta um WheelState (o que a roda sabe de si), chama uma função e aplica a força que volta num ContactResult. Toda a implementação do contato fica atrás dessa única porta — quando a ExoPhysics existir, o miolo é trocado e a assinatura fica igual.
# única função que sabe calcular contato roda-solo.
# v0: raycast + mola-amortecedor + atrito. v1: miolo trocado pela ExoPhysics.
static func query_wheel_contact(
ray: RayCast3D, state: WheelState,
input_acelerar: float, params: Dictionary, delta: float
) -> ContactResult:WheelState
Posição, base (orientação), velocidades linear e angular, massa e a compressao_anterior — o que a roda "sabe de si mesma" antes de consultar o solo.
ContactResult
in_contact, ponto, normal, a force resultante e sinkage — o afundamento, sempre 0 nesta versão de terreno rígido.
sinkage é uma promessa. Ele fica zerado na v0 porque o terreno é rígido, mas já está no contrato: é o lugar reservado pra quando a ExoPhysics substituir o raycast como fonte da força e o solo passar a ser deformável — a ponte pra terramecânica desta frente. A ExoPhysics é a biblioteca em C++/GPU que implementa a modelagem híbrida de solo descrita na proposta do PIBIC: partículas SPH perto do contato — onde o solo escoa e afunda de verdade — e uma malha contínua/semi-empírica longe dele, onde uma aproximação barata basta. Ou seja, ela não é só "um substituto futuro": é a peça que resolve o trade-off entre fidelidade física e tempo real que motiva o projeto inteiro — e o sinkage é o primeiro fio desse contrato já aparecendo aqui.O bug: o sintoma e a pista
A suspensão mola–amortecedor já rodava estável antes deste bug aparecer. Ele foi introduzido num ponto muito específico: ao ligar a força de atrito lateral (o grip). Assim que o rover tocava o chão, em vez de assentar sobre os quatro raios, ele era imediatamente arremessado pra fora da cena, girando sem controle.
Ejetado no primeiro toque
No frame em que o raio encostava no solo, o corpo disparava pra longe com rotação descontrolada — nada de assentar. Acontecia sempre, com qualquer valor de ganho.
Mudar o ganho mudava a direção, não resolvia
Variar grip_lateral não eliminava o arremesso — só trocava pra onde o rover voava. Ganho 12 saía pela diagonal inferior esquerda; 20, pela inferior direita; 30, pela superior esquerda.
A implementação que causava tudo isso tinha três linhas — e uma intenção física perfeitamente razoável:
var vel_lateral = lateral * vel_no_ponto.dot(lateral)
var forca_grip = -vel_lateral * grip_lateral * (forca_mola + forca_amortecedor)
apply_force(forca_grip, braco)A intenção: "olhe a velocidade lateral atual do ponto de contato e aplique uma força proporcional, no sentido oposto, pra cancelar o deslizamento". O conceito está certo. O problema é o que falta: nenhum limite de magnitude.
A causa raiz: realimentação com atraso de um passo
O Godot resolve a física em passos discretos (por padrão, 1/60 s). Não existe realimentação instantânea entre "a força que aplico agora" e "o efeito que ela produz" — o efeito só aparece no frame seguinte. Sem um teto, a fórmula multiplicava grip_lateral (12–30) pela carga de suspensão (já na casa de centenas de newtons), gerando milhares de newtons pra velocidades laterais de frações de m/s. Esse atraso de um passo entre causa e efeito fecha um laço de realimentação positiva:
Frame a frame, a correção não converge pra zero — ela passa do ponto e volta amplificada:
Frame N — a semente
Uma velocidade lateral pequena (ex.: 0,3 m/s) gera uma força de correção de magnitude desproporcional — milhares de N.
Ainda no frame N — a ultrapassagem
Essa força, aplicada por um delta de ~0,016 s a um corpo de ~12 kg, produz uma mudança de velocidade que não só cancela o deslizamento: ultrapassa e inverte o sinal com folga — de +0,3 m/s pra algo como −4 m/s.
Frame N+1 — a amplificação
A mesma fórmula, agora vendo uma velocidade maior e invertida, calcula uma força de correção ainda maior, no sentido oposto.
Frames N+2, N+3… — a explosão
O padrão se repete e cada frame produz uma força maior que o anterior: crescimento exponencial. Em cerca de 7–8 frames físicos a força já está na casa de dezenas de milhares de N e o corpo é ejetado da cena.
A correção: o círculo de atrito
Fisicamente, a força de atrito nunca pode exceder μ · N — o círculo de atrito —, não importa quão rápido o corpo deslize. Essa restrição simplesmente não existia na primeira versão. A correção calcula a mesma força "ideal" que cancelaria a velocidade lateral num frame, mas a limita (com clamp) ao teto físico do atrito disponível naquele contato:
var carga_normal = forca_mola + forca_amortecedor
var limite_atrito = carga_normal * coef_atrito # μ · N
var forca_grip_ideal = -vel_lateral_escalar * mass / delta
var forca_grip_mag = clamp(forca_grip_ideal, -limite_atrito, limite_atrito)
var forca_grip = lateral * forca_grip_magclamp a projeta de volta pra borda μN. Como a força nunca supera o atrito real disponível, o excesso que iniciava o ciclo de realimentação deixa de existir — a estabilidade vira garantia por construção.O mesmo teto foi aplicado à força de tração/frenagem. Duas forças, uma regra: nenhuma passa de ±μN.
Como isso se encaixa no Bloco 5
A tabela do Bloco 5 descrevia o contato em termos de juntas (prismática pra suspensão, revoluta pro giro da roda). O modelo raycast não usa junta nenhuma: ele chega no mesmo lugar por força de penalidade. Vale registrar o contraste explicitamente.
Força-baseada (raycast)
Mola–amortecedor + atrito resolvem por penalidade o que seria junta: o raio "empurra de volta" quando penetra. Simples, estável se limitada, ideal pra tempo real. É a rota deste protótipo.
Multicorpo com restrições
Cada roda é um corpo ligado por juntas reais, resolvidas por um solver de restrições (tipo Chrono::Vehicle). Mais fiel e mais caro — o modelo rigoroso do Bloco 5.