← Blog
Nosso diário12 min de leitura

Projetando a Água-viva: Forma, Pixels e Luz.

A água-viva do AirJelly não é um sprite. Seu sino é calculado a partir de uma curva, o visual em pixels vem da renderização em baixa resolução, e seu brilho vem de um pequeno mapa de luz. Aqui está como construímos essas partes e o que elas permitem que o personagem faça dentro de um produto de IA.

Translated by AI

Abra o Laboratório da Água-viva e você encontrará nosso mascote nadando pelo escritório, com tentáculos de contas atrás dele e uma mancha de luz colorida no chão. Parece um sprite, mas é desenhado do zero enquanto a página está rodando.

A forma do sino, o rosto, os tentáculos, o grão de pixel e o brilho vêm todos de um pequeno conjunto de parâmetros. Cada um é exposto como um controle deslizante, então o artigo acompanha controles que você pode experimentar no laboratório.

  • O sino é calculado linha por linha. Cinco formas de sino usam cinco curvas de largura diferentes; um rosto precisa de apenas um a três pixels por olho.
  • O mundo é renderizado em um canvas com um quarto do tamanho da viewport, e depois esticado de volta sem suavização.
  • O brilho vem de um mapa de luz em escala 1/16 com queda quadrática, níveis de luz escalonados e uma matriz de dithering 4×4.

Todas as imagens aqui são capturas de tela do laboratório. Usamos a mesma água-viva pêssego do início ao fim, com a predefinição de rosto desenhado à mão do autor. Nada foi montado para o artigo.

1. O personagem é uma função

Um personagem em pixel art geralmente começa em um editor de imagens. O nosso começou com uma pergunta: qual é a menor descrição de “uma água-viva” que ainda permite que alguém faça a sua parecer diferente da minha?

A resposta acabou sendo uma curva de largura. O sino é desenhado uma linha de cada vez, de cima para baixo, e a única coisa que muda entre um sino, uma cúpula, uma bolha, uma lanterna e uma gota é a função que diz qual é a largura de cada linha. Uma cúpula se abre rápido com um ombro suave; uma bolha é mais larga no meio e estreita nas duas pontas; uma lanterna se abre no primeiro sexto e se recolhe no último quinto; uma gota atinge a largura total cerca de três quartos do caminho para baixo e então estreita; um sino simples é um alargamento reto.

Água-viva em pixels do AirJelly com corpo em forma de sino, comparando curvas de largura do sino enquanto mantém a mesma paleta pêssego e rosto.Sino
Água-viva em pixels do AirJelly com corpo em forma de cúpula, comparando curvas de largura do sino enquanto mantém a mesma paleta pêssego e rosto.Cúpula
Água-viva em pixels do AirJelly com corpo arredondado em forma de bolha, comparando curvas de largura do sino enquanto mantém a mesma paleta pêssego e rosto.Bolha
Água-viva em pixels do AirJelly com corpo em forma de lanterna, comparando curvas de largura do sino enquanto mantém a mesma paleta pêssego e rosto.Lanterna
Água-viva em pixels do AirJelly com corpo em forma de gota, comparando curvas de largura do sino enquanto mantém a mesma paleta pêssego e rosto.Gota
Cinco tipos de corpo, um conjunto de controles deslizantes. Apenas a curva de largura difere. (Altura do sino aumentada aqui para que as curvas apareçam; o sino do próprio autor é curto e achatado.)

Duas pequenas regras ficam por baixo da curva. O controle deslizante de “arredondamento” (1–4) decide quantas linhas a coroa leva para se abrir. Cada linha também tem uma largura ímpar. Essa coluna central importa mais do que parece: os olhos e a boca podem ficar exatamente no centro em vez de meio pixel fora.

A partir daqui o resto do corpo segue. Largura, largura do topo, altura, arredondamento e achatamento são cinco controles deslizantes. Como a forma é calculada em vez de armazenada, o sino também pode se deformar: quando você o arrasta, a linha que você agarrou se inclina mais do que as linhas distantes do cursor. Essa única regra é o que faz ele parecer borracha macia em vez de um cartão rígido.

Água-viva em pixels do AirJelly em repouso antes de ser arrastada.Em repouso
Água-viva em pixels do AirJelly sendo arrastada perto do topo, com o corpo inclinado e os tentáculos arrastando.Agarra perto do topo
Água-viva em pixels do AirJelly sendo arrastada perto da borda inferior, mostrando deformação ao redor do ponto de agarre.Agarra perto da borda
Arrastada para a direita, agarrada perto do topo e perto da borda. O sino se inclina, os tentáculos arrastam e a água deixa um rastro — tudo real, no meio do arrasto.

