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

【題外話】html

上一篇文章介紹了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界面的顯示方式

 

【1、Model模型的結構】app

上一篇文章使用Model自帶的Draw方法實現了直接將載入的Model繪製到指定的位置上去,可是有時候繪製出來的效果並不符合咱們的預期,好比下圖(下圖的模型是經過Maya建立的一個屋子):ide

經過ILSpy查看Microsoft.Xna.Framework.Graphics.Model,能夠看到其Draw方法的代碼以下:學習

 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方法將其繪製到屏幕上。this

爲了瞭解Model的渲染,咱們首先須要瞭解Model的結構。實際上,在一個Model對象中,包含Bone集合(model.Bones)、Mesh集合(model.Meshes)以及根Bone(model.Root)三個屬性,其結構和關係以下spa

能夠看到對於每一個ModelMesh,包含一組ModelMeshPart與一個ParentBone。其中,
.net

  • 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(),啓用後效果以下:

這樣的效果就達到了咱們的預期,按上述的方法實現的代碼以下:

 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 }
View Code

不過這與剛纔看到的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 }

 

【2、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是這樣寫的:

 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 }
View Code

能夠看到在啓用默認光照裏其實是給環境光AmbientLightColor以及三束定向光(包括光線的方向、漫反射顏色及鏡面反射顏色)設置了預先定義好的顏色,並啓用了這些光源,這三束定向光的顏色(Light1的漫反射光的顏色以下,但其鏡面反射光的顏色爲黑色)和方向大體以下。

下圖第一個爲啓用了默認光照後的模型(上一篇文章中的dude),第2、3、四個爲只啓用默認光照的環境光及0、一、2三束定向光後的模型,第五個爲沒有啓用默認光照的模型(如同上一篇產生的效果同樣):

固然,在不少狀況下(好比戶外的日光等),咱們僅須要一個光源,屆時咱們只要禁用(DirectionalLight*.Enabled = false)其餘兩個定向光便可,固然咱們可能還須要修改光源的顏色等等。

除了使用EnableDefaultLighting,BasicEffect還提供了比較豐富的參數能夠設置。首先來看下上述例子中Effect默認的屬性:

其中與光線有關的:

  • LightingEnabled:是否開啓光照(默認爲false)。
  • PreferPerPixelLighting:是否開啓逐像素的光照(默認爲false,爲逐頂點光照),逐像素光照相對於逐點光照效果更好,但速度也更慢,同時還須要顯卡支持Pixel Shader Model 2.0,若是顯卡不支持的話會自動使用逐頂點光照代替。
  • AmbientLightColor:環境光顏色(默認爲Vector3.Zero)。爲了在局部光照模型(模型間的光照互不影響)中加強真實感,引入了環境光的概念。環境光不依賴任何光源,但其影響全部物體。
  • DiffuseColor:漫反射顏色(默認爲Vector3.One)。光線照到物體後,物體進行漫反射,其顏色與光線的方向有關。
  • SpecularColor:鏡面反射顏色。光線照到物體後,物體進行全反射,其顏色不只與光線的方向有關,還與觀察(相機)的方向有關。
  • EmissiveColor:放射顏色(默認爲Vector3.Zero)。放射光是指物體發出的光線,但在局部光照模型中,實際上不會對其餘物體產生影響。
  • DirectionalLight0、DirectionalLight一、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。

 

【3、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;

 

【4、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
相關文章
相關標籤/搜索