Um corpo só, quatro bengalas.
As duas entradas anteriores desta frente estabeleceram que a VehicleConstraint existe e por que o Godot não a entrega. Ficou faltando o principal: como ela funciona. Abri o código-fonte do Jolt e li a implementação inteira — e a resposta reorganiza o que eu achava sobre a relação entre raycast e constraint. A peça central é que o veículo do Jolt tem um único corpo rígido, e que o raycast não é um detalhe do contato: ele é o que descobre com quem a constraint está amarrada.
VehicleConstraint não liga o chassi às rodas — ela liga o chassi ao chão. Como o chão muda a cada instante, alguém precisa encontrá-lo: é o raycast, disparado do ponto de fixação da suspensão. A distância que ele mede, menos o raio da roda, vira o comprimento atual da mola. Daí em diante tudo é constraint resolvida no solver — inclusive um limite de atrito que é, literalmente, o mesmo clamp μ·N que eu escrevi à mão no protótipo.A pergunta que as outras duas entradas deixaram aberta
Em Constraints, juntas e o Jolt por dentro eu estabeleci o paradigma: no raycast eu empurro, no multicorpo eu amarro. Em O tanque do Jolt e o teto do Godot eu mapeei as peças do veículo do Jolt pelos headers e provei que o VehicleBody3D do Godot não é uma exposição delas. As duas entradas descrevem o que existe. Nenhuma responde como a peça opera — e sem isso a frase “o veículo do Jolt usa raycast e constraints juntos” fica sendo slogan, não entendimento.
jrouwe/JoltPhysics e li os arquivos da pasta Jolt/Physics/Vehicle/ — headers e implementação. Todas as citações de arquivo:linha desta página conferem com o commit 2e28006e.A revelação: existe um corpo rígido só
A expectativa natural, depois de duas semanas estudando multicorpo, é que um veículo do Jolt seja um esqueleto: chassi, quatro rodas, tudo ligado por juntas. Não é. Os membros da classe não deixam margem pra dúvida:
Body * mBody; // Body of the vehicle ← singular
Wheels mWheels; // Wheel states of the vehicleE Wheel não deriva de Body. É uma estrutura de dados com alguns floats:
float mSteerAngle = 0.0f; // ângulo de esterço
float mAngularVelocity = 0.0f; // velocidade de giro (rad/s)
float mAngle = 0.0f; // rotação atual, [0, 2 pi]float. Ela não gira na física — gira num número, que o jogo depois usa pra girar a malha na tela.Um esqueleto
Cinco corpos rígidos (chassi + 4 rodas), ligados por juntas — a imagem que eu construí na quinzena conceitual, com HingeJoint3D no eixo e SliderJoint3D na suspensão.
Um caixote com bengalas
Um corpo rígido, e N sondas apontando pra baixo. Cada sonda tateia o chão e, onde encosta, vira um pistão que empurra o caixote. Nenhuma roda é simulada como corpo.
VehicleConstraint do Jolt não é a resposta pronta que eu imaginava. Ela é um modelo lumped muito bem feito, da mesma família do meu protótipo raycast, e não um multicorpo articulado. Isso muda a expectativa sobre o que “expor a VehicleConstraint no Godot” resolveria.Então a constraint é entre o veículo e o chão
Uma constraint liga dois corpos. Se o veículo é um corpo só, quem é o segundo? A resposta está no argumento que o Jolt passa ao montar a equação da suspensão:
w->mSuspensionPart.CalculateConstraintPropertiesWithStiffnessAndDamping(
inDeltaTime,
*mBody, // corpo 1 · o veículo
r1_plus_u,
*w->mContactBody, // corpo 2 · O CORPO QUE A RODA ESTÁ TOCANDO
r2, ...);É aqui que a peça encaixa. Uma dobradiça sabe, desde que foi criada, quais dois corpos ela liga; a ligação é fixa. Um veículo não sabe — o corpo do outro lado muda a cada instante: uma pedra, uma rampa, uma plataforma em movimento, outro veículo. Alguém precisa descobrir, a cada passo, quem é o corpo 2 e em que ponto exato ele está.
O raycast, linha por linha
O teste de contato é uma classe abstrata, VehicleCollisionTester, com implementações trocáveis. A mais simples é a de raio puro — VehicleCollisionTesterRay::Collide, em VehicleCollisionTester.cpp:19.
De onde o raio sai
Origem = o ponto onde a suspensão é parafusada no chassi (mPosition, espaço local), convertido pro mundo. Direção = a direção da suspensão (padrão {0, -1, 0}), girada junto com o corpo — se o veículo inclina, os raios inclinam junto.VehicleConstraint.cpp:201–202
Que tamanho ele tem
ray_length = mSuspensionMaxLength + mRadius. O raio vai até onde o chão encostaria na roda com a suspensão totalmente estendida — nem um centímetro além.VehicleCollisionTester.cpp:32
O que ele recusa como chão
O próprio veículo (IgnoreSingleBodyFilter, pra não bater no chassi), sensores (:56) e superfícies mais íngremes que 80° — normal.Dot(up) > cos(maxSlopeAngle) (:62). Sem esse último filtro, o veículo escalaria paredes usando a suspensão como pé.
E a conversão que é o coração da coisa
outSuspensionLength = max(0, ray_length × fraction − wheel_radius). A distância medida até o chão, menos o raio da roda, é o comprimento atual da mola.VehicleCollisionTester.cpp:100 · aqui a geometria do mundo vira o estado do sistema mecânico.
mRadius na ponta. O que sobra é o curso da suspensão.Três bengalas diferentes, mesma interface
O VehicleCollisionTester é abstrato de propósito: o modo de detectar o chão é plugável, e a escolha é um dial explícito entre custo e fidelidade.
| Tester | O que dispara | Custo × fidelidade |
|---|---|---|
| …TesterRay | Um raio — uma linha infinitamente fina. | Mais barato. Um degrau “aparece” de repente sob o raio: a roda sobe num tranco. |
| …TesterCastSphere | Uma esfera varrida ao longo da direção da suspensão. | Intermediário. A esfera sobe suave num meio-fio, como uma roda de verdade faria. |
| …TesterCastCylinder | Um cilindro — a forma real da roda, com largura. | Mais caro e mais fiel. Sente a largura do pneu e o contato inclinado. |
VehicleWheel3D não me dá essa escolha. Vale registrar como requisito quando a frente de integração roda–solo voltar à pauta.O que mais a etapa de percepção coleta
Achar o chão é só o começo. Depois do contato, o Jolt monta mais duas coisas que eu, no meu protótipo, calculo na mão — e que explicam por que o veículo dele se comporta bem em situações que o meu trataria como caso especial.
Ladeira sai de graça
As direções de tração e de derrapagem são construídas a partir da normal do contato, não do chassi: longitudinal = normal × right, depois lateral = longitudinal × normal. Numa ladeira, “pra frente” é a frente da ladeira — sem nenhum caso especial no código.VehicleConstraint.cpp:261–271
Plataforma móvel sai de graça
Ele guarda mContactBody->GetPointVelocity(contactPosition) e trabalha com a diferença de velocidade, não com a velocidade absoluta do veículo. Resultado: andar sobre uma plataforma em movimento, ou sobre outro veículo, funciona sem uma linha extra.VehicleConstraint.cpp:246
O ciclo de um passo: percepção × resolução
A VehicleConstraint é duas coisas ao mesmo tempo, e é isso que permite os dois paradigmas conviverem:
class VehicleConstraint : public Constraint, public PhysicsStepListenerPhysicsStepListener — “me avise antes do passo”
É o gancho que dá à constraint uma chance de rodar código fora do solver, uma vez por passo. É onde o raycast acontece.
Constraint — “e me inclua dentro do passo”
É o que faz o solver de impulsos considerar as equações do veículo junto com todas as outras restrições da cena, iterando várias vezes.
As quatro equações de cada roda
Encontrado o chão, cada roda passa a contribuir com quatro restrições para o solver — todas do mesmo tipo primitivo, AxisConstraintPart (restrição ao longo de um eixo):
AxisConstraintPart mSuspensionPart; // movimento ao longo da normal — a mola
AxisConstraintPart mSuspensionMaxUpPart; // o batente rígido no fim do curso
AxisConstraintPart mLongitudinalPart; // frente/trás — tração e freio
AxisConstraintPart mLateralPart; // lado — derrapagemA mola, em três detalhes que revelam cuidado
O erro da mola é uma subtração
c = suspensionLength − mSuspensionMaxLength − mSuspensionPreloadLength. Se a suspensão está totalmente estendida, c = 0 e não há força. Comprimida, c fica negativo e a mola empurra.VehicleConstraint.cpp:506
Rigidez em frequência, não em N/m
Você declara “esta suspensão oscila a 1.5 Hz com amortecimento 0.5”, e o Jolt converte pra rigidez usando a massa efetiva do veículo naquele ponto. É afinar suspensão como engenheiro afina, não chutando constante de mola.
A mola só empurra
Na hora de resolver, o intervalo de impulso permitido é [0, FLT_MAX] — mínimo zero. O chão não gruda na roda. Óbvio fisicamente, mas alguém teve que escrever aquele 0.0f.VehicleConstraint.cpp:576
VehicleConstraint.cpp:514), entra a mSuspensionMaxUpPart — uma constraint rígida que simplesmente proíbe continuar afundando. Mola macia até o limite, batente duro depois. É exatamente o par que o SliderJoint3D do Godot não consegue formar: lá só sobra a segunda metade.O reencontro: μ · N, o mesmo clamp do meu bug
Falta a pergunta mais importante pro meu projeto: como o Jolt impede que a tração e o grip apliquem mais força do que o pneu aguenta? O callback padrão responde em duas linhas:
outLongitudinalImpulse = inLongitudinalFriction * inSuspensionImpulse;
outLateralImpulse = inLateralFriction * inSuspensionImpulse;O inSuspensionImpulse vem de w->GetSuspensionLambda() — o impulso que a mola aplicou, ou seja, a carga normal daquela roda. Traduzindo:
| Meu protótipo raycast | Jolt | |
|---|---|---|
| Fórmula do limite | μ · N | μ · N — idêntica |
| De onde vem o N | Eu calculo a força da mola | GetSuspensionLambda() — o impulso que o solver já aplicou neste passo |
| Onde o limite age | Eu limito a força antes de aplicar | Vira o mín/máx do impulso que o solver tem permissão de usar |
| Se estourar | O erro cresce no quadro seguinte — foi o bug da ejeção | Não há como estourar: o limite é parte da equação |
WheeledVehicleController o μ não é fixo: é uma curva por deslizamento — o pneu agarra mais a 6% de slip (1.2) do que parado (0.0) ou patinando muito (1.0). É comportamento real de pneu. No tanque, μ é constante: esteira não tem curva de slip.WheeledVehicleController.cpp:47–55Dois detalhes de engenharia que valem registrar
A constraint anticapotamento
Existe uma mPitchRollPart que mantém o “pra cima” do veículo dentro de um cone, pra ele não tombar (VehicleConstraint.cpp:400). É conveniência de jogo, não física pura — num rover eu provavelmente vou querer desligar, porque capotar é um resultado legítimo que eu quero medir.
Nem toda roda faz raycast todo passo
Existe mNumStepsBetweenCollisionTestActive: nos passos pulados, o Jolt usa PredictContactProperties, que extrapola assumindo que o chão é um plano infinito com a mesma normal. Raycast é caro; na maior parte do tempo o chão não mudou.VehicleConstraint.cpp:190, 205–222
O que essa leitura muda
Raycast e constraint são etapas, não escolhas
O raycast é a percepção (1× por passo, fora do solver): acha o chão e converte geometria em estado. A constraint é a resolução (N× por passo, dentro do solver): converte estado em impulso. Dizer “raycast ou multicorpo” é uma falsa escolha — o Jolt encadeia os dois.
Meu protótipo já tem a primeira metade
O RayCast3D do Godot me dá a percepção completa: eu sei onde bati, com que normal, contra qual corpo. O que eu faço na mão, com força explícita, é a segunda metade.É por isso que o problema aparece como instabilidade: eu escrevo a resolução do lado de fora do solver.
E é exatamente a segunda metade que falta no Godot
Nem pela VehicleConstraint (não exposta), nem pelo SliderJoint3D (que no backend Jolt perde a mola e vira só batente). O levantamento feature a feature está em O tanque do Jolt e o teto do Godot.
A VehicleConstraint não é o rover articulado
Ela é um modelo lumped de um corpo só, muito bem feito. Rocker-bogie com braços que pivotam continua sendo um problema de multicorpo de verdade — que nem o Jolt entrega pronto.Corrige uma expectativa que eu carregava: “expor a VehicleConstraint no Godot” resolveria o veículo, não o rover articulado.
TrackedVehicleController é o modelo de referência e a via do nó pronto do Godot está fechada. Se for rocker-bogie articulado, nenhuma das duas vias resolve sozinha — seria multicorpo montado à mão, com o contato roda–solo ainda por cima. A decisão do esquema de direção continua pendente com o professor.Fontes
Todas as citações vêm do repositório clonado localmente, commit 2e28006e, pasta Jolt/Physics/Vehicle/. Os links apontam pro mesmo arquivo na branch master.