使用 XNA 框架为 Windows Phone 7 进行 3D 图形






4.96/5 (61投票s)
了解如何为 Windows Phone 7 使用 3D 图形和效果。
引言
如今,具有 3D 图形的应用程序非常受欢迎,其中最好的例子就是现代视频游戏。我们已经习惯了个人电脑(或现代游戏机)上那些以逼真的 3D 图形和视觉效果的美感而令人惊叹的游戏,而手机游戏在视觉方面则远远落后。
随着移动技术的发展,现在有能力在小巧的手机外壳中放入强大的硬件,因此,现代手机可以处理渲染相当复杂的 3D 场景的问题。
在本文中,我们将介绍 Microsoft XNA 框架,它允许您为新的 Windows Phone 7 设备开发具有 3D 图形的应用程序。
必备组件
在开始使用本文中的示例之前,请确保您的 PC 符合以下描述的要求,并且已安装所有必需的软件。
如前所述,本文将使用 XNA 框架 4.0。代码将在 Visual Studio 2010 IDE 中使用额外的 Windows Phone Developer Tools 进行开发。
Windows Phone Developer Tools 的系统要求列在该产品的发行说明中(http://download.microsoft.com/download/1/7/7/177D6AF8-17FA-40E7-AB53-00B7CED31729/Release%20Notes%20-%20WPDT%20RTM.htm);此处仅列出主要项目。
- 操作系统:Windows Vista SP2(不含 Starter 版本)、Windows 7(不含 Starter 版本)。
- 硬件:运行 Windows Phone 模拟器需要支持 DirectX 10.1 的显卡。
您可以使用 XNA 框架 4.0 在 Windows XP 上为 Windows 或 Xbox 360 开发游戏,但仅在 Windows Vista/7 上支持为 Windows Phone 7 开发。
您可以在显卡制造商的网站上找到有关您的显卡的信息。建议您更新显卡驱动程序。
您可以在 DirectX SDK 的一部分——DirectX Caps Viewer 中找到有关支持的 DirectX 版本的信息。转到DXGI 1.1 Devices\[Your Video Card Name],如果您看到 Direct3D 10.1,那么一切都会顺利。
现在我们准备安装 Windows Phone Developer Tools,您可以在 http://create.msdn.com/en-us/resources/downloads 找到它们。我也建议在安装 Windows Phone Developer Tools 之前安装最新的 DirectX SDK。
安装完成后,在 Visual Studio 中创建一个新的 Windows Phone Game (4.0) 项目。
点击“运行”按钮,如果一切正确,您应该会看到一个带有蓝色屏幕的 Windows Phone 模拟器(首次启动可能需要更长时间)。
XNA 框架基础
XNA 框架允许开发人员为 Windows、Xbox 360 和 Windows Phone 7 创建应用程序。这项技术是专门为使为各种硬件平台开发应用程序尽可能简单和方便而创建的。(在我看来,这是真实的。)
以下图表描述了 XNA 框架的工作原理。
Windows 版 XNA 框架基于 .NET Framework,而 Xbox 360 和 Windows Phone 7 版 XNA 框架基于 .NET Compact Framework。图形系统基于 DirectX 9。应用程序在名为 XNA Game Studio 的 Visual Studio 2010 扩展中开发,该扩展增加了对 XNA 框架的支持、新的项目模板和其他有用的功能。
以下图表显示了 XNA 框架的主要组成部分。
XNA 框架包含游戏开发所需的所有必要元素,例如用于处理向量和矩阵的数学库,用于统一处理各种输入控制器等的库。此外,XNA 框架还包含附加元素(扩展框架),可以解决开发游戏时开发人员面临的许多问题。
扩展框架由应用程序模型和内容管道组成。
应用程序模型 - 是应用程序的框架(模板)。每个新的 XNA Game 项目都有一个 Game1
类,该类包含一组方法。每个方法都有其自己的目的。例如,Draw
方法应用于将所有内容渲染到屏幕上,LoadContent
方法应用于加载所有游戏内容,如模型、图像、歌曲等。
该框架的目的在于开发人员无需考虑以下问题:
- 如何创建游戏循环?
- 何时需要处理用户输入?
- 如何将渲染速度与视频适配器的刷新率同步?
请看以下图表,它描述了 XNA 应用程序的流程。
内容管道 - 统一游戏内容处理。所有游戏内容都放置在一个特殊的存储区,并通过 XNA Game Studio 中包含的导入器和处理器进行处理。因此,您无需花费时间创建自己的导入器。
3D 计算机图形学基础
在我看来,3D 图形编程是一个相当复杂的主题,因此在开始开发第一个 3D 图形应用程序之前,您必须对 3D 计算机图形学的基础有一个了解。
本文的这一部分是关于 3D 图形的快速教程。
我强烈建议您在开始自己的项目之前阅读一些关于 3D 图形学的书籍和文章,本教程仅应帮助您理解本文的其余部分。如果您熟悉 3D 图形学,可以跳过本文的这一部分。
坐标系
首先要提到的是,几乎所有现代 3D 图形都是多边形图形。这意味着所有模型都由多边形(通常是三角形)组成,而三角形又由顶点组成。这些顶点位于 3D 坐标系统中。
有两种类型的坐标系:在左手坐标系中,Z 轴“朝屏幕内”方向(当 Y 轴朝上,X 轴朝右时),而在右手坐标系中则“朝外”。XNA 框架仅使用右手坐标系。
顶点操作
对顶点(以及 3D 模型)的操作通常包括更改顶点位置,这等于改变坐标系。这些操作可以用矩阵形式描述(这对硬件来说非常有用),即乘以基本的变换矩阵,例如旋转矩阵、平移矩阵和缩放矩阵。
例如,如果您想将顶点按 (dx1,dy1,dz1) 平移,然后绕 X 轴旋转 A 弧度,然后再按 (dx2,dy2,dz2) 平移,您必须将原始顶点位置向量乘以平移矩阵,然后是旋转矩阵,然后再次是平移矩阵。
请务必在矩阵形式中保持这些操作的顺序。矩阵乘法是不可交换运算。
在 XNA 框架中,这可能看起来像这样。
Vector3 position = new Vector3(x, y, y);
Matrix transformationMatrix = Matrix.CreateTranslation(dx1, dy1, dz1) *
Matrix.CreateRotationX(a) * Matrix.CreateTranslation(dx2, dy2, dz2);
Vector3 newPosition = Vector3.Transform(position, transformationMatrix);
实际上,几乎所有必要的操作都可以用平移、旋转和缩放矩阵来描述。
3D 计算机图形学的主要问题
现在,凭借一些 3D 图形理论的基础知识,我们可以继续解决 3D 计算机图形学的主要问题:如何创建我们庞大的 3D 游戏世界,然后将其渲染到 2D 屏幕上?
简而言之,我们首先必须将每个模型放置在 3D 空间中,然后在该世界的某个位置放置相机,最后将一切从相机视图区域投影到屏幕上。一切都始于我们在某些建模工具或源代码中创建我们的模型(或顶点或图元)。此时,每个顶点都位于其局部坐标系统中。
例如,当我们创建一个机器人模型时,我们通常将其放置在其局部坐标系统的中心。这个机器人(手臂、腿或头部)的每个部分都相对于模型本身进行定位。
这是局部坐标系。
然后,我们必须将此模型放置在游戏级别的某个位置。为此,我们需要平移、缩放和旋转我们的模型,使其放置在世界坐标系统中所需的位置。
这可以通过将模型的坐标乘以世界矩阵来完成。
当每个顶点都放置在所需位置后,我们需要决定渲染我们游戏世界的哪个部分。我们通常知道玩家在游戏世界中的虚拟位置,因此我们将虚拟相机放置在该位置。虚拟相机由其位置、方向和朝向描述。在这里,我们进入了相机坐标系。
实际上,转换到相机坐标系可以通过相同的平移、旋转和缩放操作来实现,这些操作共同构成了视图矩阵。
在 XNA 框架中,视图矩阵由以下参数创建:相机位置、相机目标、相机向上向量(相机朝向)。当我们有了相机视图区域后,我们将设置远近裁剪平面,以使后续操作对硬件稍微容易一些。
然后,必须将创建的截头锥体内的所有内容投影到投影平面上。这是通过将相机坐标系中的坐标乘以投影矩阵来实现的。
投影矩阵比世界矩阵或视图矩阵稍微复杂一些。现在我将不详细介绍。现在唯一需要知道的是,透视投影的矩阵可以由 XNA 框架根据视场角、纵横比以及近远平面距离创建。
由于投影平面是 2D 的,我们可以轻松地将其转换为 2D 屏幕。
总结一下:3D 顶点在 2D 屏幕上的位置可以通过将其在局部坐标系统中的位置乘以世界矩阵、视图矩阵,然后是投影矩阵来获得。
XNA 框架中的 3D 图形基础
现在我们可以开始了解 XNA 框架中 3D 图形的基础原理了。
首先,我们将把一个茶壶的 3D 模型加载到我们的项目中。
如前所述,游戏内容是通过内容管道处理的。XNA Game Studio 4.0 中有一个新的项目类型 - 内容项目。您可以在创建新 XNA 项目时在解决方案资源管理器中看到它(图片中的 WindowsPhoneGame1Content
)。
现在,将模型添加(添加 -> 现有项)到内容项目中。目前我们不看内容处理器和内容导入器的属性(可以在属性选项卡中找到)。XNA Game Studio 会在没有我们协助的情况下选择正确的导入器和处理器。我们只需要将模型与源代码中的变量关联起来并进行渲染。
我们这样操作。
Model model;
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
model = Content.Load<Model>("teapot");
}
在进入渲染方法之前,我们将设置屏幕分辨率,以设置所需的屏幕方向。
protected override void Initialize()
{
this.graphics.PreferredBackBufferHeight = 800;
this.graphics.PreferredBackBufferWidth = 480;
base.Initialize();
}
从现在开始,在模拟器的垂直方向上,3D 空间中的 Y 轴将朝上。
现在让我们进入绘图部分。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds);
Matrix view = Matrix.CreateLookAt(new Vector3(0,0,2), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45),
GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
model.Draw(world, view, projection);
base.Draw(gameTime);
}
让我们仔细看看这段新代码。
Draw
方法的第一行简单地清除了帧缓冲区,并用蓝色填充。
下一行创建世界矩阵。在这种情况下,世界矩阵设置为绕 Y 轴旋转的矩阵,以使画面更加“生动”。旋转角度等于应用程序启动以来经过的秒数。我们在世界矩阵中没有设置任何平移,这意味着模型将被放置在坐标系的中心。
之后,我们设置视图矩阵。相机位于距离坐标系中心稍远的位置,朝向观察者,并指向坐标系中心。
紧接着,我们创建投影矩阵。在这种情况下,我们设置了非常常见的值:45 度的视场角,纵横比取自当前视口。近远平面之间的距离设置得当,以便我们的模型能够处于视图区域内。
现在所有主要矩阵都已设置完毕,我们调用模型的 Draw
方法来将模型渲染到屏幕上。
我们应该在模拟器屏幕上看到以下画面。
现在,为了让画面看起来更美观,我们将稍微修改我们的矩阵。通过将世界矩阵乘以平移矩阵,我们将模型放置得更低。我们还将相机移近模型的位置。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateTranslation(0,-0.4f,0);
Matrix view = Matrix.CreateLookAt(new Vector3(0,0,1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45),
GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
model.Draw(world, view, projection);
base.Draw(gameTime);
}
我们应该得到这样的结果。
这是在 XNA 框架中渲染 3D 模型的最简单方法。如您所见,我们的模型没有光照,因此看起来不太好。我们将在稍后解决这个问题;现在让我们谈谈调用 Draw
方法时会发生什么。如果您熟悉任何旧版本的 XNA 框架,您可能会对没有遍历所有模型网格并设置效果参数的代码感到惊讶。
原因是所有这些代码现在都隐藏在模型的 Draw
方法中。另一方面,如果您使用这个新的 Draw
方法,您将失去调整效果参数的可能性。
为了再次获得这种可能性,我们将采用一种使用基本效果(这种效果实际上称为 BasicEffect
)的旧方法进行渲染。它是这样的。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
Matrix world = Matrix.CreateRotationY(
(float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateTranslation(0,-0.4f,0);
Matrix view = Matrix.CreateLookAt(new Vector3(0,0,1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45),
GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.PreferPerPixelLighting = true;
effect.LightingEnabled = true;
effect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);
effect.SpecularColor = new Vector3(1, 1, 1);
effect.SpecularPower = 24;
effect.DirectionalLight0.Direction = new Vector3(1, -1, 0);
effect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0);
effect.DirectionalLight0.SpecularColor = new Vector3(1, 1, 1);
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
base.Draw(gameTime);
}
这段代码只有在 BasicEffect
被设置为内容处理器属性中的默认效果时才会生效。
在继续之前,我们需要理解我们刚刚创建的代码,因为稍后我们会用到它。
每个模型都由网格组成,这些网格位于模型的局部坐标系统中。因此,在渲染每个网格之前,我们需要将其坐标系变换到世界空间。模型还包含骨骼,即网格的变换矩阵。
您可以在此处找到 Model 类的描述:http://msdn.microsoft.com/en-us/library/dd904249.aspx。
因此,首先,我们需要存储模型骨骼。
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
然后遍历每个网格,并使用给定的效果(通常在内容处理器的属性中设置)渲染它。我们还需要为这些效果设置所有必需的参数。主要参数是世界、视图和投影矩阵。
设置模型效果的世界矩阵时,请确保使用骨骼数组中的变换矩阵。
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
// set effect parameters
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
XNA 框架中的 Windows Phone 7 3D 效果
Windows 和 Xbox 360 的 XNA 框架完全支持自定义着色器效果,但对于 Windows Phone 7,目前仅支持五种内置效果。在本文的这一部分,我们将逐一介绍这些效果。
BasicEffect
我们在上一个示例中已经看到过 BasicEffect
的应用,现在我们将详细介绍这种效果,因为它最常用于。它也是 XNA Game Studio 中的默认效果。
BasicEffect
实现 Blinn-Phong 光照模型,支持最多三个方向光,支持逐顶点和逐像素渲染。它支持带纹理或不带纹理的模型。
让我们开始定位光源。
我们将在左边有一个红光,右边有一个蓝光,在模型前面有一个绿光。实际上,我们需要设置光的方向(而不是位置),但这里我使用位置是为了让事情更清晰一些。
我们可以在代码中这样做,只需设置 DirectionalLight0
、DirectionalLight1
和 DirectionalLight2
参数。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// TODO: Add your drawing code here
Matrix world = Matrix.CreateRotationY(
(float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateTranslation(0, -0.4f, 0);
Matrix view = Matrix.CreateLookAt(
new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45),
GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.PreferPerPixelLighting = true;
effect.LightingEnabled = true;
effect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);
effect.SpecularColor = new Vector3(1, 1, 1);
effect.SpecularPower = 24;
// Set direction of light here, not position!
effect.DirectionalLight0.Direction = new Vector3(1, -1, 0);
effect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0);
effect.DirectionalLight0.SpecularColor = new Vector3(1, 0, 0);
effect.DirectionalLight0.Enabled = true;
// Set direction of light here, not position!
effect.DirectionalLight1.Direction = new Vector3(0, -1, -1);
effect.DirectionalLight1.DiffuseColor = new Vector3(0, 1, 0);
effect.DirectionalLight1.SpecularColor = new Vector3(0, 1, 0);
effect.DirectionalLight1.Enabled = true;
// Set direction of light here, not position!
effect.DirectionalLight2.Direction = new Vector3(-1, -1, 0);
effect.DirectionalLight2.DiffuseColor = new Vector3(0, 0, 1);
effect.DirectionalLight2.SpecularColor = new Vector3(0, 0, 1);
effect.DirectionalLight2.Enabled = true;
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
base.Draw(gameTime);
}
下图显示了结果。
BasicEffect
的另一个很酷的功能是雾效果。例如,在开阔空间场景中,离相机足够远的物体应该稍微模糊一些,这时就可以使用雾效果。BasicEffect
中的雾由四个参数控制:FogColor
、FogEnabled
、FogStart
和 FogEnd
。FogEnabled
启用或禁用雾,FogColor
只是雾的颜色。FogStart
和 FogEnd
参数的含义可以通过查看下图来更好地理解。
为了在具有多个模型的场景中看到雾效果,首先我们将提取一个渲染单个茶壶的方法。我们还在其中设置了雾的参数。
private void DrawTeapot(Matrix world, Matrix view, Matrix projection, Matrix[] transforms)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.PreferPerPixelLighting = true;
effect.LightingEnabled = true;
effect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);
effect.SpecularColor = new Vector3(1, 1, 1);
effect.SpecularPower = 24;
effect.DirectionalLight0.Direction = new Vector3(1, -1, 0);
effect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0);
effect.DirectionalLight0.SpecularColor = new Vector3(1, 0, 0);
effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight1.Direction = new Vector3(0, -1, -1);
effect.DirectionalLight1.DiffuseColor = new Vector3(0, 1, 0);
effect.DirectionalLight1.SpecularColor = new Vector3(0, 1, 0);
effect.DirectionalLight1.Enabled = true;
effect.DirectionalLight2.Direction = new Vector3(-1, -1, 0);
effect.DirectionalLight2.DiffuseColor = new Vector3(0, 0, 1);
effect.DirectionalLight2.SpecularColor = new Vector3(0, 0, 1);
effect.DirectionalLight2.Enabled = true;
effect.FogEnabled = true;
effect.FogColor = new Vector3(0.1f, 0.1f, 0.1f); // Dark grey
effect.FogStart = 2;
effect.FogEnd = 5;
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
}
现在我们可以渲染几个与相机距离不同的茶壶。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// TODO: Add your drawing code here
Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateTranslation(0, -0.4f, 0);
Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
DrawTeapot(world, view, projection, transforms);
DrawTeapot(world * Matrix.CreateTranslation(0.5f, 0, -1), view, projection, transforms);
DrawTeapot(world * Matrix.CreateTranslation(-0.5f, 0, -2), view, projection, transforms);
DrawTeapot(world * Matrix.CreateTranslation(0.5f, 0, -3), view, projection, transforms);
DrawTeapot(world * Matrix.CreateTranslation(-0.5f, 0, -4), view, projection, transforms);
DrawTeapot(world * Matrix.CreateTranslation(0.5f, 0, -5), view, projection, transforms);
DrawTeapot(world * Matrix.CreateTranslation(-0.5f, 0, -6), view, projection, transforms);
base.Draw(gameTime);
}
在下图您可以看到,雾的强度随着离相机距离的增加而增加。
DualTextureEffect
DualTextureEffect
是一种简单的多纹理效果。它允许您将两个纹理应用到模型上。它还支持像 BasicEffect
一样的雾。DualTextureEffect
是一种很好的效果,因为它对硬件来说“便宜”(与其他效果相比),并且可用于应用光照贴图、蒙版、贴花、平铺等。
DualTextureEffect
使用 2X 调制,其中每个像素的颜色通过以下公式计算。
Color = Texture1.rgb * Texture2.rgb * 2
这意味着 Texture2
中的灰色不会改变基础颜色,任何更暗的颜色都会使像素变暗,依此类推。
让我们看一个简单的 DualTextureEffect
用法示例;在这里,我们将实现一个漂亮的全场景光照效果。实时创建阴影始终是硬件的繁重任务;此外,实时阴影通常会产生一些视觉瑕疵,这也不好。我们可以做的是在建模阶段计算光照贴图,并在游戏中使用这些漂亮的光照贴图。
这是我的建模工具中的场景光照图像。
在两个相邻的盒子前面有一个点光源,左边有一个方向光。DualTextureEffect
需要模型的两个纹理坐标通道。我导出了光照贴图并创建了漫反射纹理。
首先,我们将使用 BasicEffect
渲染我们的模型,以确保一切正常。
Model model;
Texture2D lightMap;
Texture2D diffuseMap;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
model = Content.Load<Model<("boxes");
lightMap = Content.Load<Texture2D>("light");
diffuseMap = Content.Load<Texture2D>("diffuse");
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// TODO: Add your drawing code here
Matrix world = Matrix.CreateRotationY(
MathHelper.ToRadians(30)) * Matrix.CreateScale(0.003f)
* Matrix.CreateTranslation(0, -0.4f, 0);
Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.TextureEnabled = true;
effect.Texture = diffuseMap;
// uncomment this to render with lightmap
//effect.Texture = lightMap;
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
base.Draw(gameTime);
}
使用两个不同纹理渲染模型将得到以下图片。
让我们开始使用 DualTextureEffect
。首先,我们需要设置正确的 Content Processor 参数。转到内容项目中模型的参数,并将 Content Processor -> Default Effect 设置为 DualTextureEffect
。
在渲染之前,我们还将创建一个特殊的 1x1 像素的灰色纹理。我们将暂时使用它来禁用其中一个纹理。
Texture2D grey;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
model = Content.Load<Model>("boxes");
lightMap = Content.Load<Texture2D>("light");
diffuseMap = Content.Load<Texture2D>("diffuse");
grey = new Texture2D(GraphicsDevice, 1, 1);
grey.SetData(new Color[] { new Color(128, 128, 128, 255) });
}
在 Draw
方法中,我们将使用 DualTextureEffect
而不是 BasicEffect
。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// TODO: Add your drawing code here
Matrix world = Matrix.CreateRotationY(
MathHelper.ToRadians(30)) * Matrix.CreateScale(0.003f) *
Matrix.CreateTranslation(0, -0.4f, 0);
Matrix view = Matrix.CreateLookAt(
new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45),
GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in model.Meshes)
{
foreach (DualTextureEffect effect in mesh.Effects)
{
effect.Texture = diffuseMap;
effect.Texture2 = lightMap;
// uncomment this to hide diffuse map
//effect.Texture = grey;
// uncomment this to hide light map
//effect.Texture2 = grey;
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
base.Draw(gameTime);
}
结果如下
AlphaTestEffect
如果您不熟悉计算机图形学,AlphaTestEffect
可能很难理解;另一方面,在某些情况下(如使用广告牌)它会非常有帮助。AlphaTestEffect
只是绘制纹理,但会跳过不通过 alpha 测试的像素。
让我们看一个例子。想象一下我们需要在屏幕上渲染同一模型的许多实例。这通常对硬件来说是一项艰巨的任务。
首先,我们将创建一个简单的每秒帧数计数器来观察性能。
SpriteFont font;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
font = Content.Load<SpriteFont>("defaultFont");
}
protected override void Draw(GameTime gameTime)
{
// TODO: Add your drawing code here
GraphicsDevice.Clear(Color.Black);
float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (seconds > 0)
{
spriteBatch.Begin();
spriteBatch.DrawString(font, Window.Title = (1f / seconds).ToString(),
Vector2.Zero, Color.White);
spriteBatch.End();
// set GraphicsDevice parameters to default after spritebatch work
GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
}
base.Draw(gameTime);
}
我们应该看到大约 30 帧,因为这是我们在 Game1
类的构造函数中设置的。这不是制作 FPS 计数器的最佳方法。最好是计算过去一秒内实际渲染的帧数。
以下方法将渲染 900 个模型。
private void SlowDraw(ref Matrix world, ref Matrix view,
ref Matrix projection, Matrix[] transforms)
{
for (float i = -2; i <= 2; i += 0.5f)
{
for (float j = 0; j < 100; j++)
{
DrawTeapot(world * Matrix.CreateTranslation(i, 0, -j),
view, projection, transforms);
}
}
}
您应该会看到性能明显下降。
在这种情况下可以使用一个有效的技巧。
- 将模型渲染一次到一个单独的纹理。
- 将此纹理复制到任何您需要的地方。
但在第二步,我们将遇到混合问题:只有代表我们模型的纹理的一部分应该被绘制,而其他像素应该被跳过。这就是 AlphaTestEffect
可以提供帮助的地方。纹理中的所有透明像素都不会显示在实际屏幕上(它们也不应该出现在深度缓冲区中)。
以下源代码创建了一个单独的渲染目标并将茶壶模型渲染到其中。
RenderTarget2D renderTarget;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
model = Content.Load<Model>("teapot");
font = Content.Load<SpriteFont>("defaultFont");
renderTarget = new RenderTarget2D(GraphicsDevice, 512, 512,
false, SurfaceFormat.Color, DepthFormat.Depth24);
}
private void DrawToRenderTarget(ref Matrix world, ref Matrix view,
ref Matrix projection, Matrix[] transforms)
{
// save main render target
RenderTargetBinding[] previousRenderTargets = GraphicsDevice.GetRenderTargets();
GraphicsDevice.SetRenderTarget(renderTarget);
// fill with transparent color before rendering model
GraphicsDevice.Clear(Color.Transparent);
DrawTeapot(world, view, projection, transforms);
// restore render target
GraphicsDevice.SetRenderTargets(previousRenderTargets);
}
现在我们有了一个包含单个茶壶的纹理在 renderTarget
中,我们将创建广告牌几何体,这些广告牌将克隆到整个屏幕。我们还将创建一个 AlphaTestEffect
变量并设置其参数,以便只显示 alpha 值大于某个值(例如 128)的像素。
AlphaTestEffect alphaTestEffect;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
model = Content.Load<Model>("teapot");
font = Content.Load<SpriteFont>("defaultFont");
renderTarget = new RenderTarget2D(GraphicsDevice, 512, 512,
false, SurfaceFormat.Color, DepthFormat.Depth24);
alphaTestEffect = new AlphaTestEffect(GraphicsDevice);
alphaTestEffect.AlphaFunction = CompareFunction.Greater;
alphaTestEffect.ReferenceAlpha = 128;
}
private void DrawBillboards(Matrix world, Vector3 cameraPosition,
Vector3 cameraTarget, Matrix view, Matrix projection)
{
int count = 900;
float width = 0.3f;
float height1 = 0.9f;
float height2 = -0.1f;
// Create billboard vertices.
VertexPositionTexture[] vertices = new VertexPositionTexture[count * 4];
int index = 0;
for (float i = -2; i <= 2; i += 0.5f)
{
for (float j = 0; j < 100; j++)
{
Matrix worldMatrix = world * Matrix.CreateTranslation(i, 0, -j);
Matrix billboard = Matrix.CreateConstrainedBillboard(
worldMatrix.Translation, cameraPosition, Vector3.Up,
cameraTarget - cameraPosition, null);
vertices[index].Position =
Vector3.Transform(new Vector3(width, height1, 0), billboard);
vertices[index++].TextureCoordinate = new Vector2(0, 0);
vertices[index].Position =
Vector3.Transform(new Vector3(-width, height1, 0), billboard);
vertices[index++].TextureCoordinate = new Vector2(1, 0);
vertices[index].Position =
Vector3.Transform(new Vector3(-width, height2, 0), billboard);
vertices[index++].TextureCoordinate = new Vector2(1, 1);
vertices[index].Position =
Vector3.Transform(new Vector3(width, height2, 0), billboard);
vertices[index++].TextureCoordinate = new Vector2(0, 1);
}
}
// Create billboard indices.
short[] indices = new short[count * 6];
short currentVertex = 0;
index = 0;
while (index < indices.Length)
{
indices[index++] = currentVertex;
indices[index++] = (short)(currentVertex + 1);
indices[index++] = (short)(currentVertex + 2);
indices[index++] = currentVertex;
indices[index++] = (short)(currentVertex + 2);
indices[index++] = (short)(currentVertex + 3);
currentVertex += 4;
}
// Draw the billboard sprites.
alphaTestEffect.World = Matrix.Identity;
alphaTestEffect.View = view;
alphaTestEffect.Projection = projection;
alphaTestEffect.Texture = renderTarget;
alphaTestEffect.CurrentTechnique.Passes[0].Apply();
GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionTexture>(
PrimitiveType.TriangleList, vertices, 0,
count * 4, indices, 0, count * 2);
}
protected override void Draw(GameTime gameTime)
{
// TODO: Add your drawing code here
Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds)
* Matrix.CreateTranslation(0, -0.4f, 0);
Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 100f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
DrawToRenderTarget(ref world, ref view, ref projection, transforms);
GraphicsDevice.Clear(Color.Black);
DrawBillboards(world, new Vector3(0, 0, 1.2f), Vector3.Zero, view, projection);
float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (seconds > 0)
{
spriteBatch.Begin();
spriteBatch.DrawString(font, Window.Title = (1f / seconds).ToString(),
Vector2.Zero, Color.White);
spriteBatch.End();
// set GraphicsDevice parameters to default after spritebatch work
GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
}
base.Draw(gameTime);
}
如您所见,帧率恢复到 30。
SkinnedEffect
SkinnedEffect
是一种与动画 3D 模型一起使用的效果。它支持 BasicEffect
的所有视觉效果,如多达三个光源、雾和纹理映射。此外,它还支持模型的蒙皮数据。
SkinnedEffect
的缺点是它不能开箱即用地支持模型动画。要实现此目标,我们需要使用特殊的 Content Processor。您可以在此处找到它:http://create.msdn.com/en-US/education/catalog/sample/skinned_model。
现在我们将基于 App Hub 中的应用程序创建一个简单的示例。
- 下载 SkinnedSample_4_0
- 将 SkinnedModel 和 SkinnedModelPipeline 项目添加到 Visual Studio 中的新解决方案。
- 将 dude.fbx 和 SkinningSample 项目的 Content 文件夹中的所有纹理添加到您的项目中。
- 将 dude.fbx 添加到 Visual Studio 中的 ContentProject(不要添加纹理,只需将它们添加到与模型相同的文件夹中)。
- 将 SkinnedModelProcessor 设置为 dude.fbx 的 ContentProcessor。
现在一切都设置好了,我们可以开始编写代码了。
实际上,这与 BasicEffect
示例几乎相同,只是我们这里有一个 AnimationPlayer
来动画我们的模型。
Model currentModel;
AnimationPlayer animationPlayer;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
currentModel = Content.Load<Model>("dude");
// Look up our custom skinning information.
SkinningData skinningData = currentModel.Tag as SkinningData;
if (skinningData == null)
throw new InvalidOperationException
("This model does not contain a SkinningData tag.");
// Create an animation player, and start decoding an animation clip.
animationPlayer = new AnimationPlayer(skinningData);
AnimationClip clip = skinningData.AnimationClips["Take 001"];
animationPlayer.StartClip(clip);
}
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
animationPlayer.Update(gameTime.ElapsedGameTime, true, Matrix.Identity);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
Matrix[] bones = animationPlayer.GetSkinTransforms();
Matrix world = Matrix.CreateScale(0.007f) *
Matrix.CreateRotationY(MathHelper.Pi) * Matrix.CreateTranslation(0,-0.2f,0);
Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 0.1f, 10);
// Render the skinned mesh.
foreach (ModelMesh mesh in currentModel.Meshes)
{
foreach (SkinnedEffect effect in mesh.Effects)
{
effect.SetBoneTransforms(bones);
effect.World = world;
effect.View = view;
effect.Projection = projection;
effect.EnableDefaultLighting();
effect.SpecularColor = new Vector3(0.25f);
effect.SpecularPower = 16;
}
mesh.Draw();
}
base.Draw(gameTime);
}
SkinnedEffect
不能为所有模型实现动画。动画必须在 3D 建模工具中为每个模型准备好。
EnvironmentMapEffect
EnvironmentMapEffect
是 XNA 框架支持的另一个很棒的效果。它允许您轻松地将环境贴图应用于模型。
环境贴图应表示为立方体贴图,可以动态地在运行时创建,或者在某些外部工具(例如 DirectX SDK 的 DirectX Texture Tool)中预先准备好并保存为 DDS 格式,然后在加载到游戏中。
环境立方体贴图存储六个单独的纹理,每个纹理代表从物体一侧对环境的投影,用于环境映射。立方体贴图应如下所示。
要将 EnvironmentMapEffect
应用于模型,请将模型的 Content Processor 的 Default Effect 属性设置为 EnvironmentMapEffect
。
我们用于渲染的代码将与之前的 BasicEffect
示例几乎相同。此外,EnvironmentMapEffect
支持 BasicEffect
的几乎所有效果,如雾、方向光等。
Model model;
TextureCube envMap;
Texture2D background;
Texture2D bunnyTexture;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
model = Content.Load<Model>("bunny");
envMap = Content.Load<TextureCube>("env");
background = Content.Load<Texture2D>("back");
bunnyTexture = Content.Load<Texture2D>("metal1");
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// Draw background
spriteBatch.Begin();
spriteBatch.Draw(background, new Rectangle(0, 0, 480, 800), Color.White);
spriteBatch.End();
// Restore default parameters for GraphicsDevice
GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
Matrix world = Matrix.CreateRotationY(MathHelper.PiOver4)
* Matrix.CreateTranslation(0, -0.4f, 0);
Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 1.2f), Vector3.Zero, Vector3.Up);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);
Matrix[] transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in model.Meshes)
{
foreach (EnvironmentMapEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.EnvironmentMap = envMap;
effect.Texture = bunnyTexture;
effect.View = view;
effect.Projection = projection;
effect.World = transforms[mesh.ParentBone.Index] * world;
}
mesh.Draw();
}
base.Draw(gameTime);
}
我们的兔子可能看起来有点太亮了。反射量可以通过效果的 EnvironmentMapAmount
参数进行更改。
例如,0.5 表示基础纹理和环境贴图将以 50/50 的比例混合。
EnvironmentMapEffect
还支持菲涅尔反射效果,在某些情况下可以使物体看起来更逼真。
EnvironmentMapEffect
的另一个有用技巧是它能够通过环境立方体贴图的 alpha 通道模拟复杂场景光照(详细描述在 http://blogs.msdn.com/b/shawnhar/archive/2010/08/09/environmentmapeffect.aspx)。
摘要
XNA 框架是为 Windows Phone 7、Windows 和 Xbox 360 创建出色 3D 游戏的强大工具。在本文中,我们回顾了 Windows Phone 7 的 3D 图形编程基础,希望这对您未来的项目有所帮助。