← 블로그 목록

[Study] janitor, janitor-web 분석

@juyonLee00
  • #Study
목차

해커톤 전에 ThorVG의 기능을 활용한 프로젝트인 janitor, 웹으로 배포한 janitor-web의 코드를 함께 보면 코드 작성에 도움이 될 것 같아 해당 프로젝트들을 보게 되었습니다.

목차


1. thorvg.janitor

https://github.com/thorvg/thorvg.janitor


Janitor은 ThorVG의 렌더링 기능과 성능을 게임 형태로 보여주는 데모 프로젝트입니다. 적이 파괴될 때 다수의 파티클을 사용하며, 최대 25000개의 Paint와 DropShadow, Blur 효과를 처리하는 장면이 성능 테스트의 핵심입니다.


해당 코드는 핵심 콜백이 content(), update()로 이루어져 있습니다. content()는 초기 ThorVG 객체를 생성하고, update()는 매 프레임 상태를 갱신하며 input()은 유저 입력을 처리합니다. 이렇게 코드를 구조화함으로써 렌더링으로 인한 성능 부하를 재현할 때 코드를 쉽게 추적할 수 있습니다.

virtual bool content(...) = 0;
virtual bool update(...) { return false; }

별 하나마다 독립된 Shape를 만드는 대신, 한 레이어의 별들을 하나의 Shape에 여러 appendRect() 경로로 추가해 플레이어 위치에 따라 레이어별로 다른 이동량을 적용합니다. 이를 통해 별 하나마다 Paint객체를 만들지 않고 한 Shape의 서브패스로 묶어 불필요한 Paint수를 줄일 수 있습니다. 또한 레이어는 4개로 분리하고 각자 다른 변환을 적용함으로써 별 레이어에서는 Shape 단위 변환을, 격자와 경계 효과를 묶은 구역은 Scene 단위 변환을 적용합니다. 실제 성능 부하를 일으키는 폭발 파티클은 Paint를 많이 사용하고, 의미 없이 객체 수만 늘어나는 배경은 배치 처리하였습니다.

// 레이어 수 확인
#define GALAXY_LAYER 4
#define STARS_PER_LAYER 100

// WarZone이 갖는 Shape 포인터 수
Shape* galaxy[GALAXY_LAYER];

// 레이어 생성 및 경로 추가
galaxy[i] = Shape::gen();
galaxy[i]->appendRect(...);

Enemy 기본 클래스는 이동, 위치, 회전, 충돌 반경을 관리하고 구체적인 적들 종류는 서로 다른 Shape 경로만 구현했습니다. Shape로 적들을 구별함으로써 ThorVG 렌더링 차이를 중심으로 확인하도록 하였습니다.

// Enemy가 갖는 정보

Shape* model
type
position: from / to / current
rotation: from / to
Tween time
static DURATION
static BOUND

폭발은 하나당 6개의 파편 Shape와 80개의 flash shape으로 구성되며, 이 중 80개의 flash shape에는 Addictive Blend가 적용됩니다. 각 Shape와 flash Shape 둘 다 위치, 색상, 크기, 불투명도가 독립적으로 갱신되어 다수의 Paint가 서로 다른 Transform과 색상 상태를 갖습니다.

// flash Shape에 Addictive Blend 적용
flashes[i].shape->blend(BlendMethod::Add);

다음은 Pool에 보관되는 Paint의 수명 관리에 대한 내용입니다. 이 Paint는 현재 Sccne에서 제거되더라도 이후 다시 사용할 수 있어야 하므로 Enemy의 일부 객체는 생성 시 명시적으로 ref()를 호출하고, 실제 소유 객체가 파괴될 때 unref()를 호출합니다. 이 방법을 통해 Scene에 포함된 기간과 cpp 게임 객체가 Paint를 보관하는 기간을 분리함으로써 Paint 소유권을 직접 보여줄 수 있습니다.

model->ref();
...
model->unref();

다음은 지속적인 성능 변화 추세를 용이하게 만들기 위해 프레임 시간에 시간 기반 EMA를 적용하여 표시값을 부드럽게 보이도록 설정합니다. 비정상적인 값을 대비하기 위해 큰 프레임 간격은 최대 0.25초로 제한합니다.

