Skip to content

Criando um mundo aberto no navegador, parte 30: Uma câmera que respeita as paredes

Por Oleg Sidorkin, CTO e cofundador da Cinevva

Chegou agora? Consulte o guia da série. Ele explica o que é um protótipo exploratório e reúne links para todas as partes.

A Parte 29 nos deu um único controlador capaz de conduzir qualquer corpo. Agora o corpo se move corretamente. Ele caminha, desliza, nada, plana, entra em cavernas escalando e se abaixa sob saliências. O problema é o que o observa. Durante vinte e nove partes, a câmera foi um sistema de órbita padrão que seguia o jogador e fazia exatamente uma coisa inteligente para evitar constrangimentos: recusava-se a inclinar abaixo do horizonte, para não deslizar sob um terreno plano. Essa limitação entrega o problema. Ela existe porque a câmera não fazia ideia de onde a geometria do mundo realmente estava, então a única defesa contra atravessar superfícies era proibir os ângulos em que isso tinha maior probabilidade de acontecer. Aproxime-se de uma colina e a câmera ficava dentro dela. Entre em uma das cavernas de marching cubes da Parte 7 e você passava a olhar para o interior de uma rocha. Construa uma casa com as ferramentas de autoria da Parte 16, fique em um cômodo e a câmera flutuava do lado de fora da parede, olhando para o revestimento. Esta parte faz a câmera respeitar a geometria tanto quanto o corpo já respeita.

O formato do problema e o formato da solução

Uma câmera em terceira pessoa tem uma tarefa difícil e uma dúzia de tarefas fáceis. As fáceis são acompanhar, suavizar e processar os controles de órbita, e já tínhamos tudo isso. A difícil é aquilo que a literatura acadêmica chama de restrição de visibilidade, e o estudo que todos citam, Camera Control in Computer Graphics, de Christie e Olivier, enquadra toda a área em torno dela: manter o personagem enquadrado e sem obstruções, respeitando o mundo. Em tempo de execução, isso se reduz a uma pergunta aparentemente simples, feita a cada quadro. O jogador é o pivô. O usuário girou e aplicou zoom até uma posição desejada da câmera, a certa distância atrás dele. Até onde a câmera realmente pode recuar ao longo dessa linha antes de penetrar em algo sólido? Responda a isso corretamente e a câmera se recolhe diante da colina, desliza pela haste quando você recua para um canto e para no teto da caverna em vez de atravessá-lo.

O padrão que resolve isso é antigo e comprovado. A Unreal o chama de spring arm, a Godot oferece um nó SpringArm3D, e o Cinemachine da Unity o divide entre um sistema de acompanhamento em terceira pessoa e uma extensão de desoclusão. A ideia é sempre a mesma. Pendure a câmera na extremidade de uma haste ancorada no pivô. Mantenha-a no comprimento desejado quando o caminho estiver livre, retraia-a em direção ao pivô quando algo estiver no caminho e faça-a retornar com efeito de mola quando o caminho ficar livre. Real-Time Cameras, de Mark Haigh-Hutchinson, escrito pelo responsável pelas câmeras de Metroid Prime, dedica capítulos inteiros aos modos de falha que transformam uma versão ingênua disso em algo capaz de deixar os jogadores enjoados. Adotamos o padrão e criamos nossa própria implementação, pequena o bastante para ser lida de uma só vez, em public/world/src/camera-rig.mjs.

paredepivô (cabeça do jogador)desejado (zoom do usuário)câmera + raio r da sondaℓ (limitado)
A haste mantém a câmera no zoom desejado pelo usuário até que um obstáculo invada seu caminho; então ela se retrai ao longo da mesma linha para que a esfera da sonda encoste na superfície, em vez de o plano próximo atravessá-la.

Uma haste que controla apenas o próprio comprimento

A regra de design que manteve o sistema pequeno foi tomada emprestada do trabalho com o controlador na Parte 29: controle uma única coisa por completo e recuse todo o resto. O sistema controla o comprimento da haste e nada mais. Guinada, inclinação, amortecimento da órbita, gestos de toque e da roda do mouse: tudo isso permanece com o OrbitControls, que já resolve bem esses aspectos e que não temos qualquer interesse em reescrever. Portanto, o sistema não é um controlador de câmera. É um pós-processamento executado depois dos cálculos de órbita que corrige exatamente um número: a distância entre o pivô e a câmera.