A rotação é tratada da mesma forma, mas ao contrário: em vez de rotacionar pixels para frente (o que deixaria buracos), o renderizador percorre a caixa delimitadora rotacionada, mapeia cada célula da tela de volta para o espaço do sino e pergunta “isto está dentro da linha r?”. Vizinho mais próximo, sem anti-aliasing, então uma água-viva inclinada permanece tão nítida quanto uma em pé.

O rosto

Os olhos ficam a 54% da altura do sino, espaçados a 18% da largura do sino. Essas duas proporções são todo o sistema de layout. A expressão é então apenas quais um a três pixels se acendem: um ponto, um traço, um ^, ou um ponto deslocado um pixel para cima ou para dentro. Curioso é um olho ligeiramente mais alto que o outro. Travado é ambos os olhos empurrados em direção ao nariz. Sonolento é dois traços. Tonto — acionado por três curvas bruscas em 850 ms — é dois traços largos e um balanço.

Água-viva em pixels do AirJelly com olhos de ponto neutros, demonstrando seu sistema de expressão em pixels integrado.neutro
Água-viva em pixels do AirJelly com um rosto curioso de olhos desiguais, demonstrando seu sistema de expressão em pixels integrado.curioso
Água-viva em pixels do AirJelly com sobrancelhas inclinadas e expressão focada, demonstrando seu sistema de expressão em pixels integrado.focado
Água-viva em pixels do AirJelly com um sorriso largo expressando felicidade, demonstrando seu sistema de expressão em pixels integrado.feliz
Água-viva em pixels do AirJelly com boca reta expressando estar travada, demonstrando seu sistema de expressão em pixels integrado.travado
Água-viva em pixels do AirJelly com um sorriso curvo e convencido, demonstrando seu sistema de expressão em pixels integrado.convencido
Água-viva em pixels do AirJelly com olhos fechados e rosto sonolento, demonstrando seu sistema de expressão em pixels integrado.sonolento
Água-viva em pixels do AirJelly com olhos horizontais relaxados e rosto calmo, demonstrando seu sistema de expressão em pixels integrado.calmo
Água-viva em pixels do AirJelly com olhos altos e boca aberta expressando surpresa, demonstrando seu sistema de expressão em pixels integrado.surpreso
Água-viva em pixels do AirJelly com o rosto do estado tonto usado durante o movimento, demonstrando seu sistema de expressão em pixels integrado.tonto
Nove expressões e o rosto tonto (rosto desenhado à mão desativado para que os olhos integrados apareçam). Cada uma é de um a três pixels por olho, mais uma boca opcional duas linhas abaixo.

O que vende o rosto não são os pixels, mas para onde eles olham. Os olhos são a única parte do corpo que tem suas próprias regras de movimento: eles piscam um pixel contra a direção em que você puxa o sino, e enquanto você nada com o teclado eles lideram a curva antes do corpo. A boca só aparece quando a água-viva está falando ou emotando; enquanto fala, ela abre e fecha em uma senoide rápida. Nada disso precisou de um quadro de animação para ser desenhado.

Tentáculos

Não há braços orais nem sprites de tentáculos. Cada tentáculo é uma corrente de contas quadradas sob integração de Verlet, ancorada a 28% abaixo do sino, com três passagens de restrição de distância por quadro para manter as contas na distância de seu “espaço”. O que os faz parecer submersos são três ondas senoidais em três frequências diferentes — uma deriva lenta, um balanço mais rápido e um enrolamento ponderado em direção à cauda — para que as raízes fiquem calmas e as pontas ondulem. Mova o cursor através deles e eles se abrem e rodopiam; pare e eles se acomodam no próprio ritmo.

Os controles deslizantes dos tentáculos (quantidade 3–12, segmentos 4–18, gravidade, atrito, tensão, corrente, enrolamento, esquiva) são expostos diretamente, e quatro “predefinições de movimento” de um clique — Parado, Vivo, Selvagem, Deriva — não são nada mais que quatro combinações salvas desses seis números.

Cor

Sete paletas de três cores vêm por padrão — sino, tentáculos, olhos — mais seletores de cor livres para cada um:

PaletaSinoTentáculosOlhos
oceano#8ae2ff#8ae2ff#303030
pôr do sol#ffb38a#ff8fc7#3a2a2a
lima#b6f5a0#8ce6c0#26331f
uva#c7a2ff#a98cff#2c2640
rosa#ff9ec4#ffc0dd#3a2630
dourado#ffe08a#ffd06a#3a3320
mono#cfd8e3#aab4c2#20242b

A cor da campânula faz mais do que colorir a campânula. O motor a escreve na variável de destaque da página (e a cor dos tentáculos no destaque secundário), então toda a interface do laboratório se re-tematiza para qualquer água-viva que você esteja criando. Preto, branco, cinzas e o vermelho semântico nunca mudam; todo o resto segue você. E, como veremos na parte 3, ela também se torna a cor da luz que você carrega.

Se a campânula procedural não for suficiente, você pode desativá-la e pintar a sua própria em uma grade de 6×6 a 28×28, e pintar até oito rostos personalizados por cima. Treze dos nossos colegas de equipe fizeram exatamente isso; suas águas-vivas agora vivem na cena do escritório como NPCs, cada uma com seu próprio corpo, número de tentáculos, expressão e falas de diálogo.

2. O visual pixelado é uma resolução

A forma mais comum de obter um “visual pixelado” na web é renderizar em resolução total e pós-processar — ajustar cores, adicionar uma sobreposição de grade, talvez desfocar e requantizar. Fizemos o oposto: o mundo é de fato minúsculo. Ele é desenhado em um canvas com um quarto do tamanho da janela — uma célula do mundo equivale a quatro pixels de tela na escala padrão — e o navegador é instruído a esticá-lo sem suavização.

Cada chamada de desenho no motor escreve valores de cor empacotados de 32 bits diretamente nesse pequeno buffer — a campânula, as contas dos tentáculos, os ladrilhos do terreno, até o texto. Não há nenhum caminho vetorial em lugar algum na camada do mundo, então nada pode ser suavizado por acidente. As posições são arredondadas para a grade de células em cada âncora: o topo da campânula, a linha dos olhos, cada ladrilho do terreno, o deslocamento da câmera usado pela grade de iluminação.

Isso tem uma consequência agradável para o controle deslizante de “tamanho do pixel”. Como ele altera quantos pixels de tela uma célula do mundo cobre (4 × 0,7 a 4 × 1,6), ele muda tanto o grão da imagem quanto o tamanho aparente da água-viva, em um único movimento. Não há zoom separado.

Água-viva do AirJelly na escala de tamanho do pixel 0,7, mostrando pixels mais finos e um personagem menor na cena do escritório.escala 0,7
Água-viva do AirJelly na escala de tamanho do pixel 1,0, mostrando o grão de pixel e o tamanho de personagem de referência na cena do escritório.escala 1,0
Água-viva do AirJelly na escala de tamanho do pixel 1,6, mostrando pixels mais grossos e um personagem maior na cena do escritório.escala 1,6
O controle deslizante de tamanho do pixel em 0,7, 1,0 e 1,6. O grão e o tamanho do personagem mudam juntos — o texto da sala atrás também fica mais grosso.

Dois canvases e uma camada de texto real

Uma decisão estrutural da qual gostamos especialmente: o mundo são dois canvases com uma camada HTML entre eles. Os pisos das salas e os títulos são pintados no canvas inferior. O texto no estilo de blog que vive dentro do escritório é HTML comum, selecionável e acessível, por cima disso. O canvas superior — ondulações da água, itens, portais, a própria água-viva — deixa cada célula intocada transparente, então o texto é lido através da água vazia, mas a água-viva nada por cima dele.

Água-viva do AirJelly e pixels de água em camadas sobre um título, parágrafo e link de download em HTML real, demonstrando a composição de dois canvases.início · a água-viva sobre texto HTML real
A sala inicial. O título, o parágrafo e o link são HTML comum situado entre os dois canvases; a água-viva e os pixels de água são pintados por cima deles. Nenhum glifo é rasterizado.

O resultado é um texto que parece afundado na cena em vez de colado sobre ela. O canvas de fundo também é armazenado em cache: ele só se repinta quando a câmera se move por uma célula inteira ou quando a cena muda.

Dither, não gradientes

