spine與lottie-web對比


spine動畫介紹
spine是什么
Spine 是一款針對游戲開發的 2D 骨骼動畫編輯工具,提供高效簡潔的工作流程,以創建游戲所需的動畫。

spine出現的背景
傳統的幀動畫每幀都需要一張圖片,會產生大量的資源。每新增一個動畫都會大大增加游戲的磁盤空間和內存要求,流暢播放幀率則更甚。這不僅極大增加了美工的工作量,當必須縮減動畫數量以符合大小限制時,也會對最終成品產生影響。

Spine動畫優勢
最小的體積: 傳統的動畫需要提供每一幀圖片。而 Spine 動畫只保存骨骼的動畫數據,它所占用的空間非常小,並能為你的游戲提供獨一無二的動畫。
美術需求: Spine 動畫需要的美術資源更少,能為您節省出更多的人力物力更好的投入到游戲開發中去。
流暢性: Spine 動畫使用差值算法計算中間幀,這能讓你的動畫總是保持流暢的效果。
裝備附件: 圖片綁定在骨骼上來實現動畫。如果你需要可以方便的更換角色的裝備滿足不同的需求。甚至改變角色的樣貌來達到動畫重用的效果。
混合: 動畫之間可以進行混合。比如一個角色可以開槍射擊,同時也可以走、跑、跳或者游泳。(動畫直接可以平滑過渡)
程序動畫: 可以通過代碼控制骨骼,比如可以實現跟隨鼠標的射擊,注視敵人,或者上坡時的身體前傾等效果。

spine資源素材
.json文件或二進制.skel文件:包含所有骨架信息(二進制形式加載更快、gc更小)
.png文件:包含當前版本所有圖片的集合,也可單一素材,可導出一張或多張
.atlas文件:包含打包的圖集信息,記錄素材圖片在雪碧圖上的位置信息特征,一個atlas文件可對應多個素材圖片

spine資源結構

spine基礎概念
骨架Skeleton:指代的是數據的集合,包含構成此骨架的所有骨骼、插槽、附件及其他信息。
骨骼bones:一個人物本身由多個關節的骨骼組成。除了根骨骼以外,每個骨骼都有對應的父骨骼,骨骼與骨骼之間的關系最終構造成類似樹的結構。
插槽slot:一個骨骼bone下可能有多個slot插槽,每個slot插槽下可以放置一個附件實例。
插槽本身的存在有兩個重要的意義,一個是靈活的控制渲染順序,一個是分組同類附件。
一個插槽可以有多個附件,但一次只能看到一個。舉個簡單的栗子,圖中手槍所在的位置的插槽是"武器"插槽,而該插槽可以放置不同的武器附件,例如"手槍"附件或"菜刀"附件。
附件attachment:slot插槽內當前渲染的附件實例,即真實上屏渲染的實物素材。
皮膚skin:skin可以看做是attachment的集合,或者可以認為是attachment的一個映射查詢表,一個人物可以由多套skin,通過切換skin的方式去查詢不同的附件映射表,便可以變相的實現人物的全身換裝。

其他相關概念
關鍵幀:在編輯器中,動畫是借助關鍵幀完成的,從開始到結束的過渡動畫,由spine補間處理。
權重與網格:權重用於將網格頂點綁定到一個或多個骨骼。變換骨骼時,頂點也會隨之變換。權重令網格能夠隨着操縱骨骼而自動變形,從而讓原本復雜的網格變形動畫變得與骨骼動畫一樣簡單。
區域附件:普通的圖片展示附件。
點附件:空間中的一個點和旋轉,相比骨頭的優勢可以為不同的皮膚設置更改位置和旋轉,例如不同的槍從不同的位置射擊。
網格附件:支持在圖片內設置多邊形,之后可操縱多邊形的頂點,以有效的方式讓圖片彎曲和變形。
邊界框附件:附加到骨骼上的多邊形,骨骼變化的時候也會隨之變形,可用於撞擊檢測,創建物理主體等。
剪裁附件:剪裁功能讓你可以定義一個多邊形區域,與邊界框附件類似,它會屏蔽繪制順序中的其他插槽。
路徑附件:用於設置路徑。
IK約束:反向動力學約束 子骨頭終點固定的場景。
變換約束:變換約束指的是將對骨骼的世界旋轉、移動縮放等復制到多個骨骼上。
路徑約束:使用路徑來調整骨骼變換,骨骼可以沿着路徑,也可以調整旋轉以指向路徑。

