Skinned Mesh原理解析和一個(gè)最簡單的實(shí)現(xiàn)示例_第1頁
Skinned Mesh原理解析和一個(gè)最簡單的實(shí)現(xiàn)示例_第2頁
Skinned Mesh原理解析和一個(gè)最簡單的實(shí)現(xiàn)示例_第3頁
Skinned Mesh原理解析和一個(gè)最簡單的實(shí)現(xiàn)示例_第4頁
Skinned Mesh原理解析和一個(gè)最簡單的實(shí)現(xiàn)示例_第5頁
已閱讀5頁,還剩18頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

1、Skinned Mesh原理解析和一個(gè)最簡單的實(shí)現(xiàn)示例作者:n5Email: happyfirecnBlog: 2008-10月Version:1.00 Date:2008-10-19講述骨骼動(dòng)畫的資料很多,但大部分都是針對(duì)DX8或DX9的SkinnedMesh進(jìn)行講解。我覺得對(duì)于骨骼動(dòng)畫初學(xué)者增加了不必要的負(fù)擔(dān),還沒有理解骨骼動(dòng)畫的實(shí)質(zhì)就已被DX復(fù)雜的架構(gòu)搞得暈頭轉(zhuǎn)向了。這篇文章把注意力集中在骨骼動(dòng)畫的基本組成結(jié)構(gòu)和原理上,并實(shí)現(xiàn)了一個(gè)最簡單純手工的自定義骨骼動(dòng)畫例子幫助理解(使用最簡單的OpenGL指令,甚至沒有使用矩陣)。這篇文章在我學(xué)習(xí)理解骨骼動(dòng)畫的過程中逐步完善,是對(duì)這個(gè)技術(shù)的理解總

2、結(jié),屬于學(xué)習(xí)筆記。學(xué)習(xí)過程中參考了很多資料,其中給我啟發(fā)最大的是Frank Luna寫的”Skinned Mesh Character Animation with Direct3D 9.0c”。由于本人自身也是初學(xué)者,所以錯(cuò)誤和不精確的地方在所難免,歡迎指正和討論,請(qǐng)發(fā)郵件到happyfirecn或加QQ群1769475。另外文本不涉及任何高級(jí)骨骼動(dòng)畫技術(shù),也不涉及DX架構(gòu)的SkinnedMesh技術(shù)和硬件加速,但本文中會(huì)引用SkinnedMesh中的約定俗成的名詞,如Transform Matrix,Bone Offset Matrix等。一)3D模型動(dòng)畫基本原理和分類3D模型動(dòng)畫的基本原

3、理是讓模型中各頂點(diǎn)的位置隨時(shí)間變化。主要種類有Morph動(dòng)畫,關(guān)節(jié)動(dòng)畫和骨骼蒙皮動(dòng)畫(Skinned Mesh)。從動(dòng)畫數(shù)據(jù)的角度來說,三者一般都采用關(guān)鍵幀技術(shù),即只給出關(guān)鍵幀的數(shù)據(jù),其他幀的數(shù)據(jù)使用插值得到。但由于這三種技術(shù)的不同,關(guān)鍵幀的數(shù)據(jù)是不一樣的。Morph(漸變,變形)動(dòng)畫是直接指定動(dòng)畫每一幀的頂點(diǎn)位置,其動(dòng)畫關(guān)鍵中存儲(chǔ)的是Mesh所有頂點(diǎn)在關(guān)鍵幀對(duì)應(yīng)時(shí)刻的位置。關(guān)節(jié)動(dòng)畫的模型不是一個(gè)整體的Mesh,而是分成很多部分(Mesh),通過一個(gè)父子層次結(jié)構(gòu)將這些分散的Mesh組織在一起,父Mesh帶動(dòng)其下子Mesh的運(yùn)動(dòng),各Mesh中的頂點(diǎn)坐標(biāo)定義在自己的坐標(biāo)系中,這樣各個(gè)Mesh是作為

4、一個(gè)整體參與運(yùn)動(dòng)的。動(dòng)畫幀中設(shè)置各子Mesh相對(duì)于其父Mesh的變換(主要是旋轉(zhuǎn),當(dāng)然也可包括移動(dòng)和縮放),通過子到父,一級(jí)級(jí)的變換累加(當(dāng)然從技術(shù)上,如果是矩陣操作是累乘)得到該Mesh在整個(gè)動(dòng)畫模型所在的坐標(biāo)空間中的變換(從本文的視角來說就是世界坐標(biāo)系了,下同),從而確定每個(gè)Mesh在世界坐標(biāo)系中的位置和方向,然后以Mesh為單位渲染即可。關(guān)節(jié)動(dòng)畫的問題是,各部分Mesh中的頂點(diǎn)是固定在其Mesh坐標(biāo)系中的,這樣在兩個(gè)Mesh結(jié)合處就可能產(chǎn)生裂縫。第三類就是骨骼蒙皮動(dòng)畫即Skinned Mesh了,骨骼蒙皮動(dòng)畫的出現(xiàn)解決了關(guān)節(jié)動(dòng)畫的裂縫問題,而且效果非常酷,發(fā)明這個(gè)算法的人一定是個(gè)天才,因

5、為Skinned Mesh的原理簡單的難以置信,而效果卻那么好。骨骼動(dòng)畫的基本原理可概括為:在骨骼控制下,通過頂點(diǎn)混合動(dòng)態(tài)計(jì)算蒙皮網(wǎng)格的頂點(diǎn),而骨骼的運(yùn)動(dòng)相對(duì)于其父骨骼,并由動(dòng)畫關(guān)鍵幀數(shù)據(jù)驅(qū)動(dòng)。一個(gè)骨骼動(dòng)畫通常包括骨骼層次結(jié)構(gòu)數(shù)據(jù),網(wǎng)格(Mesh)數(shù)據(jù),網(wǎng)格蒙皮數(shù)據(jù)(skin info)和骨骼的動(dòng)畫(關(guān)鍵幀)數(shù)據(jù)。下面將具體分析。二)Skinned Mesh原理和結(jié)構(gòu)分析Skinned Mesh中文一般稱作骨骼蒙皮動(dòng)畫,正如其名,這種動(dòng)畫中包含骨骼(Bone)和蒙皮(Skinned Mesh)兩個(gè)部分,Bone的層次結(jié)構(gòu)和關(guān)節(jié)動(dòng)畫類似,Mesh則和關(guān)節(jié)動(dòng)畫不同:關(guān)節(jié)動(dòng)畫中是使用多個(gè)分散的Mes