Essa decisão parece organizada, mas quase deu errado de imediato por causa da maneira como o OrbitControls funciona. No início de cada atualização, ele lê a posição atual da câmera e deriva dela o raio da órbita. Normalmente isso é imperceptível. Porém, no instante em que nosso sistema aproxima a câmera para desviar de uma parede, no quadro seguinte o OrbitControls lê essa posição encurtada, conclui que o usuário deve ter aproximado o zoom e incorpora o encurtamento ao zoom desejado pelo usuário. Depois de alguns quadros, a câmera desaba sobre a cabeça do jogador e não volta a se afastar. A solução consiste em duas chamadas que envolvem a atualização da órbita, e isso representa toda a integração. Antes da execução do OrbitControls, beforeControls() restaura a câmera à distância completa e não encurtada do quadro anterior, para que os cálculos da órbita sempre leiam o zoom verdadeiro do usuário. Depois da execução do OrbitControls, afterControls(dt) lê a posição desejada recém-calculada pela órbita, resolve a colisão, amortece o comprimento e posiciona a câmera onde ela realmente deve renderizar. A intenção do usuário e a correção de colisão nunca interferem uma na outra, e o movimento de aproximação continua exatamente tão responsivo quanto era antes da existência do sistema. O teste sem interface gráfica que escrevemos para o sistema verifica isso com precisão: empurre a haste contra uma parede por sessenta quadros, remova a parede e o comprimento retorna com efeito de mola ao zoom completo de dez metros do usuário, em vez de ficar preso à distância da colisão.

Um contrato, qualquer colisor

O sistema nunca pergunta do que o mundo é feito. Um colisor é apenas um objeto com um método probe, e o sistema fornece a ele um raio, uma distância máxima e o raio da sonda da câmera, recebendo de volta um número: a distância máxima que a câmera pode percorrer antes de ser bloqueada por esse colisor. O sistema consulta todos os colisores registrados e considera o impacto mais próximo. Esse é o contrato inteiro, seguindo o mesmo princípio empregado pelo controlador de personagem quando transformou a locomoção em comportamentos plugáveis. O terreno se conecta, os objetos de cena se conectam, as estruturas das construções se conectam, cada um por trás do mesmo probe, e o sistema permanece sem conhecer nenhum deles. Adicionar um novo tipo de obstáculo significa adicionar um colisor a uma lista, não editar a câmera.

Um detalhe do contrato prova seu valor: o raio da sonda. Não lançamos um raio fino do pivô até a câmera; lançamos uma esfera grande o bastante para conter o plano próximo da câmera. Um único raio impede que o centro da câmera atravesse a parede, mas o plano próximo tem largura, então seus cantos já estariam enterrados na parede antes que o raio central sequer registrasse um impacto. Varrer uma pequena esfera em vez de um raio é o que toda implementação publicada faz — a Unreal expõe isso como o tamanho da sonda — e essa é a diferença entre uma câmera que repousa corretamente contra uma superfície e outra que permite enxergar através dela nas bordas da tela.

Não estimamos esse raio; nós o derivamos. O ponto do plano próximo mais distante da câmera é um de seus cantos, e a distância até ele resulta diretamente da projeção. Com o plano próximo n e o campo de visão vertical θ, a meia-altura é h=ntan(θ/2), a meia-largura é w=haspect, e o canto fica em

rnear=n2+w2+h2

O raio da sonda é essa distância até o canto multiplicada por uma pequena margem de segurança, com um mínimo fixo para nunca cair abaixo de um valor razoável em um frustum muito estreito. Sempre que o campo de visão, a proporção da tela ou o plano próximo muda, o raio é recalculado, para que um redimensionamento da janela ou um zoom que altere a projeção não deixe silenciosamente a sonda pequena demais para cobrir os cantos que deveria proteger.

O colisor que conhece as cavernas

