Pular para o conteúdo principal
sessão · 5 setmodo · construçãoalvo · Godot 4.7 + Jolt 5.6código · gdjolt/estado · ponte atravessada
Decisão de arquitetura · a faixa B

Dois mundos, uma cena.

As três entradas anteriores desta frente terminaram no mesmo lugar: o Godot não expõe a VehicleConstraint do Jolt, e não há arranjo de nós Joint3D que a substitua. Medir esse vão já estava feito. Esta entrada é sobre atravessá-lo — e a travessia tem um nome: uma GDExtension em C++ que roda um PhysicsSystem do Jolt próprio, compilado por mim, em vez de tentar alcançar o que está embutido no motor.

Em resumo: o Godot deixa de ser o dono da física e passa a ser renderizador e input. Quem simula é o Jolt, num mundo paralelo que a extensão cria e avança. A prova de que isso funciona não é estética: o veículo rodando dentro do Godot produz exatamente os mesmos números que o mesmo veículo rodando num binário de terminal sem Godot nenhum.
01

A bifurcação: dois desenhos possíveis

Antes de escrever uma linha, havia duas maneiras de chegar na VehicleConstraint. Uma delas já estava descartada pela evidência da faixa A — mas vale registrar por quê, porque é a pergunta que qualquer pessoa faz primeiro.

Desenho B · alcançar o Jolt embutido

O Godot 4 já usa o Jolt como backend de física. A ideia óbvia é pegar carona: pedir ao motor o PhysicsSystem que ele já mantém e adicionar a constraint lá dentro.

Não funciona. O módulo embutido não expõe os símbolos do Jolt na API de extensão — nem a VehicleConstraint, nem os VehicleCollisionTester, nem os controllers. Não é questão de dificuldade: a superfície simplesmente não existe.

Desenho A · mundo Jolt próprio

A extensão compila o Jolt como dependência sua e cria o próprio PhysicsSystem. Tem acesso total à biblioteca, porque a biblioteca é dela.

É o caminho escolhido. Custa manter dois mundos em sincronia — o da física e o da cena — e transforma o clone do Jolt em dependência de build. Em troca, nada fica fora de alcance.

A consequência que importa: a partir daqui, ~/Projects/JoltPhysics deixa de ser material de leitura e vira dependência de compilação. A versão do Jolt passa a ser uma decisão do projeto, não um detalhe do motor — o que é exatamente o controle que faltava.
02

O que foi construído, em ordem de dependência

Quatro peças, cada uma só fazendo sentido se a anterior fechou. A ordem não é burocracia: é o que permite saber onde está o erro quando algo quebra.

PeçaO que ela prova
godot-cppQue os bindings compilam contra a versão do editor que eu tenho de fato. Sem isso, nada mais é verificável.
JoltProbeQue a ponte existe — a extensão carrega, a classe aparece no ClassDB, a propriedade chega ao Inspector e o Godot chama o meu _physics_process em C++ a cada passo. Sem uma linha de física.
vehicle_probeQue o Jolt sozinho está certo. Mesmo veículo, mesma VehicleConstraint, num binário de terminal — sem Godot em lugar nenhum.
JoltVehicleQue as duas metades se juntam: o mundo do Jolt avança dentro do loop do Godot e o resultado chega nas transforms da cena.

O vehicle_probe existir separado é o que torna o resto diagnosticável. Quando algo der errado adiante, a pergunta "é o Jolt ou é a ponte?" tem resposta em um comando.

GODOTinput do teclado_physics_processMeshInstance3DJOLT (meu)SetDriverInputPhysicsSystem::UpdateGetWorldTransformJoltVehiclea GDExtension em C++
Um passo de física. O Godot fornece o tique e o input; o Jolt resolve; as transforms voltam para a cena. O motor não simula nada aqui.

E é assim que a extensão aparece do lado do editor: JoltVehicle é um nó como qualquer outro na árvore, com os quatro filhos que recebem a transform de cada roda. Nada aqui denuncia que a física por trás não é a do motor.

Árvore de nós do projeto de demo no editor do Godot, com o nó Vehicle e os filhos Chassis, Wheel0 a Wheel3
A cena de demo no editor. Vehicle é a classe registrada pela GDExtension; Wheel0..3 são só malhas que ela posiciona.
03

A armadilha: paridade de ABI

O erro mais caro da sessão não foi de lógica nem de física. Foi de flag de compilação — e é o tipo de coisa que só se aprende batendo.

Sintoma

