본문 바로가기

개발자 강화/프론트엔드

ThorVG에 대해 알아봅시다! (feat. 로띠/닷로띠 차이점)

Intro

이번 여름 FECONF25에서 박춘언님의 로띠 세션을 들었다.

 

우와 로띠 엔지니어가 직접 해주는 세션이라고? 두근두근!하는 마음에 무작정 달려가 들었다.

그곳에서 닷로띠와 그 렌더링 엔진인 ThorVG을 알게 되었다.

 

그리고, 춘언님께서 바로 이 ThorVG의 창시자셨다!

 

새로운 것을 배울 생각에 심장이 두근거렸다!

이제 ThorVG에 대해 알아보자!


ThorVG란?

https://www.thorvg.org/

 

ThorVG | Thor Vector Graphics

Thor Vector Graphics (ThorVG) is an open-source, lightweight, and high-performance vector graphics engine designed to deliver scalable graphics and rich animations across a wide range of devices. The name “Thor” reflects a dual inspiration—combining

www.thorvg.org

 

ThorVG의 "Thor"는 북유럽 신화의 토르에서 유래했다.

immense strength & lightning-fase agility

 

강력하고 빠르면서도 가벼운 벡터 그래픽을 목적으로 한다.

 

2020년, ThorVG는 'Simplicity leads to reliability'라는 철학으로 시작했다.

"임베디드 시스템부터 웹까지,
그 사이 모든 곳을 위한 차세대 고성능 벡터 그래픽 엔진"

 

데스크톱용 무거운 그래픽 엔진과

임베디드/모바일의 요구 사이의 간극을 메우고자 했다.

 

그 결과, 150KB라는 초경량 사이즈로 (ㄹㅈㄷ...)

마이크로 컨트롤러(ESP32)부터 데스크톱까지 돌아가는 엔진이 탄생했다.

 

현재 다양한 곳에서 사용되고 있다.

- Tizen OS(삼성 워치, TV의 OS)

- Godot platform(오픈소스 게임 엔진)

- LVGL(임베디드 ui 라이브러리)

- dotLottie Player

 

 


 

ThorVG의 핵심 특징

 

ThorVG의 기술적 특징(초경량, cross platform, 성능 최적화 등)은

이전 FECONF25 로띠 세션 후기에서 서술했다!

 

해당 글을 참고해주세용

⬇️

[FECONF25] 개발자를 위한 모션 그래픽 솔루션: Lottie의 기술 진화와 활용 전략

 

[FECONF25] 개발자를 위한 모션 그래픽 솔루션: Lottie의 기술 진화와 활용전략

feconf25에 참석해 세션을 들은 후 작성하는 후기 글입니다. 로띠 개발자와 디자이너의 세션을 직접 들을 수 있다니 완전 럭키비키자늠!!! 이 세션 후기를 우리 레브잇 올팜 디자이너 이솔비님께

developer-dreamer.tistory.com

 


기존 Lottie의 렌더러들

왜 ThorVG 이야기 하다 말고, 다른 렌더링 엔진이냐 하면...

ThorVG가 어떤 문제를 해결하고자 했는지 먼저 알아보기 위해!

 

각 플랫폼별 Lottie 플레이어는 서로 다른 렌더링 방식을 사용한다

After Effects에서 애니메이션을 JSON 형태로 export한다.

그리고, 각 플랫폼에서 이 json을 각자의 방식으로 해석해서 그린다.

 

플랫폼 렌더러 방식
Web https://github.com/airbnb/lottie-web SVG DOM / Canvas 2D
IOS https://github.com/airbnb/lottie-ios Core Animation (Swift)
Android https://github.com/airbnb/lottie-android Android Canvas + SKIA

 

After Effects → JSON (공통 스펙)
                  ↓
        ┌────────┼────────┐
        ↓        ↓        ↓
    JavaScript  Swift    Kotlin
      (Web)     (iOS)   (Android)

 

도식화 하자면 이런 구조인 셈.

 

동일한 JSON 속성 "채우기"를 각 플랫폼이 어떻게 해석할까?

실제로 각 라이브러리가 얼마나 다른지 궁금해서 소스 코드도 찾아봤다.

예를 들어 JSON의 "ty: "fl" (Fill, 채우기)을 처리하는 코드를 보자.

 

lottie-web (JavaScript)

player/js/elements/helpers/shapes/SVGFillStyleData.js

// SVGFillStyleData.js
function SVGFillStyleData(elem, data, styleOb) {
  this.initDynamicPropertyContainer(elem);
  this.getValue = this.iterateDynamicProperties;
  this.o = PropertyFactory.getProp(elem, data.o, 0, 0.01, this); // opacity
  this.c = PropertyFactory.getProp(elem, data.c, 1, 255, this);  // color
  this.style = styleOb;
}

 

