← 블로그 목록

[Study Note] thorvg.web의 lottie player 코드 흐름 추적하기(1)

@jinlee0310
  • #Study
목차

TL;DR

ThorVG의 web 리포지토리의 lottie player가 어떤 과정을 거쳐 화면에 그려지는지 top-down 방식으로 공부한 내용을 정리하였습니다. 우선은 web 리포지토리의 wasm binding을 알아보고 duration api를 기준으로 코어까지 흐름을 추적합니다.

1. ThorVG.web

1. pnpm run build

build script를 실행하면 wasm_player_setup.sh가 실행되고 rollup이 실행된다.

// package.json
"build": "npm run clean && sh ./wasm_player_setup.sh && THORVG_VERSION=$(npm run version --silent) rollup -c --bundleConfigAsCjs",

wasm_player_setup.sh에는 build_wasm_player를 삭제하고 wasm_player_build.sh를 실행하는 명령어가 있다.

# wasm_player_setup.sh
rm -rf build_wasm_player && sh ./wasm_player_build.sh $EMSDK/

그리고 wasm_player_build에는 2가지 단계를 실행하는데 첫번째는 meson과 ninja를 이용해 ThorVG를 빌드하고 두번째는 wasm binding을 하는 과정이다. 참고로 wasm build는 emcc를 가지고 하는데 sh 파일 내에는 emcc를 사용하는 명령어가 없다. 이유는 meson-cross-file이 컴파일/링커로 em++을 지정하고 ninja가 대신 호출하기 때문이다. 먼저 wasm 바인딩 쪽 코드를 살펴본다.

2. wasm binding

핵심은 emscripten의 embind를 써서 C++ 코드를 JS/TS에서 그대로 쓸 수 있게 만드는 방식이다. wasm binding을 하는 코드는 wasm 폴더 내에 작성되어 있고 wasm을 사용할 수 있게 만드는 ts 파일은 packages 폴더 내에 있다.

// packages/lottie-player/src/base-lottie-player
wasmModule = await Module({
  locateFile: (path, prefix) =>
    path.endsWith(".wasm") ? this.wasmUrl || _wasmUrl : prefix + path,
});

ts 파일에서 Module을 할당한다.

// packages/lottie-player/src/base-lottie-player
this.TVG = new wasmModule.TvgLottieAnimation(engine, `#${this.canvas!.id}`);

그리고 LottieAnimation 인스턴스를 생성한다. 이후에 TVG를 이용해 wasm 파일에서 노출시킨 api들을 사용한다.

이 TvgLottieAnimation은 wasm 폴더의 tvgWasmLottieAnimation 파일에 구현되어있다.

// wasm/lottie-player/tvgWasmLottieAnimation.cpp
EMSCRIPTEN_BINDINGS(thorvg_bindings) {
  register_type<ArrayBuffer>("ArrayBuffer");
  register_type<Float32Array>("Float32Array");
  register_type<AssetResolverCallback>(
      "(src: string, data: unknown) => { name: string, buffer: ArrayBuffer, mimetype: string }");

  emscripten::function("init", &init);
  emscripten::function("term", &term);

  class_<TvgLottieAnimation>("TvgLottieAnimation")
      .constructor<string, string>()          // new TvgLottieAnimation(engine, canvasId)
      .function("render",  &TvgLottieAnimation::render)
      .function("load",    &TvgLottieAnimation::load)
      .function("frame",   &TvgLottieAnimation::frame)
      .function("resize",  &TvgLottieAnimation::resize)
      // ... 나머지 메서드들
      .function("setAssetResolver", &TvgLottieAnimation::setAssetResolver);
}

TvgLottieAnimation을 이용해 사용할 수 있는 api들이 선언되어있다. TvgLottieAnimation은 사실 ThorVG core를 사용할 수 있게 해주는 wrapper이다. 실제 lottie를 렌더링하고 파싱하는 것은 core에서 동작한다. 이 wrapper를 통해 JS/TS가 ThorVG 코어를 사용할 수 있게 되는 것이다. 아래 글에서는 load와 duration api를 따라가며 ThorVG core에서 lottie를 어떻게 파싱하는지 알아보겠다.

2. ThorVG

1. duration

먼저 맛보기로 duration api를 따라간다. load/render는 무거우니 구조 감을 먼저 잡으려고 단순한 duration부터 본다

duration은 core/renderer의 tvgAnimation에서 노출되고 loader의 LottieLoader::duration에 구현되어 있다.

float Animation::duration() const noexcept
{
	auto loader = to<PictureImpl>(pImpl->picture)->loader;
	
	if (!loader) return 0;
	if (!loader->animatable()) return 0;
	
	return static_cast<AnimLoader*>(loader)->duration();
}
float LottieLoader::duration()
{
	return (segmentEnd - segmentBegin) / frameRate;
}

여기서 segmentEnd는 segment 함수에 정의된 segmentEnd = frameCnt = (endFrame - startFrame); 이고 segmentBegin은 부모 클래스인 AnimLoader에서 default 값을 지정해준다. 따라서 시작점은 0으로 초기화, 끝점은 lottie를 파싱한 뒤 정해진다.

그리고 framerate는 아래와 같이 정의되어있다

if (!strncmp(p, "\"fr\":", 5)) {
	p += 5;
	auto e = strstr(p, ",");
	if (!e) e = strstr(p, "}");
	frameRate = toFloat(p, nullptr);
	p = e;
	continue;
}

frameRate는 lottie의 재생 속도를 의미한다. 1초에 몇 프레임을 재생할 지 뜻하는 값(프레임 수/초)이고 Lottie json의 fr 필드에서 파싱해온다. 따라서 duration의 계산식인 (segmentEnd-segmentBegin) / frameRate총 프레임 수 / 프레임 수 / 초 가 되기 때문에 초가 된다. cf). strncmp는 문자열을 비교하는 함수인데 frameRate에서는 json을 훑고 있는 포인터 p와 “,f,r,”,: 를 비교한다.

이후 글에서는 Lottie player의 핵심인 load, render, update에 대해 알아보겠다

댓글

Discussion 원문