Pular para o conteúdo principal
sessão · 24 agofonte · código-fonte do Joltcommit · 2e28006epasta · Jolt/Physics/Vehicle/escopo · como funciona por dentro
Leitura de código · a mecânica interna

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.

Em resumo: a 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.
Corpos rígidos
Um — as rodas são dados, não corpos
Papel do raycast
Achar o segundo corpo da constraint
Ritmo
Percepção 1× · resolução N× por passo
Reencontro
μ · N, o mesmo clamp do meu bug
00

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.

Nota de método. Nada aqui vem de documentação ou tutorial. Clonei o 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.
01

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:

Jolt/Physics/Vehicle/VehicleConstraint.h:218
Body *   mBody;      // Body of the vehicle       ← singular
Wheels   mWheels;    // Wheel states of the vehicle

E Wheel não deriva de Body. É uma estrutura de dados com alguns floats:

Jolt/Physics/Vehicle/Wheel.h:136–138
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]
A roda tem velocidade de giro guardada num float. Ela não gira na física — gira num número, que o jogo depois usa pra girar a malha na tela.
o que eu esperava

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.

o que é

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.

Por que isso importa pro rover. Se o objetivo for um rover articulado de verdade — rocker-bogie, braços que pivotam, rodas com inércia própria —, a 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.
02

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:

Jolt/Physics/Vehicle/VehicleConstraint.cpp:508
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, ...);
O segundo corpo da constraint é o chão — e o chão de agora pode não ser o chão do próximo quadro.

É 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á.

A frase que reorganiza tudo. O raycast não é “o jeito barato de detectar o chão”. Ele é o que encontra o segundo corpo da constraint. Sem ele, a constraint não tem contra o que empurrar — e por isso os dois paradigmas não competem: um é pré-requisito do outro.
03

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.

chassi · o único RigidBodymPosition · origem do raiochão · o corpo 2 da constrainta roda não existe na físicaé o raio subtraído no fim da contasuspensionLengthmRadiusray_lengthmaxLength+ raiosuspensionLength = (distância medida) − mRadius
A bengala de cego. Não existe roda na simulação: existe um raio que mede a distância até o chão e finge que tem uma esfera de raio mRadius na ponta. O que sobra é o curso da suspensão.
04

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.

TesterO que disparaCusto × fidelidade
…TesterRayUm raio — uma linha infinitamente fina.Mais barato. Um degrau “aparece” de repente sob o raio: a roda sobe num tranco.
…TesterCastSphereUma 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.
…TesterCastCylinderUm cilindro — a forma real da roda, com largura.Mais caro e mais fiel. Sente a largura do pneu e o contato inclinado.
Relevância direta pro rover. Rover em terreno acidentado é exatamente o caso em que a diferença entre raio e cilindro aparece — pedra, degrau, borda de cratera. Esse dial existe pronto no Jolt; no Godot, o VehicleWheel3D não me dá essa escolha. Vale registrar como requisito quando a frente de integração roda–solo voltar à pauta.
05

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.

eixos no plano do chão

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

velocidade do chão

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

06

O ciclo de um passo: percepção × resolução

A VehicleConstraint é duas coisas ao mesmo tempo, e é isso que permite os dois paradigmas conviverem:

Jolt/Physics/Vehicle/VehicleConstraint.h:65
class VehicleConstraint : public Constraint, public PhysicsStepListener

PhysicsStepListener — “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.

um passo de físicaFASE A · OnStep() — fora do solver1. callback de input (volante, acelerador)2. RAYCAST por roda → acha o chão3. barras estabilizadoras4. motor distribui torqueacontece 1× por passopercepçãoFASE B · dentro do solverSetupVelocityConstraint() · monta equaçõesSolveVelocityConstraint() · aplica impulsosSolvePositionConstraint() · corrige derivaacontece N× por passoresolução
Dois ritmos, nenhum conflito. O raycast roda uma vez, fora do solver, e produz estado. Os impulsos rodam várias vezes, dentro do solver, e produzem movimento. Não são etapas concorrentes: são etapas encadeadas.
07

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):

Jolt/Physics/Vehicle/Wheel.h:140–143
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 — derrapagem

A 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

E depois da mola, a parede. Se a suspensão passa do curso mínimo (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.
08

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:

Jolt/Physics/Vehicle/WheeledVehicleController.h:193–195
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:

impulso máximo do pneu = μ × Ncoeficiente de atrito × carga normal — o círculo de atrito
É exatamente o clamp que eu implementei na mão pra parar de ejetar o rover da cena.
Meu protótipo raycastJolt
Fórmula do limiteμ · Nμ · N — idêntica
De onde vem o NEu calculo a força da molaGetSuspensionLambda() — o impulso que o solver já aplicou neste passo
Onde o limite ageEu limito a força antes de aplicarVira o mín/máx do impulso que o solver tem permissão de usar
Se estourarO erro cresce no quadro seguinte — foi o bug da ejeçãoNão há como estourar: o limite é parte da equação
O que eu tiro disso. A correção que eu fiz no protótipo não foi gambiarra — é o caminho canônico, o mesmo que uma biblioteca de física madura usa. A diferença entre os dois não está na física; está em onde o limite mora: fora do solver, como um cuidado meu, ou dentro dele, como parte da formulação. Isso é a versão concreta da distinção “penalidade × constraint” que abriu esta frente.
Um refinamento a mais no carro. No 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–55
09

Dois detalhes de engenharia que valem registrar

artifício de jogo

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.

otimização

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

10

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.

Em aberto. Se o rover for skid-steer, o 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.