2 pontos por GN⁺ 2024-05-03 | 1 comentários | Compartilhar no WhatsApp
  • O ShapeUp nasceu do desafio da Wheel Reinvention Jam de “revisitar softwares existentes sob uma nova perspectiva” e acabou se tornando um modelador 3D completo, com demo executável no navegador e até exportação .obj
  • O fator-chave que permitiu criar uma ferramenta 3D em uma semana foi o uso de ray marched SDF, que possibilitou implementar rapidamente cenas com cor, sombras suaves e ambient occlusion em comparação com um renderizador baseado em triângulos
  • A implementação foi mantida simples, centrada em um único arquivo C, e o modelo armazena até 100 Shapes em um array estático para reduzir a carga de gerenciamento de memória
  • O raylib ajudou a abrir rapidamente uma janela OpenGL, mas sua API centrada em int, a falta de validação de parâmetros, a dependência de GLFW e as limitações do raygui acabaram exigindo uso direto de OpenGL ou a recriação de funcionalidades
  • O resultado final teve 2024 linhas de C e 250 linhas de GLSL, cerca de 2300 linhas no total, com suporte a abrir e salvar arquivos, execução em várias plataformas e exportação .obj

Como o ShapeUp virou um modelador 3D

  • A Wheel Reinvention Jam foi um evento de programação de uma semana voltado a revisitar sistemas de software existentes sob uma nova perspectiva
  • O objetivo inicial surgiu da frustração com compiladores TypeScript lentos: criar um subconjunto de TypeScript mais rápido que o tsc
    • Parecia viável começar a partir do parser TypeScript do esbuild ou do Bun
    • Mas a demo de sucesso acabaria sendo apenas “um comando de terminal termina antes do outro”, o que não era visualmente interessante, então a direção mudou para 3D
  • O ShapeUp foi criado como um modelador 3D em que se editam formas com o mouse
    • Já havia experiência anterior escrevendo shaders SDF, mas modelar alterando código diretamente não parecia natural
    • O objetivo era permitir edição de formas baseadas em SDF com o mouse

Por que SDF tornou possível um projeto de uma semana

  • A base de renderização do ShapeUp é ray marched signed distance fields (SDFs)
  • Uma cena em SDF podia ser implementada mais rapidamente do que um renderizador baseado em triângulos, mesmo incluindo cor, sombras suaves e ambient occlusion
  • Um exemplo de Inigo Quilez criando um personagem em estilo Pixar usando SDF em uma única sessão serviu como referência de direção técnica
  • O ShapeUp aplica modelagem com SDF por meio da manipulação direta das formas, em vez de edição de código

Implementação em C e estrutura de dados

  • O ShapeUp foi escrito em C e usa raylib para criar a janela OpenGL
  • C foi escolhido por compilar rápido, não esconder comportamentos complexos na sintaxe, ser familiar e poder compilar tanto para nativo quanto para WebAssembly
  • O modelo é composto por um conjunto de structs Shape
    • Cada Shape tem posição, tamanho, ângulo, raio das bordas, grau de blob, cor, eixo de espelhamento e sinalizador de subtração
  • A lista de Shapes é gerenciada com um array estático, em vez de alocação dinâmica
    • MAX_SHAPE_COUNT é 100
    • O estado é controlado por Shape shapes[MAX_SHAPE_COUNT], shape_count e selected_shape
    • Essa abordagem elimina a possibilidade de falha de alocação e vazamentos
  • O limite de 100 Shapes não foi um grande problema no uso real
    • Como não houve tempo para otimizar o renderizador, a taxa de quadros caía antes mesmo de chegar a 100
    • Se houvesse mais tempo, a ideia seria dividir o modelo em pequenos blocos e executar ray marching dentro de cada bloco

Como a memória é usada

  • O ShapeUp usa alocação dinâmica de memória em apenas 3 pontos
    • Ao salvar: alocação de um buffer capaz de conter o documento inteiro
    • Na exportação .OBJ: alocação de um buffer para conter todos os vértices
    • Na geração do shader GLSL: alocação de um buffer para o código-fonte do shader
  • Em cada caso, há apenas um free no fim da função
  • Seria possível fazer malloc para cada Shape e guardar ponteiros em um array dinâmico, mas esse projeto não precisava dessa estrutura
  • Uma vantagem do C era permitir controle direto sobre o layout de memória
  • Se arrays dinâmicos ou hash maps fossem necessários, ferramentas como stb_ds.h poderiam ser usadas

