天天看點

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

上一篇文章介紹了3D開發基礎與XNA開發程式的整體結構,以及使用Model類的Draw方法将模型繪制到螢幕上。本文接着上一篇文章繼續,介紹XNA中模型的結構、BasicEffect的使用以及使用者輸入和界面顯示的方式等,本文盡量把遇到的概念都解析清楚,但又避開複雜的數學方面的知識,希望對沒有接觸過3D開發的同學有所幫助。

【題外話】

【系列索引】

  1. 從零3D基礎入門XNA 4.0(1)——3D開發基礎
  2. 從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

【文章索引】

  1. Model模型的結構
  2. BasicEffect效果的設定
  3. XNA的使用者輸入
  4. XNA界面的顯示方式

【一、Model模型的結構】

上一篇文章使用Model自帶的Draw方法實作了直接将載入的Model繪制到指定的位置上去,但是有時候繪制出來的效果并不符合我們的預期,比如下圖(下圖的模型是通過Maya建立的一個屋子):

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

通過ILSpy檢視Microsoft.Xna.Framework.Graphics.Model,可以看到其Draw方法的代碼如下:

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
1 public void Draw(Matrix world, Matrix view, Matrix projection)
 2 {
 3     int count = this.meshes.Count;
 4     int count2 = this.bones.Count;
 5     Matrix[] array = Model.sharedDrawBoneMatrices;
 6     if (array == null || array.Length < count2)
 7     {
 8         array = new Matrix[count2];
 9         Model.sharedDrawBoneMatrices = array;
10     }
11     this.CopyAbsoluteBoneTransformsTo(array);
12     for (int i = 0; i < count; i++)
13     {
14         ModelMesh modelMesh = this.meshes[i];
15         int index = modelMesh.ParentBone.Index;
16         int count3 = modelMesh.Effects.Count;
17         for (int j = 0; j < count3; j++)
18         {
19             Effect effect = modelMesh.Effects[j];
20             if (effect == null)
21             {
22                 throw new InvalidOperationException(FrameworkResources.ModelHasNoEffect);
23             }
24             IEffectMatrices effectMatrices = effect as IEffectMatrices;
25             if (effectMatrices == null)
26             {
27                 throw new InvalidOperationException(FrameworkResources.ModelHasNoIEffectMatrices);
28             }
29             effectMatrices.World = array[index] * world;
30             effectMatrices.View = view;
31             effectMatrices.Projection = projection;
32         }
33         modelMesh.Draw();
34     }
35 }      

View Code

其中可見,Draw方法通過周遊模型的Mesh,然後再周遊每個Mesh的Effect,并對每個Effect進行設定,最後使用Mesh的Draw方法将其繪制到螢幕上。

為了了解Model的渲染,我們首先需要了解Model的結構。實際上,在一個Model對象中,包含Bone集合(model.Bones)、Mesh集合(model.Meshes)以及根Bone(model.Root)三個屬性,其結構和關系如下:

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

可以看到對于每個ModelMesh,包含一組ModelMeshPart與一個ParentBone。其中,

  • ModelMesh表示單個可以獨立移動的實體對象。例如,一個car的Model可以包含一個車體(body)的ModelMesh、四個車輪(wheel)的ModelMesh與一對門(door)的ModelMesh。
  • ModelMeshPart表示單個相同材料的部件,其代表一個單獨的繪制調用(draw call)。例如,上述車身可以包含着色的表面、使用環境映射(environment mapping)效果的擋風玻璃以及使用法線貼圖(normalmap texture)效果的座椅等等。
  • ModelBone表示了對應的ModelMesh如何變換,其包含一個Transform的變換矩陣。ModelBone是以樹形存儲的,每個ModelBone都有一個父節點以及若幹個子節點。上述的每個ModelMesh都有一個ParentBone,ModelMesh可以根據ModelBone的變換來确定最終顯示的位置等。例如,上述車門的ModelBone與車輪的ModelBone是車身的子節點等等。

是以周遊一個Model中所有的ModelMesh,然後周遊其中所有的ModelMeshPart,并且根據ModelMesh的ParentBone來将每一個ModelMeshPart繪制到指定的位置上就可以繪制出完整的Model。

