Computer >> 컴퓨터 >  >> 프로그래밍 >> Bash 프로그래밍

Bash용 OpenGL 바인딩: 셸 스크립트로 3D 그래픽을 구동하다

Perl 5의 설계와 '글루 언어(glue language)'로서의 적합성을 다룬 지난 글에서, 저는 예전에 Bash용 OpenGL 바인딩을 작성했다고 언급했습니다. 증거 없이 하기에는 다소 믿기 힘든 말이었기에, 하드 드라이브의 먼지 쌓인 구석에서 프로젝트를 다시 꺼내고, 코드를 손보고, 폰트 지원을 개선하고, 문서를 작성해 제 사이트와 GitHub에 공개했습니다. 직접 체험하려면 Bash와 OpenGL을 모두 지원하는 시스템이 필요하지만, 미리 준비한 영상도 있습니다.

충격적인 고백

그럼 이제 고백하겠습니다. My DeLorean runs Perl에서 설명했던 드로리안 계기판의 Perl 그래픽은 사실 이 Bash용 OpenGL 프로젝트와 역사를 함께합니다. 재미있고도 아이러니하게도, 저는 13년 전 Frozen Bubble이라는 게임을 보고 누군가가 실시간 비디오 게임을 Perl로 작성했다는 사실에 기술적 감성이 상처받아 이 프로젝트를 시작했습니다. 당시 제 주력 언어는 C++였고, 게임 개발 목적으로 OpenGL을 공부하고 있었습니다. 저는 친구들에게 "차라리 3D면서 Bash로 작성됐다면 더 심각했겠지"라고 선언했습니다. 입 밖에 내버린 그 아이디어는 계속 저를 괴롭혔고, 결국 실시간 그래픽에 Perl을 쓰는 '악몽'을 능가해보겠다는 마음으로 직접 시도하기로 했습니다.

Bash 확장하기

Bash에 OpenGL 지원을 추가하는 가장 직접적인 방법은 소스 코드를 수정해 각 OpenGL 함수를 셸 '내장 명령(builtin)'으로 추가하는 것이었지만, 커스텀 Bash를 설치할 의향이 있는 사람만 사용할 수 있다는 점에서 아무도 원하지 않을 것 같았습니다. 또한 'Bash다운' 방식이라는 정신에도 맞지 않았습니다.

그다음으로 떠오른 아이디어는 클라이언트-서버 구조였습니다. 각 OpenGL 함수를 실행 경로(path)에 설치하고, 함수 호출 시 클라이언트가 OpenGL 서버에 연결해 해당 함수 실행 메시지를 전달하는 방식입니다. 이것은 매우 '끔찍하면서도' 매력적인 해법이었지만, 아무리 최적화해도 전체 목표 달성을 주장할 만큼 빠르지는 않았습니다.

마침내 표준 입력(stdin)으로 OpenGL 명령을 읽고, 사용자 입력 이벤트를 표준 출력(stdout)으로 내보내는 'OpenGL 인터프리터' 프로세스를 두는 설계로 정착했습니다. 이렇게 하면 Bash 스크립트가 파이프에 연결된 이 인터프리터를 실행하고, 각 OpenGL 명령은 파이프에 데이터를 쓰는 Bash 함수가 됩니다. 덤으로 제가 보유한 모든 효율화 기술을 쏟아부어, 정적으로 컴파일된 해시 테이블과 레드-블랙 트리를 활용한 순수 C로 인터프리터를 작성했습니다.

OpenGL 즉시 모드(Immediate Mode)

OpenGL의 세부 사항은 이 프로젝트가 드로리안 계기판 프로젝트와 교차하는 지점입니다. 우선 한 걸음 물러나 OpenGL API를 간단히 짚고 넘어가겠습니다.

