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 원문