← 블로그 목록

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

@jinlee0310
목차

2. ThorVG

본격적으로 load api를 따라가면서 ThorVG가 어떻게 lottie를 파싱하는지 알아보자.

대략적인 흐름을 그려보자면 loader->parser->builder 순으로 파이프라인을 타고 있는데 parser는 rapidjson을 이용해 json을 parse하고 composite tree를 만든다. 이 composite tree를 가지고 builder에서 씬을 그린다.

이제 아래에서 코드를 자세히 뜯어보자.

1. load

// wasm/lottie-player/tvgWasmLottieAnimation
bool load(string data, string mimetype, uint32_t width, uint32_t height)
{
	errorMsg = NoError;
	
	if (!canvas) return false;
	
	if (data.empty()) {
		errorMsg = "Invalid data";
		return false;
	}
	
	canvas->remove(); 
	
	delete(animation);
	animation = LottieAnimation::gen();
	animation->picture()->origin(0.5f, 0.5f); //center-aligned
	
	string filetype = mimetype;
	if (filetype == "json") {
		filetype = "lottie+json";
	}
	
	animation->picture()->resolver(resolver.func, &resolver.data);
	
	if (animation->picture()->load(data.c_str(), data.size(), filetype.c_str()) != Result::Success) {
		errorMsg = "load() fail";
		return false;
	}
	
	animation->picture()->size(&psize[0], &psize[1]);
	
	/* need to reset size to calculate scale in Picture.size internally before calling resize() */
	this->width = 0;
	this->height = 0;
	
	resize(width, height);  
	
	if (canvas->add(animation->picture()) != Result::Success) {
		errorMsg = "add() fail";
		return false;
	}
	
	updated = true;
	
	return true;
}

TvgLottieAnimation에서 노출하는 load의 구현체는 이렇게 되어 있다. 여기서 ThorVG의 코어에 진입하는 코드는 animation->picture()->load(data.c_str(), data.size(), filetype.c_str()) 이부분이다.

인자로 받은 data와 filetype을 load 함수에 넘겨주고 있다.

2. LoadMgr

picture->load는 tvgPicture에서 LoadMgr에 data,size, MIMEType을 넘겨준다.

// renderer/tvgLoaderMgr
static FileType _convert(const char* mimeType) {
    if (!strcmp(mimeType, "svg") || !strcmp(mimeType, "svg+xml")) type = FileType::Svg;
    else if (!strcmp(mimeType, "ttf") || !strcmp(mimeType, "otf")) type = FileType::Sfnt;
    else if (!strcmp(mimeType, "lot") || !strcmp(mimeType, "lottie+json")) type = FileType::Lot;
    else if (!strcmp(mimeType, "raw")) ...
    else if (!strcmp(mimeType, "png"))  ...
    else if (!strcmp(mimeType, "jpg") || !strcmp(mimeType, "jpeg")) ...
    else if (!strcmp(mimeType, "webp")) ...
}

LoadMgr은 넘겨받은 MIMEType을 기반으로 어떤 Loader를 사용할 지 결정한다. lottie-player에서는 lottie+json을 넘겨주고 있으니 LottieLoader를 사용한다.

3. LottieLoader

LottieLoader의 과정을 자세히 살펴보자 json 파싱은 LottieParser를 이용해 파싱하고 파싱이 끝나면 결과물을 comp에 저장한다. 이 comp를 builder에게 넘겨준다.

// loaders/lottie/tvgLottieLoader
bool LottieLoader::prepare() {
    LottieParser parser(content, dirName, builder->expressions());
    if (!parser.parse()) return false;
    comp = parser.comp;
    if (parser.slots) { ... apply(...); }
    builder->build(comp);      // 파싱 끝 → 빌더 호출
    ...
}

json을 파싱하는 코드는 아래와 같고 rapidjson을 파서로 사용한다. 결과물은 LottieCompositeTree이고 이는 lottie json을 오브젝트 그래프로 옮긴 것이다.

// loaders/lottie/tvgLottieParser
bool LottieParser::parse() {
    if (!parseNext()) return false;
    enterObject();
    comp = new LottieComposition;

    while (auto key = nextObjectKey()) {
        if (KEY_AS("v"))       comp->version   = getStringCopy();
        else if (KEY_AS("fr")) comp->frameRate = getFloat();
        else if (KEY_AS("ip")) startFrame      = getFloat();  // in-point
        else if (KEY_AS("op")) endFrame        = getFloat();  // out-point
        else if (KEY_AS("w"))  comp->w = getFloat();
        else if (KEY_AS("h"))  comp->h = getFloat();
        else if (KEY_AS("assets"))  parseAssets();
        else if (KEY_AS("layers"))  comp->root = parseLayers(comp->root);
        else if (KEY_AS("fonts"))   parseFonts();
        else if (KEY_AS("chars"))   parseChars(glyphs);
        else if (KEY_AS("markers")) parseMarkers();
        else if (KEY_AS("slots"))   captureSlots(key);
        else skip();
    }
    comp->root->inFrame  = startFrame;
    comp->root->outFrame = endFrame;
    postProcess(glyphs);
    return true;
}

4. LottieBuilder

빌더에서는 LottieCompositeTree에서 ThorVG 렌더러 오브젝트로 변환한다. root에 Scene을 넣어주고 ThorVG의 Shape, Text, Picture 등으로 씬을 그린다.

// loader/lottie/tvgLottieBuilder
void LottieBuilder::build(LottieComposition* comp) {
    comp->root->scene = Scene::gen();      // thorvg Scene 생성
    _buildComposition(comp, comp->root);   // 재귀적 자식 레이어 → Scene/Shape/Text 매핑
    auto clip = Shape::gen();
    clip->appendRect(0, 0, comp->w, comp->h);
    comp->root->scene->clip(clip);         // viewport clip
    to<SceneImpl>(comp->root->scene)->size({comp->w, comp->h});
}

3. update, render, draw

이렇게 만들어진 tvg 데이터들을 picture가 갖고 있고 load 작업이 끝나면 canvas->add(animation->picture())를 통해 캔버스에 추가한다. 그리고 프레임이 바뀌면 update, render(draw, sync) 작업을 순서대로 진행한다. base-lottie-player에서 play를 호출하면 rAF를 이용해서 update에 새로운 프레임 number를 넣어준다. 그러면 canvas->update를 호출해서 씬을 갱신한다. 그 후 render를 호출하면 canvas->draw, canvas->sync를 차례대로 호출하여 씬을 그리고 canvas에 동기화 한다. 이러한 방식을 통해 lottie json을 화면에 재생하게 되는 것이다.

댓글

Discussion 원문