Criando um modelador 3D em C em uma semana
- Participei do evento de programação de uma semana chamado "Wheel Reinvention Jam" no outono passado.
- O objetivo era revisitar sistemas de software existentes com uma nova perspectiva.
- Criei um modelador chamado "ShapeUp"; assistir ao vídeo de demonstração do ShapeUp antes de ler isso ajuda a entender melhor.
- O ShapeUp pode ser usado diretamente no navegador.
Escolha da linguagem: C
- Entrei no Jam por causa da insatisfação com a lentidão do compilador TypeScript.
- Começando com os parsers TypeScript do esbuild ou do Bun, parecia viável um projeto que implementasse um subconjunto rápido de TypeScript.
- Mas uma comparação de velocidade de execução de comandos de terminal não seria uma demonstração interessante, então mudei para um projeto 3D.
- Graças aos campos de distância assinada com ray marching (SDFs), parecia possível criar um projeto 3D do zero em uma semana.
- Uma cena usando SDF pode ser implementada muito mais rápido do que um renderizador equivalente baseado em triângulos.
- Eu já tinha escrito shaders SDF antes, mas em nível bem básico, e editar código para modelar não parecia natural.
- Eu queria editar formas com o mouse, e vi essa Jam como a chance de tornar isso real.
- Nomeei o projeto de ShapeUp.
Vantagens de usar C
- C é uma linguagem muito simples e primitiva, então imaginei que iria gastar muito tempo resolvendo a falta de estruturas de dados embutidas e corrigindo bugs de ponteiros.
- Mas a simplicidade de C acabou sendo uma vantagem:
- Compila rapidamente
- A sintaxe não esconde operações complexas
- É simples o suficiente para não ficar procurando sintaxe o tempo todo
- Compila facilmente para nativo e WebAssembly
- As desvantagens de C podem ser evitadas com os hábitos que desenvolvi ao usá-la por 22 anos
- O ShapeUp é simples demais, sendo composto por apenas um arquivo C pequeno
Estrutura de dados do ShapeUp
- O modelo é composto por um array de structs chamado Shapes.
- Os Shapes ficam armazenados em um array estaticamente alocado.
- Sem risco de falha de alocação ou vazamento de memória
- O limite de 100 Shapes não foi realmente uma limitação
- Com pouco tempo para otimizar o renderizador, a taxa de frames provavelmente teria caído antes de chegar a 100
- Com mais tempo, eu teria dividido o modelo em blocos menores e feito ray marching dentro de cada bloco
- A alocação dinâmica acontece em apenas 3 pontos, chamando
malloc
- Armazenamento (alocar um buffer grande o suficiente para conter o documento inteiro)
- Exportação OBJ (alocar um buffer grande o suficiente para conter todos os vértices)
- Geração de shader GLSL (buffer para o código-fonte do shader)
- Em todos os casos, há um único
free no final da função.
- Um exemplo de como gerenciar memória em C pode ser simples.
- Linguagens como C#, JavaScript e Python forçam uma estrutura de alocação em que cada Shape faz malloc separado e o ponteiro é armazenado em um array dinâmico.
- É ótimo poder controlar o layout de memória com C.
Interface do usuário
- Implementada com immediate mode user interface (IMGUI)
- Gosto da UI no estilo IMGUI
- Depuração muito fácil
- Uso de uma linguagem de programação real para posicionar elementos (ao contrário de CSS, constraints e SwiftUI)
- Como a maioria dos IMGUIs, usa enum para rastrear onde está o foco e o que o mouse está fazendo
- Este projeto não precisou de arrays dinâmicos ou hashmaps, mas se precisasse usaria algo como
stb_ds.h.
Problemas com a biblioteca raylib
- A escolha de usar C foi boa, mas a raylib se tornou um problema
- Há escolhas de design estranhas que prejudicam a experiência de desenvolvedor:
- Uso de
int onde se espera um tipo enum, impedindo a checagem de tipos do compilador e fazendo com que a função não se auto-documente
- Não há validação de parâmetros padrão (por decisão de design)
- Não assume responsabilidade pelas dependências (não conserta problemas do GLFW nem envia patches)
- A biblioteca de UI raygui é só um brinquedo
- Não consegue exibir números de ponto flutuante
- Não lida com roteamento de eventos de mouse para elementos sobrepostos ou recortados
- Não permite criar cantos arredondados
- Não permite estilizar para ficar bonito
- Também há bugs
- Bug que impede trocar a fonte
- As funções de desenho não compartilham vértices entre triângulos, causando lacunas de pixel
- Sempre que encontrava problemas, reportava, mas a maioria era fechada como "won't fix"; acabei desistindo porque daria muito tempo escrever relatórios de bugs
- Foi bom ter a criação de janelas OpenGL, mas paguei um preço alto pela conveniência
- Felizmente, encontrei uma saída: usar funções OpenGL diretamente ou implementar recursos do zero
- Penso em usar sokol daqui para frente
Desenvolvimento durante a semana
- O ShapeUp foi dividido em 4 partes principais para serem concluídas em 6 dias:
- Interface do usuário (ferramenta 3D, atalhos de teclado, barra lateral, controle de jogo)
- Gerador de shader GLSL + renderizador de ray marching
- Seleção por mouse em GPU
- Marching cubes para exportação
- Cada uma delas não era difícil, mas foi difícil acertar a prioridade certa sem se desviar.
- Problemas chatos ou que demandavam muito tempo foram resolvidos por design ou com soluções ingênuas que funcionam em 90% dos casos.
- Às vezes, adiar uma funcionalidade por cerca de um dia permitia achar a solução quase sem perceber.
- Procuro manter um modelador 3D sempre funcionando e melhorá-lo gradualmente conforme o tempo permitir.
- Pensei nisso como construir uma pirâmide. Se você construir por níveis, a pirâmide pode não ficar completa até o topo, mas em qualquer ponto de parada ela pode ser uma pirâmide completa.
Resultado do projeto
- Uma semana depois, fica com um programa 3D capaz de criar modelos significativos e exportá-los como arquivo
.obj.
- Roda em múltiplas plataformas e tem abertura/ gravação de arquivos.
- O projeto tem 2.024 linhas de código C e 250 linhas de GLSL.
- É um pouco surpreendente que algo em torno de 2.300 linhas consiga representar um modelador 3D razoavelmente útil.
- Recebi pedidos para mostrar uma demo do ShapeUp no resumo do Jam e na conferência Handmade Seattle.
- As pessoas pareceram impressionadas com o ShapeUp, mas não parece que foi considerado um grande feito; é um projeto relativamente simples.
- Se há algo de especial no que fiz, foi o senso de escolher o que construir, o conhecimento necessário para fazê-lo e a disciplina para entregar em menos de uma semana.
Opinião GN⁺
- Um projeto interessante que mostra bem as vantagens de simplicidade e velocidade de C. Contudo, pelo baixo nível de abstração, talvez não seja viável usá-lo diretamente em projetos comerciais. Implementar em C todas as funcionalidades de uma ferramenta moderna de modelagem 3D provavelmente exigiria esforço enorme.
- É impressionante concluir um programa funcional em uma semana. Mas, sob a perspectiva de longo prazo de manutenção e expansão de recursos, C++ ou Rust podem ser opções melhores.
- A técnica de renderização com SDF é rápida e simples, mas parece limitada em liberdade e qualidade de modelagem. Ferramentas de modelagem comerciais normalmente usam tecnologias de superfície como SubD e NURBS. Ainda assim, para jogos e demos, onde o tempo real importa, o ray marching com SDF ainda parece ter alto valor.
- O caso ilustra bem o desafio de escolher uma biblioteca open source. É importante avaliar com atenção documentação, qualidade do código e suporte. A implementação própria também pode ser uma boa alternativa.
- Construir primeiro um programa funcional e melhorá-lo gradualmente ao longo do tempo também é muito útil no trabalho real. Parece importante ajustar bem a prioridade: completar recursos centrais e depois refinar os detalhes.
1 comentários
Comentários do Hacker News