Onde a imagem precisa de um gradiente — principalmente na iluminação — usamos dithering ordenado em vez de interpolação suave. Uma matriz de Bayer 4×4, indexada por coordenadas do mundo para que o padrão permaneça fixo enquanto você rola, quebra a luz em faixas planas e então dispersa as emendas. “Passos de luz” é exposto como um controle deslizante (2–8, padrão 3). Em 2, o piso parece uma xilogravura de dois tons; em 8, fica próximo do suave. Três é onde ainda parece feito à mão.

Cena do escritório do AirJelly com 2 passos de iluminação, demonstrando forte contraste semelhante a xilogravura ao redor da água-viva brilhante.2 passos
Cena do escritório do AirJelly com 3 passos de iluminação, demonstrando os níveis de luz em faixas padrão ao redor da água-viva brilhante.3 passos (padrão)
Cena do escritório do AirJelly com 8 passos de iluminação, demonstrando transições de luz mais suaves ao redor da água-viva brilhante.8 passos
O escritório com passos de luz em 2, 3 e 8 (brilho da água-viva aumentado para o piso ficar visível). Dois é uma xilogravura; oito é quase suave; três é o padrão.

A correção de cor — um matiz global e um ajuste de saturação — passa por uma tabela de consulta 32×32×32, que discretamente quantiza a paleta para cinco bits por canal como efeito colateral. Todo canvas de pré-visualização na interface tem a suavização desativada; o único lugar em que o motor usa um desfoque CSS é nas transições da interface, nunca no mundo.

3. O brilho é um campo de luz

Uma água-viva que não brilha não é uma água-viva. A implementação óbvia — um halo radial suave misturado sobre a cena, ou uma passada de desfoque — foi descartada no primeiro dia porque produz gradientes suaves de subpixel que parecem errados em uma grade de pixels rígida. Um halo suave flutua sobre o mundo; ele não pertence a ele.

Então, em vez de desenhar um brilho, nós iluminamos a sala. Cada luz — a água-viva, lâmpadas, itens brilhantes — é estampada em um pequeno mapa de luz com um quarto da resolução do canvas (um dezesseis avos da tela), com uma queda (1 − d/r)²: não fisicamente correta, mas suave, barata e fácil de ajustar. O piso e os móveis são então clareados pela quantidade de luz que os alcança, nos mesmos três passos planos com o mesmo dither. Dois mapas são mantidos — um para o piso e outro para objetos com um raio 1,6× mais amplo, para que os adereços fiquem dentro de um halo ligeiramente maior do que o chão sob eles.

Cena do escritório do AirJelly com composição de luz desativada, fornecendo a linha de base para a comparação de iluminação da água-viva.composição de luz desativada
Cena do escritório do AirJelly com composição de luz ativada, mostrando luz pêssego e faixas com dither no piso ao redor da água-viva.composição de luz ativada
O mesmo ponto com a passada de iluminação desativada e ativada. Com ela ativada, o piso fica em faixas e com dither, e a luz pêssego da água-viva incide sobre tudo ao seu redor — a própria água-viva não tem halo pintado. (Raio e intensidade do brilho aumentados para ficar visível na página.)

A passada de sombreamento multiplica cada pixel pela luz ambiente mais o campo de luz, limitado a um teto. Esse teto — 1,25, significando que a luz pode empurrar uma cor até 125% de si mesma e não mais — é todo o orçamento de “bloom”. É daí que vem o núcleo ligeiramente superexposto do brilho, e é a diferença entre uma lanterna e um reflexo de lente.

Três detalhes de implementação são fáceis de perder:

  • A cor da campânula também é a cor da luz. Escolha uma água-viva rosa e o piso próximo fica rosa; o mesmo valor hexadecimal atualiza a cor de destaque do laboratório.
  • A cor é normalizada pelo seu canal de pico, então uma campânula azul-marinho escura ainda projeta um tom azul-marinho em força total em vez de mal iluminar a sala.
  • O tremeluzir atualiza a cada 8 quadros. Uma leve instabilidade combina mais com este mundo de pixels do que um cintilar perfeitamente suave.

Mais uma regra impede que as lâmpadas pareçam erradas: qualquer coisa que emita luz é excluída de ser iluminada. Itens com um componente de luz são desenhados com uma máscara emissiva, e a passada de sombreamento copia esses pixels sem alteração — então uma lâmpada nunca é escurecida pela sua própria luz ambiente.

Os controles

Tudo isso é exposto no painel Cena → Luz. Estes são os números com que acabamos lançando:

