1 Grafo de cena e laço de renderização — Resumo

Versão de revisão. Esta é a passagem rápida pelas três decisões que sustentam a cena e não aparecem nela. O desenvolvimento completo, com os exemplos inteiros, está na versão integral deste capítulo e no capítulo correspondente do livro. Use isto para conferir se o critério ficou de pé, e volte lá onde ele não estiver.

Três decisões entram na cena aqui, e nenhuma delas muda um pixel do que se vê hoje. As três cobram a conta em capítulos que ainda não chegaram. É por isso mesmo que elas são difíceis de defender agora.

1.1 As três que não aparecem na imagem

Em 1978, um jogo de arcade acelerava sozinho conforme a tela esvaziava, e ninguém escreveu uma linha para isso acontecer.

Em 1978, num escritório da Taito em Tóquio, Tomohiro Nishikado terminou um jogo com 55 alienígenas descendo em formação sobre um canhão. Parte do hardware ele montou à mão, porque o que havia à venda não dava conta. Ao testar, viu algo estranho: conforme os alienígenas eram destruídos, os que sobravam desciam mais depressa. Ele contou depois que percebeu o efeito e o manteve, por achar que melhorava o jogo.

A causa não estava em curva de dificuldade nenhuma. Cada alienígena era desenhado um a um, e a tela cheia custava mais tempo que a tela quase vazia. Menos coisa a desenhar, laço mais curto, formação mais rápida. Repare no que isso significa: a velocidade do mundo estava amarrada ao custo de desenhá-lo. O programa não tinha noção de tempo, tinha noção de quadros, e as duas coisas coincidiam enquanto o quadro custasse sempre o mesmo.

Nishikado teve sorte, e a sorte tem endereço. Ele escrevia para uma máquina só, e todo mundo jogava aquilo no mesmo gabinete. O ambiente que se monta aqui roda em três classes de aparelho, e nenhuma delas desenha na cadência das outras. Pior: nenhuma mantém a própria o tempo todo, porque o celular reduz quando esquenta e o navegador reduz quando a aba perde o foco.

Daí saem as três decisões. A estrutura troca a lista de objetos com coordenadas de mundo por uma árvore de nós, cada um guardando a transformação em relação ao pai. O relógio faz o laço avançar a cena pelo tempo transcorrido, e não pelo número de quadros desenhados. O teto é o orçamento de tempo que cada quadro tem para gastar, declarado antes de existir algo caro a que atribuir a culpa.

flowchart LR
    E["Estrutura:<br/>arvore de nos com<br/>transformacao local"] --> CE["Cobra no capitulo<br/>de manipulacao"]
    R["Relogio:<br/>a cena avanca pelo<br/>tempo transcorrido"] --> CR["Cobra na primeira<br/>maquina de outra pessoa"]
    T["Teto:<br/>orcamento por quadro<br/>declarado antes"] --> CT["Cobra no dia em que<br/>entra a peca pesada"]
Figura 1: As três decisões invisíveis e o momento em que cada uma delas cobra a conta.

E aqui está o desconforto de ensinar isto. Uma cena com coordenadas absolutas desenha a mesma imagem que uma cena hierárquica, e um laço que conta quadros anima igualzinho na sua máquina. Então por que decidir agora, sem poder conferir a vantagem? Porque as três cobram tarde e caro, cada uma com endereço certo. A estrutura cobra no capítulo de manipulação. O relógio, na primeira máquina alheia. O teto, no dia da primeira peça pesada.

Pare um instante e responda. Se alguém abrisse o seu ambiente no aparelho dele agora, o que mudaria de comportamento? E quantas dessas mudanças você conseguiria demonstrar na sua própria máquina? A diferença entre as duas respostas é o tamanho da dívida que você não vê.

Circula em tutorial de gráficos um conselho que eu acho ruim: primeiro faça funcionar, depois otimize. Ele acerta sobre otimização e erra sobre estrutura. Trocar um algoritmo lento por um rápido é mudança local. Trocar uma cena plana por uma cena hierárquica, não. O custo dessa segunda cresce com quanto código já se apoiou na decisão antiga. As três entram aqui não porque este seja o momento em que rendem, mas porque é o último em que são baratas.

1.2 Quem carrega quem cabe numa frase