SVG 엘리먼트의 속성으로 설정할 데이터만 준비한다.

실제 렌더링은 브라우저의 SVG 엔진이 담당한다.

 

 

lottie-ios (Swift)

Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/FillRenderer.swift

// FillRenderer.swift
func updateShapeLayer(layer: CAShapeLayer) {
  layer.fillColor = color
  layer.opacity = Float(opacity)
  layer.fillRule = fillRule.caFillRule
  hasUpdate = false
}

 

CAShaperLayer의 속성만 설정하면 된다.

실제 렌더링은 iOS의 Core Animation이 GPU를 활용해 처리한다.

 

 

lottie-android (Kotlin)

lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java

// FillContent.java
@Override 
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha, ...) {
  int color = ((ColorKeyframeAnimation) this.colorAnimation).getIntValue();
  int alpha = (int) (parentAlpha * fillAlpha);
  paint.setColor((alpha << 24) | (color & 0xFFFFFF));
  
  path.reset();
  for (int i = 0; i < paths.size(); i++) {
    path.addPath(paths.get(i).getPath(), parentMatrix);
  }
  
  canvas.drawPath(path, paint);  // 직접 그린다!
}

 

Paint 객체를 설정하고, Path를 조합한 뒤, Canvas에 직접 그린다.

매 프레임마다 이 과정을 반복한다.

 

  web iOS Android
코드량 15줄 30줄 170줄
렌더링 주체 브라우저 SVG 엔진 Core Animation (GPU) 직접 Canvas 드로잉
색상 설정 style 속성  layer.fillColor paint.setColor()
핵심 API  SVG DOM  CAShapeLayer android.graphics.Canvas

 

web, ios는 속성만 설정하면 시스템이 알아서 그려준다.

android는 개발자가 직접 그리는 방식이다.

이 때문에 코드량의 차이가 꽤 난다.

 

여기에서 발생하는 문제점

1. 세 플랫폼이 공유하는 건 JSON 스펙 뿐이다.

2. 렌더링 엔진은 각각 다른 언어로 따로 구현된다.

3. 같은 파일이 플랫폼마다 다르게 보일 수 있다.

4. 기능 지원 범위가 각각 다르다.

 

같은 로띠 파일인데 브라우저, 안드, ios에서 각각 조금씩 다르게 보이면

디자이너와 FE 개발자 모두 환장할 듯...

 


dotLottie & ThorVG의 방향성

 

dotLottie와 ThorVG라는 단일 C++ 엔진을 사용해 모든 플랫폼에서 동일한 렌더링을 보장한다.

web, ios, android 마다 각각 다르던 렌더링 엔진을 하나로 통합하다니 정말 혁신인듯...ㄹㅇ

 

dotLottie와 ThorVG는 어떻게 렌더링 하는가?

After Effects → .lottie (압축 포맷)
                   ↓
           dotlottie-rs (Rust 코어)
                   ↓
              ThorVG (C++ 렌더러)
                   ↓
            uniffi-rs (FFI 바인딩)
                   ↓
        ┌──────────┼──────────┐
        ↓          ↓          ↓
       WASM      Swift      Kotlin
       (Web)     (iOS)    (Android)

 

 

각 레이어를 좀 더 자세히 살펴보자

 

.lottie 포맷

.lottie는 사실 ZIP 파일이다

 

.json (2MB) -> .lottie (800KB)

약 60% 용량 압축이 가능하다.

 

이 내부에는 이런 것들이 들어있다.

animation.json = Lottie 애니메이션 데이터
images/ = 이미지 에셋
manifest.json = 메타데이터 (테마, 여러 애니메이션 정보 등)

 

기존 .json과 달리 여러 애니메이션을 하나로 묶거나, 테마 기능을 지원한다

 

dolottie-rs (Rust 코어)

플레이어 로직을 담당한다

 

- .lottie 파일 로드 & 압축 해제

- 애니메이션 상태 관리 (현재 프래임, 재생/정시, 속도)

- State Machine (인터랙션)

- 테마 적용

 

닷로띠 플레이어로 Rust를 사용하는 이유

- C++처럼 빠르고, 메모리 버그 없음

- WASM 컴파일이 쉬움

- FFI(다른 언어 연결)가 잘 됨

 

"무엇을 그릴지"까지만 결정하고, 실제 그리는 건 ThorVG에 위임한다.

 

ThorVG (C++ 렌더러)

실제로 픽셀을 찍는 렌더링 엔진이다.

 

1. 기존 lottie:

- iOS: CAShapeLayer.fillColor = color (iOS가 그림)