A libJolt.a compilou. O programa de teste compilou. O link falhou com dezenas de undefined reference to JPH::AssertFailed — um símbolo que eu nunca escrevi e não estava tentando usar.

Causa

A lib foi compilada em modo Distribution, que define NDEBUG e portanto desliga os asserts do Jolt. O meu programa compilou sem NDEBUG, então os headers do Jolt ligaram JPH_ENABLE_ASSERTS e passaram a esperar um símbolo que a lib não tinha.

O NDEBUG foi só o sintoma visível. O problema real é maior: o Jolt muda o layout das structs conforme um punhado de defines e conforme as instruções SIMD habilitadas. Quando esses conjuntos divergem entre a lib e quem a inclui, o caso gentil é um erro de link como este. O caso ruim é compilar, linkar e quebrar em runtime, sem mensagem nenhuma.

O que precisa baterPor quê
NDEBUGDecide se JPH_ENABLE_ASSERTS existe — e portanto se o símbolo JPH::AssertFailed é esperado no link.
JPH_OBJECT_STREAMAdiciona RTTI e atributos de serialização às classes.
JPH_DOUBLE_PRECISIONTroca o tipo Real — muda o tamanho de toda posição no sistema.
JPH_PROFILE_ENABLEDInsere campos de profiling dentro das structs.
-mavx2, -mfma, ...Mudam alinhamento e a implementação SIMD inline nos headers.
A solução não foi anotar os flags certos. Foi parar de digitá-los: tanto o SConstruct da extensão quanto o script do programa de teste leem o compile_commands.json que o CMake gerou ao compilar a própria lib, e espelham dali. Se o build do Jolt for reconfigurado, os dois acompanham sozinhos — a paridade deixa de depender de disciplina.
gdjolt/SConstruct · o trecho que garante a paridade
entry = next(e for e in entries if "/Jolt/" in e["file"])
args = shlex.split(entry["command"])

defines = [a[2:] for a in args if DEFINE_RE.match(a)]   # JPH_* e NDEBUG
simd    = [a for a in args if SIMD_RE.match(a)]         # -mavx2, -mfma, ...
04

O detalhe de versão que o plano não previa

O roadmap dizia "clonar o godot-cpp na branch que casa com o editor". Não dá: o repositório não tem branch 4.7 — as branches vão até a 4.5, mais a master, e a tag mais recente é godot-4.5-stable.

O que resolve é que a masterembute o arquivo extension_api-4-7.json, e o parâmetro api_version=4.7 seleciona esse arquivo. Antes de confiar nisso, comparei o arquivo embutido com o --dump-extension-api tirado do binário do editor instalado aqui: mesmo version_full_name, mesmo conjunto de classes.

Vale guardar: o binário do Godot sabe descrever a própria API. --dump-extension-api e --dump-gdextension-interface geram exatamente o que o godot-cpp consome. Se um dia a versão do editor não tiver correspondente no repositório dos bindings, o caminho é compilar contra o dump do próprio binário via custom_api_file=.
05

A prova: os mesmos números dos dois lados

Uma ponte que parece funcionar não vale nada. O teste que decide é comparar o veículo rodando dentro do Godot com o mesmo veículo rodando num binário de terminal sem Godot algum — e ver se os números batem.

Grandeza (após 3 s, acelerando)Jolt puroDentro do Godot
Altura do corpo0.8440.844
Comprimento da suspensão · dianteira0.37730.3773
Comprimento da suspensão · traseira0.35130.3513
Impulso de suspensão · dianteira55.455.4
Impulso de suspensão · traseira67.167.1

Iguais até a última casa. Isso diz uma coisa específica e valiosa: a ponte não distorce a física. O que o Godot mostra é o que o Jolt calculou, não uma aproximação atravessada por conversão de unidades ou por um passo de tempo diferente.

medido: Σ impulso de suspensão = 245,0 N·s
teoria: m·g·Δt = 1500 × 9,81 / 60 = 245,2 N·s
erro: 0,1 %

E os números fecham com a teoria, não só entre si. Em regime, a soma dos quatro impulsos de suspensão tem que sustentar o peso do veículo em um passo — e sustenta, com 0,1 % de erro. A repartição também é a esperada: a traseira comprime mais que a dianteira (0,3513 contra 0,3773) e carrega mais impulso (67,1 contra 55,4). É transferência de carga sob aceleração, aparecendo sozinha, sem ninguém ter programado "transferência de carga".

