XNA 3.0 Game Programming Recipes翻译2-1
在3D世界建立不同相机模式
在3D程序里,相机是最基本的组件之一。相机代表用户在3D世界的视点。XNA把相机所见的3D世界呈现在屏幕上。你用XNA把三维场景上的内容渲染到画面上之前您必须先指定相机的位置和查看方向。
在你创建3D程序前本章将从一些基本议题开始。
本章的第二部分包含一些高级示例。最后展示如何读进你自己的内容。
本章包含如下内容:
学习相机在三维中应用的目的以及如何定义
基于用户输入在您的三维场景中旋转和移动相机
你无权浪费显卡,确保镜头看不到的场景部分不渲染
在您的3D世界流畅演示fly-bys
使用天空盒给你的3D场景添加漂亮的背景
用post-processing效果为你游戏的最终图像增加抛光
写你自己的内容引入器,允许在内容管道使用自定义的资源文件
2-1. 建立相机:位置,目标,视景体
问题
渲染你的3D世界到屏幕前,你需要建立自己的相机。指定视图和投影矩阵。渲染前,两个矩阵都需要,因此显卡能够正确转换您的3D世界到2D画面。
方案
在3D世界建立相机指定两个矩阵。
你可以保持相机位置和方向在一个简单矩阵-视图矩阵。创建视图矩阵,XNA需要知道相机的位置,方向,和朝上的向量。
你也可以保持视景体,这是相机实际看到的3D世界部分,另一个矩阵叫投影矩阵。
如何实现
视图矩阵有相机位置和方向的定义。你可以简单的调用Matrix.CreateLookAt方法创建这个矩阵:
viewMatrix = Matrix.CreateLookAt(camPosition, camTarget, camUpVector);
这个方法用3个向量做参数:相机的位置,目标,和相机正上方向量。位置向量很直观,他指示你要把相机放在3D世界的什么位置。接下来,你需要指定相机要观察的3D世界的点。但Up向量用来做什么?
考虑下面的例子:你的眼就是相机。你试着定义一个和你的眼镜有相同位置和朝向的相机。第一个向量容易找:位置向量就是你的眼在3D场景中的位置。接下来,目标向量也不难;让我们看着图2-1的X。这时,你的相机的目标向量指向书上X的位置。这还过的去,但这时,还有很多方法可以保持你的眼在同一位置看X。

只用位置和目标向量来定义,你可以绕着这个轴转动你的眼,例如上下颠倒。当你这么做,你的眼的位置和目标仍然不变,但你看见的图像将完全不同,因为一切都转动了。这就是为什么你要定义朝上方向的向量。
一旦你知道你的相机的位置向量,朝哪里看,和相机正上的方向,就可以说相机被”唯一的定义”。视图矩阵被这3个向量唯一化,用Matrix.CreateLookAt方法构造:
Matrix viewMatrix;
Vector3 camPosition = new Vector3(10, 0, 0);
Vector3 camTarget = new Vector3(0, 0, 0);
Vector3 camUpVector = new Vector3(0, 1, 0);
viewMatrix = Matrix.CreateLookAt(camPosition, camTarget, camUpVector);
注意 虽然相机的位置和目标向量指向3D空间的真实点,Up向量指示上的方向。例如,说一个相机在位置(300,0,0)观察点(200,0,0)。如果你想指出相机的上方向向量很容易,你传递(0, 1, 0)而不必是相机上方的3D空间的点如(300, 1, 0)
注意 XNA提供向量的快捷变量,例如Vector3.Up是(0, 1, 0),Vector3.Forward是(0, 0, -1)等。
XNA另一个必须的矩阵是投影矩阵。你可以把这个矩阵想成把3D空间的点投放到2D屏幕的神奇的东西,但我宁愿你把它看作持有你的相机镜头相关信息的矩阵。
让我们先看图2-2,显示在左边的3D场景部分是相机的所见,如你所见,金字塔形状。右边,是这个金字塔的2D截面。