OpenGL의 본질은 3D 좌표와 텍스처로 그래픽을 계획하는 프로그램을 작성하고, 그 데이터를 그래픽 카드로 보내 2D 화면에 렌더링하는 것입니다. 극단적으로 단순화하면, 화면에서 칠할 영역을 기술하는 수학과 그 영역 내 각 픽셀의 색을 기술하는 수학, 두 가지 집합으로 이루어집니다.

가장 흔한 연산은 가상 3D 공간에서 삼각형의 세 꼭짓점을 기술하고, OpenGL이 2D 화면상 어디에 위치할지 계산하게 한 뒤, 그 영역에 2D 이미지를 늘려 페인팅하도록 지시하는 것입니다. 경우에 따라 다른 이미지와 합성하거나 밝기 연산으로 변형하기도 합니다. 프로그램의 메인 루프는 버퍼를 지우고, 보이는 모든 폴리곤을 찍고, 화면으로 전송함으로써 한 프레임의 영상을 생성합니다. 이 모든 과정을 16밀리초 이내에 완료하면 초당 60프레임을 유지하며 부드러운 그래픽을 구현할 수 있습니다.

OpenGL API의 기원에 대해 권위 있게 말할 수는 없지만, X11 디스플레이 프로토콜과의 호환성 때문일 수도 있고 단순히 좋은 발상이라서일 수도 있는, 스트리밍 개념 위에 구축된 것으로 보입니다. 따라서 대부분의 OpenGL 함수는 반환값이 없습니다. 반면 다른 API들은 각 함수 호출이 연산 결과를 알려주는 상태 값을 반환합니다. glVertex3f(1,2,3) 같은 OpenGL 함수를 파이프로 세 개의 숫자를 쓰는 print 문으로 상상하면, 내부 동작을 어렵잖게 이해할 수 있습니다. 실제로 Bash 바인딩에서는 glVertex 1 2 3이 실행되면 문자 그대로 파이프에 glVertex 1 2 3을 씁니다. Bash 스크립트는 자신의 그래픽 명령이 의도대로 동작하는지 전혀 알지 못한 채 맹목적으로 실행됩니다.

수년간 OpenGL API에는 여러 차례 대규모 개정이 있었고, 많은 책들이 '보관(retained)' 모드와 '즉시(immediate)' 모드에 관해 장 하나를 통째로 할애하지만, 결국 두 가지 개념으로 요약됩니다:

  • 매 프레임마다 파이프로 모든 데이터를 다시 보내는 것은 느리니, 일부를 반대편에 캐싱하자.
  • 내장 수학만으로는 모든 사람의 요구를 충족할 수 없으니, 자신만의 커스텀 수학을 기술할 수 있는 언어를 제공하자.

새로운 OpenGL API가 제공하는 캐싱과 커스텀 수학은 훨씬 효율적이며 최상위 비디오 게임에는 분명히 필요하지만, 설정하고 다루고 배우는 데 더 많은 노력이 들고, 취미 개발자에게는 오히려 해가 될 수 있습니다. 고급 그래픽 이펙트나 고품질 모델을 다루지 않는 한 굳이 필요하지도 않습니다.

따라서 Bash OpenGL 바인딩은 '폐기 예정(deprecated)' API만 다루지만, 학습과 실험 목적으로는 여전히 살펴볼 가치가 충분합니다.

디스플레이 리스트(Display Lists)

다행히 초기 OpenGL API에는 사용하기 쉬운 캐싱 메커니즘이 몇 가지 내장되어 있습니다. 대략 다음과 같은 순서로 동작합니다:

"OpenGL아, 원격 끝에서 37번 객체를 생성해줘"

"여기 37번 객체를 기술하는 데이터가 있어"

"다음 렌더링 단계에서 37번 객체를 사용해줘"

더욱 편리하게도, Bash 바인딩에서는 숫자 대신 이름을 사용할 수 있습니다.