ControleIntervaloPadrão
Brilho ambiente0.2 – 10.55
Cor ambiente—#6d8fae
Passos de luz2 – 83
Teto de destaque1 – 1.51.25
Brilho da água-viva · raio8 – 8032
Brilho da água-viva · intensidade0 – 1.50.6
Brilho da água-viva · tremeluzir0 – 0.60.05

O piso só é re-sombreado quando o nível, os parâmetros do shader ou as posições das luzes mudam; luzes fora da viewport são descartadas antes de serem estampadas; a tabela de correção de cor é armazenada em cache. Na prática, o mundo inteiro — fundo, simulação de água, itens, tentáculos, iluminação, água-viva — cabe confortavelmente em um único quadro de animação em um laptop, em uma resolução em que uma janela de 1440px de largura tem apenas 360 células de largura.

4. Algumas coisas sobre como ele se move

Não estritamente sobre aparência, mas elas moldam como o personagem é percebido, então brevemente:

  • O impulso do teclado se acumula e se amortece, limitado a pouco menos de uma célula por quadro. A velocidade também alimenta a deformação da campânula, então um nado rápido visivelmente a comprime contra a “corrente”.
  • Um entalhe da roda alimenta um impulso na mesma velocidade que o teclado e equivale a uma célula de deslocamento em regime permanente.
  • A câmera segue lentamente enquanto você nada, mais rápido enquanto se acomoda, e muito lentamente por 1,6 segundos após um arrasto para não voltar de repente.
  • A velocidade horizontal define uma inclinação alvo. Uma mola rígida e amortecida a segue e deixa a campânula ultrapassar ligeiramente quando para.
  • O cursor empurra as contas dos tentáculos para longe dentro de 40 px. Dentro de 58 px, ele também adiciona um redemoinho e um pulso radial, com peso maior na cauda.
  • A cada duas células de deslocamento, um pulso suave é adicionado à simulação de onda 2D. Essas ondas podem acionar itens, que podem propagar o pulso.

5. Por que criar o personagem a partir de dados

Não começamos com uma teoria sobre personagens de IA. Começamos com um editor de água-viva e então percebemos uma vantagem prática: os mesmos parâmetros que tornam o personagem personalizável também o tornam legível para um agente.

Parâmetros são mais fáceis para um agente usar

Uma sprite sheet é opaca para um modelo. Nossa água-viva tem cerca de trinta números e alguns enums, pequeno o suficiente para um prompt, um blob JSON ou uma chamada de ferramenta.

  • Um agente pode ler os parâmetros e saber como é a água-viva do seu usuário.
  • Ele pode transformar “faça uma roxa sonolenta com tentáculos longos” em um preset sem gerar uma nova imagem. O NPC de cada colega de equipe é uma linha de dados em vez de um recurso de arte.
  • Expressão, brilho e movimento são estado de execução, então o mascote pode parecer concentrado enquanto o agente trabalha ou tonto quando fica confuso.
  • Os parâmetros podem variar amplamente enquanto o resultado ainda parece o mesmo tipo de água-viva.

Essa é a parte útil para o AirJelly. O mascote pode atuar como o corpo do agente porque sua aparência e comportamento são dados que o agente pode ler e alterar. Nunca precisamos pedir a um modelo para editar uma imagem opaca.

Por que ainda gostamos de pixels (e ASCII)

Há uma razão prática. Um sprite em pixel é uma pequena grade de índices de paleta; arte ASCII já é texto. Ambos podem ser descritos de forma compacta e editados célula por célula. Para as mudanças precisas de que precisamos, um sprite 16×16 simplesmente deixa menos espaço para um modelo improvisar do que uma pintura.

Há também uma razão mais subjetiva: gostamos de grades rígidas. Elas são honestas sobre serem feitas por uma máquina, e um erro de um pixel muitas vezes parece charmoso em vez de estranho. Esse tom combina com um produto sobre uma IA vivendo no seu computador. Pode ser por isso que os terminais parecem confortáveis em ferramentas de IA também.

Então foi aqui que chegamos: um personagem descrito por números e desenhado em uma grade. Por acaso parece retrô, mas a parte útil é como é fácil tanto para uma pessoa quanto para um agente alterar com precisão.

Uma última coisa. Se você ajustar uma água-viva de que gosta no laboratório, exporte seus parâmetros e envie para xiaohua@airjelly.ai — vamos colocá-la no site. Treze já vivem no escritório; sempre há espaço para mais uma.