6、h,而Skinned Mesh中Mesh是一個(gè)整體,也就是說只有一個(gè)Mesh,實(shí)際上如果沒有骨骼讓Mesh運(yùn)動(dòng)變形,Mesh就和靜態(tài)模型一樣了。Skinned Mesh技術(shù)的精華在于蒙皮,所謂的皮并不是模型的貼圖(也許會(huì)有人這么想過吧),而是Mesh本身,蒙皮是指將Mesh中的頂點(diǎn)附著(綁定)在骨骼之上,而且每個(gè)頂點(diǎn)可以被多個(gè)骨骼所控制,這樣在關(guān)節(jié)處的頂點(diǎn)由于同時(shí)受到父子骨骼的拉扯而改變位置就消除了裂縫。Skinned Mesh這個(gè)詞從字面上理解似乎是有皮的模型,哦,如果貼圖是皮,那么普通靜態(tài)模型不也都有嗎?所以我覺得應(yīng)該理解為具有蒙皮信息的Mesh或可當(dāng)做皮膚用的Mesh,這個(gè)皮膚就是Mes

7、h。而為了有皮膚功能,Mesh還需要蒙皮信息,即Skin數(shù)據(jù),沒有Skin數(shù)據(jù)就是一個(gè)普通的靜態(tài)Mesh了。Skin數(shù)據(jù)決定頂點(diǎn)如何綁定到骨骼上。頂點(diǎn)的Skin數(shù)據(jù)包括頂點(diǎn)受哪些骨骼影響以及這些骨骼影響該頂點(diǎn)時(shí)的權(quán)重(weight),另外對(duì)于每塊骨骼還需要骨骼偏移矩陣(BoneOffsetMatrix)用來將頂點(diǎn)從Mesh空間變換到骨骼空間。在本文中,提到骨骼動(dòng)畫中的Mesh特指這個(gè)皮膚Mesh,提到模型是指骨骼動(dòng)畫模型整體。骨骼控制蒙皮運(yùn)動(dòng),而骨骼本身的運(yùn)動(dòng)呢?當(dāng)然是動(dòng)畫數(shù)據(jù)了。每個(gè)關(guān)鍵幀中包含時(shí)間和骨骼運(yùn)動(dòng)信息,運(yùn)動(dòng)信息可以用一個(gè)矩陣直接表示骨骼新的變換,也可用四元數(shù)表示骨骼的旋轉(zhuǎn),也可以

8、隨便自己定義什么只要能讓骨骼動(dòng)就行。除了使用編輯設(shè)定好的動(dòng)畫幀數(shù)據(jù),也可以使用物理計(jì)算對(duì)骨骼進(jìn)行實(shí)時(shí)控制。下面分別具體分析骨骼蒙皮動(dòng)畫中的結(jié)構(gòu)部件。1)理解骨骼和骨骼層次結(jié)構(gòu)(Bone Hierarchy)首先要明確一個(gè)觀念:骨骼決定了模型整體在世界坐標(biāo)系中的位置和朝向。先看看靜態(tài)模型吧,靜態(tài)模型沒有骨骼,我們在世界坐標(biāo)系中放置靜態(tài)模型時(shí),只要指定模型自身坐標(biāo)系在世界坐標(biāo)系中的位置和朝向。在骨骼動(dòng)畫中,不是把Mesh直接放到世界坐標(biāo)系中,Mesh只是作為Skin使用的,是依附于骨骼的,真正決定模型在世界坐標(biāo)系中的位置和朝向的是骨骼。在渲染靜態(tài)模型時(shí),由于模型的頂點(diǎn)都是定義在模型坐標(biāo)系中的,所以

9、各頂點(diǎn)只要經(jīng)過模型坐標(biāo)系到世界坐標(biāo)系的變換后就可進(jìn)行渲染。而對(duì)于骨骼動(dòng)畫,我們設(shè)置模型的位置和朝向,實(shí)際是在設(shè)置根骨骼的位置和朝向,然后根據(jù)骨骼層次結(jié)構(gòu)中父子骨骼之間的變換關(guān)系計(jì)算出各個(gè)骨骼的位置和朝向,然后根據(jù)骨骼對(duì)Mesh中頂點(diǎn)的綁定計(jì)算出頂點(diǎn)在世界坐標(biāo)系中的坐標(biāo),從而對(duì)頂點(diǎn)進(jìn)行渲染。要記住,在骨骼動(dòng)畫中,骨骼才是模型主體,Mesh不過是一層皮,一件衣服。如何理解骨骼?請(qǐng)看第二個(gè)觀念:骨骼可理解為一個(gè)坐標(biāo)空間。在一些文章中往往會(huì)提到關(guān)節(jié)和骨骼,那么關(guān)節(jié)是什么?骨骼又是什么?下圖是一個(gè)手臂的骨骼層次的示例。骨骼只是一個(gè)形象的說法,實(shí)際上骨骼可理解為一個(gè)坐標(biāo)空間,關(guān)節(jié)可理解為骨骼坐標(biāo)空間的原點(diǎn)

10、。關(guān)節(jié)的位置由它在父骨骼坐標(biāo)空間中的位置描述。上圖中有三塊骨骼,分別是上臂,前臂和兩個(gè)手指。Clavicle(鎖骨)是一個(gè)關(guān)節(jié),它是上臂的原點(diǎn),同樣肘關(guān)節(jié)(elbow joint)是前臂的原點(diǎn),腕關(guān)節(jié)(wrist)是手指骨骼的原點(diǎn)。關(guān)節(jié)既決定了骨骼空間的位置,又是骨骼空間的旋轉(zhuǎn)和縮放中心。為什么用一個(gè)4X4矩陣就可以表達(dá)一個(gè)骨骼,因?yàn)?X4矩陣中含有的平移分量決定了關(guān)節(jié)的位置,旋轉(zhuǎn)和縮放分量決定了骨骼空間的旋轉(zhuǎn)和縮放。我們來看前臂這個(gè)骨骼,其原點(diǎn)位置是位于上臂上某處的,對(duì)于上臂來說,它知道自己的坐標(biāo)空間某處(即肘關(guān)節(jié)所在的位置)有一個(gè)子空間,那就是前臂,至于前臂里面是啥就不考慮了。當(dāng)前臂繞肘關(guān)

