Pular para o conteúdo principal
tema 1 · protótipoengine · Godot · Jolt Physicsmodelo · raycast (força-baseado)sinkage · 0 · terreno rígidofrente · Integração roda–solo
Protótipo · rover por raycast

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.

Em resumo: troquei o motor de veículo pronto do Godot por um feito à mão, roda por roda. É mais trabalho — mas assim a física do contato com o chão fica isolada num único lugar. E é justamente esse lugar que vai ser substituído, mais pra frente, pela física de solo real (a ExoPhysics).
Corpo
RigidBody3D + 4 RayCast3D
Forças por roda
Suspensão · grip · tração
Fronteira
query_wheel_contact()
Futuro
Mesma assinatura → ExoPhysics
00

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.

Base · o que a Godot faz

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.

Base · o que fica pra nós

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ê.

Dois detalhes que o resto do texto usa. Primeiro: a massa do rover foi ajustada pra 12 kg (o padrão doRigidBody3D é 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.
prototipo-veiculo/scripts/rover.gd
# chamado a cada fatia de física (~1/60 s); delta = duração da fatia
func _physics_process(delta):
Base · RayCast3D

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.

solochassi · origem do raioraio(RayCast3D)target_position(alcance = curso)ponto de contatonormal
O RayCast3D é só um sensor — uma régua invisível presa ao chassi. Ele não empurra nada: informa se colidiu, onde (ponto de contato) e a inclinação do chão ali (a normal). O alcance (target_position) faz o papel do curso da suspensão.
Base · suspensão

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.

tempo →altura do chassisó mola — balança pra sempremola + amortecedor — assenta
Resposta do chassi a um baque, ao longo do tempo. Só a mola conserva a energia e oscila com amplitude constante — o carro viraria um pula-pula. O amortecedor dissipa energia a cada ciclo, então a oscilação decai e o corpo assenta.
02

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.

Antes

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.

Agora

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.

Arquivos. Projeto Godot isolado em 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:

1ª pessoa — câmera presa ao rover.
3ª pessoa — câmera externa, vendo o rover de longe.
03

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.

Força 1 · normal

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.

Forças 2 e 3 · no plano

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.

|Fplano| ≤ μ · Ncírculo de atrito — a força no plano nunca passa do coeficiente de atrito μ vezes a carga normal N (mola + amortecedor)
O rover (caixa verde) no editor do Godot; numa das quinas, o raycast de uma roda desenha os três vetores de força e o círculo de atrito no ponto de contato.
Debug no editor do Godot: o rover é a caixa verde. Numa das quinas, a visualização de uma roda — os três vetores de força (suspensão ao longo da normal, grip lateral e tração) somados no ponto de contato, contidos 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.

04

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.

prototipo-veiculo/scripts/ground_contact.gd
# ú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:
Entra · wheel_state.gd

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.

Sai · contact_result.gd

ContactResult

in_contact, ponto, normal, a force resultante e sinkage — o afundamento, sempre 0 nesta versão de terreno rígido.

O campo 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.
05

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.

sintoma

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.

a pista

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.

cena (vista de cima)roverganho 12ganho 20ganho 30
O mesmo bug, três ganhos. Mudar grip_lateral (12 → 20 → 30) só mudava a direção do arremesso, nunca o eliminava. Essa foi a pista central: erro de sinal ou de referencial daria sempre o mesmo tipo de erro; um sintoma que muda de forma sensível e aparentemente aleatória com o ganho é a cara de uma instabilidade numérica.

A implementação que causava tudo isso tinha três linhas — e uma intenção física perfeitamente razoável:

atrito lateral · primeira versão (com o bug)
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.

06

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:

v_lat pequenadeslizamento do ponto de contato (ex.: 0,3 m/s)
F = −v · ganho · Nforça sem teto — milhares de N
Δvaplica por Δtcorpo de ~12 kg, passo de ~0,016 s
v_lat maior e invertidaultrapassa o alvo, inverte o sinal — e realimenta

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.

Correção maior que o alvo é a semente da divergência: o erro do próximo frame já nasce maior que o deste.

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.

Assinatura clássica da integração explícita: quando o ganho de uma força corretiva é grande demais pro passo de tempo, a correção supera o alvo sistematicamente em vez de convergir.
velocidade lateral (m/s)0+0,3−4+12−28+60estoura → ejeçãoNN+1N+2N+3N+4
Velocidade lateral do ponto de contato, frame a frame. Cada correção ultrapassa o zero e inverte o sinal, com amplitude crescente — em poucos passos a magnitude sai da faixa estável e o corpo é ejetado.
Por que a direção mudava com o ganho. Não era aleatório — era determinístico, só que sensível de um jeito não intuitivo. Cada valor de ganho determina a taxa de crescimento e o número de inversões de sinal antes de a simulação estourar. A direção final do arremesso depende de em qual fase do ciclo (positiva ou negativa) a explosão numérica atingiu magnitude suficiente pra dominar o corpo. Daí o sintoma parecer errático.
07

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:

prototipo-veiculo/scripts/ground_contact.gd · correção
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_mag
R = μNF_ideal ≫ μNclamp → μN|F| ≤ μN
O círculo de atrito. A força que cancelaria a velocidade lateral num único frame (F_ideal) pode ser enorme; o clamp 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.
Sem teto (bug)força de correção, ~frame 6~10.000+ N ↗
Com clamp (μN)qualquer frame≤ μN

O mesmo teto foi aplicado à força de tração/frenagem. Duas forças, uma regra: nenhuma passa de ±μN.

Lição geral — vale como princípio, não só como fix pontual. Qualquer força que dependa da velocidade atual de um corpo pra corrigir essa mesma velocidade, numa simulação de passos discretos, precisa de um limite físico explícito no cálculo. Sem esse teto, o sistema pode divergir mesmo com a intenção física correta — o erro não está no conceito ("cancele o deslizamento"), está na ausência de um limite que impeça a correção de ultrapassar o problema que deveria resolver.
08

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.

Abordagem "jogo"

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.

Abordagem "engenharia"

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.