spine渲染流程

1、atlasAttachmentLoader實例負責atlas文件的解析,解析后與素材建立“關聯關系”
2、SkeletonData實例對骨骼數據、插槽數據做預處理
3、Skeleton實例渲染層上屏渲染的真實直接數據源,渲染層將讀取Skeleton實例上的插槽信息,渲染對應的附件
(updateWorldTransform觸發骨骼位置的計算、setToSetUpPose更新實例)

Lottie動畫介紹
Lottie介紹
Lottie一個復雜幀動畫的解決方案,提供了一套從設計師使用AE到各端開發者實現動畫的工具流。在設計師通過 AE 完成動畫后,使用 AE 插件 Bodymovin導出動畫數據,前端直接引用lottie-web庫,默認渲染方式是svg,原理就是用JS操作svg API。前端完全不需要關心動畫的過程,json文件里有每一幀動畫的信息,而庫會幫我們執行每一幀。

為什么使用Lottie
Lottie之前復雜動畫的實現方式
1、GIF:占用空間大,有些動畫顯示效果不佳,需要適配分辨率,還原度低
2、幀動畫:占用空間大,適配問題
3、組合式動畫:通過大量代碼實現復雜動畫
Lottie解決的問題
1、支持跨平台,開發成本較低,一套Lottie動畫可以在Android/IOS/Web多端使用
2、還原度高、兼容性好
3、占用空間小,多分辨率適配

Lottie數據結構

{
    "fr": 60, // 幀率
    "ip": 0,  // 起始關鍵幀
    "op": 30, // 結束關鍵幀
    "w": 1280,// 視圖寬
    "h": 720, // 視圖高
    "assets": [ ], // 資源集合 
    "layers": [{   // 圖層
        "ty": 0,   // 圖層類型。
        "refId": "comp_0", // 引用的資源,圖片/預合成層
        "ks": {},  // 變換 下面單獨介紹
        layer: [], // 該圖層包含的子圖層
        shapes: [],// 形狀圖層
        "w": 1334,
        "h": 750,
        "bm": 0  // 混合模式
    }], // 圖層集合
    "masker": [] // 蒙層集合
}

assets 資源集合
一個數組,資源信息包含的是矢量圖信息,如形狀,大小等等,也包含位圖;還可能是預合成層,即對已存在的某些圖層進行分組,把它們放置到新的合成中,作為新的一個資源對象,這兒layers的對象結構是上面一級屬性中的layers圖層集合是一樣的
layers 圖層信息
layers對象也是一個數組,數組中的每個元素對應一個圖層,圖層信息包括的圖層的位置,大小,形狀,起始關鍵幀,結束關鍵幀等,一個個圖層疊加起來構成最終的動畫效果