Como a UI foi implementada

  • A UI foi implementada no estilo immediate mode user interface (IMGUI)
  • Entre as vantagens do IMGUI estão a facilidade de depuração e o fato de permitir posicionar elementos com uma linguagem de programação real, em vez de CSS, constraints ou SwiftUI
  • O elemento em foco e as ações do mouse são rastreados com um enum Control
    • Estados de interação como posição, tamanho, ângulo, cor, mover, girar, escalar, girar câmera e grau de blob são representados por valores do enum
    • focused_control e mouse_action armazenam o estado atual da UI

Onde raylib e raygui atrapalharam

  • O raylib foi útil para abrir rapidamente uma janela OpenGL, mas com o tempo passou a desacelerar o desenvolvimento
  • O ponto mais incômodo da API do raylib era a falta de informação de tipo
    • Mesmo onde se espera um tipo enum, a API usa int, impedindo a checagem de tipos pelo compilador
    • Só pela assinatura da função nem sempre fica claro o significado dos parâmetros
    • Por exemplo, em IsGestureDetected(unsigned int gesture), gesture parece ser um ID de gesto registrado, mas na prática é um enum Gesture
    • Como a documentação é centrada nos arquivos de cabeçalho, para descobrir quais int realmente representam enums às vezes era preciso olhar a implementação
  • A ausência de validação básica de parâmetros também ampliava os problemas
    • LoadFileData(const char *fileName, int * dataSize) gera segfault se dataSize for NULL
    • O cabeçalho não indica que dataSize é um parâmetro de saída nem que ele não pode ser nulo
    • Sem validação, ficava mais difícil rastrear problemas simples, e em alguns casos o comportamento incorreto podia ocorrer silenciosamente
  • O tratamento de dependências também ficou abaixo do esperado
    • Havia problemas do GLFW que o raylib não contornava nem corrigia com patches
    • Para o usuário final, importa mais se o recurso do raylib funciona direito do que a implementação interna da criação da janela
  • A biblioteca de UI raygui também tinha muitas limitações para este projeto
    • Não conseguia exibir números de ponto flutuante, então foi preciso criar manualmente campos de texto para float
    • Não conseguia lidar com roteamento de eventos do mouse em elementos sobrepostos ou cortados
    • Não oferecia suporte aos cantos arredondados comuns em interfaces
    • Era difícil deixá-la visualmente agradável com estilo
  • Bugs também atrapalharam o fluxo de desenvolvimento
    • Um bug nas ferramentas do raygui impediu trocar a fonte padrão excessivamente estilizada
    • Funções de desenho como DrawCircle(...) não compartilham vértices entre triângulos, então quando a matriz atual tem escala ou rotação surgem pequenos espaços entre pixels por erro de ponto flutuante
  • Problemas encontrados foram reportados por algum tempo, mas a maioria acabou fechada como “wont fix”, e depois disso os relatos foram interrompidos
  • A solução alternativa foi usar diretamente funções de OpenGL ou implementar do zero os recursos necessários
  • No futuro, a ideia é usar sokol no lugar de raylib

As quatro coisas que precisavam ser concluídas em 6 dias

  • O ShapeUp precisava completar quatro partes principais em 6 dias
    • Interface do usuário: gizmos 3D, atalhos de teclado, barra lateral e controle por gamepad
    • Gerador de shaders GLSL e renderizador por ray marching
    • Seleção de mouse baseada em GPU
    • Marching cubes para exportação
  • A dificuldade estava menos em cada recurso isoladamente e mais em manter as prioridades
  • Problemas complicados ou demorados eram evitados com mudanças de design ou resolvidos com soluções simples que funcionassem em 90% dos casos
  • Às vezes a solução aparecia depois de deixar uma funcionalidade para o dia seguinte
  • A forma de trabalhar era manter sempre um modelador 3D funcional e ir melhorando aos poucos conforme o tempo permitisse
    • Isso é comparado a construir sempre uma pequena pirâmide completa em cada etapa, em vez de algo que só vira pirâmide no final

Resultado final

  • Ao fim da semana, o ShapeUp já conseguia criar modelos 3D significativos e exportá-los como arquivos .obj
  • Ele roda em várias plataformas e também suporta abrir e salvar arquivos
  • O tamanho do código é de 2024 linhas de C e 250 linhas de GLSL
  • Chama atenção o fato de ter sido possível criar um modelador 3D razoavelmente útil com cerca de 2300 linhas
  • O projeto em si é relativamente simples, mas foram decisivos o senso para escolher o que construir, o conhecimento necessário para construir e a disciplina para terminar tudo em uma semana