11、節(jié)旋轉(zhuǎn)時(shí),實(shí)際是前臂坐標(biāo)空間在旋轉(zhuǎn),從而其中包含的子空間也在繞肘關(guān)節(jié)旋轉(zhuǎn),在這個(gè)例子中是finger骨骼。和實(shí)際生物骨骼不同的是,我們這里的骨骼并沒有實(shí)質(zhì)的骨頭,所以前臂旋轉(zhuǎn)時(shí),他自己沒啥可轉(zhuǎn)的,改變的只是坐標(biāo)空間的朝向。你可以說上圖的藍(lán)線在轉(zhuǎn),但實(shí)際藍(lán)線并不存在,藍(lán)線只是畫上去表示骨骼之間關(guān)系的,真正轉(zhuǎn)的是骨骼空間,我們能看到在轉(zhuǎn)的是wrist joint,也就是兩個(gè)finger骨骼的坐標(biāo)空間,因?yàn)樗麄兪亲涌臻g,會(huì)跟隨父空間運(yùn)動(dòng),就好比人跟著地球轉(zhuǎn)一樣。骨骼就是坐標(biāo)空間,骨骼層次就是嵌套的坐標(biāo)空間。關(guān)節(jié)只是描述骨骼的位置即骨骼自己的坐標(biāo)空間原點(diǎn)在其父空間中的位置,繞關(guān)節(jié)旋轉(zhuǎn)是指骨骼坐標(biāo)空間(

12、包括所有子空間)自身的旋轉(zhuǎn),如此理解足矣。但還有兩個(gè)可能的疑問,一是骨骼的長度問題,由于骨骼是坐標(biāo)空間,沒有所謂的長度和寬度的限制,我們看到的長度一方面是蒙皮后的結(jié)果,另一方面子骨骼的原點(diǎn)(也就是關(guān)節(jié))的位置往往決定了視覺上父骨骼的長度,比如這里upper arm線段的長度實(shí)際是由elbow joint的位置決定的。第二個(gè)問題,手指的那個(gè)端點(diǎn)是啥啊?實(shí)際上在我們的例子中手指沒有子骨骼,所以那個(gè)端點(diǎn)并不存在:)那是為了方便演示畫上去的。實(shí)際問題中總有最下層的骨骼,他們不能決定其他骨骼了,他們的作用只剩下控制Mesh頂點(diǎn)。對(duì)了,那么手指的長度如何確定?我們看到的長度應(yīng)該是由蒙皮決定的,也就是由Me

13、sh中屬于手指的那些點(diǎn)離腕關(guān)節(jié)的距離決定。經(jīng)過一段長篇大論,我們終于清楚骨骼和骨骼層次是啥了,但是為什么要將骨骼組織成層次結(jié)構(gòu)呢?答案是為了做動(dòng)畫方便,設(shè)想如果只有一塊骨骼,那么讓他動(dòng)起來就太簡單了,動(dòng)畫每一幀直接指定他的位置即可。如果是n塊呢?通過組成一個(gè)層次結(jié)構(gòu),就可以通過父骨骼控制子骨骼的運(yùn)動(dòng),牽一發(fā)而動(dòng)全身,改變某骨骼時(shí)并不需要設(shè)置其下子骨骼的位置,子骨骼的位置會(huì)通過計(jì)算自動(dòng)得到。上文已經(jīng)說過,父子骨骼之間的關(guān)系可以理解為,子骨骼位于父骨骼的坐標(biāo)系中。我們知道物體在坐標(biāo)系中可以做平移變換,以及自身的旋轉(zhuǎn)和縮放變換。子骨骼在父骨骼的坐標(biāo)系中也可以做這些變換來改變自己在其父骨骼坐標(biāo)系中的位

14、置和朝向等。那么如何表示呢?由于4X4矩陣可以同時(shí)表示上述三種變換,所以一般描述骨骼在其父骨骼坐標(biāo)系中的變換時(shí)使用一個(gè)矩陣,也就是DirectX SkinnedMesh中的FrameTransformMatrix。實(shí)際上這不是唯一的方法,但應(yīng)該是公認(rèn)的方法,因?yàn)榫仃嚥还饪梢酝瑫r(shí)表示多種變換還可以方便的通過連乘進(jìn)行變換的組合,這在層次結(jié)構(gòu)中非常方便。在本文的例子-最簡單的skinned mesh實(shí)例中,我只演示了平移變換,所以只用一個(gè)3d坐標(biāo)就可以表示子骨骼在父骨骼中的位置。下面是Bone Class最初的定義:class Bone public:float m_x, m_y, m_z;/這個(gè)坐

15、標(biāo)是定義在父骨骼坐標(biāo)系中的;OK,除了使用矩陣,坐標(biāo)或某東西描述子骨骼的位置,我們的Bone Class定義中還需要一些指針來建立層次結(jié)構(gòu),也就是說我們要能通過父骨骼找到子骨骼或反之。問題是我們需要什么指針呢?從父指向子還是反之?結(jié)論是看你需要怎么用了。在骨骼動(dòng)畫中,主要是父骨骼帶動(dòng)子骨骼,所以一般是指定父的位置,然后計(jì)算出子的位置,那么需要在Bone Class中加入子骨骼的指針,因?yàn)樽庸趋烙衝個(gè),所以需要n個(gè)指針嗎?不一定,看看DirectX的做法,只需要兩個(gè)就搞定了,指向第一子的和指向兄弟骨骼的。這樣事先就不需要知道有多少子了。下面是修改后的Bone Class:class Bone B

