2 pontos por GN⁺ 2024-05-03 | 1 comentários | Compartilhar no WhatsApp

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:
    1. Interface do usuário (ferramenta 3D, atalhos de teclado, barra lateral, controle de jogo)
    2. Gerador de shader GLSL + renderizador de ray marching
    3. Seleção por mouse em GPU
    4. 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

 
GN⁺ 2024-05-03
Comentários do Hacker News
  • Concordo plenamente com o autor sobre as limitações do Raylib
    • Estou desenvolvendo um jogo no estilo tower defense começando com Raylib, mas enfrento limitações semelhantes
    • Há problemas como transição de tela cheia inconsistente entre plataformas, impossibilidade de enumerar modos de tela, impossibilidade de alternar recursos de renderização em tempo de execução e não conseguir salvar shaders compilados
    • Raylib é ótimo para prototipagem, mas é difícil ir além disso sem aceitar limitações severas
    • O desenvolvimento avançou demais e agora está tarde demais para substituir o Raylib por SDL etc.
  • Guardar formas em arrays de alocação estática é uma abordagem adorável, sem risco de falha de alocação ou vazamento de memória. Na prática, o limite de 100 formas não é uma restrição.
  • Espero que esse projeto continue evoluindo. Em alguns meses, pode se tornar uma alternativa séria para casos de uso específicos do Blender/FreeCAD.
  • Adorei a demonstração ao vivo no vídeo. Fora a criação do app, não acredito que se consiga fazer um vídeo assim em apenas uma semana.
  • Um texto interessante sobre decisões de processamento de memória e outras áreas. Ao voltar a estudar C mergulhando na Parte 2 de Crafting Interpreters, isso me lembrou do que C faz bem.
  • Obrigado pelo esforço de tornar possível com apenas 2024 linhas de código em C o que foi alcançado aqui :)
  • Existe algo realmente poderoso em pegar uma ferramenta que você conhece bem e fazer algo legal com ela. Gostei do texto.
  • Concordo totalmente com o argumento a favor de C. Concordo ainda mais com o ponto de que "a sintaxe não esconde operações complexas; por ser simples, não é preciso ficar procurando o tempo todo". Além disso, se houver algo para procurar sobre C, é muito fácil e útil. Essa é a vantagem de uma linguagem simples e antiga.
  • Às vezes, sinto que C é tudo que precisamos.
  • A velocidade de desenvolvimento é impressionante. E a explicação em vídeo foi muito divertida de assistir!