9. Floating origin: por que o mundo treme longe de casa

Um número de 32 bits não consegue ser grande e preciso ao mesmo tempo.

Os motores guardam posição em float de 32 bits. Um float tem 24 bits para os dígitos e o resto para a "vírgula". Isso significa que a menor diferença que ele consegue representar cresce com o tamanho do número. Perto de zero, ele distingue milionésimos. A 100 mil metros da origem, só distingue quase um centímetro. A 10 milhões, só um metro.

Espaçamento entre floats vizinhos (o "passo mínimo") em cada distância

Qualquer movimento menor que o passo mínimo é arredondado para zero ou para o passo inteiro. Um personagem andando a 0,02 m por frame a 1 milhão de metros da origem só se move quando o acumulado passa de 0,06 m: ele anda aos pulos. Física, sombras e animações sofrem o mesmo.

Veja tremer

O jogador anda em linha reta, devagar, e a câmera o acompanha. As posições passam por float de 32 bits (Math.fround), como num motor. Arraste o slider de distância devagar, de 1 metro até 10 milhões, e observe três coisas: as pedras do chão, o rastro (um ponto por frame) e o gráfico da posição.

Perto da origem o rastro é uma fileira de pontos igualmente espaçados e o gráfico é uma reta. Conforme a distância sobe, os pontos do rastro se agrupam (o jogador fica parado uns frames e pula) e o gráfico vira escada. Bem longe, os degraus viram metros inteiros. Aumentar a velocidade encurta o intervalo entre degraus, mas o tamanho de cada degrau só depende da distância.

Aqui a posição verdadeira avança em double e só o valor guardado na cena é arredondado para float. Se o motor também somar em float (posição += velocidade × dt), é pior: quando o movimento de um frame cai abaixo de metade do passo mínimo, a soma devolve o mesmo número e o jogador trava de vez.

A solução: mover o mundo, não o jogador

Quando o jogador se afasta mais que um limite da origem (por exemplo 1000 m), o jogo desloca tudo pela posição dele: cada objeto da cena recebe posição −= posição_do_jogador, incluindo o próprio jogador, que volta para (0, 0, 0). O mundo "escorrega" por baixo dos pés dele. Ninguém percebe, porque tudo se moveu junto.

// a cada frame, ou quando o jogador cruza a borda de um chunk
se |jogador.posição| > LIMITE:
  desloc = jogador.posição
  para cada objeto raiz na cena:      // chunks, itens, inimigos, partículas, o próprio jogador
    objeto.posição -= desloc
  origem_do_mundo += desloc           // quantas vezes já deslocamos, para saber a posição "verdadeira"
NomeO que é
LIMITEdistância que dispara o deslocamento. 1000 a 5000 m é comum: longe o bastante para não disparar toda hora, perto o bastante para o float ainda estar preciso.
desloco vetor subtraído de todo mundo. Costuma ser arredondado para múltiplo do tamanho do chunk, para os chunks continuarem alinhados.
origem_do_mundoacumulado dos deslocamentos. Posição verdadeira de qualquer coisa = posição na cena + origem_do_mundo. É isso que entra no ruído e nas chaves dos itens.
O que quebra se você esquecer. Tudo que guarda posição fora da hierarquia da cena: sistemas de partículas em espaço de mundo (as partículas ficam para trás), corpos físicos com velocidade (a velocidade continua certa, mas um Rigidbody em movimento pode dar um solavanco se você mover o transform no meio do passo de física; use Physics.SyncTransforms ou faça o deslocamento no FixedUpdate), câmera com suavização (ela tenta "alcançar" a posição antiga e dá um tranco; desloque a câmera junto e zere o histórico), e trilhas/linhas renderizadas (TrailRenderer).
Como se junta com os chunks. A grade 3x3 usa a posição verdadeira (posição + origem_do_mundo) para decidir as coordenadas de chunk. A malha de cada chunk fica em coordenadas locais pequenas. O momento mais limpo de deslocar é exatamente quando o jogador troca de chunk: os chunks já vão ser rearranjados mesmo.