16、one* m_pSibling;Bone* m_pFirstChild;float m_x, m_y, m_z;/pos in its parent's spacefloat m_wx, m_wy, m_wz; /pos in world space;同時(shí)增加了一組坐標(biāo),存放計(jì)算好的世界坐標(biāo)系坐標(biāo)。將各個(gè)骨骼相對(duì)于其父骨骼擺放好,就行成了一個(gè)骨骼層次結(jié)構(gòu)的初始位置,所謂初始是指定義骨骼層次時(shí),那后來呢?后來動(dòng)畫改變了骨骼的相對(duì)位置,準(zhǔn)確的說一般是改變了骨骼自身的旋轉(zhuǎn)而位置保持不變(特殊情況總是存在,比如雷曼,可以把拳頭扔出去的那個(gè)家伙),總之骨骼動(dòng)了,位置變化了。初始位置很重要,因?yàn)橥?/p>

17、過初始位置骨骼層次間的變換,我們確定了骨骼之間的關(guān)系,然后在動(dòng)畫中你可以只用旋轉(zhuǎn)。假設(shè)我們通過某種方法建立了骨骼層次結(jié)構(gòu),那么每一塊骨骼的位置都依賴于其父骨骼的位置,而根骨骼沒有父,他的位置就是整個(gè)骨骼體系在世界坐標(biāo)系中的位置。可以認(rèn)為root的父就是世界坐標(biāo)系。但是初始位置時(shí),根骨骼一般不是在世界原點(diǎn)的,比如使用3d max character studio創(chuàng)建的biped骨架時(shí),一般兩腳之間是世界原點(diǎn),而根骨骼-骨盆位于原點(diǎn)上方(+z軸上)。這有什么關(guān)系呢?其實(shí)也沒什么大不了的,只是我們在指定骨骼動(dòng)畫模型整體坐標(biāo)時(shí),比如設(shè)定坐標(biāo)為(0,0,0),則根骨骼-骨盆被置于世界原點(diǎn),假如xy平面是

18、地面,那么人下半個(gè)身子到地面下了。我們想讓兩腳之間算作人的原點(diǎn),這樣設(shè)定(0,0,0)的坐標(biāo)時(shí)人就站在地面上了,所以可以在兩腳之間設(shè)定一個(gè)額外的根骨骼放在世界原點(diǎn)上,或者這個(gè)骨骼并不需要真實(shí)存在,只是在你的骨骼模型結(jié)構(gòu)中保存骨盆骨骼到世界原點(diǎn)的變換矩陣。在微軟X文件中,一般有一個(gè)Scene_Root節(jié)點(diǎn),這算一個(gè)額外的骨骼吧,他的變換矩陣為單位陣,表示他初始位于世界原點(diǎn),而真正骨骼的根Bip01,作為Scene_root的子骨骼,其變換矩陣表示相對(duì)于root的位置。說這么多其實(shí)我只是想解釋下,為什么要存在Scene_Root這種額外的骨骼,以及加深理解骨骼定位骨骼動(dòng)畫模型整體的世界坐標(biāo)的作用。

19、有了骨骼類,現(xiàn)在讓我們看一下建立骨骼層次的代碼,在bone class中增加一個(gè)構(gòu)造函數(shù)和兩個(gè)成員函數(shù):class Bonepublic: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

20、_pSibling = pSibling; m_pSibling->m_pFather = m_pFather; ;注意我增加了一個(gè)成員變量,Bone* m_pFather,這是指向父骨骼的指針,在這個(gè)例子中計(jì)算骨骼動(dòng)畫時(shí)本不需要這個(gè)指針,但我為了畫一條從父骨骼關(guān)節(jié)到子骨骼關(guān)節(jié)的連線,增加了它,因?yàn)槊總€(gè)骨骼只有第一子骨骼的指針,繪制父骨骼時(shí)從父到子畫線就只能畫一條,所以記錄每個(gè)骨骼的父,在繪制子骨骼時(shí)畫這根線。有了這個(gè)函數(shù),就可以創(chuàng)建骨骼層次了,例如:Bone* g_boneRoot;Bone* g_bone1, *g_bone21, *g_bone22;void buildBones(

21、)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);接下來是骨骼層次中最核心的部分,更新骨骼!由于動(dòng)畫的作用,某個(gè)骨骼的變換(TransformMatrix)變了,這時(shí)就要根據(jù)新的

22、變換來計(jì)算,所以這個(gè)過程一般稱作UpdateBoneMatrix。因?yàn)楣趋赖淖儞Q都是相對(duì)父的,要變換頂點(diǎn)必須使用世界變換矩陣,所以這個(gè)過程是根據(jù)更新了的某些骨骼的骨骼變換矩陣(TransformMatrix)計(jì)算出所有骨骼的世界變換矩陣(也即CombinedMatrix)。在本文的例子中,骨骼只能平移,甚至我們沒有用矩陣,所以當(dāng)有骨骼變動(dòng)時(shí)要做的只是直接計(jì)算骨骼的世界坐標(biāo),因此函數(shù)命名為ComputeWorldPos,相當(dāng)于UpdateBoneMatrix后再用頂點(diǎn)乘以CombinedMatrix。class Bone/give father's world pos, compute

23、the bone's world posvoid ComputeWorldPos(float fatherWX, float fatherWY, float fatherWZ)m_wx = fatherWX+m_x;m_wy = fatherWY+m_y;m_wz = fatherWZ+m_z;if(m_pSibling!=NULL)m_pSibling->ComputeWorldPos(fatherWX, fatherWY, fatherWZ);if(m_pFirstChild!=NULL)m_pFirstChild->ComputeWorldPos(m_wx, m_wy

24、, m_wz);其中的遞歸調(diào)用使用了微軟例子的思想。有了上述函數(shù),當(dāng)某骨骼運(yùn)動(dòng)時(shí)就可以讓其子骨骼跟隨運(yùn)動(dòng)了,但是怎么讓骨骼運(yùn)動(dòng)呢?這就是動(dòng)畫問題了。我不打算在這個(gè)簡單的例子中使用關(guān)鍵幀動(dòng)畫,而只是通過程序每幀改變某些骨骼的位置,DEMO中animateBones就是做這個(gè)的,你可以在里面改變不同的骨骼看看效果。在本文下面會(huì)對(duì)骨骼的關(guān)鍵幀動(dòng)畫做簡單的討論。至此,我們定義了骨骼類的結(jié)構(gòu),手工創(chuàng)建了骨骼層次(實(shí)際引擎應(yīng)該從文件讀入),并且可以根據(jù)新位置更新骨骼了(實(shí)際引擎應(yīng)該從動(dòng)畫數(shù)據(jù)讀入新的變換或使用物理計(jì)算),這樣假如我們用連線將骨骼畫出來,并且讓某個(gè)骨骼動(dòng)起來,我們就會(huì)看見他下面的子骨骼跟著動(dòng)

