작년 가을에 "Wheel Reinvention Jam"이라는 일주일 동안의 프로그래밍 이벤트에 참여함
기존 소프트웨어 시스템을 새로운 시각으로 다시 살펴보는 것이 목적이었음
"ShapeUp"이라는 3D 모델러를 만들었고, 이 글을 읽기 전에 ShapeUp의 데모 동영상을 먼저 보는 것이 이해에 도움됨
ShapeUp은 브라우저에서 직접 사용해 볼 수 있음
언어 선택: C
Typescript 컴파일러가 느린 것에 대한 불만으로 인해 Jam에 참여
esbuild나 Bun의 Typescript 파서로 시작한다면 Typescript의 빠른 부분 집합을 구현하는 프로젝트가 가능해 보였음
하지만 터미널 명령어 실행 속도 비교로는 흥미로운 데모가 되지 않을 것 같아 3D 프로젝트로 방향을 전환함
Ray marched signed distance fields(SDFs) 기술 덕분에 일주일 만에 처음부터 3D 프로젝트를 만드는 것이 가능해 보였음
동등한 삼각형 기반 렌더러보다 SDF를 사용한 장면이 훨씬 빠르게 구현 가능
이전에 SDF 셰이더를 작성해본 적은 있었지만 매우 기초적인 수준이었고, 코드를 편집하여 모델링하는 것은 자연스럽지 않게 느껴졌음
마우스로 모양을 편집하고 싶었고, 이번 Jam이 그것을 실현할 기회라고 생각했음
프로젝트 이름을 ShapeUp이라고 지음
C언어 사용의 장점
C는 매우 단순하고 원시적인 언어여서 내장 데이터 구조의 부족을 해결하고 포인터 버그를 고치는데 많은 시간을 소비할 것이라고 여겨짐
하지만 C의 단순성은 장점이 됨
빠르게 컴파일 됨
문법이 복잡한 연산을 숨기지 않음
간단해서 끊임없이 문법을 찾아볼 필요가 없음
native와 web assembly로 쉽게 컴파일 가능
C의 결점들은 22년 동안 사용하면서 개발한 습관으로 피할 수 있음
ShapeUp은 작은 단일 C 파일로 구성되어 매우 간단함
ShapeUp의 데이터 구조
모델은 Shapes라는 구조체의 배열로 구성됨
Shapes는 정적으로 할당된 배열에 저장됨
할당 실패나 메모리 누수의 위험이 없음
100개의 Shape 제한은 실제로는 제한적이지 않았음
렌더러 최적화 시간이 부족하여 100개가 되기 전에 프레임 속도가 떨어졌을 것
시간이 있었다면 모델을 작은 블록으로 나누고 각 블록 내에서 raymarching을 수행했을 것
동적 메모리는 단 3곳에서만 malloc을 호출함
저장 (전체 문서를 담을 수 있을 만큼 큰 버퍼 할당)
OBJ 내보내기 (모든 꼭지점을 담을 수 있을 만큼 큰 버퍼 할당)
GLSL 셰이더 생성 (셰이더 소스용 버퍼)
모든 경우 함수 끝에 단일 free가 있음
C에서 메모리 관리가 간단할 수 있다는 것을 보여주는 예시
C#, Javascript, Python 같은 언어는 Shape마다 개별적으로 malloc하고 해당 포인터를 동적 배열에 저장하는 할당 구조를 강제함
C는 메모리 레이아웃을 제어할 수 있어서 좋음
사용자 인터페이스
immediate mode user interface(IMGUI)로 구현됨
IMGUI 방식의 UI를 좋아함
디버깅이 매우 쉬움
요소를 배치하기 위해 실제 프로그래밍 언어 사용 (CSS, constraints, SwiftUI와 달리)
대부분의 IMGUI와 마찬가지로 enum을 사용하여 어떤 요소에 포커스가 있는지 또는 마우스가 어떤 동작을 하고 있는지 추적함
이 프로젝트에는 동적 배열이나 해시맵이 필요하지 않았지만, 필요했다면 stb_ds.h와 같은 것을 사용했을 것
Raylib 라이브러리의 문제점
C를 사용하기로 결정한 것은 좋았지만, raylib은 문제가 됨
개발자 경험을 해치는 이상한 설계 선택이 있음
enum 타입이 예상되는 곳에 int를 사용하여 컴파일러 타입 검사를 방지하고 함수가 self document 되지 않음
기본 매개변수 유효성 검사를 하지 않음 (설계 선택)
종속성에 대한 책임을 지지 않음 (GLFW 이슈를 해결하거나 패치를 제출하지 않음)
raygui UI 라이브러리는 장난감에 불과함
부동 소수점 숫자를 표시할 수 없음
겹치거나 클리핑된 요소에 대한 마우스 이벤트 라우팅을 처리하지 않음
둥근 모서리를 만들 수 없음
보기 좋게 스타일을 지정할 수 없음
버그도 있음
폰트 변경 방지 버그
그리기 함수가 삼각형 사이의 꼭지점을 공유하지 않아 픽셀 간격이 발생
문제를 발견할 때마다 보고했지만 대부분 "won't fix"로 닫혔고 버그 리포트 작성에 시간이 많이 걸려 포기함
OpenGL 창을 만들어준 것은 좋았지만 그 편의성에 큰 대가를 치름
다행히 OpenGL 함수를 직접 사용하거나 기능을 처음부터 구현하는 탈출구를 찾을 수 있었음
앞으로는 sokol을 사용할 예정
일주일 동안의 개발 과정
ShapeUp은 6일 동안 완료해야 하는 4가지 주요 부분으로 구성됨
사용자 인터페이스 (3D 도구, 키보드 단축키, 사이드바, 게임 컨트롤러)
GLSL 셰이더 생성기 + Ray marching 렌더러
GPU 기반 마우스 선택
Marching cubes for export
각각은 어렵지 않았지만, 우선순위를 올바르게 정하고 빠져나가지 않는 것이 어려웠음
까다롭거나 시간이 많이 걸리는 문제는 설계를 통해 해결하거나 90% 경우에 작동하는 멍청한 해결책을 사용하는 것이 도움됨
때로는 기능을 하루 정도 미루면 무의식적으로 해결책을 찾을 수 있었음
항상 작동하는 3D 모델러를 가지고 있고 시간이 허락하는 대로 점진적으로 개선하려고 노력함
피라미드를 만드는 것처럼 생각함. 층별로 만들면 마지막까지 피라미드가 완성되지 않지만, 어느 단계에서 멈추더라도 완전한 피라미드가 되도록 만들 수 있음
프로젝트 결과
일주일 후에는 의미 있는 3D 모델을 만들고 .obj 파일로 내보낼 수 있는 3D 프로그램을 가지게 됨
멀티플랫폼에서 실행되고 파일 열기/저장 기능도 있음
프로젝트는 2024줄의 C코드와 250줄의 GLSL로 구성됨
약 2300줄 정도로 어느 정도 쓸모 있는 3D 모델러를 표현할 수 있다는 것이 약간 놀라움
Jam 요약과 Handmade Seattle 컨퍼런스에서 ShapeUp 데모를 보여달라는 요청을 받음
사람들은 ShapeUp에 감명을 받은 듯 했지만 큰 성과를 거둔 것 같지는 않음. 비교적 간단한 프로젝트임
내가 한 일에 특별한 것이 있다면, 무엇을 만들지 선택할 수 있는 감각, 그것을 만드는 데 필요한 지식, 그리고 일주일 안에 해내는 규율이었음
GN⁺의 의견
C언어의 단순함과 속도의 장점을 잘 보여주는 흥미로운 프로젝트임. 하지만 C의 낮은 추상화 수준 때문에 상용 프로젝트에 그대로 사용하긴 어려워 보임. 현대적인 3D 모델링 도구의 기능을 모두 C로 직접 구현하려면 엄청난 노력이 필요할 것으로 예상됨
일주일 만에 동작하는 프로그램을 완성한 것이 인상적임. 하지만 장기적인 관점에서 코드 유지보수와 기능 확장을 고려했을 때 C++이나 Rust 같은 언어를 선택하는 것이 더 나은 선택일 수 있음
SDF를 이용한 렌더링 기법은 빠르고 간단하지만 모델링의 자유도나 퀄리티 면에서는 한계가 있어 보임. 상용 모델링 툴은 SubD나 NURBS 같은 표면 모델링 기술을 주로 사용함. 하지만 게임이나 데모 등 실시간성이 중요한 분야에서는 SDF 렌더링이 여전히 활용 가치가 높아 보임
오픈소스 라이브러리 선택의 어려움을 잘 보여주는 사례. 문서화와 코드 품질, 지원 여부 등을 잘 파악하고 신중하게 선택해야 함. 자체 구현도 좋은 대안이 될 수 있음
동작하는 프로그램을 먼저 만들고 점진적으로 개선해 나가는 방식은 실무에서도 매우 유용함. 핵심 기능부터 완성하고 세부 사항을 개선하는 식으로 우선순위를 잘 조절하는 것이 중요해 보임