减去金字塔左侧的顶端后的图像被称为视景体。所有你指示XNA渲染的对象,只有那些在视景体内的才渲染到屏幕。
XNA能为你创建这样的视景体,储存在投影矩阵。你可以用Matrix.CreatePerspectiveFieldOfView方法创建:
projectionMatrix = Matrix.CreatePerspectiveFieldOfView(viewAngle, aspectRatio, nearPlane, farPlane);
第一个参数是视角。这相当于金字塔开角度的一半,正如显示在图2-2的。如果你想找出你眼镜的视角只要把手靠近眼镜,你会发现角度值约90度。PI是180度,PI/2是90度。因为你要指出一半的视角,你需要传递PI/4给第一个参数。
注意 通常,当你渲染3D场景到屏幕你会使用人类视角。尽管如此,有时,你可能想用其他视角。通常情况下,你把场景渲染到纹理,例如看到光。这时,更大的视角可能意味着更大的范围被照亮。
下一个你要指定的参数和”source”没有任何关系,这是屏幕分辨率(宽高比)。这是把三维场景渲染到2D窗口的分辨率。
理想情况下,这相当于你的后备缓冲的分辨率,可以这样得到:
float aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio;
当用方形窗口宽和高都是300,比值为1。尽管如此,渲染到800×600的全屏窗口会更大,宽屏或HDTV会更大。如果错误的传递1替换800/600,结果图像将横向拉伸。
最后两个参数和视景体有关。想象有一个物体非常接近相机。这个对象将阻碍整个视线,并有机会让整个窗口只有一个纯色。为解决这问题,XNA允许你定义一个面靠近顶端。每个在这个面和相机之间的物体不被绘制。这个距离叫做“截断距离”。这个面叫近截面,你可以指定相机和近截面的距离做第三个参数。
注意 截断一词是用来表明,一些物体不被绘制来改善程序帧速率。
同样的事发生在离相机非常远的位置;这些物体看起来很小,但他们同样花费显卡的程序时间来渲染。比这第二个面远的物体也被截断。这个面叫远截面。是您的视景体的最后边界。你可以指定相机和远截面的距离做第四个参数。
警告 就算运行一个非常简单的3D场景,也别设置原截面太远。如设置为100000这样疯狂的值。16位深度缓冲将有2^16 = 65535个可能的值。即使深度分布是线性的,如果两个对象有竞争像素且两个物体之间的距离小于100k/65535 = 1.53单位,您的显卡将无法确定哪个对象离相机更近。
在现实生活中,这是很糟糕的,因为标度是二次的,导致整个场景的3/4似乎和相机有相同的距离。因此近截面和远截面之间的距离应该在数百以下。如果深度缓冲小于16位,这个距离要更小。
一个典型的症状是您所有的物体看上去有锯齿状边缘.
用法
你想要你的视图矩形在程序的更新期被更新,因为位置和方向由用户输入决定。投影矩阵只有在窗口分辨率改变时才需要改变,例如,转到全屏模式。
一旦你计算视图和投影矩阵,你需要传递他们给你用来渲染物体的效果,就像你在下面的Draw方法代码中看到的。这使得您的显卡着色器把您的三维场景中所有顶点的的相应像素转到窗口。
代码
下面的代码展示如何创建视图矩阵和投影矩阵。说你有一个对象位于世界空间原点(0, 0, 0),你想把相机指向X方向+10单位的位置 ,Y轴为Up向量。此外,你渲染3D场景到800×600窗口并且所有三角形在相机
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
BasicEffect basicEffect;
GraphicsDevice device;
CoordCross cCross;
Matrix viewMatrix;
Matrix projectionMatrix;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
投影矩阵只在窗口分辨率发生变化时才需要改变。如果这是不会发生的,你必须确定它只有一次,例如在您的程序初始化阶段。
protected override void Initialize()
{
base.Initialize();
float viewAngle = MathHelper.PiOver4;
float aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio;
float nearPlane =
float farPlane =
projectionMatrix = Matrix.CreatePerspectiveFieldOfView(viewAngle, aspectRatio, nearPlane, farPlane);
}
protected override void LoadContent()
{
device = graphics.GraphicsDevice;
basicEffect = new BasicEffect(device, null);
cCross = new CoordCross(device);
}
protected override void UnloadContent()
{
}
用户输入来移动相机这使你要改变视图矩阵,一个想法是在更新阶段。
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
Vector3 camPosition = new Vector3(10, 10, -10);
Vector3 camTarget = new Vector3(0, 0, 0);
Vector3 camUpVector = new Vector3(0, 1, 0);
viewMatrix = Matrix.CreateLookAt(camPosition, camTarget, camUpVector);
base.Update(gameTime);
}
传递视图和投影矩阵给Effect将绘制场景。这里,我渲染一个2D截面,你可以看见3D世界的(0, 0, 0)原点。实际渲染代码在CoordCross.cs文件
protected override void Draw(GameTime gameTime)
{
device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0);
basicEffect.World = Matrix.Identity;
basicEffect.View = viewMatrix;
basicEffect.Projection = projectionMatrix;
basicEffect.Begin();
foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
{
pass.Begin();
cCross.DrawUsingPresetEffect();
pass.End();
}
basicEffect.End();
base.Draw(gameTime);
}
}
扩展阅读
前两个矩阵是XNA正确渲染3D场景到2D窗口所需要的所有矩阵。从三维到二维涉及许多挑战,这些多亏了XNA。但是,如果您希望能够创建和调试大型3D程序,清楚地了解引擎中正在发生的事情是必要的。
Z-Buffer
第一个挑战在于指明三维场景的哪些物体会占用最后图像的像素。当从3D空间到2D空间,就可能有多个对象要显示在相同的像素,像2-3所示。2D窗口像素上连一条射线到3D世界中的点,4-14中有更详细的解释。这种射线显示为虚线,如图2-3,连接两个物体。在这种情况下,像素显然应得到对象A的颜色 ,因为对象A比对象B更接近相机。

但是,如果对象B先被绘制,相应的像素帧缓存将被分配对象B的颜色。之后,物体A被绘制,而现在,显卡决定像素是否需要被物体A的颜色覆盖。
解决这个问题,图形卡内有第二个存储图像,与窗口同样大小。此刻颜色分配给一个像素的帧缓冲,相机和对象之间的距离也存储在这第二个图像。这个距离是0和1之间的值, 0对应相机和近截面之间的距离,1对应相机远和截面之间的距离。由此,这第二个图像被称为深度缓冲或Z缓冲。
因此,如何解决这问题。 目前对象B被绘制,Z缓冲被检查。 因为对象B是第一个被绘制,z缓冲区将是空的。因此,您的帧缓冲区中的所有相应像素获得对象B的颜色。同时,z缓冲中的相应像素获得一个相机和对象B之间距离的值。
下一刻,对象A被绘制。对于每个可被对象A占用的像素,z缓冲先被检查。 z缓冲区已经包含一个用对象B覆盖的值。但是储存在z-buffer中的距离比相机到A的距离大,所以显卡知道应该用对象A的颜色覆盖像素。

浙公网安备 33010602011771号