1 comentários

 
GN⁺ 2024-05-03
Opiniões do Hacker News
  • Concordo totalmente com o autor sobre as limitações da Raylib. Estou criando um jogo no estilo tower defense que comecei agora com Raylib, e tenho enfrentado muitas dessas mesmas limitações e outros problemas
    Por exemplo, a alternância para tela cheia não funciona de forma consistente entre plataformas, não dá para enumerar os modos de tela, é difícil alternar recursos de renderização em tempo de execução, há problemas para salvar shaders compilados etc.
    Ainda assim, agradeço o trabalho que o Ray colocou nessa biblioteca e pretendo continuar apoiando. A Raylib é ótima para criar protótipos rapidamente, mas, a menos que você aceite restrições severas, não é fácil ir muito além disso
    Com certeza aprendi algumas coisas, mas neste ponto o desenvolvimento já avançou demais para eu trocar todo o código relacionado à Raylib por algo como SDL

    • Acho divertido o detalhe de que Raylib não vem de ray tracing, mas de Ray, o nome de quem a criou
      Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
    • Raylib é fácil de começar, mas quando o projeto fica um pouco mais complexo ela começa a morder. Já a SDL exige mais tempo de configuração inicial, mas escala muito bem conforme o projeto cresce. A qualidade do código também é excepcional
    • A Raylib tem muitos problemas que provavelmente não serão corrigidos, mas é difícil culpar só a Raylib pelo problema de tela cheia. A tela cheia no Windows está, na prática, quebrada a ponto de ser quase inutilizável há décadas, e é bem possível que outras plataformas sejam parecidas
      A estratégia hoje é simplesmente usar modo janela sem borda e fingir que tela cheia de verdade não existe
    • Tenho uma sensação parecida. Comecei um projeto há uns 2 meses e escolhi Raylib; as coisas básicas realmente rodam com muita facilidade, mas quanto mais uso, mais encontro pequenos incômodos aleatórios. Agora já investi demais no projeto para voltar atrás no uso da Raylib
      O maior problema no momento é tratamento de fontes e renderização de texto. Acho que vou ter que trocar fontes TTF por fontes bitmap pré-geradas, mas isso deve ser bem sofrido quando eu for localizar depois
      Depois de vir do Love2D, os dois recursos de que mais sinto falta são renderizar facilmente texto com várias cores e recortar texturas com facilidade para repetir ou criar tiles. Na Raylib, é preciso quebrar o texto manualmente com base na marcação de cores, aplicar offsets de largura e então chamar a função de desenho para cada pedaço, levando em conta até as quebras de linha
      Quando desenho muito texto na tela, o FPS também parece cair bastante, talvez porque o batching das chamadas de desenho de texto esteja sendo quebrado. Antes havia uma função para desenhar textura em tiles, mas ela foi removida por algum motivo
    • Ver isso me deu vontade de dar uma olhada na raylib. Há exemplos bonitinhos rodando em WebAssembly: https://www.raylib.com/examples.html
      Uma coisa que sempre me incomodou em Wasm e gráficos 3D/2D no navegador é que pequenos problemas, como rolagem, aparecem com frequência. Veja o exemplo “Background scrolling & parallax” aqui: https://www.raylib.com/examples.html
      Testei em vários dispositivos e, se meus olhos não estão me enganando, aquilo definitivamente não é rolagem suave. Parece absurdo que, em 2024, rolagem suave em 2D ainda não seja um problema resolvido
  • “As Shapes são mantidas em um array alocado estaticamente. Não há falha de alocação, não há vazamentos, não há firulas. Adorável. O limite de 100 Shapes, na prática, não foi uma restrição. Como quase não houve tempo para otimizar o renderizador, a taxa de quadros provavelmente cairia antes mesmo de chegar a 100.”
    É o melhor exemplo de evitar otimização prematura que vi recentemente

    • Eu diria que é quase o oposto. Foi evitar abstração e generalização prematuras
    • É o melhor exemplo da diferença entre pessoas que constroem coisas e pessoas que só ficam sentadas discutindo como construí-las
  • Texto realmente interessante; gostei de ele falar sobre várias decisões, como a forma de lidar com memória e os problemas que encontrou na raylib. Por coincidência, estou revisando C agora ao entrar na parte 2 de Crafting Interpreters, então foi bom relembrar no que C é bom

  • A demonstração em tempo real no vídeo é muito boa. Deixando de lado a criação do app, acho que, se eu tentasse, nem aquele vídeo eu conseguiria fazer em uma semana

    • Fazer o vídeo levou mais tempo que o app. Não sei como os youtubers conseguem fazer isso com tanta constância
  • Muito tempo atrás, trabalhei em um sistema operacional para telefones de mesa. Havia apenas 64K de RAM, então não existia gerenciamento dinâmico de memória; usávamos muitas variáveis estáticas para que o compilador posicionasse tudo em tempo de compilação
    É fácil esquecer que muitas aplicações talvez nem precisem de gerenciamento dinâmico de memória. Em muitos casos, basta alocar alguns buffers de tamanho fixo e tratar de forma limpa as situações excepcionais em que esses buffers ficam cheios
    Nesse contexto, C é de fato muito mais seguro. Não há vazamentos de memória, e a única preocupação é buffer overflow. Se todas as variáveis forem alocadas estaticamente, dá para gerenciar isso usando sizeof com cuidado
    Isso não quer dizer que Rust e Go não sejam ótimas opções hoje, mas o velho e modesto C ainda funciona muito bem e não precisa ser um pesadelo de complexidade

  • Fugindo um pouco do tema, fiquei feliz por ver pela primeira vez uma interface em WebAssembly em que o texto não parece borrado. É realmente a primeira vez
    Se ampliarmos para programas e alguns sistemas operacionais, por exemplo até o Windows, surgiu um problema geral nos últimos anos à medida que uma certa forma de rasterizar texto virou tendência comum e configuração padrão
    Infelizmente, muitas vezes o usuário não consegue desativar o antialiasing para obter texto nítido e, mesmo nas raras vezes em que há uma opção, o antialiasing ainda é aplicado a interfaces como menus

    • Esse texto nítido não é tão impressionante assim. A fonte não tem curvas nem smoothing. Em qualquer resolução, vai parecer um texto anguloso em blocos
    • Fiquei curioso sobre qual é a ligação entre WebAssembly e rasterização. Parece realmente interessante
  • Gosto muito de projetos assim. Ainda gosto da natureza de baixo nível do C. Hoje uso bastante Rust e Elixir/Erlang, mas muitas vezes sinto falta da simplicidade e da explicitude do C
    Por isso também uso bastante Zig, que mantém muito da filosofia do C, mas melhora a linguagem de um jeito bem interessante

    • Erlang também é uma linguagem bem simples
  • Concordo muito com a avaliação dele sobre C. Especialmente a parte de que “a sintaxe não esconde comportamentos complexos. É simples o bastante para você não precisar ficar consultando o tempo todo”; além disso, mesmo quando é preciso procurar algo sobre C, isso é muito fácil e instrutivo
    Uma linguagem simples e antiga tem suas vantagens

  • Se você alocar cada Shape separadamente com malloc e armazenar esses ponteiros em um array dinâmico, certamente dá para tornar as coisas mais difíceis para si mesmo. Há quem diga que linguagens como C# forçam esse tipo de estrutura de alocação, mas fico curioso sobre o que impediria alguém de usar em C# um array fixo de structs, como o autor fez em C

    • Nada impede. Em C# também não é incomum usar arrays de structs desse jeito
  • Gostaria que alguém continuasse este projeto. Com mais alguns meses de polimento, ele poderia se tornar uma alternativa séria ao Blender ou ao FreeCAD para certos usos, e a curva de aprendizado parece bem mais suave

    • Também vale conferir o MagicaCSG, que é uma versão mais refinada e ainda gratuita: https://ephtracy.github.io/index.html?page=magicacsg#ss-caro...
    • Correção: ah, este programa já oferece exportação de malha por marching cubes. Basta ver o vídeo no YouTube no site. Eu não sabia disso
      Porém, como ele funciona fundamentalmente com SDF, a experiência de modelagem e os dados armazenados são diferentes dos de uma malha tradicional com triângulos, vértices etc.
      Converter SDF em malha é possível com métodos como marching cubes, mas há grande chance de que esses dados ainda precisem ser limpos depois em aplicativos do tipo Blender
      Se o renderizador também for baseado em SDF, SDF é excelente. Mas a maioria não é
      Desculpe se você já sabia disso
    • Se você gosta de SDF, o Womp é um bom ponto de partida. O Tinkercad também é um CAD de entrada bastante decente
    • Também vale conferir Dune3D e Salome-Platform
    • Pode até ser uma alternativa ao Blender, mas infelizmente não parece ser uma alternativa para trabalhos de CAD