25、了。當(dāng)然只有骨骼是不夠的,我們要讓Mesh跟隨骨骼運(yùn)動(dòng),下面就是蒙皮了。2)蒙皮信息和蒙皮過程2-1)Skin info的定義上文曾討論過,Skinned Mesh中Mesh是作為皮膚使用,蒙在骨骼之上的。為了讓普通的Mesh具有蒙皮的功能,必須添加蒙皮信息,即Skin info。我們知道Mesh是由頂點(diǎn)構(gòu)成的,建模時(shí)頂點(diǎn)是定義在模型自身坐標(biāo)系的,即相對(duì)于Mesh原點(diǎn)的,而骨骼動(dòng)畫中決定模型頂點(diǎn)最終世界坐標(biāo)的是骨骼,所以要讓骨骼決定頂點(diǎn)的世界坐標(biāo),這就要將頂點(diǎn)和骨骼聯(lián)系起來,Skin info正是起了這個(gè)作用。下面是DEMO中頂點(diǎn)類的定義的代碼片段:#define MAX_BONE_PER_V

26、ERTEX 4class Vertexfloat m_x, m_y, m_z; /local pos in mesh spacefloat m_wX, m_wY, m_wZ;/blended vertex pos, in world space/skin infoint m_boneNum;Bone* m_bonesMAX_BONE_PER_VERTEX;float m_boneWeightsMAX_BONE_PER_VERTEX;頂點(diǎn)的Skin info包含影響該頂點(diǎn)的骨骼數(shù)目,指向這些骨骼的指針,這些骨骼作用于該頂點(diǎn)的權(quán)重(Skin weight)。由于只是一個(gè)簡單的例子,這兒沒有考慮優(yōu)化

27、,所以用靜態(tài)數(shù)組存放骨骼指針和權(quán)重,且實(shí)際引擎中Skin info的定義方式不一定是這樣的,但基本原理一致。MAX_BONE_PER_VERTEX在這兒用來設(shè)置可同時(shí)影響頂點(diǎn)的最大骨骼數(shù),實(shí)際上由于這個(gè)DEMO是手工進(jìn)行Vertex Blending并且也沒用硬件加速,可影響頂點(diǎn)的骨骼數(shù)量并沒有限制,只是恰好需要一個(gè)常量來定義數(shù)組,所以定義了一下。在實(shí)際引擎中由于要使用硬件加速,以及為了確保速度,一般會(huì)定義最大骨骼數(shù)。另外在本DEMO中,Skin info是手工設(shè)定的,而在實(shí)際項(xiàng)目中,一般是在建模軟件中生成這些信息并導(dǎo)出。Skin info的作用是使用各個(gè)骨骼的變換矩陣對(duì)頂點(diǎn)進(jìn)行變換并乘以權(quán)重

28、,這樣某塊骨骼只能對(duì)該頂點(diǎn)產(chǎn)生部分影響。各骨骼權(quán)重之和應(yīng)該為1。Skin info是針對(duì)頂點(diǎn)的,然而在使用Skin info前我們必須要使用Bone Offset Matrix對(duì)頂點(diǎn)進(jìn)行變換,下面具體討論Bone offset Matrix。(寫下這句話的時(shí)候我感覺有些不妥,因?yàn)閷?shí)際是先將所有的矩陣相乘最后再作用于頂點(diǎn),這兒是按照理論上的順序進(jìn)行講述吧,請(qǐng)不要與實(shí)際情況混淆,其實(shí)他們也并不矛盾。而且在我們的DEMO中由于沒有使用矩陣,所以變換的順序和理論順序是一致的)2-2)Bone Offset Matrix的含義和計(jì)算方法上文已經(jīng)說過:“骨骼動(dòng)畫中決定模型頂點(diǎn)最終世界坐標(biāo)的是骨骼,所以要讓

29、骨骼決定頂點(diǎn)的世界坐標(biāo)”,現(xiàn)在讓我們看下頂點(diǎn)受一塊骨骼的作用時(shí)的坐標(biāo)變換過程:mesh vertex (defined in mesh space)-<BoneOffsetMatrix>->Bone space-<BoneCombinedTransformMatrix>->World從這個(gè)過程中可看出,需要首先將模型頂點(diǎn)從模型空間變換到某塊骨骼自身的骨骼空間,然后才能利用骨骼的世界變換計(jì)算頂點(diǎn)的世界坐標(biāo)。Bone Offset Matrix的作用正是將模型從頂點(diǎn)空間變換到骨骼空間。那么Bone Offset Matrix如何得到呢?下面具體分析:Mesh s

30、pace是建模時(shí)使用的空間,mesh中頂點(diǎn)的位置相對(duì)于這個(gè)空間的原點(diǎn)定義。比如在3d max中建模時(shí)(視xy平面為地面,+z朝上),可將模型兩腳之間的中點(diǎn)作為Mesh空間的原點(diǎn),并將其放置在世界原點(diǎn),這樣左腳上某一頂點(diǎn)坐標(biāo)是(10,10,2),右腳上對(duì)稱的一點(diǎn)坐標(biāo)是(-10,10,2),頭頂上某一頂點(diǎn)的坐標(biāo)是(0,0,170)。由于此時(shí)Mesh空間和世界空間重合,上述坐標(biāo)既在Mesh空間也在世界空間,換句話說,此時(shí)實(shí)際是以世界空間作為Mesh空間了。在骨骼動(dòng)畫中,在世界中放置的是骨骼而不是Mesh,所以這個(gè)區(qū)別并不重要。在3d max中添加骨骼的時(shí)候,也是將骨骼放入世界空間中,并調(diào)整骨骼的相對(duì)

