前面咱們分析了靜態模型OBJ格式,楨動畫模型MD2,這篇主要分析骨骼動畫MD5的一些概念而且實現。ide
混合楨動畫有計算簡單,容易實現等優勢,可是在須要比較細緻的效果時,則須要更多的關鍵楨,每楨都添加相同的頂點,若是模型再細分一些,則比較恐怖了。在這基礎上,則發展出了骨骼動畫模型,原理提及來很簡單,好比咱們人類,作的各類動做具體都是由幾個關節點來控制,好比你擡腿,你只把你大腿的骨骼調動起來,而大腿的肌肉跟着骨骼向上。由些咱們只須要保存每楨的骨骼變更,而後再上面蒙上表皮。所以大量簡單了頂點存儲,而且,咱們能方便的對骨骼實時改動就能添加不一樣的動畫,可是由於骨骼的改變都是針對父骨骼來的,而蒙皮操做又是針對骷髏節點來作的,這些操做須要大量的運算。函數
下面咱們來解析MD5骨骼模型中,一些基本的概念與實現,在MD5,除去紋理圖片,有二個比較主要的文件,一個是後綴爲md5mesh的文件,一個是後綴爲md5anim的文件,二個文件如他們的後綴名所表達的意思同樣,前者和OBJ模型裏的描述比較相似,主要包含每部分的頂點,面,紋理組成,不一樣於OBJ模型的的,這些元素是變化的,所以在OBJ模型有一些新的元素,如頂點不是單獨的頂點,而是由一個或多個權重點構成,每一個權重點關聯着對應着骨骼節點,這樣骨骼節點的改變能引發權重點的改變,而權重點的改變又引發了頂點的改變,至於爲何要用到權重點來鏈接骨骼和頂點,而不是直接用骷髏和頂點關聯,首先拿咱們來講,咱們身上有些位置並非只和一個骨骼節點有關,更可能是和多個節點有關,這樣能讓動畫更真實,也避免在關節點產生重合和斷裂的現象。動畫
首先咱們來解析md5mesh文件裏的信息,在這個文件裏,主要有二大元素,一個是骨骼節點信息,一個是多個部位蒙皮信息,下面我簡化了一個md5mesh,實際確定不可能這樣,主要是用來講明各節點用的。this
在文件中我都加了註釋,簡單來講,第一行是版本信息,下面寫的解析也是針對這個10的版本,而後是命令行,骨骼節點數,蒙皮組件數,而後是骨骼節點的具體信息,在這裏包含每一個骨骼的父索引,頂點位置,四元數(包含旋轉信息).在這裏特別說下,這個骨骼節點的順序暗含他們本身的索引,還有特別一點,在md5mesh文件中,骨骼的頂點位置與旋轉信息是針對模型空間的,後面咱們會看到在md5anim也有骨骼節點下的頂點位置與旋轉信息,可是那是針對父骨骼節點座標來的。而後就是蒙皮各個部分的詳細信息,包含紋理座標位置,頂點數,面的信息,權重信息,如前面所說,一個麪包含3個頂點,每一個頂點包含多個權重,每一個權重關聯一個或多個骨骼信息。下面根據上面各個部位來定義咱們代碼裏各個類:spa
1 type ArrayList<'T> = System.Collections.Generic.List<'T> 2 let filtLine (line:string) = not (String.IsNullOrEmpty(line)) && line <> "(" && line <> ")" 3 let getLineData (line:string) = line.Split(' ','\t','\"') |> Array.filter filtLine 4 let getFloat str = snd (System.Single.TryParse(str)) 5 let getInt str = snd (System.Int32.TryParse(str)) 6 let getw x y z = 1.0f - x*x - y*y - z*z |> fun p -> if p < 0.f then 0.f else float32 (-Math.Sqrt(float p)) 7 8 //--------mesh裏用的結構 9 type Md5Joint() = 10 member val Name = "" with get,set 11 member val Index = -2 with get,set 12 //位移 13 member val Position = Vector3.Zero with get,set 14 //旋轉 15 member val Quat = Quaternion.Identity with get,set 16 member val ParentIndex = -2 with get,set 17 member this.SetValue(line:string,ind:int) = 18 let ls = getLineData line// line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p)))//&& p <> @"\t" && p.Length > 0) 19 let pos,quat = 20 let gf str = snd (System.Single.TryParse(str)) 21 let x,y,z,a,b,c = (gf ls.[2]),(gf ls.[3]),(gf ls.[4]),(gf ls.[5]),(gf ls.[6]),(gf ls.[7]) 22 let d = getw a b c 23 Vector3(x,y,z),Quaternion(a,b,c,d) 24 this.Name <- ls.[0] 25 this.ParentIndex <- snd (System.Int32.TryParse(ls.[1])) 26 this.Position <- pos 27 this.Quat <- quat 28 this 29 30 //Vert包含紋理座標,以及關聯的權重,根據權重求頂點 31 type Md5Vert() = 32 member val Index = -1 with get,set 33 member val Texcoord = Vector2.Zero with get,set 34 member val WeightStart = 0 with get,set 35 member val WeightCount = 0 with get,set 36 //通過權重求頂點實際位置 37 member val Position = Vector3.Zero with get,set 38 member val Normal = Vector3.Zero with get,set 39 member this.SetValue(line:string) = 40 let ls =getLineData line// line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p))) 41 let gf str = snd (System.Single.TryParse(str)) 42 this.Index <- snd (System.Int32.TryParse(ls.[1])) 43 this.Texcoord <- Vector2(gf ls.[2],gf ls.[3]) 44 this.WeightStart <- snd (System.Int32.TryParse(ls.[4])) 45 this.WeightCount <- snd (System.Int32.TryParse(ls.[5])) 46 this 47 member this.DataArray with get() =[| this.Texcoord.X;this.Texcoord.Y;this.Position.X;this.Position.Y;this.Position.Z|] 48 49 //三角形(包含Vert的索引) 50 type Md5Tri() = 51 member val Index = -1 with get,set 52 member val VertorIndexs = Array.create 3 0 with get,set 53 member this.SetValue(line:string) = 54 let ls = getLineData line // line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p))) 55 let gi str = snd (System.Int32.TryParse(str)) 56 this.Index <- gi ls.[1] 57 this.VertorIndexs <- [|gi ls.[2];gi ls.[3];gi ls.[4]|] 58 this 59 60 //權重,(權重頂點用於計算Md5Vert,權重的JointIndex用於獲得對應joint的四元數) 61 type Md5Weight() = 62 member val Index = -1 with get,set 63 member val JointIndex = -1 with get,set 64 member val Bias = 0.f with get,set 65 member val Position = Vector3.Zero with get,set 66 member this.SetValue(line:string) = 67 let ls = line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p))) 68 let gf str = snd (System.Single.TryParse(str)) 69 this.Index <- snd (System.Int32.TryParse(ls.[1])) 70 this.JointIndex <- snd (System.Int32.TryParse(ls.[2])) 71 this.Bias <- gf ls.[3] 72 this.Position <- Vector3(gf ls.[5],gf ls.[6],gf ls.[7]) 73 this 74 75 type Md5Mesh() = 76 let mutable vbo,ebo = 0,0 77 member val TexID = 0 with get,set 78 member val ShaderPath = "" with get,set 79 member val Verts = ArrayList<Md5Vert>() with get,set 80 member val Faces = ArrayList<Md5Tri>() with get,set 81 member val Weights = ArrayList<Md5Weight>() with get,set 82 member this.ElementCount with get() = this.Faces.Count * 3
這裏的類與文件裏各個描述部分差很少都是一一對應,很好理解,因元組在F#編譯器級別的默認支持,使咱們不用想盡辦法組織結構,讓結構和原始文件保持一致就行,而後要用到的時候因函數式操做相關便利性,不多的代碼就能拿到須要組合的數據。命令行
在下面,咱們具體處理如何加載md5mesh文件。code
1 let file = new StreamReader(fileName) 2 while not file.EndOfStream do 3 let str = file.ReadLine() 4 match str with 5 | StartsWith "joints" true -> 6 let mutable isJoint = true 7 while isJoint do 8 let joint = file.ReadLine() 9 isJoint <- not (joint.Contains("}")) 10 if isJoint then this.Joints.Add(Md5Joint().SetValue(joint,this.Joints.Count)) 11 | StartsWith "mesh" true -> 12 let mutable isMesh = true 13 let md5mesh = Md5Mesh() 14 this.Meshs.Add(md5mesh) 15 while isMesh do 16 let mesh = file.ReadLine() 17 match mesh with 18 | StartsWith "shader" true -> 19 let dict = Path.GetDirectoryName(fileName) 20 let fileName = (getLineData mesh).[1]// mesh.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p))) |> fun p -> p.[1].Trim('\"') 21 md5mesh.ShaderPath <- Path.Combine(dict,fileName) 22 | StartsWith "vert" true -> 23 md5mesh.Verts.Add(Md5Vert().SetValue(mesh)) 24 | StartsWith "tri" true -> 25 md5mesh.Faces.Add(Md5Tri().SetValue(mesh)) 26 | StartsWith "weight" true -> 27 md5mesh.Weights.Add(Md5Weight().SetValue(mesh)) 28 | StartsWith "}" true -> 29 isMesh <- false 30 | _ -> printfn "%s" ("---------"+str) 31 | _ -> printfn "%s" str 32 file.Close()
在這裏,差很少就把蒙皮文件裏的全部信息處理完畢。其實若是隻是md5mesh,他就至關於一個複雜了些,包含了權重的OBJ模型,組織方式都大同小異,不信請看下面。咱們記的在md5mesh前面骨骼也包含了頂點位置與四元數信息,根據這個,能夠求得默認的權重點具體位置,而後就能獲得頂點的具體位置,而後獲得面,而後繪製,下面這段代碼能夠在沒有md5anim文件裏,繪製一個靜態的,至關於OBJ模型同樣功能的模型。 orm
1 //先求得頂點的實際數據 2 this.Meshs.ForEach(fun mesh -> 3 mesh.Verts.ForEach(fun vert -> 4 for i in [vert.WeightStart .. vert.WeightStart + vert.WeightCount - 1] do 5 let weigth = mesh.Weights.[i] 6 let joint = this.Joints.[weigth.JointIndex] 7 vert.Position <- vert.Position + (joint.Position + Vector3.Transform(weigth.Position,joint.Quat)) * weigth.Bias 8 ) 9 ) 10 this.Meshs.ForEach(fun mesh -> mesh.CreateVBO())
這段代碼比較簡單,就是上面所說,求面中的頂點,頂點根據權重求,權重根據骨骼當前狀態來獲得,仍是和上面同樣說明下,md5mesh裏的骨骼節點是模型座標系下的,因此骨骼節點不須要作轉化。對象
這裏說下四元數,在3D中,咱們表示旋轉通常有矩陣,歐拉角,四元數,日常咱們所用都是矩陣與歐拉角,四元數用到複數,理解起來比較麻煩,我如今也只是記着一些四元數的特性,能實現平滑插值,點p用四元數旋轉後獲得點p1=ap(a的逆).四元數和矩陣同樣,知足結合律,可是不知足交換律。四元數的有向量部分v(x,y,z)和一個份量w,幾何意義能夠描述爲對於一個向量n,旋轉@角,四元數就是[w=cos(@/2),sin(@/2)*n]=[w=cos(@/2),sin(@/2)*nx,sin(@/2)*ny,sin(@/2)*nz],根據這個定義,能夠推導出一些四元數的特性,如四元數的共軛和四元數表明相反的角位移,上面的p1=ap(a的逆).blog
若是沒有md5anim文件,MD5文件也就和OBJ文件同樣,只是一個靜態的模型,下面讓咱們來分析md5anim的相關格式。下面同樣給出一個簡化了的樣式。
各信息我給出了基本標註,比較重要的每秒多少楨,楨的具體信息,這個順序與前面md5mesh是對應的,父索引也是同樣的,不一樣的是,後面二個整數,一個表示應該讀frame的那些數據,一個表示讀的位置的起點。給出對應的代碼格式。
1 type Md5JointInfo() = 2 member val Name = "" with get,set 3 member val Index = -2 with get,set 4 member val ParentIndex = -2 with get,set 5 member val Flags = 0 with get,set 6 member val StartIndex = 0 with get,set 7 member this.SetValue(line:string,ind:int) = 8 let ls = getLineData line 9 this.Name <- ls.[0] 10 this.ParentIndex <- getInt ls.[1] 11 this.Index <- ind 12 this.Flags <- getInt ls.[2] 13 this.StartIndex <- getInt ls.[3] 14 this 15 16 type Md5BaseFrame() = 17 member val Index = -2 with get,set 18 //位移 19 member val Positions = ArrayList<Vector3>() with get,set 20 //旋轉 21 member val Quats = ArrayList<Quaternion>() with get,set 22 member this.SetValue(line:string) = 23 let ls = getLineData line 24 let pos,quat = 25 let gf str = snd (System.Single.TryParse(str)) 26 let x,y,z,a,b,c = (gf ls.[0]),(gf ls.[1]),(gf ls.[2]),(gf ls.[3]),(gf ls.[4]),(gf ls.[5]) 27 let d = getw a b c 28 Vector3(x,y,z),Quaternion(a,b,c,d) 29 this.Positions.Add(pos) 30 this.Quats.Add(quat) 31 32 type Md5Frame() = 33 member val Index = -2 with get,set 34 member val Points = ArrayList<float32>() with get,set 35 member this.SetValue(line:string) = 36 let ls = getLineData line 37 let ds = ls |> Array.map (fun p -> getFloat p) 38 this.Points.AddRange(ds) 39 40 //楨動畫計算得出以下內容 41 type Md5SkeletonJoin() = 42 member val ParentIndex = -2 with get,set 43 //位移 44 member val Position = Vector3.Zero with get,set 45 //旋轉 46 member val Quat = Quaternion.Identity with get,set 47 type Md5FrameSkeleton = ArrayList<Md5SkeletonJoin>
分別定義了,Md5JointInfo,Md5BaseFrame,Md5Frame,你們能夠看出多了Md5SkeletonJoin與Md5FrameSkeleton,沒有與文件裏的信息對應,這裏就是要你們前面老注意的一個地方,在md5mesh文件,給的骨骼節點座標已是模型座標系下的,而md5anim給出的骨骼節點座標只是針對父骨骼節點裏的,Md5SkeletonJoin與Md5FrameSkeleton就是Md5Frame根據父骨骼節點求出的在模型座標系下的座標。
下面首先是加載md5anim信息的代碼:
1 let path = Path.Combine(Path.GetDirectoryName(fileName),animName.Value) 2 let animFile = new StreamReader(path) 3 while not animFile.EndOfStream do 4 let str = animFile.ReadLine() 5 match str with 6 | StartsWith "frameRate" true -> 7 this.Animation.FrameRate <- getFloat (getLineData str).[1] 8 | StartsWith "hierarchy" true -> 9 let mutable isJoinHierarchy = true 10 while isJoinHierarchy do 11 let joint = animFile.ReadLine() 12 isJoinHierarchy <- not (joint.Contains("}")) 13 if isJoinHierarchy then this.Animation.JointInfos.Add(Md5JointInfo().SetValue(joint,this.Animation.JointInfos.Count)) 14 | StartsWith "bounds" true -> 15 let mutable isbound = true 16 while isbound do 17 let bound = animFile.ReadLine() 18 isbound <- not (bound.Contains("}")) 19 if isbound then 20 let data = getLineData bound 21 let a,b,c,x,y,z = getFloat data.[0],getFloat data.[1],getFloat data.[2],getFloat data.[3],getFloat data.[4],getFloat data.[5] 22 this.Animation.Bounds.Add(Vector3(a,b,c),Vector3(x,y,z)) 23 | StartsWith "baseframe" true -> 24 let mutable isFrame = true 25 let mf = this.Animation.BaseFrame 26 while isFrame do 27 let frameLine = animFile.ReadLine() 28 isFrame <- not (frameLine.Contains("}")) 29 if isFrame then mf.SetValue(frameLine) 30 | StartsWith "frame" true -> 31 let mutable isFrame = true 32 let mf = Md5Frame() 33 mf.Index <- getInt (getLineData str).[1] 34 this.Animation.Frames.Add(mf) 35 while isFrame do 36 let frameLine = animFile.ReadLine() 37 isFrame <- not (frameLine.Contains("}")) 38 if isFrame then mf.SetValue(frameLine) 39 | _ -> printfn "%s" str 40 animFile.Close() 41 //把骨骼動畫中,各節點由父骨骼節點座標轉化成模型座標 42 this.Animation.CreateFrameSkeleton() 43 //生成紋理 44 this.Meshs.ForEach(fun mesh -> if mesh.TexID = 0 && File.Exists mesh.ShaderPath then mesh.TexID <- TexTure.Load(mesh.ShaderPath)) 45
這部分代碼也是一些IO操做,把讀到的信息都放入Md5Animation裏去,這個類主要作二件事,一是獲得正確的Md5SkeletonJoin與Md5FrameSkeleton,就是獲得Md5Frame根據父骨骼節點求出的在模型座標系下的座標。而後一些,就是根據當前時間,當前楨率獲得正確的插值,這部分和MD2插值差很少。請看主要代碼:
1 type Md5Animation() = 2 let mutable currentTime = 0.f 3 member val FrameRate = 24.f with get,set 4 //JointInfos集合對象的索引就是自己在文件中的位置,他們自己的順序就是正序加1 5 member val JointInfos = ArrayList<Md5JointInfo>() with get,set 6 member val Bounds = ArrayList<Vector3*Vector3>() with get,set 7 member val Frames = ArrayList<Md5Frame>() with get,set 8 member val BaseFrame = Md5BaseFrame() with get,set 9 member val FrameSkeletonList = ArrayList<Md5FrameSkeleton>() with get,set 10 //二個目標,一是轉化Frames裏的數據成對應一楨的全部骨骼節點信息 11 //二是把全部骨骼節點在父骨骼座標系中的位置轉化成模型座標系 12 member this.CreateFrameSkeleton() = 13 for frame in this.Frames do 14 let md5FrameSkeleton = Md5FrameSkeleton() 15 for jointInfo in this.JointInfos do 16 //skeleton的順序由於JointInfos的特殊性,也是正序加1 17 let skeleton = Md5SkeletonJoin() 18 md5FrameSkeleton.Add(skeleton) 19 skeleton.ParentIndex <- jointInfo.ParentIndex 20 let position = this.BaseFrame.Positions.[jointInfo.Index] 21 let quat = this.BaseFrame.Quats.[jointInfo.Index] 22 let setFlags index = 23 let flag = int (Math.Pow(float 2,float index)) 24 if jointInfo.Flags &&& flag = flag then frame.Points.[jointInfo.StartIndex + index] 25 else 26 match index with 27 | 0 -> position.X 28 | 1 -> position.Y 29 | 2 -> position.Z 30 | 3 -> quat.X 31 | 4 -> quat.Y 32 | 5 -> quat.Z 33 | _ -> quat.W 34 let x,y,z,a,b,c = setFlags 0,setFlags 1,setFlags 2,setFlags 3,setFlags 4,setFlags 5 35 let w = getw a b c 36 //currentPos,currentQuat都是針對父骨骼來的座標,要轉化獲得模型座標 37 let currentPos,currentQuat=Vector3(x,y,z),Quaternion(a,b,c,w) 38 skeleton.Position <- currentPos 39 skeleton.Quat <- currentQuat 40 if skeleton.ParentIndex >= 0 then 41 let parentSkeleton = md5FrameSkeleton.[skeleton.ParentIndex] 42 //先獲得currentPos通過父骨骼四元數旋轉後的值 43 let pos = Vector3.Transform(currentPos,parentSkeleton.Quat) 44 //模型座標示下的點 45 skeleton.Position <- parentSkeleton.Position + pos 46 //模型座標系下的四元數 47 skeleton.Quat <- Quaternion.Normalize(parentSkeleton.Quat * skeleton.Quat) 48 this.FrameSkeletonList.Add(md5FrameSkeleton) 49 member this.CurrentTime 50 with get() = 51 if currentTime > float32 this.Frames.Count/this.FrameRate then currentTime <- 0.f 52 currentTime 53 and set value = currentTime <- value 54 member this.GetCurrentFrameSkeleton() = 55 //獲得當前的時間所在動畫循環的位置 56 let current = this.CurrentTime * this.FrameRate 57 //獲得所在位置的當前楨索引,與運動到下一楨的位置 58 let currentFrame,currentStep= int (Math.Floor(float current)),current - float32 (Math.Floor(float current)) 59 //獲得下一楨索引,到楨尾就從頭開始 60 let nextFrame = if currentFrame < this.Frames.Count - 1 then currentFrame + 1 else 0 61 //獲得當前楨,下一楨具體信息 62 let currentSkeleton,nexSkeleton = this.FrameSkeletonList.[currentFrame],this.FrameSkeletonList.[nextFrame] 63 let joints = ArrayList<Md5Joint>() 64 for i in [|0 .. this.JointInfos.Count - 1|] do 65 //根據當前楨位置求得對應四元數與頂點的插值 66 let lerpPosition = Vector3.Lerp(currentSkeleton.[i].Position,nexSkeleton.[i].Position,currentStep) 67 let slerpQuat = Quaternion.Slerp(currentSkeleton.[i].Quat,nexSkeleton.[i].Quat,currentStep) 68 joints.Add(Md5Joint(Index = i,Position = lerpPosition,Quat = slerpQuat)) 69 joints
關鍵部分我都寫了註釋,應該容易看明白,如上面所說,二件事,一是CreateFrameSkeleton,這個首先根據flag與startIndex讀取文件,而後把在父骨骼座標系中的點轉化成模型座標系下的點。二是GetCurrentFrameSkeleton,分別獲得所在時間的當前楨與下一楨,而後根據在這楨之間的位置插值獲得各骨骼節點正確的座標。渲染部分在這裏,考慮到由於一個MD5模型原本包含幾部分Mesh,而後每部分Mesh又包含各楨的狀況,再想用MD2中關鍵楨頂點信息作VBO不現實,故直接用VA來輸出渲染。
1 type Md5Model(fileName:string,?animName:string) = 2 member this.Render()= 3 //生成骨骼節點的信息 4 let joints = this.Animation.GetCurrentFrameSkeleton() 5 //根據骨骼節點生成頂點.也就是蒙皮 6 this.Meshs.ForEach(fun mesh -> 7 mesh.Verts.ForEach(fun vert -> 8 vert.Position <- Vector3.Zero 9 for i in [vert.WeightStart .. vert.WeightStart + vert.WeightCount - 1] do 10 let weigth = mesh.Weights.[i] 11 let joint = joints.[weigth.JointIndex] 12 vert.Position <- vert.Position + (joint.Position + Vector3.Transform(weigth.Position,joint.Quat)) * weigth.Bias 13 ) 14 ) 15 //頂點繪製 16 this.Meshs.ForEach(fun mesh -> mesh.Render()) 17 18 type Md5Mesh() = 19 member this.Render() = 20 let vboData = Array2D.init this.ElementCount 5 (fun i j -> 21 let a,b = i/3,i%3 22 this.Verts.[this.Faces.[a].VertorIndexs.[b]].DataArray.[j] 23 ) 24 GL.InterleavedArrays(InterleavedArrayFormat.T2fV3f,0,vboData) 25 if this.TexID > 0 then 26 GL.Enable(EnableCap.Texture2D) 27 GL.BindTexture(TextureTarget.Texture2D,this.TexID) 28 GL.DrawElements(BeginMode.Triangles,this.ElementCount,DrawElementsType.UnsignedInt,[|0..this.ElementCount - 1|]) 29
到此,整個過程就差很少了,下面給出效果圖:
代碼:源碼與執行文件 http://files.cnblogs.com/zhouxin/MD5Load.zip 其中\bin\Release\CgTest.exe爲可執行文件
其中EDSF先後左右移動,鼠標右鍵加移動鼠標控制方向,空格上升,空格在SHIFT降低。再發現,整個工程中,去掉OBJ,MD2模型後,加上DLL一共27M,壓縮下才5M,能上傳上來,前面每次都分開上傳給你們形成不便了,其中爲了突出MD5的重點,相應的法線沒有自動生成,相關方法能夠看前面OBJ,MD2裏的,計算過程同樣。
CPU和GPU各應該執行的操做讓個人理解應該是,一次計算好久變一次應該交給CPU,而在渲染過程快速,大量執行的代碼應該交給GPU來算,下一步目標,改進裏面關於骨骼位置的計算,以及相應蒙皮的操做應該交給GPU,也就是放到着色器中去處理。