첫 번째 주요 객체 타입은 텍스처입니다. 2D 이미지를 그래픽 카드에 로드한 후 폴리곤을 '페인팅'하는 데 사용합니다. 두 번째는 '디스플레이 리스트'로, OpenGL 명령 시퀀스를 녹화한 뒤 단일 OpenGL 명령인 것처럼 재생하는 방식입니다.

디스플레이 리스트는 임시 프로토타이핑에 매우 유용합니다. 간단한 버텍스 명령으로 3D(또는 2D) 모델을 기술하는 점들을 찍고, 그 시퀀스를 디스플레이 리스트로 녹화하면 이후에는 단일 명령으로 해당 모델을 렌더링할 수 있습니다.

이 강력함의 예시는 Examples 디렉토리의 Robot.sh에서 확인할 수 있습니다. 시작 시 로봇 신체 각 부분마다 디스플레이 리스트를 하나씩 생성하고, 실행 중에는 프레임당 단 58줄의 텍스트만 출력해 로봇을 렌더링합니다. Bash는 16밀리초 안에 58줄의 텍스트를 충분히 생성할 수 있고(12년 전에도 가능했습니다), 덕분에 데모는 일반적인 하드웨어에서도 풀 속도로 실행될 수 있었습니다.

디스플레이 리스트는 드로리안 계기판 소프트웨어에서 사용 중인 Perl 그래픽으로 가져온 핵심 기법이기도 합니다. C에 비해 Perl의 함수 호출은 비용이 큽니다. 특히 OpenGL 1.4처럼 함수 중심적인 API에서는 더욱 그렇습니다. 하지만 명령들을 디스플레이 리스트로 묶으면 Perl이 비디오 프레임당 처리할 작업량이 크게 줄어듭니다. Bash 프로젝트에서 이 기법을 배우지 않았다면 Perl 프로젝트는 지금만큼 성공하지 못했을 것입니다.

왜 12년 전에 공개하지 않았는가?

Bash로 멋진 애니메이션 데모를 만들 수는 있었지만, 실제 목표는 완전한 게임을 만드는 것이었습니다. 저는 오래된 비행 시뮬레이션 게임 Terminal Velocity의 클론을 목표로 삼았고, 큐브 필드를 나는 우주선까지는 구현했습니다(Examples의 Flight.sh 참조). 하지만 다음 관문은 충돌 감지였습니다. Bash에서 사용할 수 있는 원시 자료구조는 변수 이름의 전역 '연관 배열'과 배열 변수뿐이고(Bash 4부터 연관 배열 변수 지원), 모든 산술 연산은 정수로 처리됩니다. 고정소수점 정수 연산으로 3D 행렬 회전까지는 구현했지만, 충돌 감지 구현은 너무 큰 장벽으로 보였습니다. 폰트 API에도 만족스럽지 못했습니다. 이런 문제들의 해법을 고민하던 중 프로젝트는 점점 할 일 목록에서 밀려났습니다.

바로 여기서 저는 (글로 적었듯이) "Bash는 끔찍한 글루 언어다"라는 결론에 도달했습니다. 이 프로젝트는 농담(혹은 교육 도구)으로 시작해 농담으로 끝났지만, 같은 프로그래밍 스타일이 Perl에서는 실용적인 애플리케이션에 상당히 유용하다는 사실이 밝혀졌습니다. 13년 전 Frozen Bubble을 조금 더 진지하게 받아들였다면, 지금 가장 좋아하는 언어가 된 Perl을 더 일찍 시작할 수 있었을지도 모릅니다.

버그 수정 외에 이 프로젝트를 더 발전시킬 계획은 없지만, OpenGL 개념을 배우고 싶은 사람이나 M4로 Doom 클론을 작성하고 싶은 강한 열망과 많은 여가를 가진 사람에게는 적어도 유용할 거라 생각합니다.

즐기세요! 그리고 이 링크를 널리 전달해 주세요. 그러지 않으면 아무도 믿지 않을 테니까요.