셰이더 드로잉: 환경(a)

개요

GPT의 도움으로 WebGPU를 이용해 프래그먼트 셰이더만으로 그림을 그릴 수 있는 환경을 구축한 적이 있었다. 셰이더 파일을 수정하면 브라우저로 결과를 확인할 수 있고, 이미지나 영상으로 저장하고, 웹사이트로 배포하고, 자주 사용하는 사용자 정의 함수를 분류별로 묶어 모듈처럼 붙여 쓸 수 있었다. 사용하는 데 불편함은 없었지만 개선할 여지가 있다고 생각했다.

  • WebGPU를 지원하지 않는 환경을 고려할 수 없다.
  • 렌더 및 추출이 브라우저에 의존적이다.
  • 제작 과정, 기능 명세를 스스로 기록해두지 않았다.
  • 그래픽 파이프라인에 대한 이해가 부족하다.

처음부터 지나치게 편리하게 제작했기에 실제 작동과 그래픽 파이프라인 전반의 이해가 부족하고, 이를 어딘가에 기록해두지 않아 제작 과정 전반과 겪었던 문제 등을 상기하기 어렵다. 따라서 아래 사양을 기준으로 로컬 프레임워크를 직접 만들고, 그 과정을 기록하기로 했다.

  • 로컬 환경에서 셰이더 파일 작성
  • 로컬 윈도우(창, Window)로 즉시 렌더 결과 확인 가능
  • 원하는 프레임의 이미지, 영상 저장 가능
  • 다양한 입력 전달 가능
  • 웹사이트로 빌드 가능

모든 과정은 이전처럼 GPT와 대화하며 진행할 예정이나, 결과물 자체보다는 프로그래밍 언어, 프로그래밍 전반, 그리고 그래픽 파이프라인의 이해를 중심에 둘 생각이다.

***

Hello World

작업을 시작하기 위해 Rust를 학습했다. Rust 공식 문서를 참고해 학습 코스를 설계하고 Codex와 대화하며 진행했으며, 구체적인 내용은 아래와 같다.

학습 내용

  1. 변수·타입 (Variable·Type)
  2. 함수·조건·반복 (Function·Control flow)
  3. 소유·대여 (Ownership·Borrowing)
  4. 구조체·메서드 (Struct·Method)
  5. 열거형·패턴 매칭 (Enum·Pattern matching)
  6. 콜렉션·이터레이터 (Collection·Iterator)
  7. 모듈·테스트 (Module·Test)
  8. 제네릭·트레이트 (Generic·Trait)
  9. API 활용 (API)
  10. 라이프타임 (Lifetime)
  11. 비동기 (Async·Await)

나름 시간을 들여 학습하긴 했으나 결국 기본적인 내용이기에 Rust의 사용을 따로 정리하며 진행하지 않을 예정이다. 진행하며 필요한 내용이 있다면 추가 학습하기로 한다.

창 띄우기

이미지를 실시간으로 확인하기 위해 우선 운영체제와 연동하여 창을 생성하고 이벤트 루프에 따라 제거 및 조작할 수 있어야 한다. 이를 위해 winit이라는 Rust 라이브러리를 사용할 것이다.

winit은 기본적으로 EventLoop를 생성한 뒤 run_app 메서드의 인자로 ApplicationHandler라는 trait을 구현한 타입을 받아 실제로 실행시키는 흐름으로 작동한다. ApplicationHandler에는 resumed라는 메서드가 있다. 이 메서드는 어플리케이션이 재개될 때 호출된다. winit은 여러 플랫폼을 지원하며, 플랫폼별 작동에 일관성을 부여하기 위해 시작할 때 또한 resumed를 호출한다. 공식 문서에 Android 등의 플랫폼에서 문제없이 작동시키기 위해 첫 resumed 수신 시 창을 생성하는 것을 권장한다.

ActiveEventLoop는 ApplicationHandler의 콜백이 실행되는 동안 현재 활성화된 이벤트 루프를 조작하기 위해 전달되는 객체로서 실제 창과 EventLoop를 연결해 준다. 이를 이용해 창을 생성하거나 이벤트 루프를 종료시킬 수 있다. ApplicationHandler의 window_event 메서드는 운영체제에서 창에 대한 이벤트가 발생했을 때 호출되며, 전달받은 WindowEvent 열거형을 통해 각 이벤트에 대한 동작을 정의할 수 있다. 이를 이용해 창을 열고 닫을 수 있는 프로그램을 만들었다.

***

struct App {
    window: Option<Window>
}
let event_loop = EventLoop::new().expect("EventLoop 생성 실패");
let mut app = App::new();
event_loop.run_app(&mut app).expect("어플리케이션 실행 실패");

창을 담을 구조체를 정의하고, EventLoop를 만든다. run_app 메서드에 ApplicationHandler가 구현된 app을 입력해 구동시킨다.

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        window_id: WindowId,
        event: WindowEvent,
    )
    {
        
    }
}

ApplicationHandler의 내장 메서드 중 resumed는 어플리케이션이 재개되거나 최초 실행 시의 작동을 정의할 수 있고, window_event는 각 이벤트 시의 작동을 정의할 수 있다.

fn resumed(&mut self, event_loop: &ActiveEventLoop) {
    if self.window.is_some() {
        return
    }
    let create_window = event_loop.create_window(
        Window::default_attributes()
        .with_title("asdf")
    );
    match create_window {
        Ok(window) => {
            self.window = Some(window);
        },
        Err(err) => {
            eprintln!("{err}: Window를 생성하지 못했습니다.");
            event_loop.exit();
        },
    }
}

창이 없을 때 create_window로 생성한 창을 app에 할당한다.

fn window_event(
    &mut self,
    event_loop: &ActiveEventLoop,
    _window_id: WindowId,
    event: WindowEvent,
) {
    match event {
        WindowEvent::CloseRequested => {
            event_loop.exit();
        }
        _ => {}
    }
}

닫기 요청 이벤트 시 종료되도록 작동을 정의한다.

fn window_event(
    &mut self,
    event_loop: &ActiveEventLoop,
    _window_id: WindowId,
    event: WindowEvent,
) {
    match event {
        WindowEvent::CloseRequested => {
            event_loop.exit();
        }
        WindowEvent::Resized(size) => {
            println!("{}×{}", size.width, size.height);
        }
        WindowEvent::KeyboardInput { event, .. } => {
            let logical_key = event.logical_key;
            let state = event.state;
            let repeat = event.repeat;

            println!("key: {logical_key:?}, state:{state:?}, repeat: {repeat}");

            if logical_key == winit::keyboard::Key::Named(
                winit::keyboard::NamedKey::Escape,
            ) {
                event_loop.exit();
            }
        }
        _ => {}
    }
}

키보드 입력, 창 크기 변경 등 필요한 작동은 WindowEvent문서를 확인해 적용하면 된다.

***

다음 글로 이어집니다.