← 블로그 목록

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