Um modelador 3D feito em C em apenas uma semana
(danielchasehooper.com)- 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
esbuildou doBun - 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
- Parecia viável começar a partir do parser TypeScript do
- 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_counteselected_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
freeno fim da função - Seria possível fazer
mallocpara 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.hpoderiam 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_controlemouse_actionarmazenam 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),gestureparece ser um ID de gesto registrado, mas na prática é um enumGesture - Como a documentação é centrada nos arquivos de cabeçalho, para descobrir quais
intrealmente representam enums às vezes era preciso olhar a implementação
- Mesmo onde se espera um tipo enum, a API usa
- A ausência de validação básica de parâmetros também ampliava os problemas
LoadFileData(const char *fileName, int * dataSize)gera segfault sedataSizeforNULL- 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
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
Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
A estratégia hoje é simplesmente usar modo janela sem borda e fingir que tela cheia de verdade não existe
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
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
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
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
sizeofcom cuidadoIsso 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
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
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
malloce 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 CGostaria 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
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