É no colisor do terreno que isso fica interessante, porque o terreno do nosso motor não é um mapa de altura. Desde a Parte 7, ele é um campo de distância com sinal, uma função que retorna a distância entre qualquer ponto no espaço e a superfície sólida mais próxima, além de indicar se esse ponto está dentro ou fora da rocha. Positivo é ar, negativo é rocha, e esse único fato permite que nossa câmera faça algo que uma câmera baseada em mapa de altura é estruturalmente incapaz de fazer. Um mapa de altura conhece a altura do solo em determinada coordenada x e z. Ele não tem o conceito de teto, porque só existe uma superfície acima de qualquer ponto. Assim, uma câmera baseada em mapa de altura pode impedir que você entre em uma colina, mas não faz ideia de que a borda de uma saliência ou o teto de uma caverna está suspenso sobre o pivô, então atravessa ambos sem parar. Um campo de distância conhece todas as superfícies em três dimensões, portanto a mesma sonda que impede a câmera de entrar na encosta de uma colina também a impede de atravessar o teto de uma caverna, sem um único caso especial.

cavernarocha sólidajogadormapa de altura: atravessa ✗SDF: para no teto ✓
Um mapa de altura armazena uma superfície por coluna, portanto nunca detecta a placa de rocha acima do jogador e deixa a haste atravessar o teto. O campo de distância é negativo dentro dessa placa, então a sonda entra em contato com ela e a câmera permanece logo abaixo do teto da caverna.

Percorrer o campo com a sonda é uma técnica com nome próprio e um artigo acadêmico por trás. Sphere Tracing, de John Hart, publicado em 1996, é o método padrão para avançar um raio por um campo de distância, e o segredo é que o campo não informa apenas se você atingiu algo: ele também informa uma distância segura pela qual você pode avançar sem atingir nada. Assim, em vez de avançar lentamente em pequenos passos fixos, você amostra o campo, avança pela folga informada e repete o processo, dando passos longos pelo ar livre e passos curtos e cuidadosos conforme se aproxima de uma superfície. Mas há uma ressalva imposta pelo nosso terreno. Um campo de distância verdadeiro informa a distância euclidiana real até a superfície mais próxima, e avançar todo o valor dessa folga é sempre seguro. Porém, onde o terreno ainda é um mapa de altura, em vez de voxels escavados, o campo que podemos amostrar de forma barata não representa a distância verdadeira, mas a folga vertical, o espaço diretamente abaixo até o chão. Em uma encosta, esse número superestima a distância que a câmera realmente pode percorrer, porque a rocha mais próxima está ao lado, não diretamente abaixo. A distância correta é menor por um fator que cresce com o gradiente, 1+h2, portanto um passo dimensionado conforme a folga informada ultrapassa o limite e pode saltar completamente por cima de uma crista. A solução é a sub-relaxação: avançar apenas uma fração da folga informada, em vez de percorrê-la por inteiro. Isso permanece seguro em inclinações de até aproximadamente sessenta graus e, nas regiões de voxels com distância verdadeira, não custa nada além de algumas amostras extras. Combinamos isso com um limite mínimo para o passo, para que uma parede de caverna com uma ou duas células de espessura nunca seja completamente ignorada, e com um limite máximo mais restrito, que mantém a marcha barata. Quando a esfera finalmente toca a superfície, uma breve bisseção refina o ponto de contato, e a câmera recua o equivalente ao próprio raio e permanece ali.

Em termos matemáticos, a haste é um raio r(t)=p+td que parte do pivô p na direção unitária d, e a marcha avança por uma fração sub-relaxada da folga até o contato informada pelo campo, limitada nas duas extremidades:

tn+1=tn+clamp(λ(Φ(r(tn))r),smin,smax)

Aqui, Φ é a distância com sinal, positiva no ar e negativa na rocha; r é o raio da sonda; λ(0,1] é o fator de sub-relaxação que impede o campo superestimado do mapa de altura de saltar uma encosta; e smin,smax são os limites do passo que impedem que uma parede fina seja ignorada e que a marcha se prolongue demais. O contato ocorre no primeiro t em que Φ(r(t))r, o que significa que a superfície da esfera alcançou a rocha, e a câmera permanece nesse comprimento de arco menos o próprio raio.

O colisor oferece ainda outro método como rede de segurança: a despenetração. A colisão deveria impedir que a câmera entrasse em um sólido, mas algumas situações conseguem escapar: um pivô atravessando uma parede fina o bastante para que a haste comece dentro dela; uma caverna recém-escavada pelas ferramentas de terreno enquanto a câmera estava na rocha que acabou de desaparecer; uma peça de construção posicionada ao redor da câmera. Nesses casos, o colisor verifica se a câmera terminou o quadro dentro de um sólido, onde Φ(c)<r. Se isso aconteceu, ele lê o gradiente do campo, que aponta diretamente para o espaço aberto porque Φ aumenta à medida que se deixa a rocha, e desloca a câmera para fora nessa direção:

cc+(rΦ(c))Φ(c)Φ(c)

Algumas dessas iterações convergem para a isosuperfície de valor r, e é isso que permite que uma ferramenta de escultura remova o chão sob a câmera e que a visão se recupere no quadro seguinte, em vez de ficar preta.

Aproximar de imediato, afastar suavemente e não se assustar com mourões

Uma haste que simplesmente salta para a distância de colisão a cada quadro é pior do que não ter haste alguma, porque o mundo está cheio de objetos finos atrás dos quais a câmera passa por um único quadro — um mourão, uma luminária, um tronco — e uma câmera que avança bruscamente para desviar de cada um e logo recua da mesma forma causa náusea. Tanto o livro de Haigh-Hutchinson quanto a muito estimada palestra de Itay Keren sobre movimento de câmera chegam à mesma intuição: a câmera deve reagir a ameaças e perigos mais depressa do que relaxa depois que eles passam. Por isso, o amortecimento é deliberadamente assimétrico. Quando um oclusor aparece e a haste precisa encurtar, ela se recolhe quase instantaneamente, porque um quadro atravessando a geometria é feio e o jogador perdoa um recolhimento rápido. Quando o oclusor sai do caminho e a haste quer se alongar, ela se afasta lentamente, e apenas depois que um curto temporizador de permanência com espaço continuamente livre chega ao fim. Essa permanência é a histerese que elimina o sobressalto. Gire rapidamente a câmera passando por um mourão fino e ele nunca permanece fora do caminho por tempo suficiente para disparar a extensão lenta; assim, a câmera desliza por ele como se nem estivesse ali, exatamente como seus olhos esperam. O Cinemachine apresenta a mesma ideia por meio de valores separados de amortecimento ao entrar e ao sair de uma colisão, e é essa assimetria que faz o comportamento parecer o de um operador de câmera, em vez do de uma mola.

No código, isso se resume a uma linha de suavização exponencial cuja taxa muda de acordo com o sinal da variação. Se é o comprimento atual da haste, a é o comprimento permitido pela colisão neste quadro e Δt é o tempo do quadro, então

