Reconstruindo o Minecraft do zero em 24 horas (sem game engine)
A meta era simples no papel: recriar a engenharia central do Minecraft em menos de 24 horas, sem Unity, sem Godot, sem nenhuma game engine. Só TypeScript, matemática e o browser.
Esse post é a versão escrita do vídeo abaixo — se preferir ver o projeto funcionando, o vídeo mostra cada etapa em tempo real.
O resultado final roda em qualquer browser — você pode jogar agora mesmo:
▶ Jogar no browser · Ver código no GitHub
Mundo infinito com chunks dinâmicos, ciclo completo de dia e noite com céu estrelado, vegetação e árvores geradas proceduralmente, shaders de iluminação e física de player.
Capítulo 1 — 3D no browser
Como um quadrado vira cubo
Um plano cartesiano comum tem dois eixos: X (largura) e Y (altura). Para existir o 3D, precisamos de um terceiro eixo — o Z. O que faz o computador calcular e projetar essa profundidade na tela plana é um pipeline de três matrizes:
- Projection Matrix — calcula a perspectiva. É ela que “espreme” o mundo 3D na tela, criando a ilusão de que objetos distantes são menores.
- Model Matrix — define a posição, rotação e escala de cada objeto no universo do jogo.
- View Matrix — é a matriz da câmera. E aqui tem um detalhe contraintuitivo: a câmera nunca se move. Para economizar processamento, o computador move o universo inteiro na direção contrária. A câmera é sempre a origem.
Three.js em vez de WebGL puro
Descer nesse nível de matemática manual é possível, mas não era o objetivo aqui. O Three.js é uma biblioteca JavaScript que roda em cima da WebGL — a API nativa dos browsers para gráficos 3D — e abstrai todo esse trabalho de matrizes. Com ele, você pensa em objetos, posições e materiais.
Capítulo 2 — Geração de mundos
Simplex Noise
Para gerar um terreno que não seja nem plano nem completamente caótico, o algoritmo certo é o Simplex Noise — uma versão otimizada do Perlin Noise, ambos algoritmos clássicos que geram mapas de ruído com transições suaves.
O que importa entender é o que o algoritmo entrega: para cada coordenada (X, Z), ele retorna um valor suave entre -1 e 1 representando a elevação do terreno — sem pulos bruscos entre pontos vizinhos.
Capítulo 3 — Armazenando o mundo sem crashar o browser
Memória linear com Uint8Array
Como armazenar milhares de blocos sem travar o browser? JavaScript tem arrays tipados de baixo nível — e vamos usar o Uint8Array, um array de inteiros de 8 bits que vive direto na memória linear, sem o overhead de arrays comuns.
Cada chunk tem 32 × 32 × 32 blocos. Isso dá 32.768 posições — cada uma sendo um inteiro que mapeia para um tipo de bloco:
0 → ar
1 → terra
2 → pedra
3 → grama
...
Para acessar um bloco em posição (x, y, z) dentro do chunk:
const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
const blockType = chunk[index];
O mundo é composto por vários chunks, cada um sendo um Uint8Array. Com a estrutura de dados definida, bastou cruzar o Simplex Noise com o Three.js lendo as posições dos arrays — e o terreno apareceu.
Capítulo 4 — Shaders, otimização e geração procedural
Iluminação
Em vez de shaders pesados logo de início, a abordagem foi uma iluminação direcional simples — o sol projeta sombras nos blocos com base no seu ângulo. Funciona bem e o custo de processamento é baixo.
Render distance e face culling
A primeira regra de otimização: só processar o que o jogador pode ver.
- Render distance rígida: o jogo só carrega chunks dentro de um raio ao redor do jogador. O resto é descartado da memória.
- Face culling (meshing algorithm): faces entre dois blocos sólidos nunca são visíveis — não faz sentido renderizá-las. O algoritmo constrói a mesh incluindo apenas as faces expostas ao ar. Em um chunk denso, isso elimina a maior parte da geometria.
Geração procedural
O mundo é infinito porque novos chunks são gerados sob demanda. A cada frame, o jogo calcula a distância do jogador até a borda do mundo carregado. Se for menor que 32 blocos, um novo chunk é gerado naquela direção. Como gerar um chunk é pesado, o processamento roda em uma Web Worker separada — sem bloquear a thread principal.
Capítulo 5 — Texturas, vegetação, dia e noite
Texture Atlas
Para adicionar texturas sem multiplicar materiais, a estratégia é o texture atlas: uma única imagem com todas as texturas do jogo lado a lado. O shader mapeia cada face do bloco para a região correta dessa imagem. Um material, muitas texturas.
Vegetação e árvores
Depois que o terreno é gerado, uma segunda passagem percorre camada por camada. Para cada bloco com o topo em contato com o ar:
- Vegetação: adicionada de forma aleatória com uma probabilidade configurável.
- Árvores: um bloco de madeira é colocado no chão, o tronco é empilhado com altura aleatória, e a copa é calculada como uma esfera matemática ao redor do topo — raio definido, blocos de folhas preenchendo o volume.
Ciclo dia e noite
- O sol se move gradualmente. Como já emite luz direcional, a iluminação do mundo muda automaticamente conforme ele se desloca.
- A lua sempre está do lado oposto ao sol.
- O céu escurece gradualmente conforme o sol some no horizonte.
- As estrelas ganham opacidade conforme o céu escurece — somem de dia, aparecem à noite.
- Nuvens são geradas no início e se movem continuamente. Quando saem do campo de visão, novas são geradas — dando a impressão de vento.
Capítulo 6 — Ações e resultado final
Com o mundo pronto, adicionei uma hotbar para o jogador selecionar o bloco ativo e as ações de quebrar e colocar blocos.
Ao quebrar um bloco, percebemos um problema: a mesh do chunk inteiro era reconstruída — 32.768 blocos reprocessados para alterar um único. A solução foi dividir cada chunk em subchunks de 8 × 8 × 32: ao alterar um bloco, só o subchunk correspondente é reconstruído — 2.048 blocos em vez de 32.768.
Esse projeto existiu para provar um ponto: as bases de um jogo de mundo aberto não são magia — são estruturas de dados, matemática linear e algoritmos bem escolhidos.
Se tiver curiosidade sobre algum dos conceitos aqui — Simplex Noise, pipeline de matrizes 3D ou o algoritmo de meshing — me conta nos comentários.