- Android: canvas.drawPath(path, paint) (Android가 그림)

- Web: SVG DOM 조작

 

2. ThorVG

- 모든 플랫폼: 픽셀 버퍼 [x][y] = color (ThorVG가 직접 그림)

 

플랫폼 그래픽 API에 의존하지 않고, 자체적으로 픽셀 버퍼에 직접 그린다.

그래서 어디에 실행하든 결과가 동일하다.

 

uniffi-rs (FFI 바인딩)

FFI(Foreign Function Interface)는 다른 언어에서 Rust 함수를 호출할 수 있게 해준다.

Rust 코드 하나 작성
        ↓
    uniffi-rs가 분석
        ↓
┌───────┼───────┐
↓       ↓       ↓
Swift  Kotlin  C++/WASM
자동    자동     자동
생성    생성     생성

 

개발자가 Swift/Kotlin 바인딩을 직접 작성할 필요 없이,

uniffi-rs가 자동 생성해준다.

 

최종 결과물

플랫폼 결과물 사용 방식
Web WASM(.wasm) <canvas>에 버퍼 복사
iOS Swift Framework SwiftUI/UIKit View
Android Kotlin Library(AAR) Compose/XML View

 


ThorVG는 "채우기"를 어떻게 구현할까?

https://github.com/thorvg/thorvg

 

GitHub - thorvg/thorvg: An open-source C++ vector graphics engine supporting SVG and Lottie formats, featuring advanced renderin

An open-source C++ vector graphics engine supporting SVG and Lottie formats, featuring advanced rendering backends such as WebGPU for high-performance graphics. - thorvg/thorvg

github.com

 

위에서 lottie의 각 플랫폼별 렌더링 엔진이 채우기를 구현하는 방법을 알아봤다.

그렇다면 thorVG는 어떻게 할까?

 

1. .lottie- -> animation.json -> Shape 변환

- 닷로띠 파일은 zip 컨테이너이다.

- dolottie-rs가 이를 압축해제하면 내부의 animation.json이 추출된다.

- ThorVG는 이 JSON을 파싱해서 Shape로 변환한다.

 

src/loaders/lottie/tvgLottieBuilder.cpp

// animation.json의 SolidFill을 ThorVG Shape로 변환
bool LottieBuilder::updateSolidFill(...) {
    auto fill = static_cast<LottieSolidFill*>(*child);
    auto opacity = fill->opacity(frameNo, tween, exps);
    
    if (opacity == 0) return false;

    auto color = fill->color(frameNo, tween, exps);
    ctx->propagator->fill(color.r, color.g, color.b, opacity);  // Shape::fill() 호출
    ctx->propagator->fillRule(fill->rule);

    return false;
}

 

기존 lottie 플레이어들의 fill 처리와 비교해보자

 

플랫폼 핵심코드 드로잉 주체
lottie-web this.c = PropertyFactory.getProp(...) SVG에 위임
lottie-ios layer.fillColor = color Core Animation에 위임
lottie-android paint.setColor(); canvas.drawPath() 직접 드로잉
ThorVG shape->fill(r,g,b,a) 자체 렌더러로 드로잉

 

2. Shape의 fill 메서드

src/renderer/tvgShape.cpp

Result Shape::fill(uint8_t r, uint8_t g, uint8_t b, uint8_t a) noexcept
{
    to<ShapeImpl>(this)->fill(r, g, b, a);
    return Result::Success;
}

Result Shape::fill(Fill* f) noexcept  // 그라디언트용
{
    return to<ShapeImpl>(this)->fill(f);
}

 

색상 정보를 내부 구현체에 저장한다.

 

3. 실제 픽셀을 채우는 렌더러

src/renderer/sw_engine/tvgSwFill.cpp

// Linear Gradient 채우기 - 픽셀 버퍼에 직접 그린다
void fillLinear(const SwFill* fill, uint32_t* dst, uint32_t y, uint32_t x, uint32_t len, ...) {
    float rx = x + 0.5f;
    float ry = y + 0.5f;
    float t = (fill->linear.dx * rx + fill->linear.dy * ry + fill->linear.offset) * (SW_COLOR_TABLE - 1);
    float inc = (fill->linear.dx) * (SW_COLOR_TABLE - 1);

    // 각 픽셀을 순회하며 색상 계산
    for (uint32_t j = 0; j < len; ++j, ++dst) {
        *dst = _fixedPixel(fill, t2);  // 픽셀 버퍼에 직접 쓰기!
        t2 += inc2;
    }
}

 

*dst = ... 이 부분에서 실제로 픽셀 버퍼에 색상을 쓴다.

플랫폼 API(SVG, CAShaperLayer, android, graphics)를 쓰지 않고,