31、位置使得和mesh相吻合(即設(shè)置骨骼的TransformMatrix),得到骨架的初始姿勢以及相應(yīng)的Transform Matrix(按慣例模型做成兩臂側(cè)平舉直立,骨骼也要適合這個(gè)姿態(tài))。由于骨骼的Transform Matrix(作用是將頂點(diǎn)從骨骼空間變換到上層空間)是基于其父骨骼空間的,只有根骨骼的Transform是基于世界空間的,所以要通過自下而上一層層Transform變換(如果使用行向量右乘矩陣,這個(gè)Transform的累積過程就是C=Mbone*Mfather*Mgrandpar*.*Mroot),得到該骨骼在世界空間上的變換矩陣 - Combined Transform Mat

32、rix,即通過這個(gè)矩陣可將頂點(diǎn)從骨骼空間變換到世界空間。那么這個(gè)矩陣的逆矩陣就可以將世界空間中的頂點(diǎn)變換到某塊骨骼的骨骼空間。由于Mesh實(shí)際上就是定義在世界空間了,所以這個(gè)逆矩陣就是Offset Matrix。即OffsetMatrix就是骨骼在初始位置(沒有經(jīng)過任何動(dòng)畫改變)時(shí)將bone變換到世界空間的矩陣(CombinedTransformMatrix)的逆矩陣,有一些資料稱之為InverseMatrix。在幾何流水線中,是通過變換矩陣將頂點(diǎn)變換到上層空間,最終得到世界坐標(biāo),逆矩陣則做相反的事,所以Inverse這種提法也符合慣例。那么Offset這種提法從字面上怎么理解呢?Offset

33、即骨骼相對(duì)于世界原點(diǎn)的偏移,世界原點(diǎn)加上這個(gè)偏移就變成骨骼空間的原點(diǎn),同樣定義在世界空間中的點(diǎn)經(jīng)過這個(gè)偏移矩陣的作用也被變換到骨骼空間了。從另一角度理解,在動(dòng)畫中模型中頂點(diǎn)的位置是根據(jù)骨骼位置動(dòng)態(tài)計(jì)算的,也就是說頂點(diǎn)跟著骨骼動(dòng),但首先必須確定頂點(diǎn)和骨骼之間的相對(duì)位置(即頂點(diǎn)在該骨骼坐標(biāo)系中的位置),一個(gè)骨骼可能對(duì)應(yīng)很多頂點(diǎn),如果要保存這個(gè)相對(duì)位置每個(gè)頂點(diǎn)對(duì)于每塊受控制的骨骼都要保存,這樣就要保存太多的矩陣了。所以只保存mesh空間到骨骼空間的變換(即OffsetMatrix),然后通過這個(gè)變換計(jì)算每個(gè)頂點(diǎn)在該骨骼空間中的坐標(biāo),所以O(shè)ffsetMatrix也反應(yīng)了mesh和每塊骨骼的相對(duì)位置,只

34、是這個(gè)位置是間接的通過和世界坐標(biāo)空間的關(guān)系表達(dá)的,在初始位置將骨骼按照模型的形狀擺好是關(guān)鍵之處。以上的分析是通過將mesh space和world space重合得到Offset Matrix的計(jì)算方法。那么如果他們不重合呢?那就要先計(jì)算頂點(diǎn)從mesh space變換到world space的變換矩陣,并乘上(還是右乘為例)Combined Matrix的Inverse Matrix從而得到Offset Matrix。但是這不是找麻煩嗎?因?yàn)镸esh的原點(diǎn)在哪兒并不重要,為啥不讓他們重合呢?還有一個(gè)問題是,既然Offset Matrix可以計(jì)算出來,為啥還要在骨骼動(dòng)畫文件中同時(shí)提供Transf

35、ormMatrix和OffsetMatrix呢?實(shí)際上文件中確實(shí)可以不提供OffsetMatrix,而只在載入時(shí)計(jì)算。但TransformMatrix不可缺少,動(dòng)畫關(guān)鍵幀數(shù)據(jù)一般只存儲(chǔ)骨骼的旋轉(zhuǎn)和根骨骼的位置,骨骼間的相對(duì)位置還是要靠TransformMatrix提供。在微軟的X文件結(jié)構(gòu)中提供了OffsetMatrix,原因是什么呢?我不知道。我猜想一個(gè)可能的原因是為了兼容性和靈活性,比如mesh并沒有定義在世界坐標(biāo)系,而是作為一個(gè)object放置在3d max中,在導(dǎo)出骨骼動(dòng)畫時(shí)不能簡單的認(rèn)為mesh的頂點(diǎn)坐標(biāo)是相對(duì)于世界原點(diǎn)的,還要把這個(gè)object的位置考慮進(jìn)去,于是導(dǎo)出插件要計(jì)算出Of

36、fsetMatrix并保存在x文件中以避免兼容性問題。關(guān)于OffsetMatrix和TransformMatrix含有平移,旋轉(zhuǎn)和縮放的討論:首先,OffsetMatrix取決于骨骼的初始位置(即TransformMatrix),由于骨骼動(dòng)畫中我們使用的是動(dòng)畫中的位置,初始位置是什么樣并不重要,所以可以在初始位置中只包含平移,而旋轉(zhuǎn)和縮放在動(dòng)畫中設(shè)置(一般也僅僅使用旋轉(zhuǎn),這也是為啥動(dòng)畫通常中可以用一個(gè)四元數(shù)表示骨骼的關(guān)鍵幀)。在這種情況下,OffsetMatrix只包含平移即可。因此一些引擎的Bone中不存放Transform矩陣,而只存放骨骼在父骨骼空間中的坐標(biāo),然后旋轉(zhuǎn)只在動(dòng)畫幀中設(shè)置,最

37、基本的骨骼動(dòng)畫即可實(shí)現(xiàn)。但也可在Transform和Offset Matrix中包括旋轉(zhuǎn)和縮放,這樣可以提高創(chuàng)建動(dòng)畫時(shí)的容錯(cuò)性。在本文DEMO中,我們也沒有使用矩陣保存Bone Offset,而只用了一個(gè)坐標(biāo)保存偏移位置。class BoneOffsetpublic:float m_offx, m_offy, m_offz;在Bone class中,有一個(gè)方法用來計(jì)算Bone Offsetclass Bonepublic:BoneOffset m_boneOffset;/called after ComputeWorldPos() when bone loaded but not animat

38、edvoid ComputeBoneOffset()m_boneOffset.m_offx = -m_wx;m_boneOffset.m_offy = -m_wy;m_boneOffset.m_offz = -m_wz;if(m_pSibling!=NULL)m_pSibling->ComputeBoneOffset();if(m_pFirstChild!=NULL)m_pFirstChild->ComputeBoneOffset();在ComputeBoneOffset()中,使用計(jì)算好的骨骼的世界坐標(biāo)來計(jì)算bone offset,這兒的計(jì)算只是取一個(gè)負(fù)數(shù),在實(shí)際引擎中,如果bo

39、ne offset是一個(gè)矩陣,這兒就應(yīng)該是求逆矩陣,其實(shí)由于旋轉(zhuǎn)矩陣是正交的,只要求出旋轉(zhuǎn)矩陣的轉(zhuǎn)置矩陣,并將平移部分取反即可,本文不做討論了。注意由于我們計(jì)算Bone offset時(shí)是使用計(jì)算好的世界坐標(biāo),所以在這之前必須在初始位置時(shí)對(duì)根骨骼調(diào)用ComputeWorldPos()以計(jì)算出各個(gè)骨骼在初始位置時(shí)的世界坐標(biāo)。2-3)最終:頂點(diǎn)混合(vertex blending)現(xiàn)在我們有了Skin info,有了Bone offset,可謂萬事具備,只欠東風(fēng)了。現(xiàn)在就可以做頂點(diǎn)混合了,這是骨骼動(dòng)畫的精髓所在,正是這個(gè)技術(shù)消除了關(guān)節(jié)處的裂縫。頂點(diǎn)混合后得到了頂點(diǎn)新的世界坐標(biāo),對(duì)所有的頂點(diǎn)執(zhí)行ver

40、tex blending后,從Mesh的角度看,Mesh deform(變形)了,變成動(dòng)畫需要的形狀了。首先,讓我們看看使用單塊骨骼對(duì)頂點(diǎn)進(jìn)行作用的過程,以下是DEMO中的相關(guān)代碼:class Vertexpublic:void ComputeWorldPosByBone(Bone* pBone, float& outX, float& outY, float& outZ)/step1: transform vertex from mesh space to bone spaceoutX = m_x+pBone->m_boneOffset.m_offx;outY

41、= m_y+pBone->m_boneOffset.m_offy;outZ = m_z+pBone->m_boneOffset.m_offz;/step2: transform vertex from bone space to world sapceoutX += pBone->m_wx;outY += pBone->m_wy;outZ += pBone->m_wz;這個(gè)函數(shù)使用一塊骨骼對(duì)頂點(diǎn)進(jìn)行變換,將頂點(diǎn)從Mesh坐標(biāo)系變換到世界坐標(biāo)系,這兒使用了骨骼的Bone Offset Matrix和 Transform Matrix (嗯,我知道這兒沒用矩陣,但意思

42、是一樣的對(duì)嗎)對(duì)于多塊骨骼,對(duì)每塊骨骼執(zhí)行這個(gè)過程并將結(jié)果根據(jù)權(quán)重混合(即vertex blending)就得到頂點(diǎn)最終的世界坐標(biāo)。進(jìn)行vertex blending的代碼如下:class Vertexvoid BlendVertex()/do the vertex blending,get the vertex's pos in world spacem_wX = 0;m_wY = 0;m_wZ = 0;for(int i=0; i<m_boneNum; +i)float tx, ty, tz;ComputeWorldPosByBone(m_bonesi, tx, ty, tz

43、);tx*= m_boneWeightsi;ty*= m_boneWeightsi;tz*= m_boneWeightsi;m_wX += tx;m_wY += ty;m_wZ += tz;這些函數(shù)我都放在Vertex類中了,因?yàn)橹皇且粋€(gè)簡單DEMO所以沒有特別考慮引擎結(jié)構(gòu)問題,在BlendVertex()中,遍歷影響該頂點(diǎn)的所有骨骼,用每塊骨骼計(jì)算出頂點(diǎn)的世界坐標(biāo),然后使用Skin Weight對(duì)這些坐標(biāo)進(jìn)行加權(quán)平均。tx,ty,tz是某塊骨骼作用后頂點(diǎn)的世界坐標(biāo)乘以權(quán)重后的值,這些值相加后就是最終的世界坐標(biāo)了。現(xiàn)在讓我們用一個(gè)公式回顧一下Vertex blending的整個(gè)過程Vworld

44、 = Vmesh * BoneOffsetMatrix * CombindMatrix1 * Weight1 + Vmesh* BoneOffsetMatrix * CombinedMatrix2 * Weight2+ + Vmesh * BoneOffsetMatrix * CombindMatrixN * WeightN(這個(gè)公式使用的是行向量左乘矩陣)由于BoneOffsetMatrix和Combined Matrix都是矩陣,可以先相乘這樣就減少很多計(jì)算了,在實(shí)際PC游戲中可以使用VS進(jìn)行硬件加速計(jì)算。3)動(dòng)畫數(shù)據(jù)和播放動(dòng)畫正如前面所說,本例子中并沒有使用動(dòng)畫數(shù)據(jù),但動(dòng)畫數(shù)據(jù)在骨骼動(dòng)畫

45、中確實(shí)最重要的,因?yàn)槲覀兊淖罱K目的就是播放動(dòng)畫。所以作為DEMO的補(bǔ)充,這兒簡要討論一下動(dòng)畫數(shù)據(jù)相關(guān)問題。其實(shí)我覺得動(dòng)畫的處理在骨骼動(dòng)畫中是很靈活的,需要專門的一篇文章討論。本文的最開始說,3D模型動(dòng)畫的基本原理是讓模型中各頂點(diǎn)的位置隨時(shí)間變化。骨骼動(dòng)畫的情況是,骨骼的位置隨時(shí)間變化,頂點(diǎn)位置隨骨骼變化。所以動(dòng)畫數(shù)據(jù)中必然包含的是骨骼的運(yùn)動(dòng)信息。可以在動(dòng)畫幀中包含某時(shí)刻骨骼的Transform Matrix,但骨骼一般只是做旋轉(zhuǎn),所以也可以用一個(gè)四元數(shù)表示。但有時(shí)候骨骼層次整體會(huì)在動(dòng)畫中進(jìn)行平移,所以可能需要在動(dòng)畫幀中包含根骨骼的位置信息。播放動(dòng)畫時(shí),給出當(dāng)前播放的時(shí)間值,對(duì)于每塊需要?jiǎng)赢嫷墓?/p>

