【题外话】

上一篇文章介绍了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创建的一个屋子):

通过ILSpy查看Microsoft.Xna.Framework.Graphics.Model,可以看到其Draw方法的代码如下:

 public void Draw(Matrix world, Matrix view, Matrix projection)
{
int count = this.meshes.Count;
int count2 = this.bones.Count;
Matrix[] array = Model.sharedDrawBoneMatrices;
if (array == null || array.Length < count2)
{
array = new Matrix[count2];
Model.sharedDrawBoneMatrices = array;
}
this.CopyAbsoluteBoneTransformsTo(array);
for (int i = ; i < count; i++)
{
ModelMesh modelMesh = this.meshes[i];
int index = modelMesh.ParentBone.Index;
int count3 = modelMesh.Effects.Count;
for (int j = ; j < count3; j++)
{
Effect effect = modelMesh.Effects[j];
if (effect == null)
{
throw new InvalidOperationException(FrameworkResources.ModelHasNoEffect);
}
IEffectMatrices effectMatrices = effect as IEffectMatrices;
if (effectMatrices == null)
{
throw new InvalidOperationException(FrameworkResources.ModelHasNoIEffectMatrices);
}
effectMatrices.World = array[index] * world;
effectMatrices.View = view;
effectMatrices.Projection = projection;
}
modelMesh.Draw();
}
}

其中可见,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(),启用后效果如下:

这样的效果就达到了我们的预期,按上述的方法实现的代码如下:

 Matrix world = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up);

 Matrix[] transforms = new Matrix[model.Bones.Count];
this.model.CopyAbsoluteBoneTransformsTo(transforms); foreach (ModelMesh mesh in model.Meshes)
{
Int32 boneIndex = mesh.ParentBone.Index; foreach (ModelMeshPart part in mesh.MeshParts)
{
BasicEffect effect = part.Effect as BasicEffect; effect.EnableDefaultLighting();
effect.World = transforms[boneIndex] * world;
effect.View = cameraView;
effect.Projection = cameraProjection;
} mesh.Draw();
}

不过这与刚才看到的Model.Draw的代码并不相同。实际上,XNA为了简化操作,已经将ModelMeshPart的每个Effect放到了ModelMesh的Effects集合中,只需要遍历这个集合就可以,而无需再遍历ModelMeshPart,再获取Effect了。所以上述代码可以简化为如下的代码:

 Matrix world = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up);

 Matrix[] transforms = new Matrix[model.Bones.Count];
this.model.CopyAbsoluteBoneTransformsTo(transforms); foreach (ModelMesh mesh in model.Meshes)
{
Int32 boneIndex = mesh.ParentBone.Index; foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.World = transforms[boneIndex] * world;
effect.View = cameraView;
effect.Projection = cameraProjection;
} mesh.Draw();
}

【二、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是这样写的:

 internal static Vector3 EnableDefaultLighting(DirectionalLight light0, DirectionalLight light1, DirectionalLight light2)
{
light0.Direction = new Vector3(-0.5265408f, -0.5735765f, -0.6275069f);
light0.DiffuseColor = new Vector3(1f, 0.9607844f, 0.8078432f);
light0.SpecularColor = new Vector3(1f, 0.9607844f, 0.8078432f);
light0.Enabled = true;
light1.Direction = new Vector3(0.7198464f, 0.3420201f, 0.6040227f);
light1.DiffuseColor = new Vector3(0.9647059f, 0.7607844f, 0.4078432f);
light1.SpecularColor = Vector3.Zero;
light1.Enabled = true;
light2.Direction = new Vector3(0.4545195f, -0.7660444f, 0.4545195f);
light2.DiffuseColor = new Vector3(0.3231373f, 0.3607844f, 0.3937255f);
light2.SpecularColor = new Vector3(0.3231373f, 0.3607844f, 0.3937255f);
light2.Enabled = true;
return new Vector3(0.05333332f, 0.09882354f, 0.1819608f);
}

可以看到在启用默认光照里实际上是给环境光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 = ;
graphics.PreferredBackBufferHeight = ;

这样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