메모리 버퍼에 직접 픽셀 값을 쓴다.


 

이렇게 ThorVG의 코드를 보고 나니 dotlottie-rs의 코드도 궁금해졌다

 

dotLottie-rs는 어떻게 애니메이션을 추출할까?

https://github.com/LottieFiles/dotlottie-rs

 

GitHub - LottieFiles/dotlottie-rs: A universal, high-performance Lottie and dotLottie player built with Rust. Offers smooth rend

A universal, high-performance Lottie and dotLottie player built with Rust. Offers smooth rendering across platforms, low resource consumption, and extensive compatibility. Features FFI bindings for...

github.com

 

ThorVG가 렌더링을 담당한다면,

dotlottie-rs는 .lottie 파일을 열고 animation.json을 추출하는 역할을 한다

 

dotlottie-rs/src/fms/mod.rs

// 1. ZIP 열기
let archive = ZipArchive::new(dotlottie_bytes)?;

// 2. manifest.json 파싱
let manifest: Manifest = read_and_parse("manifest.json")?;

// 3. animation JSON 추출
let animation_json = read_file("animations/{id}.json")?;

// 4. 이미지를 Base64로 임베딩
for asset in animation_json.assets {
    let image_bytes = read_file("images/{asset.p}")?;
    asset.p = format!("data:image/png;base64,{}", encode_base64(image_bytes));
    asset.e = 1;  // embedded 플래그
}

// 5. 완성된 JSON을 ThorVG에 전달
renderer.load_data(animation_json)

 

이미지를 Base64로 변환해서 JSON에 직접 삽입한다.

ThorVG는 외부 파일 참조 없이 JSON 하나만 받으면 된다.

 

dotlottie-rs/src/dotlottie_player.rs

pub fn load_dotlottie_data(&mut self, file_data: &[u8], width: u32, height: u32) -> bool {
    // 1. .lottie 파일 열기 (위의 DotLottieManager)
    let manager = DotLottieManager::new(file_data)?;
    
    // 2. animation.json 추출 (이미지 임베딩 완료된 상태)
    let animation_data = manager.get_active_animation()?;
    
    // 3. ThorVG 렌더러에 전달
    self.renderer.load_data(&animation_data, width, height)
}

 

플레이어에서 추출한 animation.json을 ThorVG 렌더러로 전달한다

 


 

.lottie (ZIP 컨테이너)
        ↓
dotlottie-rs - 압축 해제
        ↓
animation.json 추출 (내부의 "ty": "fl" = Fill)
        ↓
tvgLottieBuilder.cpp - updateSolidFill()
        ↓
tvgShape.cpp - Shape::fill(r, g, b, a)
        ↓
tvgSwFill.cpp - fillLinear() / fillRadial()
        ↓
    *dst = color  // 픽셀 버퍼에 직접 쓰기

 

요약하자면... 이런 흐름으로 이뤄지는 것이다.

 

기존 lottie는 JSON 스펙만 공유하고, 각 플랫폼에서 알아서 그렸다면

dotLottie+ThorVG는 그리는 엔진까지 공유해서 결과도 동일하다는 의미이다!

 


dotLottie를 실제로 써본다면?

FECONF25를 듣고, 실무에 닷로띠를 적용한 후 블로그글을 작성했습니다.

아래 글을 참고해주세용

⬇️

[개발] FECONF25 로띠 세션에서 알게 된 닷로띠를 실무에 적용하기

 

[개발] FECONF25 로띠 세션에서 알게 된 닷로띠를 실무에 적용하기

보통 컨퍼런스를 듣고 와서"아~ 재밌었다~"로 끝나게 되면...기억 지속이 잘 안된다 그럼 답은 뭐다?"이론 및 실습" 괜히 대학교 과목이 이론 3시간 실습 1시간으로 구성된 게 아님(기억 속에 주마

developer-dreamer.tistory.com

 


마치며

 

오픈 소스 코드를 직접 까보면서 처음 공부해봤다.

주로 사용하는 언어가 아니어서 어떻게 이해해야 할지 고민이 많이 됐었다.

 

내가 닷로띠를 사용했던 경험을 떠올리면서, '이 내부에는 어떻게 돌아가고 있을까?'

하는 관점으로 하나씩 차근차근 접근하니까 조금씩 이해되기 시작했다!

 

역시 근본적인 기술을 이해하는 과정은 정말 즐거운 것 같다.

좀 더 공부하다보면 ThorVG의 오픈소스에 기여도 할 수 있겠지?

앞으로 더 공부해봐야겠다!

 

 

작성 시작: 2025.12.22. 20:40

작성 종료: 2025.12.23. 00:25

728x90