不過對于每個ModelMeshPart,其實際渲染的效果都存在Effect的屬性中,對于預設來說,Effect均為BasicEffect。此外,對于ModelBone,其變換矩陣都是相對其自身的Parent來的,不過Model類也提供了一個方法,即CopyAbsoluteBoneTransformsTo(),即可将每個Bone相對于RootBone的變換矩陣複制到一個矩陣數組中,然後将其應用到Effect中即可。這種方式與上述提到的Model.Draw類似,不過自己寫的話就可以自定義每個ModelMeshPart渲染的效果,當然也可以設定每個ModelMeshPart的渲染位置。

那麼接下來就按照這個思路去實作,同時在設定每一個Effect時,使用Effect提供的使用預設光照的方法EnableDefaultLighting(),啟用後效果如下:

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

這樣的效果就達到了我們的預期,按上述的方法實作的代碼如下:

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
1 Matrix world = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up);
 2 
 3 Matrix[] transforms = new Matrix[model.Bones.Count];
 4 this.model.CopyAbsoluteBoneTransformsTo(transforms);
 5 
 6 foreach (ModelMesh mesh in model.Meshes)
 7 {
 8     Int32 boneIndex = mesh.ParentBone.Index;
 9 
10     foreach (ModelMeshPart part in mesh.MeshParts)
11     {
12         BasicEffect effect = part.Effect as BasicEffect;
13         
14         effect.EnableDefaultLighting();
15         effect.World = transforms[boneIndex] * world;
16         effect.View = cameraView;
17         effect.Projection = cameraProjection;
18     }
19 
20     mesh.Draw();
21 }      

不過這與剛才看到的Model.Draw的代碼并不相同。實際上,XNA為了簡化操作,已經将ModelMeshPart的每個Effect放到了ModelMesh的Effects集合中,隻需要周遊這個集合就可以,而無需再周遊ModelMeshPart,再擷取Effect了。是以上述代碼可以簡化為如下的代碼:

1 Matrix world = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up);
 2 
 3 Matrix[] transforms = new Matrix[model.Bones.Count];
 4 this.model.CopyAbsoluteBoneTransformsTo(transforms);
 5 
 6 foreach (ModelMesh mesh in model.Meshes)
 7 {
 8     Int32 boneIndex = mesh.ParentBone.Index;
 9     
10     foreach (BasicEffect effect in mesh.Effects)
11     {
12         effect.EnableDefaultLighting();
13         effect.World = transforms[boneIndex] * world;
14         effect.View = cameraView;
15         effect.Projection = cameraProjection;
16     }
17 
18     mesh.Draw();
19 }      

【二、BasicEffect效果的設定】

首先用ILSpy檢視下BasicEffect的EnableDefaultLighting()的代碼:

public void EnableDefaultLighting()
{
    this.LightingEnabled = true;
    this.AmbientLightColor = EffectHelpers.EnableDefaultLighting(this.light0, this.light1, this.light2);
}      

其中this.light0-2為BasicEffect的DirectionalLight0-2,即BasicEffect可以時候的三個光源。而EffectHelpers的EnableDefaultLighting是這樣寫的:

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
1 internal static Vector3 EnableDefaultLighting(DirectionalLight light0, DirectionalLight light1, DirectionalLight light2)
 2 {
 3     light0.Direction = new Vector3(-0.5265408f, -0.5735765f, -0.6275069f);
 4     light0.DiffuseColor = new Vector3(1f, 0.9607844f, 0.8078432f);
 5     light0.SpecularColor = new Vector3(1f, 0.9607844f, 0.8078432f);
 6     light0.Enabled = true;
 7     light1.Direction = new Vector3(0.7198464f, 0.3420201f, 0.6040227f);
 8     light1.DiffuseColor = new Vector3(0.9647059f, 0.7607844f, 0.4078432f);
 9     light1.SpecularColor = Vector3.Zero;
10     light1.Enabled = true;
11     light2.Direction = new Vector3(0.4545195f, -0.7660444f, 0.4545195f);
12     light2.DiffuseColor = new Vector3(0.3231373f, 0.3607844f, 0.3937255f);
13     light2.SpecularColor = new Vector3(0.3231373f, 0.3607844f, 0.3937255f);
14     light2.Enabled = true;
15     return new Vector3(0.05333332f, 0.09882354f, 0.1819608f);
16 }      