static double ema_dt = 1.0 / 60.0;
static const double half_life = 0.25;
...
ema_dt += alpha * (dt - ema_dt);

추가로 assets.h에 포함된 메모리 데이터를 사용해 필요한 리소스를 로드함으로써 경로를 효율화하고 단일 실행 파일 중심으로 데모를 배포가능하게 하였습니다.



2. thorvg.janitor-web

https://github.com/thorvg/thorvg.janitor-web


웹 버전은 네이티브 Janitor를 @thorvg/webcanvas위로 올린 프로젝트입니다. ThorVG에서 적용되는 Renderer들을 선택하여 렌더링이 가능하고 멀티스레드 렌더링도 실행 가능합니다. 따라서 네이티브 Janitor의 동적 렌더링 부하를 브라우저에서 재현하면서 WebCanvas Renderer 선택, WASM 초기화, Worker 파일 배포와 cross-origin isolation 조건을 함께 검증합니다.


웹은 crossOriginIsolated값을 기준으로 해당 값이 false인 경우 요청된 스레드 값과 관계없이 스레드 수를 0으로 설정합니다. 스레드 수가 1 이상이면 @thorvg/webcanvas의 threaded 배포 파일을 동적으로 가져오고, 0이면 일반 single-thread 초기화를 사용합니다. 그래서 COOP, COEP가 설정되지 않은 환경에서도 single-thread 버전으로 실행가능합니다.


네이티브는 플레이어를 여러 Shape를 조합해 구성하지만, 웹 버전은 spaceship.jsonTVG.Animation으로 로드합니다. Animation::info()에서 총 프레임과 재생 시간을 구하고, 플레이어가 이동 중일때만 시간 기반 진행률로 프레임을 계산합니다. 이것을 통해 WebCanvas의 Lottie 로드, Animation 정보 조회, 수동 프레임 제어와 Picture Transform을 함께 보여줍니다.

this.model = new TVG.Animation();
this.model.load(spaceship);

게임 루프는 requestAnimationFrame()으로 진행됩니다. 각 콜백에서 현재 경과 시간을 계산하고, FPS를 갱신한 뒤 update(), render()를 호출하고 다음 프레임을 다시 예약합니다. requestAnimationFrame() 함수는 일반적으로 브라우저의 다음 repaint 직전에 콜백을 실행하고 디스플레이 갱신 주기에 맞춰서 호출됩니다. 그래서 대부분의 브라우저에서는 백그라운드 탭에서 호출이 일시 정지되거나 제한될 수 있습니다.

performance.now()
game.update(elapsed)
canvas.render()
requestAnimationFrame(loop)

웹에서도 적과 폭발 객체를 Pool 배열로 관리하지만 네이티브처럼 시작 시 폭발 객체를 미리 생성하지 않습니다. 웹에서는 Pool이 비었을 때 새 객체를 지연 생성하고, 이후부터는 반환된 객체를 재사용합니다. 이를 통해 네이티브와 유사한 재사용 수명 주기를 유지하며, 반복적인 객체 생성과 Javascript GC의 프레임 드롭을 방지할 수 있을 것이라 생각됩니다.

enemies: Enemy[][] = [[], [], [], []];
explosions: Explosion[] = [];

웹에 추가된 사운드는 AudioContextAudioBuffer Map을 사용하였고, 실제 재생 시에는 AudioBufferSourceNode와 Gain Node를 생성하였습니다. 그리고 리소스를 로드할 때 웹판은 번들러가 생성한 URL을 사용해 브라우저 요청으로 데이터를 가져옵니다.



3. 후기

원래는 해커톤 전에 웹 버전과 네이티브 버전의 차이를 분석하고 어느 버전으로 하는지 더 효율적일지를 고민해보려고 했습니다. 그런데 해커톤 기간에 cpp로 그래픽을 구현하기로 결정해버리고 구현하느라 글 작성이 늦어져 버렸습니다..그래서 글 작성이 늦어진 겸 겸사겸사 웹 버전 코드를 자세히 확인해보게 되었습니다. 해당 프로젝트를 보면서 프로젝트의 목적성에 맞는 기능을 어떻게 정할지에 대해 고민하게 되었고 웹 버전으로 다른 사이드 프로젝트를 해 보는 것에 대한 자신감이 조금 생긴 것 같습니다.

댓글

Discussion 원문