+(a)(1ekΔt),k={kinakouta>,kinkout

e o ramo de afastamento suave só é executado depois que o espaço livre se mantém durante o tempo de permanência τ. A forma 1ekΔt é importante por mais do que sua aparência elegante. Ela fixa a constante de tempo da resposta em 1/k, independentemente da taxa de quadros, de modo que a câmera produz a mesma sensação a 30 ou 144 quadros por segundo. Já a mistura ingênua com valor constante, +α(a), reagiria depressa demais em uma máquina rápida e de maneira arrastada em uma lenta.

O último comportamento se destina aos interiores apertados que deram início a toda esta parte. Quando a haste se recolhe tanto que a câmera fica praticamente em cima do jogador, ocultamos o avatar do próprio jogador e deixamos a visão próxima da primeira pessoa. É o que Breath of the Wild faz em um santuário apertado e o recurso ao qual a maioria dos jogos em terceira pessoa recorre em um canto, porque a alternativa — uma câmera prensada contra a parede, olhando para a parte de trás de uma cabeça — não serve para nada. O rig expõe um único sinalizador para isso, e o loop do mundo lê esse sinalizador e alterna a visibilidade do avatar local. Com menos de um metro de haste, você está efetivamente em primeira pessoa, as paredes são respeitadas e, assim que recua para um cômodo com espaço, o avatar reaparece gradualmente e a haste se estende.

O que será conectado a seguir

O colisor de terreno é lançado hoje e representa a metade difícil, porque o terreno está por toda parte e um campo de distância é algo trabalhoso de sondar. Os objetos de cena e as estruturas de construção dos protótipos de autoria são a metade fácil, e o contrato já está pronto para eles. Um segundo colisor, um colisor por raycast, lança um raio do pivô em direção à câmera contra uma lista de malhas e informa o impacto mais próximo da mesma forma que o colisor de terreno. A versão barata lança um único raio, o que funciona bem até a quantidade de objetos de cena aumentar; a evolução consiste em substituir o raio por uma esfera varrida usando o three-mesh-bvh, biblioteca de Garrett Johnson que envolve uma malha em uma hierarquia de volumes delimitadores para que consultas espaciais sejam executadas em tempo logarítmico, em vez de usar força bruta. De qualquer modo, o rig não muda. Ele consulta uma lista maior de colisores e usa o impacto mais próximo, que é justamente a razão de termos criado primeiro o contrato e depois os colisores.

Tecnologias mencionadas neste capítulo

Uma haste responsável por um único número. O rig de câmera é um pós-processamento sobre o OrbitControls, não um substituto para ele. Ele controla o comprimento da haste e deixa guinada, inclinação, zoom e manipulação de gestos a cargo do controlador orbital em que já confiávamos. A integração consiste em duas chamadas delimitadoras: beforeControls() restaura a distância total do quadro anterior para que os cálculos orbitais leiam o zoom real do usuário, em vez de confundirem o encurtamento causado por uma colisão com uma aproximação por dolly; e afterControls(dt) resolve a colisão e grava a posição renderizada. Sem esse par, a câmera desaba sobre o jogador ao longo de alguns quadros.

Um contrato conectável de colisores. Um colisor é qualquer objeto com um probe que responda à pergunta: “até onde a câmera pode viajar a partir do pivô ao longo deste raio antes que você a bloqueie?”. O rig consulta todos os colisores e usa o impacto mais próximo, sem precisar saber se o obstáculo é o terreno, um objeto de cena ou uma parede. Essa é a mesma disciplina de controlar uma única coisa usada pelo controlador conectável de personagem na locomoção. A sonda é uma esfera varrida dimensionada para conter o plano próximo, não um raio fino, de modo que os cantos do frustum nunca atravessem uma superfície que o raio central deixaria passar. Seu raio é derivado da projeção — a distância até um canto do plano próximo, n2+w2+h2, multiplicada por uma margem de segurança — e recalculado sempre que o campo de visão, a proporção da tela ou o plano próximo muda.

Uma sonda de terreno por distância com sinal que respeita saliências e cavernas. Como o terreno é um campo de distância com sinal, em vez de um mapa de altura, a mesma sonda que impede a câmera de atravessar uma encosta também a impede de atravessar o teto de uma caverna — caso que um raycast contra um mapa de altura é estruturalmente incapaz de detectar. A marcha usa o sphere tracing no estilo de Hart, avançando por uma fração sub-relaxada da folga informada pelo campo e limitada nas duas extremidades, porque as regiões de mapa de altura informam a folga vertical, em vez da distância verdadeira, e superestimam quanto a câmera pode se mover em uma encosta; portanto, um passo completo saltaria sobre uma crista. Uma etapa de despenetração orientada pelo gradiente funciona como rede de segurança e recupera a visão quando o chão é esculpido e removido sob a câmera.

Amortecimento assimétrico com temporizador de permanência. A haste se recolhe rapidamente quando surge um oclusor e se afasta devagar depois que ele sai do caminho, mas somente após um curto intervalo de espaço continuamente livre. Assim, passar rapidamente por um mourão nunca faz a câmera avançar de forma brusca. Abaixo de um limiar de recolhimento, o rig sinaliza uma visão próxima da primeira pessoa, e o loop do mundo oculta o avatar local — a solução padrão para interiores apertados, em vez de deixar a câmera enterrada em uma parede.

Referências

A formulação do controle da câmera como uma restrição de visibilidade vem de Marc Christie e Patrick Olivier, Controle de câmera em computação gráfica (Computer Graphics Forum, 2008). A marcha pelo campo de distância vem de John C. Hart, Sphere Tracing: um método geométrico para o ray tracing com antisserrilhamento de superfícies implícitas (The Visual Computer, 1996). O padrão de braço com mola e sua colisão por esfera de sondagem são documentados no Componente Spring Arm, da Epic, e no Desoclusor do Cinemachine e Acompanhamento em terceira pessoa, da Unity. A intuição sobre movimento e amortecimento vem de Mark Haigh-Hutchinson, Câmeras em tempo real (Morgan Kaufmann, 2009), e Itay Keren, Scroll Back: teoria e prática de câmeras em jogos de rolagem lateral (GDC 2015). O caminho de evolução do colisor de malhas é o three-mesh-bvh, de Garrett Johnson.


Parte 30 de 30. Anterior: Parte 29 — Um controlador, qualquer corpo Guia da série: /pt-BR/blog/2026-02-25-open-world-browser-series-guide