Pitch: Criei um cemitério 3D para enterrar bugs, e não escrevi uma linha de código
Todo dev tem aquele bug. Aquele que te fez questionar sua escolha de carreira, que apareceu na sexta às 17h58, que sumiu sozinho e voltou três sprints depois com HP duplo.
Eu sou técnico em Informática para Internet, recém-formado no ensino médio técnico integrado, e decidi que meu primeiro projeto de portfólio público não seria mais um to-do list ou clone de e-commerce.
Criei o Bug Obituary: um cemitério 3D interativo onde cada bug resolvido recebe um obituário gerado por IA, com nome dramático, causa mortis técnica, legado e epitáfio gravado em pedra.
A ideia
A premissa é simples: você preenche um formulário com o nome do bug, a descrição, quando ele apareceu e quando foi corrigido. A IA gera um obituário literário e irônico. O bug vira uma lápide no cemitério 3D, onde qualquer pessoa pode navegar, explorar e ler os obituários.
O visual é estilo Tim Burton: lápides com musgo, trincas, névoa exponencial, luz de lua fria e velas tremulando perto das pedras. Tudo construído com primitivos do Three.js, sem nenhum asset externo.
A metodologia: por que não escrevi código
No meu último ano do técnico, meu professor de Sistemas Web II deu uma aula que ficou comigo: como usar IA como ferramenta real de desenvolvimento. A metodologia que ele ensinou incluía sempre instruir a IA com exemplos do seu próprio código, assim ela não destoa do que você já conhece, e quando errar, você consegue identificar e corrigir com base no que sabe.
Esse projeto foi minha aplicação prática disso. Usei IA como parceira de implementação, mas cada decisão de arquitetura, cada problema identificado e cada iteração partiu de mim.
Para organizar tudo isso, criei um AGENTS.md na raiz do projeto: um documento que serve como fonte da verdade para qualquer agente de IA que trabalhe na codebase. Ele descreve a stack, a estrutura de pastas, convenções do App Router, o contrato da API, o schema do Redis, as regras de estilo, e até uma seção de proibições explícitas (como usar CapsuleGeometry no Three.js r128, que não existe nessa versão).
Quando algo estava errado, eu escrevia um prompt cirúrgico descrevendo o problema, a causa raiz e a solução esperada, em vez de só dizer "conserta isso".
As decisões técnicas
Por que Next.js 16 com App Router
O App Router muda fundamentalmente como você pensa em componentes. A regra que adotei foi simples: tudo é Server Component por padrão, e "use client" só entra quando o componente precisa de hooks, eventos ou APIs do browser.
Na prática isso significa que app/page.tsx busca os bugs diretamente no Redis no servidor e passa como prop para o GraveyardScene, que é dinamicamente importado com ssr: false porque o Three.js precisa do window. Zero waterfall de dados, zero loading state para o conteúdo inicial.
Por que Three.js e não uma biblioteca de abstração como React Three Fiber
React Three Fiber é excelente, mas adiciona uma camada de abstração que eu queria evitar para aprender de verdade como o Three.js funciona. Toda a lógica de cena vive em módulos TypeScript puros em src/three/: scene.ts, gravestone.ts, graveyard.ts, atmosphere.ts, lighting.ts, raycaster.ts e controls.ts. O componente React só monta o canvas e conecta o estado.
O estilo visual usa MeshToonMaterial em tudo, é ele que produz o look cel-shaded característico do Tim Burton. Nada de MeshStandardMaterial ou MeshPhongMaterial.
As lápides são de três tipos, sorteadas deterministicamente por um hash do ID do bug (não só o primeiro caractere, que em UUIDs v4 tende a repetir). Cada uma tem detalhes de desgaste construídos com geometria: manchas de musgo (BoxGeometry finos e achatados com MeshToonMaterial verde escuro), trincas (slices finíssimos em ângulo levemente diferente do corpo) e uma placa de nome em relevo. Nenhuma textura.
O hover funciona por raycasting: a cada frame, o Raycaster do Three.js testa o mouse contra todos os objetos da cena. Quando detecta uma lápide, sobe a hierarquia do objeto até encontrar o bugId no userData do grupo, acende o emissive em roxo frio e posiciona o card 2D do Framer Motion sobre a pedra via projeção de coordenadas 3D para CSS pixels.
Por que Redis e não PostgreSQL
Essa foi uma das decisões mais deliberadas do projeto, e vale explicar o raciocínio.
Um banco relacional como PostgreSQL faz sentido quando você tem relações entre entidades, precisa de queries complexas com joins, ou vai crescer para um modelo de dados rico. Para o Bug Obituary, o modelo de dados é extremamente simples:
- Um bug tem um ID, um nome, datas, stack e um obituário gerado.
- Não há usuários, não há relações, não há necessidade de filtros complexos.
- A operação mais frequente é "me dê todos os bugs ordenados por data", uma leitura simples.
Redis resolve isso com duas operações: um SET por bug (bug:{id} com o JSON) e um índice (bugs:index com array de { id, createdAt } ordenado). Leitura e escrita são O(1) para cada bug individual.
Além disso, o Upstash Redis tem um free tier generoso com acesso via HTTP REST — o que significa que funciona em qualquer runtime, inclusive Edge, sem precisar gerenciar conexões de pool como você faria com PostgreSQL em ambiente serverless.
Se o projeto crescesse para incluir usuários, autenticação, busca por stack ou filtros por período, aí sim um PostgreSQL (provavelmente com Neon ou Supabase no mesmo contexto de deploy) faria mais sentido. Por enquanto, Redis é a escolha certa.
Por que Groq e não OpenAI
Velocidade e custo. O Groq usa LPU (Language Processing Unit), hardware especializado que entrega tokens muito mais rápido que GPU convencional. O modelo llama-3.3-70b-versatile no free tier responde em 1-2 segundos, o que é aceitável para uma operação de escrita.
O prompt usa uma instrução strict de JSON-only no system prompt: sem preâmbulo, sem markdown, sem explicação. Qualquer desvio disso retorna 422 na API route. O parsing é simples: JSON.parse(raw) com try/catch, e se falhar, erro explícito.
Os problemas que apareceram
As lápides estavam em linha reta
O layout inicial usava uma grade simples por linhas e colunas. Com poucos bugs, todos ficavam na mesma linha: literalmente uma fileira horizontal no centro da cena.
A solução foi substituir por distribuição polar: as lápides se espalham em um arco de 126° ao redor do ponto focal da câmera, com profundidade variada. Bugs mais novos ficam mais perto da câmera, mais antigos recuam para a névoa. As posições são estáveis entre re-renders porque usam um gerador pseudo-aleatório seeded pelo ID do bug.
A meia-lua estava deitada de lado
O CylinderGeometry com thetaLength = Math.PI cria um semicírculo, mas a rotação z = Math.PI/2 que eu tinha colocado deitava a curvatura para o lado em vez de para cima. O fix foi ajustar os eixos de rotação para que o diâmetro plano ficasse horizontal e alinhado com a largura do corpo da lápide.
Só aparecia um tipo de lápide
O tipo era selecionado com parseInt(id[0], 16) % 3, só o primeiro caractere do UUID. Em UUIDs v4, os primeiros bits têm estrutura fixa, então o primeiro caractere tendia a se repetir e o módulo favorecia sempre o mesmo tipo. A solução foi um hash de Horner sobre a string inteira, que distribui os três tipos uniformemente.
A stack completa
- Next.js 16 (App Router, TypeScript strict)
- Three.js r128 (MeshToonMaterial, OrbitControls, raycasting)
- Groq API com LLaMA 3.3 70B
- Upstash Redis para persistência
- Framer Motion para animações 2D (flip do card, modal)
- Tailwind CSS v4.2
- Vercel para deploy
Resultado
O projeto está no ar, é open source, e qualquer pessoa pode enterrar um bug.
Se você quiser ver o código, entender como o AGENTS.md está estruturado ou só quiser enterrar aquele bug que te perseguiu por semanas, esses são os links:
⚰️