从零3D基础入门XNA 4.0(2)——模型和BasicEffect的更多相关文章

  1. 从零3D基础入门XNA 4.0(1)——3D开发基础

    [题外话] 最近要做一个3D动画演示的程序,由于比较熟悉C#语言,再加上XNA对模型的支持比较好,故选择了XNA平台.不过从网上找到很多XNA的入门文章,发现大都需要一些3D基础,而我之前并没有接触过 ...

  2. 零基础入门 实战mpvue2.0多端小程序框架

    第1章 课程快速预览(必看!!!)在这一章节中,老师讲带领你快速预览课程整体.其中,涉及到为什么要做这么一门实战课程.制作一个小程序的完整流程是怎么样的,以及如何做项目的技术选型. 第2章 30 分钟 ...

  3. 简单实用的Boom 3D基础入门教程分享

    Boom 3D可以很大限度的弥补声音设备或是环境的不足,满足您更加高级的声学体验.Boom 3D用简单明了的方式帮助您设计声音,即使您不是专业的声音编辑,也可以达到专业相似的效果. 打开Boom 3D ...

  4. Siki_Unity_1-3_Unity零基础入门_古迹探险

    1-3 Unity零基础入门 古迹探险 任务1/2:资料下载 链接:https://pan.baidu.com/s/1jHVymNk 密码:rbob 任务3:工程的创建和打开 Project:古迹探险 ...

  5. 从零基础入门JavaScript(1)

    从零基础入门JavaScript(1) 1.1  Javascript的简史 1995年的时候   由网景公司开发的,当时的名字叫livescript    为了推广自己的livescript,搭了j ...

  6. Cloudera Manager、CDH零基础入门、线路指导 http://www.aboutyun.com/thread-9219-1-1.html (出处: about云开发)

    Cloudera Manager.CDH零基础入门.线路指导http://www.aboutyun.com/thread-9219-1-1.html(出处: about云开发) 问题导读:1.什么是c ...

  7. 【JAVA零基础入门系列】Day2 Java集成开发环境IDEA

    开发环境搭建好之后,还需要一个集成开发环境也就是IDE来进行编程.这里推荐的IDE是IDEA,那个老掉牙的Eclipse还是先放一边吧,(手动滑稽). IDEA的下载地址:http://www.jet ...

  8. 【JAVA零基础入门系列】Day4 变量与常量

    这一篇主要讲解Java中的变量,什么是变量,变量的作用以及如何声明,使用变量. 那么什么是变量?对于初学者而言,可以将变量理解为盒子,这些盒子可以用来存放数据,不同类型的数据需要放在对应类型的盒子里. ...

  9. 【JAVA零基础入门系列】Day5 Java中的运算符

    运算符,顾名思义就是用于运算的符号,比如最简单的+-*/,这些运算符可以用来进行数学运算,举个最简单的栗子: 已知长方形的长为3cm,高为4cm,求长方形的面积. 好,我们先新建一个项目,命名为Rec ...

随机推荐

  1. delphi 发送Windwos消息控制按钮(控制计算器里的某一个按钮)

    procedure TfrmMain.btnSendClick(Sender: TObject); var hCalc, h1: Cardinal; begin WinExec('calc', SW_ ...

  2. redis配置文件redis.conf的参数说明

    打开redis.conf文件: # By default Redis does not run as a daemon. Use 'yes' if you need it. # Note that R ...

  3. 为什么内部类访问的外部变量需要使用final修饰

    因为生命周期的原因.方法中的局部变量,方法结束后这个变量就要释放掉,final保证这个变量始终指向一个对象.首先,内部类和外部类其实是处于同一个级别,内部类不会因为定义在方法中就会随着方法的执行完毕而 ...

  4. eclipse使用CXF3.1.*创建webservice服务端客户端以及客户端手机APP(二)

    eclipse使用CXF3.1.*创建webservice服务端客户端以及客户端手机APP(二) 接上篇博客,本篇博客主要包含两个内容: 4.使用Android studio创建webservice客 ...

  5. ArcGIS中使用异步回调函数查询图层Graphic

    在我们的地图的操作中经常会有一些操作是需要通过画多边形或者画线来查找某一块区域内的特定的Graphics比如我们在做的交警的项目中通过框选来查找某一块区域中的摄像机,某一块区域中的警力.警情.警员等相 ...

  6. USB摄像头无法正常读取问题

    opencv读取摄像头或者视频一种是早期版本的IplImage结构体,图片就存在结构体指针IplImage*中,另一种是Mat类,两者在操作上略有差异,且opencv2都兼容这两个版本,前面的博客也说 ...

  7. bat 批量更改文件名

    通过如下批处理命令可实现批量更改文件名: @echo set DIR="%cd%" echo DIR=%DIR% set /p ext=文件类型: set /p find=要替换内 ...

  8. 如何快速访问MSDN某一个类或方法的帮助文档

    如何快速访问MSDN某一个类或方法的帮助文档? 我一般都是在Google上搜索的如"string msdn",而不是在Msdn上直接查找(你不可能知道所有的类或方法的完整命名空间) ...

  9. 配合bootstrap实现的table 嵌套table

    不要忘了引入bootstrap.css库 html部分 <div class="container"> <div class="row"> ...

  10. Java NIO2 File API介绍

    Introduction to the Java NIO2 File API GitHub NIO2中的文件API是Java 7附带的Java平台的主要新功能之一,特别是新的文件系统API的一个子集以 ...