O que essa tabela substitui: até aqui, a frente multicorpo argumentava a partir de leitura — headers, proposals, código-fonte. Esta é a primeira vez que a afirmação vem de medição, com o número ao lado.

E, finalmente, o que os números descrevem — dirigindo o carrinho na cena de demo, com a física resolvida pelo Jolt compilado aqui:

O JoltVehicle dirigido no Godot. A suspensão que trabalha aqui é a VehicleConstraint — a mesma que o VehicleBody3D do motor não expõe.
06

Quem é dono de quê

Rodar dois mundos exige decidir, sem ambiguidade, quem manda em cada coisa. Ambiguidade aqui vira jitter, corpo tremendo ou simulação que diverge da imagem.

ResponsabilidadeDono
Estado físico (posição, velocidade, contatos)Jolt — a cena é só reflexo
Quando o passo aconteceGodot — o _physics_process dá o tique
Tamanho do passoFixo, com clamp — um travamento do processo não pode virar um salto grande demais pro solver
Transforms dos nósEscritas pela extensão a cada passo; mexer nelas por fora é sobrescrito
Input do motoristaGodot, repassado via SetDriverInput
Colisão do veículo com o mundoNão é do Godot — corpos StaticBody3D da cena não existem para o Jolt. O mundo precisa ser construído dos dois lados
A limitação honesta desta versão: o chão é um corpo criado dentro da extensão, e a malha que aparece na tela é só decoração alinhada com ele. Fazer a geometria da cena virar geometria do Jolt automaticamente é trabalho de verdade, e não estava no escopo desta etapa — mas é o próximo obstáculo real, não um detalhe.
07

A conferência: o molde do professor

A decisão de arquitetura acima foi tomada antes de eu ver o GDChrono — a GDExtension que o professor escreveu ligando o Godot ao Project Chrono. Ler o código dele depois é o melhor tipo de conferência: ou eu tinha acertado por conta, ou aprenderia onde errei.

A escolha central bate. A classe Ch::World dele guarda um chrono::ChSystemSMC próprio, e o ChManager é um Node3D com _ready() e _physics_process(double delta) — exatamente o desenho A, exatamente a mesma forma de nó. Duas pessoas chegaram no mesmo lugar sem combinar, o que é a melhor evidência de que o lugar está certo.

As diferenças estão todas em como a decisão é executada, e nas três ele está à frente — a cena construída a partir da física, um sistema de build só, e o passo desacoplado do quadro. Mais o achado que não estava no plano: o código dele já roda um rover Viper articulado sobre terreno SCM deformável.

A leitura completa, com o código lado a lado e o que ela muda no plano, está em Duas pontes, um desenho.

08

O que isso muda na frente

O teto do Godot deixou de ser o teto do projeto

A tabela comparativa listava features do Jolt marcadas como "o Godot não expõe". Essa coluna não é mais uma parede: é uma lista de coisas que agora dá para alcançar, uma a uma, pela extensão.

o gap virou backlog em vez de impedimento

A leitura de código virou execução

Tudo que a entrada Um corpo só, quatro bengalas descreveu lendo os headers — o raycast que acha o chão, a mola que vira constraint, o clamp μ·N — agora está rodando e imprimindo número. As duas entradas se checam.

dá pra medir o que antes só dava pra descrever

O protótipo raycast ganhou um par de comparação

Existe agora, na mesma máquina, um veículo por penalidade (o protótipo em GDScript) e um por constraint (este). O contraste que a primeira entrada tratou no conceito pode ser medido lado a lado.

o mesmo cenário, os dois paradigmas
09

O que continua em aberto

Adotar o padrão do GDChrono para a cena

Trocar os nós fixos Wheel0..3 por construção a partir da física, com cada nó se sincronizando sozinho — e considerar migrar o build inteiro para CMake, que dissolve a paridade de ABI em vez de administrá-la.

Geometria da cena → mundo do Jolt

Enquanto o chão for construído dentro da extensão, não dá pra modelar terreno no editor. É o obstáculo mais concreto entre este estado e um cenário de verdade.

O rover articulado

Vale repetir o que a entrada anterior estabeleceu: a VehicleConstraint é um modelo lumped, de um corpo só. Ela não é o rover articulado — é o degrau anterior. O rover pede corpos e juntas de verdade, e agora existe por onde construí-los.

O estado da frente, em uma frase: parou de ser sobre descobrir o que dá para fazer e passou a ser sobre escolher o que fazer primeiro — agora com um exemplo funcionando dentro do próprio grupo para usar de referência.

Fontes e código