Crear un modelador 3D en C en una semana
- Participé en el evento de programación de una semana llamado “Wheel Reinvention Jam” el otoño pasado.
- El objetivo era repensar los sistemas de software existentes desde una nueva perspectiva.
- Hice un modelador 3D llamado “ShapeUp”, y ver antes el video de demo de ShapeUp antes de leer este artículo ayuda a entenderlo.
- ShapeUp se puede probar directamente en el navegador.
Elección del lenguaje: C
- Me uní al Jam por la frustración con la lentitud del compilador de TypeScript.
- Si se empezaba desde el parser de TypeScript de esbuild o Bun, parecía factible un proyecto para implementar un subconjunto rápido de TypeScript.
- Pero comparando la velocidad de ejecución de comandos del terminal, no parecía una demo interesante, así que cambié el rumbo hacia un proyecto 3D.
- Gracias a la técnica de ray marching con campos de distancia firmada (SDFs), parecía posible crear desde cero un proyecto 3D en una semana.
- Una escena con SDF se puede implementar mucho más rápido que un renderizador equivalente basado en triángulos.
- Había escrito shaders SDF antes, pero en un nivel muy básico, y editar código para modelar no se sentía natural.
- Quería editar formas con el mouse y pensé que este Jam era la oportunidad para hacerlo realidad.
- Llamé al proyecto ShapeUp.
Ventajas de usar C
- C es un lenguaje muy simple y primitivo, y pensé que consumiría mucho tiempo compensar la falta de estructuras de datos incorporadas y arreglar bugs de punteros.
- Pero la simplicidad de C se convirtió en una ventaja.
- Compila rápido.
- La sintaxis no oculta operaciones complejas.
- Es simple, así que no hay que buscar la sintaxis constantemente.
- Puede compilarse fácilmente a nativo y WebAssembly.
- Las desventajas de C pueden evitarse con hábitos que he desarrollado durante 22 años usándolo.
- ShapeUp es muy simple y está conformado por un solo archivo C pequeño.
Estructuras de datos de ShapeUp
- El modelo se compone de un arreglo de una estructura llamada Shapes.
- Shapes se guarda en un arreglo de asignación estática.
- No hay riesgo de fallos de asignación ni fugas de memoria.
- El límite de 100 Shapes no resultó realmente restrictivo.
- Como faltó tiempo para optimizar el renderizador, la velocidad de frames probablemente habría caído antes de llegar a 100.
- Con más tiempo habría dividido el modelo en bloques pequeños y realizado raymarching dentro de cada bloque.
- Llamé a
malloc solo en 3 lugares de memoria dinámica.
- Guardado (asignar un buffer lo suficientemente grande para guardar el documento completo).
- Exportación OBJ (asignar un buffer lo suficientemente grande para todos los vértices).
- Generación de shader GLSL (buffer para el código fuente del shader).
- En todos los casos hay un único
free al final de la función.
- Un ejemplo de que la gestión de memoria en C puede ser simple.
- Lenguajes como C#, JavaScript y Python te fuerzan a una estructura de asignación donde haces
malloc por cada Shape y guardas ese puntero en un arreglo dinámico.
- En C es bueno poder controlar el diseño de memoria.
Interfaz de usuario
- Implementada con interfaz de usuario de modo inmediato (IMGUI).
- Me gusta la UI de tipo IMGUI porque:
- Facilita mucho la depuración.
- Usa un lenguaje de programación real para ubicar elementos (a diferencia de CSS, constraints, SwiftUI).
- Como en la mayoría de las IMGUI, usa enums para rastrear en qué control está el foco y qué hace el mouse.
- Este proyecto no necesitó arreglos dinámicos ni mapas hash; de haberlos necesitado, habría usado algo como
stb_ds.h.
Problemas de la biblioteca raylib
- Decidí usar C, pero raylib terminó siendo problemático.
- Tiene decisiones de diseño extrañas que dañan la experiencia del desarrollador:
- Usa
int donde se esperaría un enum, evitando la verificación de tipos del compilador y sin que las funciones sean autodocumentadas.
- No valida los parámetros por defecto (decisión de diseño).
- No se responsabiliza de dependencias (no corrige issues de GLFW ni envía parches).
- La librería UI raygui se queda corta:
- No puede mostrar números de punto flotante.
- No maneja el ruteo de eventos del mouse para elementos superpuestos o recortados.
- No puede crear esquinas redondeadas.
- No permite dar un estilo estético.
- También tiene bugs:
- Un bug que impide cambiar la fuente.
- Las funciones de dibujo no comparten vértices entre triángulos, por lo que se generan espacios de píxeles.
- Reporté cada problema que encontraba, pero la mayoría se cerró como “won’t fix”, y escribir reportes de bugs me llevó mucho tiempo, así que finalmente lo dejé.
- Fue bueno que creara la ventana de OpenGL, pero pagué un precio alto por esa conveniencia.
- Por suerte, encontré una salida al usar funciones de OpenGL directamente o implementar funcionalidades desde cero.
- En adelante voy a usar sokol.
Proceso de desarrollo en una semana
- ShapeUp tenía 4 partes principales que debía completar en 6 días:
- Interfaz de usuario (herramientas 3D, atajos de teclado, barra lateral, control de juego)
- Generador de shaders GLSL + renderizador de ray marching
- Selección con mouse basada en GPU
- Marching cubes for export
- Ninguna fue difícil, pero fue complicado priorizar bien y no engancharse.
- En problemas complicados o que consumían mucho tiempo, ayudaba resolverlos por diseño o usar una solución sencilla que funcionara en 90% de los casos.
- A veces, al posponer una funcionalidad un día, podía encontrar la solución de forma casi inconsciente.
- Mantener siempre un modelador 3D funcional y mejorarlo gradualmente en la medida que el tiempo lo permite fue mi objetivo.
- Lo pensé como construir una pirámide: si la haces por capas, puede que al final no quede una pirámide completa, pero en cualquier etapa puedes hacer que lo sea.
Resultado del proyecto
- Una semana después, tenía un programa 3D capaz de crear modelos 3D significativos y exportarlos como archivos
.obj.
- Funciona de manera multiplataforma y además tiene abrir/guardar archivos.
- El proyecto consta de 2,024 líneas de C y 250 líneas de GLSL.
- Me sorprendió un poco que con alrededor de 2,300 líneas sea posible representar un modelador 3D razonablemente útil.
- Me pidieron mostrar una demo de ShapeUp en el resumen del Jam y en la conferencia Handmade Seattle.
- Parecía que a la gente le impresionó ShapeUp, aunque no parece que fuera un gran logro; era un proyecto relativamente simple.
- Lo más valioso de lo que hice fue el instinto para elegir qué construir, el conocimiento para hacerlo y la disciplina para lograrlo en menos de una semana.
Opinión de GN⁺
- Es un proyecto interesante que muestra bien las ventajas de la simplicidad y la velocidad de C. Pero por el bajo nivel de abstracción de C, parece difícil usarlo tal cual en un proyecto comercial; se espera que implementarlo todo de una herramienta moderna de modelado 3D directamente en C requeriría un esfuerzo enorme.
- Resulta impresionante haber logrado un programa funcional en una semana, pero en un horizonte de largo plazo, considerando mantenimiento y expansión de funcionalidades, puede ser mejor elegir un lenguaje como C++ o Rust.
- La técnica de renderizado con SDF es rápida y sencilla, pero parece tener límites en libertad de modelado y calidad. Las herramientas de modelado comerciales usan principalmente modelado de superficie como SubD o NURBS. Sin embargo, en juegos, demos y escenarios donde la inmediatez importa, el renderizado con SDF sigue teniendo un alto valor.
- Es un buen ejemplo de lo complicado que puede ser elegir una librería open source; hay que evaluar bien documentación, calidad de código y soporte disponible antes de elegir, y una implementación propia también puede ser una buena alternativa.
- Construir primero un programa funcional y mejorarlo gradualmente también es muy útil en la práctica. Parece importante ajustar bien prioridades para terminar primero las funciones centrales y después pulir los detalles.
1 comentarios
Opinión en Hacker News