上一篇文章介紹了3D開發基礎與XNA開發程式的整體結構,以及使用Model類的Draw方法将模型繪制到螢幕上。本文接着上一篇文章繼續,介紹XNA中模型的結構、BasicEffect的使用以及使用者輸入和界面顯示的方式等,本文盡量把遇到的概念都解析清楚,但又避開複雜的數學方面的知識,希望對沒有接觸過3D開發的同學有所幫助。
【題外話】
【系列索引】
- 從零3D基礎入門XNA 4.0(1)——3D開發基礎
- 從零3D基礎入門XNA 4.0(2)——模型和BasicEffect
【文章索引】
- Model模型的結構
- BasicEffect效果的設定
- XNA的使用者輸入
- XNA界面的顯示方式
【一、Model模型的結構】
上一篇文章使用Model自帶的Draw方法實作了直接将載入的Model繪制到指定的位置上去,但是有時候繪制出來的效果并不符合我們的預期,比如下圖(下圖的模型是通過Maya建立的一個屋子):

通過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方法将其繪制到螢幕上。
為了了解Model的渲染,我們首先需要了解Model的結構。實際上,在一個Model對象中,包含Bone集合(model.Bones)、Mesh集合(model.Meshes)以及根Bone(model.Root)三個屬性,其結構和關系如下:
可以看到對于每個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(),啟用後效果如下:
這樣的效果就達到了我們的預期,按上述的方法實作的代碼如下:
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是這樣寫的:
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的漫反射光的顔色如下,但其鏡面反射光的顔色為黑色)和方向大緻如下。
下圖第一個為啟用了預設光照後的模型(上一篇文章中的dude),第二、三、四個為隻啟用預設光照的環境光及0、1、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、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。
【三、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();
}
【相關連結】
- Model Class:http://msdn.microsoft.com/en-us/library/Microsoft.Xna.Framework.Graphics.Model.aspx
- Models, meshes, parts, and bones:http://blogs.msdn.com/b/shawnhar/archive/2006/11/20/models-meshes-parts-and-bones.aspx
- What Is a Model Bone?:http://msdn.microsoft.com/en-us/library/dd904249.aspx
- BasicEffect Lighting:http://rbwhitaker.wikidot.com/basic-effect-lighting
- BasicEffect Fog:http://rbwhitaker.wikidot.com/basic-effect-fog
- 一起學WP7 XNA遊戲開發(七. 3d基本光源):http://www.cnblogs.com/randylee/archive/2011/03/09/1978312.html
- 【D3D11遊戲程式設計】學習筆記十二:光照模型:http://blog.csdn.net/bonchoix/article/details/8430561
如果您覺得本文對您有所幫助,不妨點選下方的“推薦”按鈕來支援我! 本文及文章中代碼均基于“署名-非商業性使用-相同方式共享 3.0”,文章歡迎轉載,但請您務必注明文章的作者和出處連結,如有疑問請私信我聯系! |