Skinned Mesh原理解析和一個最簡單的實現示例
Skinned Mesh原理解析和一個最簡單的實現示例
2018年1月:本文以及demo程式已儲存到github上 :
作者:n5
2008-10月
Histroy:
Version:1.02 Date:2010-1-1
修改了一些錯誤(用刪除線標出),補充了一些材料,
ps:本文以後只維護CSDN blog上的版本,請轉載者保留原始連結:http://blog.csdn.net/n5/archive/2008/10/19/3105872.aspx
Version:1.01Date:2008-11-01
修改了一些不精確的用語
Version:1.00 Date:2008-10-19
講述骨骼動畫的資料很多,但大部分都是針對DX8或DX9的請發郵件到[email protected]
。另外文字不涉及任何高階骨骼動畫技術,也不涉及DX架構的SkinnedMesh技術和硬體加速,但本文中會引用SkinnedMesh中的約定俗成的名詞,如Transform Matrix,Bone Offset Matrix等。
一)3D模型動畫基本原理和分類
3D模型動畫的基本原理是讓模型中各頂點的位置隨時間變化。主要種類有Morph動畫,關節動畫和骨骼蒙皮動畫(Skinned Mesh)。從動畫資料的角度來說,三者一般都採用關鍵幀技術,即只給出關鍵幀的資料,其他幀的資料使用插值得到。但由於這三種技術的不同,關鍵幀的資料是不一樣的。
Morph(漸變,變形)動畫是直接指定動畫每一幀的頂點位置,其動畫關鍵中儲存的是
關節動畫的模型不是一個整體的Mesh,而是分成很多部分(Mesh),通過一個父子層次結構將這些分散的Mesh組織在一起,父Mesh帶動其下子Mesh的運動,各Mesh中的頂點座標定義在自己的座標系中,這樣各個Mesh是作為一個整體參與運動的。動畫幀中設定各子Mesh相對於其父Mesh的變換(主要是旋轉,當然也可包括移動和縮放),通過子到父,一級級的變換累加(當然從技術上,如果是矩陣操作是累乘)得到該Mesh在整個動畫模型所在的座標空間中的變換(從本文的視角來說就是世界座標系了,下同),從而確定每個Mesh在世界座標系中的位置和方向,然後以Mesh為單位渲染即可。關節動畫的問題是,各部分Mesh中的頂點是固定在其Mesh座標系中的,這樣在兩個Mesh結合處就可能產生裂縫。
第三類就是骨骼蒙皮動畫即Skinned Mesh了,骨骼蒙皮動畫的出現解決了關節動畫的裂縫問題,而且效果非常酷,發明這個演算法的人一定是個天才,因為Skinned Mesh的原理簡單的難以置信,而效果卻那麼好。骨骼動畫的基本原理可概括為:在骨骼控制下,通過頂點混合動態計算蒙皮網格的頂點,而骨骼的運動相對於其父骨骼,並由動畫關鍵幀資料驅動。一個骨骼動畫通常包括骨骼層次結構資料,網格(Mesh)資料,網格蒙皮資料(skin info)和骨骼的動畫(關鍵幀)資料。下面將具體分析。
二)Skinned Mesh原理和結構分析
Skinned Mesh中文一般稱作骨骼蒙皮動畫,正如其名,這種動畫中包含骨骼(Bone)和蒙皮(Skinned Mesh)兩個部分,Bone的層次結構和關節動畫類似,Mesh則和關節動畫不同:關節動畫中是使用多個分散的Mesh,而Skinned Mesh中Mesh是一個整體,也就是說只有一個Mesh,實際上如果沒有骨骼讓Mesh運動變形,Mesh就和靜態模型一樣了。Skinned Mesh技術的精華在於蒙皮,所謂的皮並不是模型的貼圖(也許會有人這麼想過吧),而是Mesh本身,蒙皮是指將Mesh中的頂點附著(繫結)在骨骼之上,而且每個頂點可以被多個骨骼所控制,這樣在關節處的頂點由於同時受到父子骨骼的拉扯而改變位置就消除了裂縫。Skinned Mesh這個詞從字面上理解似乎是有皮的模型,哦,如果貼圖是皮,那麼普通靜態模型不也都有嗎?所以我覺得應該理解為具有蒙皮資訊的Mesh或可當做面板用的Mesh,這個面板就是Mesh。而為了有面板功能,Mesh還需要蒙皮資訊,即Skin資料,沒有Skin資料就是一個普通的靜態Mesh了。Skin資料決定頂點如何繫結到骨骼上。頂點的Skin資料包括頂點受哪些骨骼影響以及這些骨骼影響該頂點時的權重(weight),另外對於每塊骨骼還需要骨骼偏移矩陣(BoneOffsetMatrix)用來將頂點從Mesh空間變換到骨骼空間。在本文中,提到骨骼動畫中的Mesh特指這個面板Mesh,提到模型是指骨骼動畫模型整體。骨骼控制蒙皮運動,而骨骼本身的運動呢?當然是動畫資料了。每個關鍵幀中包含時間和骨骼運動資訊,運動資訊可以用一個矩陣直接表示骨骼新的變換,也可用四元數表示骨骼的旋轉,也可以隨便自己定義什麼只要能讓骨骼動就行。除了使用編輯設定好的動畫幀資料,也可以使用物理計算對骨骼進行實時控制。
下面分別具體分析骨骼蒙皮動畫中的結構部件。
1)理解骨骼和骨骼層次結構(Bone Hierarchy)
首先要明確一個觀念:骨骼決定了模型整體在世界座標系中的位置和朝向。
先看看靜態模型吧,靜態模型沒有骨骼,我們在世界座標系中放置靜態模型時,只要指定模型自身座標系在世界座標系中的位置和朝向。在骨骼動畫中,不是把Mesh直接放到世界座標系中,Mesh只是作為Skin使用的,是依附於骨骼的,真正決定模型在世界座標系中的位置和朝向的是骨骼。在渲染靜態模型時,由於模型的頂點都是定義在模型座標系中的,所以各頂點只要經過模型座標系到世界座標系的變換後就可進行渲染。而對於骨骼動畫,我們設定模型的位置和朝向,實際是在設定根骨骼的位置和朝向,然後根據骨骼層次結構中父子骨骼之間的變換關係計算出各個骨骼的位置和朝向,然後根據骨骼對Mesh中頂點的繫結計算出頂點在世界座標系中的座標,從而對頂點進行渲染。要記住,在骨骼動畫中,骨骼才是模型主體,Mesh不過是一層皮,一件衣服。
如何理解骨骼?請看第二個觀念:骨骼可理解為一個座標空間。
在一些文章中往往會提到關節和骨骼,那麼關節是什麼?骨骼又是什麼?下圖是一個手臂的骨骼層次的示例。
骨骼只是一個形象的說法,實際上骨骼可理解為一個座標空間,關節可理解為骨骼座標空間的原點。關節的位置由它在父骨骼座標空間中的位置描述。上圖中有三塊骨骼,分別是上臂,前臂和兩個手指。Clavicle(鎖骨)是一個關節,它是上臂的原點,同樣肘關節(elbow joint)是前臂的原點,腕關節(wrist)是手指骨骼的原點。關節既決定了骨骼空間的位置,又是骨骼空間的旋轉和縮放中心。為什麼用一個4X4矩陣就可以表達一個骨骼,因為4X4矩陣中含有的平移分量決定了關節的位置,旋轉和縮放分量決定了骨骼空間的旋轉和縮放。我們來看前臂這個骨骼,其原點位置是位於上臂上某處的,對於上臂來說,它知道自己的座標空間某處(即肘關節所在的位置)有一個子空間,那就是前臂,至於前臂裡面是啥就不考慮了。當前臂繞肘關節旋轉時,實際是前臂座標空間在旋轉,從而其中包含的子空間也在繞肘關節旋轉,在這個例子中是finger骨骼。和實際生物骨骼不同的是,我們這裡的骨骼並沒有實質的骨頭,所以前臂旋轉時,他自己沒啥可轉的,改變的只是座標空間的朝向。你可以說上圖的藍線在轉,但實際藍線並不存在,藍線只是畫上去表示骨骼之間關係的,真正轉的是骨骼空間,我們能看到在轉的是wrist joint,也就是兩個finger骨骼的座標空間,因為他們是子空間,會跟隨父空間運動,就好比人跟著地球轉一樣。
骨骼就是座標空間,骨骼層次就是巢狀的座標空間。關節只是描述骨骼的位置即骨骼自己的座標空間原點在其父空間中的位置,繞關節旋轉是指骨骼座標空間(包括所有子空間)自身的旋轉,如此理解足矣。但還有兩個可能的疑問,一是骨骼的長度問題,由於骨骼是座標空間,沒有所謂的長度和寬度的限制,我們看到的長度一方面是蒙皮後的結果,另一方面子骨骼的原點(也就是關節)的位置往往決定了視覺上父骨骼的長度,比如這裡upper arm線段的長度實際是由elbow joint的位置決定的。第二個問題,手指的那個端點是啥啊?實際上在我們的例子中手指沒有子骨骼,所以那個端點並不存在:)那是為了方便演示畫上去的。實際問題中總有最下層的骨骼,他們不能決定其他骨骼了,他們的作用只剩下控制Mesh頂點。對了,那麼手指的長度如何確定?我們看到的長度應該是由手指部分的頂點和蒙皮決定的,也就是由Mesh中屬於手指的那些點離腕關節的距離決定。
經過一段長篇大論,我們終於清楚骨骼和骨骼層次是啥了,但是為什麼要將骨骼組織成層次結構呢?答案是為了做動畫方便,設想如果只有一塊骨骼,那麼讓他動起來就太簡單了,動畫每一幀直接指定他的位置即可。如果是n塊呢?通過組成一個層次結構,就可以通過父骨骼控制子骨骼的運動,牽一髮而動全身,改變某骨骼時並不需要設定其下子骨骼的位置,子骨骼的位置會通過計算自動得到。上文已經說過,父子骨骼之間的關係可以理解為,子骨骼位於父骨骼的座標系中。我們知道物體在座標系中可以做平移變換,以及自身的旋轉和縮放變換。子骨骼在父骨骼的座標系中也可以做這些變換來改變自己在其父骨骼座標系中的位置和朝向等。那麼如何表示呢?由於4X4矩陣可以同時表示上述三種變換,所以一般描述骨骼在其父骨骼座標系中的變換時使用一個矩陣,也就是DirectX SkinnedMesh中的FrameTransformMatrix。實際上這不是唯一的方法,但應該是公認的方法,因為矩陣不光可以同時表示多種變換還可以方便的通過連乘進行變換的組合,這在層次結構中非常方便。在本文的例子-最簡單的skinned mesh例項中,我只演示了平移變換,所以只用一個3d座標就可以表示子骨骼在父骨骼中的位置。下面是Bone Class最初的定義:
class Bone
{
public:
float m_x, m_y, m_z;//這個座標是定義在父骨骼座標系中的
};
OK,除了使用矩陣,座標或某東西描述子骨骼的位置,我們的Bone Class定義中還需要一些指標來建立層次結構,也就是說我們要能通過父骨骼找到子骨骼或反之。問題是我們需要什麼指標呢?從父指向子還是反之?結論是看你需要怎麼用了。如果使用矩陣,需要將父子骨骼矩陣級聯相乘,無論你的矩陣是左乘列向量還是右乘行向量,從哪邊開始乘不重要,只要乘法中父子矩陣的左右位置正確,所以可以在骨骼中只存放指向父的指標,從子到父每次得到父矩陣迴圈相乘。也可以像DX中那樣從根開始相乘並遞迴。在文字的DEMO中由於沒用矩陣,直接使用座標相加計算座標,所以要指定父的位置,然後計算出子的位置,那麼需要在Bone Class中加入子骨骼的指標,因為子骨骼有n個,所以需要n個指標嗎?不一定,看看DirectX的做法,只需要兩個就搞定了,指向第一子的和指向兄弟骨骼的。這樣事先就不需要知道有多少子了。下面是修改後的Bone Class:
class Bone
{
Bone* m_pSibling;
Bone* m_pFirstChild;
float m_x, m_y, m_z;//pos in its parent's space
float m_wx, m_wy, m_wz; //pos in world space
};
同時增加了一組座標,存放計算好的世界座標系座標。
將各個骨骼相對於其父骨骼擺放好,就行成了一個骨骼層次結構的初始位置,所謂初始是指定義骨骼層次時,那後來呢?後來動畫改變了骨骼的相對位置,準確的說一般是改變了骨骼自身的旋轉而位置保持不變(特殊情況總是存在,比如雷曼,可以把拳頭扔出去的那個傢伙),總之骨骼動了,位置變化了。初始位置很重要,因為通過初始位置骨骼層次間的變換,我們確定了骨骼之間的關係,然後在動畫中你可以只用旋轉。
假設我們通過某種方法建立了骨骼層次結構,那麼每一塊骨骼的位置都依賴於其父骨骼的位置,而根骨骼沒有父,他的位置就是整個骨骼體系在世界座標系中的位置。可以認為root的父就是世界座標系。但是初始位置時,根骨骼一般不是在世界原點的,比如使用3d max character studio建立的biped骨架時,一般兩腳之間是世界原點,而根骨骼-骨盆位於原點上方(+z軸上)。這有什麼關係呢?其實也沒什麼大不了的,只是我們在指定骨骼動畫模型整體座標時,比如設定座標為(0,0,0),則根骨骼-骨盆被置於世界原點,假如xy平面是地面,那麼人下半個身子到地面下了。我們想讓兩腳之間算作人的原點,這樣設定(0,0,0)的座標時人就站在地面上了,所以可以在兩腳之間設定一個額外的根骨骼放在世界原點上,或者這個骨骼並不需要真實存在,只是在你的骨骼模型結構中儲存骨盆骨骼到世界原點的變換矩陣。在微軟X檔案中,一般有一個Scene_Root節點,這算一個額外的骨骼吧,他的變換矩陣為單位陣,表示他初始位於世界原點,而真正骨骼的根Bip01,作為Scene_root的子骨骼,其變換矩陣表示相對於root的位置。說這麼多其實我只是想解釋下,為什麼要存在Scene_Root這種額外的骨骼,以及加深理解骨骼定位骨骼動畫模型整體的世界座標的作用。
有了骨骼類,現在讓我們看一下建立骨骼層次的程式碼,在bone class中增加一個建構函式和兩個成員函式:
class Bone
{
public:
Bone(float x, float y, float z)
:m_pSibling(NULL),m_pFirstChild(NULL),m_pFather(NULL),
m_x(x),m_y(y),m_z(z){}
void SetFirstChild(Bone* pChild)
{
m_pFirstChild = pChild; m_pFirstChild->m_pFather = this;
}
void SetSibling(Bone* pSibling)
{
m_pSibling = pSibling; m_pSibling->m_pFather = m_pFather;
}
};
注意我增加了一個成員變數,Bone* m_pFather,這是指向父骨骼的指標,在這個例子中計算骨骼動畫時本不需要這個指標,但我為了畫一條從父骨骼關節到子骨骼關節的連線,增加了它,因為每個骨骼只有第一子骨骼的指標,繪製父骨骼時從父到子畫線就只能畫一條,所以記錄每個骨骼的父,在繪製子骨骼時畫這根線。
有了這個函式,就可以建立骨骼層次了,例如:
Bone* g_boneRoot;
Bone* g_bone1, *g_bone21, *g_bone22;
void buildBones()
{
g_boneRoot = new Bone(0, 0, 0);
g_bone1 = new Bone(0.1, 0, 0);
g_bone21 = new Bone(0.0, 0.1, 0);
g_bone22 = new Bone(0.1, 0.0, 0);
g_boneRoot->SetFirstChild(g_bone1);
g_bone1->SetFirstChild(g_bone21);
g_bone21->SetSibling(g_bone22);
}
接下來是骨骼層次中最核心的部分,更新骨骼!由於動畫的作用,某個骨骼的變換(TransformMatrix)變了,這時就要根據新的變換來計算,所以這個過程一般稱作UpdateBoneMatrix。因為骨骼的變換都是相對父的,要變換頂點必須使用世界變換矩陣,所以這個過程是根據更新了的某些骨骼的骨骼變換矩陣(TransformMatrix)計算出所有骨骼的世界變換矩陣(也即CombinedMatrix)。在本文的例子中,骨骼只能平移,甚至我們沒有用矩陣,所以當有骨骼變動時要做的只是直接計算骨骼的世界座標,因此函式命名為ComputeWorldPos,相當於UpdateBoneMatrix後再用頂點乘以CombinedMatrix。
class Bone
{
//give father's world pos, compute the bone's world pos
void ComputeWorldPos(float fatherWX, float fatherWY, float fatherWZ)
相關推薦
Skinned Mesh原理解析和一個最簡單的實現示例
Skinned Mesh原理解析和一個最簡單的實現示例2018年1月:本文以及demo程式已儲存到github上 :作者:n52008-10月Histroy:Version:1.02 Date:2010
Skinned Mesh 原理解析和一個最簡單的實現示例
Mesh space 是建模時使用的空間, mesh 中頂點的位置相對於這個空間的原點定義。比如在 3d max 中建模時(視 xy 平面為地面, +z 朝上),可將模型兩腳之間的中點作為 Mesh 空間的原點,並將其放置在世界原點,這樣左腳上某一頂點座標是( 10 , 10 , 2
Skinned Mesh原理解析和一個最簡單的實現示例 .
作者:n5 2008-10 月 Histroy: Version:1.02 Date:2010-1-1 修改了一些錯誤(用刪除線標出 ),補充了一些材料, ps:本文以後只維護CSDN blog上的版本,請轉載者保留原始連結:h
linux下qt的安裝和一個最簡單的小程式hello world
唉。。十一買車票真的有點鬱悶啊!網上買票還得排隊,去哪說理去!我是邊買車票,變自學QT,第一次用QT 環境,有點不太熟悉,配置配置linux下的QT開發環境,自己研究了一天,終於讓我弄好啦,老天不負有
TensorFlow筆記(3)——利用TensorFlow和MNIST資料集訓練一個最簡單的手寫數字識別模型
前言 當我們開始學習程式設計的時候,第一件事往往是學習列印"Hello World"。就好比程式設計入門有Hello World,機器學習入門有MNIST。 MNIST是一個入門級的計算機視覺資料集,它包含各種手寫數字圖片: 它也包含每一張圖片對應的標籤,告訴我們這個是數字幾。比如,上
python只使用Queue和Thread自己實現一個最簡單的執行緒池
我的思路就是就是寫一個TifCutting類繼承自Thread,這個類裡有個屬性Queue;有一個addTask新增任務的方法,這個方法是把需要執行的函式放到Queue裡;因為繼承自Thread類,一定有一個重寫的run方法,這個方法是從自己的Queue屬性裡
idea外掛開發(01)---最簡單的helloworld版,不需要知道原理,先跟我做一個最簡單的彈框外掛
前言 用了那麼多idea外掛,也想自己做一個外掛,下面就是入門版本 你不需要先知道所有的概念,先跟著我的步驟做一個小;例子,後面再說原理 本次以windos系統為例 開始 1.你得安裝一個環境,供idea外掛的開發 2.開啟idea,新建一個idea外掛開發的
程式基本演算法習題解析 任意給一個四位數(各位數不完全相同), 各位上的數可組成一個最大數和一個最小數, 它們的差又能組成一個最大數和一個最小數, 直到某一步得到的差將會出現迴圈重複。
這是《程式基本演算法習題解析》中的一道練習題。 題目: 任意給一個四位數(各位數不完全相同), 各位上的數可組成一個最大數和一個最小數, 它們的差又能組成一個最大數和一個最小數, 直到某一步得到的差將會出現迴圈重複。寫一個程式統計所有滿足
TensorFlow筆記(3)——利用TensorFlow和MNIST資料集訓練一個最簡單的手寫數字識別模型...
前言 當我們開始學習程式設計的時候,第一件事往往是學習列印"Hello World"。就好比程式設計入門有Hello World,機器學習入門有MNIST。 MNIST是一個入門級的計算機視覺資料集,它包含各種手寫數字圖片:
用java去解析一個最簡單的XML檔案
此處僅僅是去解析最基本的XML檔案,XML檔案如下: <Books> <Book> <Name>Java入門</Name> <Price>30.00</Price>
Dubbo入門-分散式原理詳解--搭建一個最簡單的Demo框架
Dubbo背景和簡介 Dubbo開始於電商系統,因此在這裡先從電商系統的演變講起。 1,單一應用框架(ORM) 當網站流量很小時,只需一個應用,將所有功能如下單支付等都部署在一起,以減少部署節點和成本。 缺點:單一的系統架構,使得在開發過程中,佔用的資源越來越多,而且隨著流量的增加越來越難以維護 2.
寫一個最簡單的gulp 實例
今天 blog png ruby 官網 base 1.0 pat fault 今天寫了一個簡單的gulp 實例 分享給大家! 比較適合gulp 初學者 首選: 看看gulp官網了解一些基本的定義 官網地址 : http://www.gulpjs.com.cn/ 搭建n
javaWeb之寫一個最簡單的servlet
tran oid w3c write 分享 瀏覽器 servle code mapping 1. 創建一個類servletTest2 繼承HttpServlet類。 public class servletTest2 extends HttpServlet {
Go語言建立一個最簡單的服務端點
一個 nds Coding port struct pac quest com handler handlers/handlers.go package handlers import ( "encoding/json" "net/http" )
一個最簡單的cell按鈕點擊回調
eight property sin font 簡單的 cell 舉例 定義 ont 在cell.h定義 @property(nonatomic,strong)void(^pushType)(NSInteger); 在cell.m按鈕點擊時 _pushType(1);(舉
搭建一個最簡單的node服務器
node string str console 參數 地址 param color json 搭建一個最簡單的node服務器 1、創建一個Http服務並監聽8888端口 2、使用url模塊 獲取請求的路由和請求參數 var http = require(‘
2018.3.29學習總結之如何運行一個最簡單的Servlet程序
ati get png aid 父類 eclips 網上 自己 nco 1,我編寫了我的第一個Servlet程序。HelloServlet 繼承自HttpServlet。因此需要導入javax.servlet開頭的一系列包,那麽這些包來自哪裏呢?答案是Tomcat安裝目錄下
PHP分頁初探 一個最簡單的PHP分頁代碼的簡單實現
too 查詢 use img 多少 contain 網站 實現 ice PHP分頁代碼在各種程序開發中都是必須要用到的,在網站開發中更是必選的一項。 要想寫出分頁代碼,首先你要理解SQL查詢語句:select * from goods limit 2,7。PHP分頁代碼核心
300行ABAP代碼實現一個最簡單的區塊鏈原型
指向 repo 方法調用 輸入參數 transacti ui控件 挖礦 太多的 work 不知從什麽時候起,區塊鏈在網上一下子就火了。 這裏Jerry就不班門弄斧了,網上有太多的區塊鏈介紹文章。我的這篇文章沒有任何高大上的術語,就是300行ABAP代碼,實現一個最簡單的區
vue2 + iview-admin 1.3 + django 2.0 一個最簡單的增刪改查例子
iview-admin axios django 前後端分離 api 以下為利用iview-admin + django 做的一個最基本的增刪改查例子。 前端iview-admin git clone https://github.com/iview/iview-admin.git cd