"ks": {
     "ddd": 0,  // 是否為3d
     "ind": 16, // layer唯一Id
     "ty": 2,   // 圖層類型
     "nm": "右手耶",// 圖層名稱
     "parent": 19, // 父圖層,使用index標識
     "refId": "image_14", // 引用的資源 圖片/預合成層
     "sr": 1,    // 時間拉伸
     "ao": 0,    // 沿路徑運動時是不是頭朝正
    "ip": 2.16, // 該圖層開始關鍵幀
    "op": 54,   // 該圖層結束關鍵幀
    "st": 0.72, // 開始時間
    "bm": 0,    //混合模式
    "ks": {
        "o": {}, // 透明度
        "r": {}, // 旋轉
        "p": {
            'a': 1,
            'k':[{
                't': 0, // 帶有t的元素, 即為幀動畫
                's': [300, 700, 0]
                },{
                't': 49, // 關鍵幀為49時 位置信息變為(300,1800,0)
                's': [300, 1800, 0]
                }
            ],
            'ix': 2
        }, // 位置
        "a": {}, // 錨點
        "s": {}  // 縮放            
 }

詳細資源結構請參考:https://www.processon.com/view/link/5c2ece6ae4b08a768398b06d

Lottie渲染流程

部分代碼解析
// loadAnimation的方法: 加載、解析json、播放動畫

function loadAnimation(params) {
    var animItem = new AnimationItem(); // 創建動畫對象
    setupAnimation(animItem, null);// 主要是添加一些時間監聽函數
    animItem.setParams(params);    // 根據輸入的參數和json數據,渲染成相應的動畫
    return animItem;
}

// 選擇渲染器

AnimationItem.prototype.setParams = function(params) {
    var animType = params.animType ? params.animType : params.renderer ? params.renderer : 'svg';
    switch(animType){
        case 'canvas':
            this.renderer = new CanvasRenderer(this, params.rendererSettings);
            break;
        case 'svg':
            this.renderer = new SVGRenderer(this, params.rendererSettings);
            break;
        default:
            this.renderer = new HybridRenderer(this, params.rendererSettings);
            break;
    }
    // 初始化一系列參數
    // assetLoader 加載數據
    this.preloadImages();// 預加載 圖片資源
    this.loadSegments(); 
    this.updaFrameModifier();
    this.waitForFontsLoaded();
}

// 圖層創建所有的元素

BaseRenderer.prototype.buildAllItems = function(){
    var i, len = this.layers.length;
    for(i=0;i<len;i+=1){
        this.buildItem(i);
    }
};
SVGRenderer.prototype.buildItem  = function(pos){
    var elements = this.elements;
    var element = this.createItem(this.layers[pos]);

    elements[pos] = element; // 將元素添加到svg中
    this.appendElementInPos(element,pos);
};

// 根據圖層類型,創建相應的svg元素類的實例

BaseRenderer.prototype.createItem = function(layer){
    switch(layer.ty){ 
        case 2:
            return this.createImage(layer);
        case 0:
            return this.createComp(layer);
        case 1:
            return this.createSolid(layer);
        case 3:
            return this.createNull(layer);
        case 4:
            return this.createShape(layer);
        case 5:
            return this.createText(layer);
        case 13:
            return this.createCamera(layer);
    }
    return this.createNull(layer);
};

Lottie常用API


lottie的主要方法
lottie.play()  // 播放動畫。
lottie.stop()  // 停止動畫。 動畫關閉
lottie.pause() // 暫停動畫。 動畫停止在暫停前一幀
lottie.setSpeed(speed) // 設置播放速度,參數 speed 為 Number ,1為正常速度。
lottie.goToAndStop(value, isFrame) // 跳到某一幀並暫停播放。第一個參數是 Number 。第二個參數是 Boolean,設置true則表明第一個參數代表的是幀數,false代表第一個參數為時間值(單位毫秒),默認 false。
lottie.goToAndPlay(value, isFrame) // 跳到某一幀並播放。
lottie.setDirection(direction) // 設置播放方向。1 為正着播,-1反着播。
lottie.playSegments(segments, forceFlag) // 播放某一片段。第一個參數為一維數組或多維數組,每個數組包含兩個值(開始幀,結束幀),第二個參數是一個 Boolean ,決定是否立即強制播放該片段。
lottie.destroy()     // 注銷動畫。
lottie.setQuality() // 播放質量,默認 high,改變貝塞爾平滑度從而影響幀之間的補間動畫片段數量。
lottie.loadAnimation({
  container: element, // 容器節點
  renderer: 'svg', // 渲染模式 默認svg
  loop: true,      // 是否循環播放
  autoplay: true,  // 是否自動播放
  assetsPath: ''   // 圖片資源路徑
  initialSegment: [12, 40] // 初始化動畫幀片段 (默認顯示片段首幀)
  animationData:'amim.json', // JSON數據,與path互斥
  path: 'data.json'        // JSON文件路徑
  rendererSettings: {
    context: canvasContext;  // 指定canvasContext
    clearCanvas: boolean;    // 是否先清除canvas畫布,canvas模式獨占,默認false。
    progressiveLoad: boolean;// 是否開啟漸進式加載,只有在需要的時候才加載dom元素,在有大量動畫的時候會提升初始化性能,但動畫顯示可能有一些延遲,svg模式獨占,默認為false。
    hideOnTransparent: boolean;// 當元素opacity為0時隱藏元素,svg模式獨占,默認為true。
    className: string;      // 容器追加class,默認為''
  }
})

lottie的事件監聽

complete      // 動畫播放結束時觸發(循環播放不會觸發)
loopComplete  // 進入下一個循環時觸發
enterFrame    // 每進入一幀觸發一次
segmentStart  // 進入片段播放時觸發
config_ready  // 初始化配置完成時觸發
data_ready    // 在所有的segments被加載完畢后觸發 image資源的加載前觸發
loaded_images // 所有圖片資源加載完畢的時候會觸發
DOMLoaded  // dom 元素加載完成時觸發,這個是比較可靠的可以替換data_ready的事
destroy   // 注銷動畫時觸發

Lottie性能優化
1、降幀
2、資源壓縮、資源緩存預加載
3、setSubframe 按照ae設置幀渲染
4、setQuality 縮減形變補間動畫貝塞爾平滑度
5、多段動畫資源合並

lottie動畫播放基本原理
1、先將動畫實例化為AnimationItem
2、requestAnimationFrame每次觸發時,調用advanceTime() -> setCurrentRawFrameValue() -> gotoFrame() 計算出要更新的屬性值
3、調用 renderer 的 renderFrame() 來更新界面
Lottie的setSubframe()解密

AnimationItem.prototype.setSubframe = function (flag) {
  this.isSubframeEnabled = !!flag;
};

AnimationItem.prototype.gotoFrame = function () {
  this.currentFrame = this.isSubframeEnabled ? this.currentRawFrame : ~~this.currentRawFrame;

  if (this.timeCompleted !== this.totalFrames && this.currentFrame > this.timeCompleted) {
    this.currentFrame = this.timeCompleted;
  }
  this.trigger('enterFrame');
  this.renderFrame();
};

currentFrame 是指要計算播放的當前幀數,關閉了 subFrame 時,會對其取整(~~),這樣就不會存在小數位的幀數,那么自然就按照原始的幀數(幀率*總時間)來播放

Lottie的setQuality()解密
setQuality實際上是控制補間動畫的數量,也可以理解為會影響貝塞爾平滑度

var buildBezierData = (function () {
    var storedData = {};
    return function (pt1, pt2, pt3, pt4) {
      var bezierName = (pt1[0] + '_' + pt1[1] + '_' + pt2[0] + '_' + pt2[1] + '_' + pt3[0] + '_' + pt3[1] + '_' + pt4[0] + '_' + pt4[1]).replace(/\./g, 'p');
      if (!storedData[bezierName]) {
        var curveSegments = defaultCurveSegments;
        var k;
        var i;
        var len;
        var ptCoord;
        var perc;
        var addedLength = 0;
        var ptDistance;
        var point;
        var lastPoint = null;
        if (pt1.length === 2 && (pt1[0] !== pt2[0] || pt1[1] !== pt2[1]) && pointOnLine2D(pt1[0], pt1[1], pt2[0], pt2[1], pt1[0] + pt3[0], pt1[1] + pt3[1]) && pointOnLine2D(pt1[0], pt1[1], pt2[0], pt2[1], pt2[0] + pt4[0], pt2[1] + pt4[1])) {
          curveSegments = 2;
        }
        var bezierData = new BezierData(curveSegments);
        len = pt3.length;
        for (k = 0; k < curveSegments; k += 1) {
          point = createSizedArray(len);
          perc = k / (curveSegments - 1);
          ptDistance = 0;
          for (i = 0; i < len; i += 1) {
            ptCoord = bmPow(1 - perc, 3) * pt1[i] + 3 * bmPow(1 - perc, 2) * perc * (pt1[i] + pt3[i]) + 3 * (1 - perc) * bmPow(perc, 2) * (pt2[i] + pt4[i]) + bmPow(perc, 3) * pt2[i];
            point[i] = ptCoord;
            if (lastPoint !== null) {
              ptDistance += bmPow(point[i] - lastPoint[i], 2);
            }
          }
          ptDistance = bmSqrt(ptDistance);
          addedLength += ptDistance;
          bezierData.points[k] = new PointData(ptDistance, point);
          lastPoint = point;
        }
        bezierData.segmentLength = addedLength;
        storedData[bezierName] = bezierData;
      }
      return storedData[bezierName];
    };
  }());


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM