3D 모델러를 C언어로 일주일 만에 만들기
- 작년 가을에 "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 렌더링이 여전히 활용 가치가 높아 보임
- 오픈소스 라이브러리 선택의 어려움을 잘 보여주는 사례. 문서화와 코드 품질, 지원 여부 등을 잘 파악하고 신중하게 선택해야 함. 자체 구현도 좋은 대안이 될 수 있음
- 동작하는 프로그램을 먼저 만들고 점진적으로 개선해 나가는 방식은 실무에서도 매우 유용함. 핵심 기능부터 완성하고 세부 사항을 개선하는 식으로 우선순위를 잘 조절하는 것이 중요해 보임