46、骼,根據(jù)這個(gè)值找出該骨骼前后兩個(gè)關(guān)鍵幀,根據(jù)時(shí)間差進(jìn)行插值,對(duì)于四元數(shù)要使用四元數(shù)球面線性插值。然后將插值得到的四元數(shù)轉(zhuǎn)換成Transform Matrix,再調(diào)用UpdateBoneMatrix(其含義上文已介紹)更新計(jì)算整個(gè)骨骼層次的CombinedMatrix。4)總結(jié)從結(jié)構(gòu)上看,SkinnedMesh包括:動(dòng)畫數(shù)據(jù),骨骼數(shù)據(jù),包含Skin info的Mesh數(shù)據(jù),以及Bone Offset Matrix。從過程上看,載入階段:載入并建立骨骼層次結(jié)構(gòu),計(jì)算或載入Bone Offset Matrix,載入Mesh數(shù)據(jù)和Skin info(如果是DX的Skinned Mesh這個(gè)過程更復(fù)雜,

47、因?yàn)檫€涉及到Matrix Palette等)。運(yùn)行階段:根據(jù)時(shí)間從動(dòng)畫數(shù)據(jù)中獲取骨骼當(dāng)前時(shí)刻的Transform Matrix,調(diào)用UpdateBoneMatrix計(jì)算出各骨骼的CombinedMatrix,對(duì)于每個(gè)頂點(diǎn)根據(jù)Skin info進(jìn)行Vertex Blending計(jì)算出頂點(diǎn)的世界坐標(biāo),最終進(jìn)行模型的渲染。三)關(guān)于本文的例子這個(gè)例子做了盡可能的簡化,只包含一個(gè)cpp文件,使用OpenGL和GLUT作為渲染器和框架,僅有400多行代碼。例子中手工創(chuàng)建了一個(gè)骨骼層次和Mesh,手工設(shè)置Skin info并自動(dòng)計(jì)算BoneOffset,使用程序控制骨骼平移演示了骨骼層次的運(yùn)動(dòng)和骨骼影響下M

48、esh頂點(diǎn)的運(yùn)動(dòng),例子中甚至沒有使用矩陣。本例子僅作理解骨骼動(dòng)畫之用。截圖中綠色網(wǎng)格是模型原始形狀,藍(lán)色是骨骼,紅色是動(dòng)畫時(shí)的模型形狀。DEMO中左數(shù)第二個(gè)骨骼做上下運(yùn)動(dòng),最下方的骨骼做x方向平移。DEMO沒有使用旋轉(zhuǎn),而實(shí)際的骨骼動(dòng)畫中往往是沒有平移只有旋轉(zhuǎn)的,因?yàn)楦觳仓荒苻D(zhuǎn)不能變長,但原理一致。代碼的執(zhí)行過程為,初始化時(shí):buildBones();/創(chuàng)建骨骼層次buildMesh(); /創(chuàng)建Mesh,設(shè)置Skin info,計(jì)算Bone offset每幀運(yùn)行時(shí):/draw original meshg_mesh->DrawStaticMesh(0,0,0);/move bonesa

49、nimateBones();/update all bone's pos in bone treeg_boneRoot->ComputeWorldPos(0, 0, 0);/update vertex pos by bones, using vertex blendingg_mesh->UpdateVertices();/draw deformed meshg_mesh->Draw();/draw boneg_boneRoot->Draw();為確保本文的完整性,下面貼出所有代碼。/A simplest Skinned Mesh demo, written by

50、 n5, 2008.10, /My email:happyfirecn/My blog: #include <GL/glut.h>#define NULL 0/-class BoneOffsetpublic:/BoneOffset transform a vertex from mesh space to bone space./In other words, it is the offset from mesh space to a bone's space./For each bone, there is a BoneOffest./If we add the offs

51、et to the vertex's pos (in mesh space), we get the vertex's pos in bone space/For example: if a vertex's pos in mesh space is (100,0,0), the bone offset is (-20,0,0), so the vertex's pos in bone space is (80,0,0)/Actually, BoneOffset is the invert transform of that we place a bone in

52、 mesh space, that is (-20,0,0) means the bone is at (20,0,0) in mesh spacefloat m_offx, m_offy, m_offz;/-class Bonepublic:Bone() 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)Bone() Bone* m_pSibling;Bone* m_pFirstChild;Bone* m_pFather; /only

53、 for draw bonevoid SetFirstChild(Bone* pChild) m_pFirstChild = pChild; m_pFirstChild->m_pFather = this; void SetSibling(Bone* pSibling) m_pSibling = pSibling; m_pSibling->m_pFather = m_pFather; float m_x, m_y, m_z;/pos in its parent's spacefloat m_wx, m_wy, m_wz; /pos in world space/give f

54、ather's world pos, compute the bone's world posvoid ComputeWorldPos(float fatherWX, float fatherWY, float fatherWZ)m_wx = fatherWX+m_x;m_wy = fatherWY+m_y;m_wz = fatherWZ+m_z;if(m_pSibling!=NULL)m_pSibling->ComputeWorldPos(fatherWX, fatherWY, fatherWZ);if(m_pFirstChild!=NULL)m_pFirstChild

55、->ComputeWorldPos(m_wx, m_wy, m_wz);BoneOffset m_boneOffset;/called after compute world pos when bone loaded but not animatedvoid ComputeBoneOffset()m_boneOffset.m_offx = -m_wx;m_boneOffset.m_offy = -m_wy;m_boneOffset.m_offz = -m_wz;if(m_pSibling!=NULL)m_pSibling->ComputeBoneOffset();if(m_pFirstChild!=NULL)m_pFirstChild->ComputeBone

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

最新文檔

評(píng)論

0/150

提交評(píng)論