可以看到在啟用預設光照裡實際上是給環境光AmbientLightColor以及三束定向光(包括光線的方向、漫反射顔色及鏡面反射顔色)設定了預先定義好的顔色,并啟用了這些光源,這三束定向光的顔色(Light1的漫反射光的顔色如下,但其鏡面反射光的顔色為黑色)和方向大緻如下。

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

下圖第一個為啟用了預設光照後的模型(上一篇文章中的dude),第二、三、四個為隻啟用預設光照的環境光及0、1、2三束定向光後的模型,第五個為沒有啟用預設光照的模型(如同上一篇産生的效果一樣):

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

當然,在很多情況下(比如戶外的日光等),我們僅需要一個光源,屆時我們隻要禁用(DirectionalLight*.Enabled = false)其他兩個定向光即可,當然我們可能還需要修改光源的顔色等等。

除了使用EnableDefaultLighting,BasicEffect還提供了比較豐富的參數可以設定。首先來看下上述例子中Effect預設的屬性:

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

其中與光線有關的:

  • LightingEnabled:是否開啟光照(預設為false)。
  • PreferPerPixelLighting:是否開啟逐像素的光照(預設為false,為逐頂點光照),逐像素光照相對于逐點光照效果更好,但速度也更慢,同時還需要顯示卡支援Pixel Shader Model 2.0,如果顯示卡不支援的話會自動使用逐頂點光照代替。
  • AmbientLightColor:環境光顔色(預設為Vector3.Zero)。為了在局部光照模型(模型間的光照互不影響)中增強真實感,引入了環境光的概念。環境光不依賴任何光源,但其影響所有物體。
  • DiffuseColor:漫反射顔色(預設為Vector3.One)。光線照到物體後,物體進行漫反射,其顔色與光線的方向有關。
  • SpecularColor:鏡面反射顔色。光線照到物體後,物體進行全反射,其顔色不僅與光線的方向有關,還與觀察(相機)的方向有關。
  • EmissiveColor:放射顔色(預設為Vector3.Zero)。放射光是指物體發出的光線,但在局部光照模型中,實際上不會對其他物體産生影響。
  • DirectionalLight0、DirectionalLight1、DirectionalLight2:三束定向光(每束都包括光線的方向、漫反射顔色與鏡面反射顔色)。

其中需要注意的是,在XNA中,顔色的存儲并不是使用的Color(ARGB或ABGR),而是使用的Vector3(或Vector4)。對于Vector3,其x、y、z三個分量存儲的分别是R、G、B分别除以255的浮點值(Vector4的w分量存儲的是Alpha通道除以255的浮點值),是以Vector3.Zero即為黑色,而Vector3.One為白色。當然XNA也提供了一個Color類,并且Color也提供了提供了直接轉換為Vector3(或Vector4)的方法ToVector3()(或ToVector4())。

除此之外,BasicEffect還支援設定霧的效果:

  • FogEnabled:是否開啟霧的效果(預設為false)。
  • FogColor:霧的顔色(預設為Vector3.Zero)。
  • FogStart:霧距離相機的開始(最近)值(預設為0.0F),這個距離之内的東西不受霧的影響。
  • FogEnd:霧距離相機的結束(最遠)值(預設為1.0F),這個距離之外的東西完全看不清。

也就是說,霧将會在距離相機(FogStart - FogEnd)的地方産生,這個距離需要根據物體所在的位置決定。設Distance為物體距離相機的距離,則Distance<FogStart<FogEnd時,物體不受霧的影響,與沒有霧時一樣;當FogStart<FogEnd<Distance時,物體完全看不清(即物體全部為霧的顔色);當FogStart<Distance<FogEnd時,物體受霧的影響,物體離FogEnd越近則越看不清。

例如當人的模型在(0, 0, 0),相機在(120, 120, 120)處,霧的顔色為Gray。下圖第一個為沒有加霧的效果,第二個為FogStart - FogEnd為200 - 300,第三個為1 - 300,第四個為1 - 100。

從零3D基礎入門XNA 4.0(2)——模型和BasicEffect

【三、XNA的使用者輸入】

在預設生成XNA程式中的Update方法裡,有一個擷取GamePad的狀态,當使用者1的GamePad按下了“Back”鍵後将會退出程式。微軟對使用者輸入的支援都在Microsoft.Xna.Framework.Input中,除了GamePad之外,微軟還支援擷取Keyboard、Mouse這兩種的狀态。此外在Microsoft.Xna.Framework.Input.Touch中,還有TouchPanel可以擷取觸摸的狀态。與GamePad相同,其他的這些狀态也都是通過微軟提供給類中的GetState()方法進行擷取。

例如要擷取鍵盤和滑鼠的狀态,我們可以通過如下方式:

KeyboardState kbState = Keyboard.GetState();
MouseState mouseState = Mouse.GetState();      

對于判斷鍵盤的按鍵,可以通過如下的方式擷取是否按下了指定按鍵:

Boolean pressed = kbState.IsKeyDown(Keys.Enter);      

而對于滑鼠的按鍵,則需要判斷按鍵的ButtonState才可以,例如判斷滑鼠左鍵是否按下:

Boolean pressed = (mouseState.LeftButton == ButtonState.Pressed);      

除此之外,如果要判斷滑鼠是否在程式區域内,可以通過如下的方式判斷

if (this.GraphicsDevice.Viewport.Bounds.Contains(mouseState.X, mouseState.Y))
{
    //TODO
}      

雖然在大多數情況下,如果讓使用者操作滑鼠的話會在程式内顯示一個自定義的指針。但有時候寫個小程式,為了簡單希望直接使用系統的指針,我們可以在程式的任意位置(構造方法、Initialize甚至Update也可)寫如下的代碼,就可以顯示滑鼠指針了,反之則可以隐藏:

this.IsMouseVisible = true;      

【四、XNA界面的顯示方式】

預設情況下,運作XNA的程式會自動以800*480的分辨率顯示,若要修改顯示的分辨率,其實非常簡單,僅需要在Game的構造方法中添加如下代碼即可:

graphics.PreferredBackBufferWidth = 1024;
graphics.PreferredBackBufferHeight = 768;      

這樣XNA的程式就能按照我們設定的分辨率顯示了。除此之外,如果我們希望XNA的程式能全屏顯示,我們還可以添加如下的代碼:

graphics.IsFullScreen = true;      

當然我們還可以讓使用者來切換全屏與視窗化,但是這行代碼寫在Update()中是不起作用的,不過XNA提供另外一個方法,就是graphics.ToggleFullScreen()。例如我們需要按F鍵進行全屏與視窗化的切換,可以編寫如下的代碼:

KeyboardState kbState = Keyboard.GetState();
if (kbState.IsKeyDown(Keys.F))
{
    graphics.ToggleFullScreen();
}      

【相關連結】

  1. Model Class:http://msdn.microsoft.com/en-us/library/Microsoft.Xna.Framework.Graphics.Model.aspx
  2. Models, meshes, parts, and bones:http://blogs.msdn.com/b/shawnhar/archive/2006/11/20/models-meshes-parts-and-bones.aspx
  3. What Is a Model Bone?:http://msdn.microsoft.com/en-us/library/dd904249.aspx
  4. BasicEffect Lighting:http://rbwhitaker.wikidot.com/basic-effect-lighting
  5. BasicEffect Fog:http://rbwhitaker.wikidot.com/basic-effect-fog
  6. 一起學WP7 XNA遊戲開發(七. 3d基本光源):http://www.cnblogs.com/randylee/archive/2011/03/09/1978312.html
  7. 【D3D11遊戲程式設計】學習筆記十二:光照模型:http://blog.csdn.net/bonchoix/article/details/8430561

如果您覺得本文對您有所幫助,不妨點選下方的“推薦”按鈕來支援我!

本文及文章中代碼均基于“署名-非商業性使用-相同方式共享 3.0”,文章歡迎轉載,但請您務必注明文章的作者和出處連結,如有疑問請私信我聯系!

繼續閱讀