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];
};
}());