Empurre uma mesa posta e o copo vai junto, sem que ninguém calcule a nova posição do copo.

O prato acompanha, o garfo acompanha, e você não pensou em nenhum dos dois. O copo está sobre a mesa, e essa relação faz o trabalho sozinha. Um ambiente virtual não tem essa cortesia de graça. Se cada objeto guardar a própria posição em relação ao mundo, arrastar a mesa move a mesa e mais nada. O prato fica boiando no ar, exatamente onde estava.

A saída é velha e é boa. Cada objeto guarda onde está em relação ao pai, e não em relação ao mundo. O prato está a doze centímetros da borda da mesa; onde ele está na sala é assunto da mesa. Cada nó guarda três coisas próprias: onde está, como está girado, em que tamanho. A posição no mundo sai de multiplicar isso pela do pai, e a conta sobe a árvore até a raiz.

Grafo de cena, em uma frase

Uma árvore enraizada cujos nós são objetos do ambiente. Cada nó guarda uma transformação local, escrita no sistema de coordenadas do pai, e nenhum deles guarda coordenada de mundo. A transformação de mundo de um nó é o produto acumulado das transformações locais no caminho da raiz até ele.

M_n = L_{r_1} \cdot L_{r_2} \cdots L_{p(n)} \cdot L_n

Repare no que a definição não exige. Ela não pede profundidade fixa, não limita o número de filhos e não diz uma palavra sobre ordem de desenho. Também não exige que a árvore corresponda a alguma coisa física. Nada impede que a lâmpada seja filha do parafuso. O resultado é uma oficina em que apertar o parafuso apaga a luz. O critério que evita isso cabe numa frase, e é do domínio, não do código.

A frase é esta: arrastar a bancada tem de levar junto tudo o que está sobre ela, sem que ninguém escreva uma linha para isso. Aplique-a e a árvore se monta sozinha. O suporte está apoiado no tampo, logo é filho dele; as peças e o painel, idem; a sala não está apoiada em nada, e por isso é a raiz. E quando duas relações competem, escolha o pai cuja movimentação a peça deve acompanhar.

flowchart TD
    S["Sala<br/>(raiz: nao esta apoiada em nada)"] --> B["Bancada<br/>(apoiada no chao da sala)"]
    B --> SU["Suporte com encaixes<br/>(apoiado no tampo)"]
    B --> P["Pecas soltas<br/>(sobre o tampo)"]
    B --> PA["Painel de leitura<br/>(preso ao tampo)"]
    SU --> EN["Encaixes<br/>(fazem parte do suporte)"]
Figura 2: A árvore da oficina, com o motivo de cada parentesco escrito ao lado do nó.

Existe uma conferência mecânica que custa seis linhas. O ambiente escreve a própria estrutura em texto, um nível por recuo, e você lê a árvore como leria um sumário. O que se procura ali é peça pendurada direto na raiz. É onde o acaso costuma deixá-la. Já vi essa impressão encerrar uma discussão de projeto em menos de um minuto. Um grupo defendia a hierarquia dele com convicção, imprimiu, e as duas peças soltas apareceram na segunda linha.

Nada disso sai de graça, e apresentar a árvore só pelo lado bom seria vender. Ela cobra indireção, porque saber onde um objeto está no mundo deixa de ser leitura e vira conta. Cobra sincronia: matriz de ancestral desatualizada devolve a posição do quadro anterior, sem erro e sem aviso. E cobra um susto conceitual, porque escala se propaga, e um nó com escala não uniforme distorce tudo o que pendura nele.

Vale a troca? Vale, e a razão cabe numa frase: sem hierarquia não existe a operação de trocar de pai, e o capítulo de manipulação inteiro se apoia nela. Prender, orientar, encaixar e soltar são quatro nomes para mexer no parentesco de um nó. Quem escolheu a cena plana não tem parentesco a mexer, e vai reimplementar as quatro operações à mão, uma a uma.

A ideia é bem mais velha que qualquer visor citado neste livro. Em 1963, Ivan Sutherland defendeu no MIT uma tese orientada por Claude Shannon. O programa dela, o Sketchpad, rodava no TX-2 do Lincoln Laboratory e se operava com uma caneta de luz apontada para a tela. Ele resolvia um problema de manutenção. Um esquema com quarenta resistores idênticos obrigava a refazer quarenta cópias a cada mudança do símbolo, e uma delas sempre ficava para trás.

A saída foi separar o desenho mestre das suas instâncias. A instância não guardava o desenho: guardava uma referência ao mestre e uma transformação própria. Reconhece a estrutura? Referência mais transformação local é exatamente o que um nó de cena guarda, e o Sketchpad ainda permitia instância dentro de instância. Era a árvore de cena antes de a palavra existir, nascida de custo de máquina cara. Desconfie de história de origem contada reta demais, inclusive desta: o raro naquele trabalho foi juntar referência, transformação e aninhamento num sistema que outras pessoas puderam ver rodando.

1.3 Quarenta e dois centímetros e nenhum aviso

Duas linhas trocadas de lugar, quase meio metro de desvio, e nenhuma mensagem de erro em lugar nenhum.

Cada nó guarda translação, rotação e escala, e para desenhar as três viram uma matriz só. Em que ordem elas se combinam? A biblioteca responde sempre igual: escala primeiro, rotação depois, translação por último, tudo multiplicado pela matriz do pai. A ordem não é gosto de quem escreveu a biblioteca. Escala e rotação são operações ancoradas na origem, e a translação é a que tira o objeto dela. Tudo que precisa da origem vem antes de perdê-la.

A armadilha de leitura, e ela não produz erro. A ordem em que as operações acontecem é o inverso da ordem em que aparecem escritas. Trocar os dois argumentos compila, executa, desenha e põe o objeto noutro lugar. Leia a expressão de dentro para fora, a partir do ponto: a matriz encostada no vetor atua primeiro, e a mais à esquerda entra por último.

O caso a examinar é o do encaixe. Uma peça precisa andar 30 cm e assumir um quarto de volta. Gire primeiro e ela roda parada na origem do pai, depois vai para o lugar certo. Translade primeiro e a rotação passa a girar em torno de um ponto onde a peça já não está. Ela descreve um arco de 90 graus em torno da origem do pai.

flowchart LR
    P0["Peca na origem do pai"] --> G["Girar primeiro"]
    P0 --> TT["Transladar primeiro"]
    G --> G2["depois levar 30 cm em x"]
    G2 --> D1["Destino (0,30; 0; 0)"]
    TT --> T2["depois girar um quarto de volta<br/>em torno da origem do pai"]
    T2 --> D2["Destino (0; 0; -0,30)"]
    D1 --> DIF["0,424 m entre os dois destinos<br/>e nenhuma mensagem de erro"]
    D2 --> DIF
Figura 3: As duas ordens possíveis para as mesmas duas operações, os dois destinos e a distância entre eles.

Os dois destinos ficam a 0{,}30\sqrt{2} um do outro, ou seja, 42 centímetros. Numa bancada de 1,4 m, isso é a peça no meio da mesa contra a peça caindo da beirada. O que torna o erro caro não é o tamanho: é a ausência de sintoma. Nada em vermelho, nada inválido, trinta linhas plausíveis a inspecionar — todas certas, porque a errada é a ordem, e ordem não tem cara de erro. O hábito que evita isso é compor num lugar só, num arquivo cuja única responsabilidade seja essa.

1.4 Trocar de dono sem sair do lugar

Você pega uma chave de fenda da bancada e ela passa a acompanhar a sua mão, sem que ninguém recalcule nada.

Trocar de pai tem nome: reparentagem. É a operação por trás de pegar, orientar, encaixar e soltar. Existe um caminho mais óbvio: copiar a posição da mão para a peça a cada quadro. Custa três linhas. Ele sobrevive porque hoje nada o denuncia. Ele quebra em quatro momentos, e todos chegam. Quando o objeto intermediário também se move. Na hora de soltar. Quando a ordem de atualização inverte. E quando um dos lados herda escala de um ancestral. São quatro roupas do mesmo defeito, que é manter duas fontes de verdade sobre onde a peça está.

L_{\text{novo}} = M_{\text{pai}}^{-1} \cdot M_{\text{mundo}}

A identidade pega a posição do objeto no mundo, que não vai mudar, e a reescreve do ponto de vista do novo pai. O que ela exige, e isto não está na fórmula, é que as duas matrizes estejam atualizadas no instante da conta. Na oficina de referência eu escrevi a operação à mão em vez de usar a pronta. O motivo é instrumental: esta versão devolve o desvio em metros entre onde o objeto estava e onde ficou.

export function reparentar(filho: Object3D, novoPai: Object3D): number {
  // Sem esta atualização a conta usa a matriz do quadro anterior, e o objeto
  // pula para onde ele estava, não para onde está.
  filho.updateWorldMatrix(true, false);
  novoPai.updateWorldMatrix(true, false);
  posicaoAntes.setFromMatrixPosition(filho.matrixWorld);
  matrizLocal.copy(novoPai.matrixWorld).invert().multiply(filho.matrixWorld);
  novoPai.add(filho);
  matrizLocal.decompose(filho.position, filho.quaternion, filho.scale);
  filho.updateWorldMatrix(true, false);
  posicaoDepois.setFromMatrixPosition(filho.matrixWorld);
  return posicaoAntes.distanceTo(posicaoDepois);
}

A demonstração da oficina parece um defeito, e é assim de propósito. Prenda a engrenagem ao eixo e ela não se move: continua deitada no tampo, a 30 cm dali, e só então começa a girar em torno do eixo, à distância. Trocar de dono e assentar no lugar são coisas diferentes. Confundir as duas custa meia tarde de briga com a função errada.

Em tese o número devolvido é zero. Na prática é o arredondamento de uma inversão de matriz, algo na casa dos micrômetros, e é por não ser zero que ele serve. Micrômetros dizem que a conta fechou; milímetros dizem que alguma matriz do caminho até a raiz estava desatualizada. A linha de corte fica em 1 milímetro, e o critério é o corpo de quem usa. A mesma causa que produz 1 milímetro aqui produz um palmo numa hierarquia mais funda.

flowchart LR
    A["Pai antigo: o tampo"] --> OP["Trocar de pai preservando<br/>a posicao de mundo"]
    B["Pai novo: o eixo"] --> OP
    OP --> R["A peca fica exatamente<br/>onde estava"]
    R --> N["Desvio impresso em metros<br/>linha de corte: 1 milimetro"]
    N --> OK["Micrometros: a conta fechou"]
    N --> RUIM["Milimetros: alguma matriz<br/>do caminho estava desatualizada"]
Figura 4: A troca de pai preservando a posição de mundo, com o desvio impresso servindo de conferência.

1.5 O relógio que ninguém pediu

Nos gabinetes de PC do fim dos anos 1980 havia um botão etiquetado turbo, e ele deixava a máquina mais lenta.

Apertado, o botão forçava o computador de volta à cadência do IBM PC original, de 4,77 MHz. Ele existia porque boa parte do software media tempo contando iterações, e num processador mais rápido o mesmo laço terminava antes. A indústria pagou por uma decisão de software em hardware: um botão físico, num gabinete de metal. Troque turbo por taxa de atualização e a história é a mesma. Um ambiente escrito num monitor de 60 Hz, aberto num visor de 90, roda uma vez e meia mais rápido se contar quadros.

A correção cabe numa multiplicação. O laço mede quanto tempo passou desde a chamada anterior e entrega esse intervalo à cena. Ela multiplica velocidade por tempo, em vez de somar um valor fixo por quadro. A 60 imagens por segundo cada quadro recebe cerca de 0,0167 s; a 90, recebe 0,0111 s. Os quadros ficam mais numerosos, e a volta do eixo leva os mesmos dois segundos nos dois aparelhos. A conferência é literal: cronometre uma volta em duas máquinas de cadências diferentes.

Dois cuidados que não produzem erro visível. O intervalo cru não serve quando o laço para de ser chamado: ao voltar do menu do sistema, ele pode valer vários segundos, e o mecanismo gira duas voltas de uma vez. O relógio da oficina limita o salto em 0,1 s e trata a suspensão como pausa. E o primeiro quadro recebe intervalo zero de propósito, porque não existe quadro anterior a medir.

flowchart LR
    Q["Quem pede o proximo quadro:<br/>a janela ou a sessao imersiva"] --> A["Ajustar o alvo<br/>de desenho"]
    A --> M["Medir o tempo desde<br/>a chamada anterior"]
    M --> L["Limitar o salto<br/>em 0,1 s"]
    L --> AV["Avancar a cena pelo<br/>tempo transcorrido"]
    AV --> D["Desenhar"]
    D --> Q
Figura 5: A ordem das etapas dentro de um quadro e quem agenda a chamada seguinte em cada regime.

Falta a pergunta que parece administrativa e é a de maior consequência: quem chama a função? Na página comum, quem chama é a janela do navegador. Numa sessão imersiva a janela deixa de mandar, e quem agenda a chamada seguinte é a sessão. O sintoma de ignorar isso é dos mais frustrantes de diagnosticar. O ambiente funciona no computador de mesa e congela ao entrar no visor, enquanto a página segue respondendo.

1.6 O envelope e o medidor que mora dentro dele

Existe uma ordem de trabalho que parece sensata e produz ambientes que só rodam na máquina de quem os escreveu.

Cada quadro é um envelope de onze milissegundos, e o que não couber nele não fica para depois: some. A imagem é literal. Um quadro que atrasa não é entregue atrasado: perde a janela de exibição, o aparelho reapresenta a imagem anterior, e o trabalho feito para produzi-lo vai fora. E no regime imersivo a conta cai sobre o sistema vestibular. Ninguém abandona uma sessão porque a animação está menos lisa; abandona porque passou mal.

flowchart TD
    V["Aparelho de maior cadencia:<br/>o visor a 90 imagens por segundo"] --> TE["Tempo por quadro:<br/>11,1 ms"]
    F["Maquina mais fraca:<br/>video integrado, sem placa dedicada"] --> TR["Trabalho que cabe<br/>dentro desse tempo"]
    TE --> EN["O envelope do quadro"]
    TR --> EN
    EN --> C["Custo:<br/>o que o nosso laco gasta<br/>e o que podemos cortar"]
    EN --> I["Intervalo:<br/>o que o corpo percebe<br/>entre duas imagens"]
Figura 6: As duas metades do teto, com origens em aparelhos opostos, e as duas grandezas que o painel exibe.

Os onze milissegundos vêm das 90 imagens por segundo do visor, e aqui está a sutileza: o teto tem duas metades, e elas vêm de aparelhos diferentes. O tempo disponível vem do aparelho de maior cadência; o trabalho que cabe nesse tempo vem da máquina mais fraca, o computador de mesa de vídeo integrado. Orçar só por ela daria 16,7 ms, que parece folga e é armadilha. Eu escolho orçar pelo visor sabendo o preço: menos objetos, menos efeitos, menos capricho onde haveria folga. Aceito porque folga desperdiçada em janela custa aparência, e envelope estourado no visor custa mal-estar.

O painel dentro da cena mostra duas grandezas que parecem a mesma. O custo é o tempo do nosso trabalho de quadro, e é o que podemos cortar. O intervalo é o tempo real entre duas imagens entregues, e é o que o corpo percebe. Custo em 3 ms com intervalo em 22 é gargalo fora do nosso laço. Otimizar o código ali é passar a tarde melhorando a metade que já estava barata. O painel é uma placa presa ao tampo, e não à câmera: dentro do visor não existe console, e texto grudado no rosto é desconforto conhecido.

Três números que este texto poderia ter dado e não deu. Quantos triângulos cabem no envelope da máquina de vídeo integrado? Não sei, porque a medida exige a cena carregada e a cena tem cinco caixas. Qual é a taxa de quadros a perseguir? Não está fixada, e a decisão de não fixá-la é minha. E a latência entre gesto e imagem exige equipamento próprio para ser medida. Número chutado ganha, pela aparência de precisão, a autoridade de medição.

O que fica pronto, então, são cinco caixas cinzentas sobre uma bancada e uma placa de texto no tampo. A imagem não é evidência de nenhuma das três decisões, porque o mesmo vídeo sairia de uma implementação errada nas três. O que existe é o instrumento, e não a medida. Leve daqui as três conferências mecânicas e aplique-as ao seu próprio ambiente. Leia a árvore impressa e justifique cada nível com uma frase sobre o domínio. Cronometre uma volta do eixo em duas máquinas de cadências diferentes. Leia o painel com o pior intervalo ao lado da média.