【題外話】html
上一篇文章介紹了3D開發基礎與XNA開發程序的總體結構,以及使用Model類的Draw方法將模型繪製到屏幕上。本文接着上一篇文章繼續,介紹XNA中模型的結構、BasicEffect的使用以及用戶輸入和界面顯示的方式等,本文儘可能把遇到的概念都解析清楚,但又避開復雜的數學方面的知識,但願對沒有接觸過3D開發的同窗有所幫助。編程
【系列索引】小程序
【文章索引】數組
上一篇文章使用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 }
其中可見,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
因此遍歷一個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 }
首先用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),第2、3、四個爲只啓用默認光照的環境光及0、一、2三束定向光後的模型,第五個爲沒有啓用默認光照的模型(如同上一篇產生的效果同樣):
固然,在不少狀況下(好比戶外的日光等),咱們僅須要一個光源,屆時咱們只要禁用(DirectionalLight*.Enabled = false)其餘兩個定向光便可,固然咱們可能還須要修改光源的顏色等等。
除了使用EnableDefaultLighting,BasicEffect還提供了比較豐富的參數能夠設置。首先來看下上述例子中Effect默認的屬性:
其中與光線有關的:
其中須要注意的是,在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還支持設置霧的效果:
也就是說,霧將會在距離相機(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程序中的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的程序會自動以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(); }
【相關連接】