§5.2 3D编程

那么你如何从一个模型文件得到导入的3D数据,就像你屏幕上的rocket.x?你的屏幕只能显示2D数据;那意味着你需要一种方式来转换3D数据到你的2D屏幕。这称之为projection投影,有许多方式做到这件事。在3D游戏的早期,像ray-casting 技术被使用,也全部由CPU完成。对于屏幕的每一个像素列的上下边界经由一个简单的公式计算,这引导了第一批3D游戏,像Ultima Underground、Wolfenstein 3D,以及后来的Doom,它是相当流行的。不久之后,更加真实的游戏像Descent、Terminal Velocity、Quake等等被开发了,这些游戏使用了一种更好的方式来转换3D数据到2D。在90年代中期,3D硬件变得更加流行,并且越来越多的游戏PC突然有能力以一种非常有效率的方式来帮助多边形的渲染。幸运的是,你再也不必担心3D游戏的早期问题。今天,3D图像已经在GPU(图形卡上的graphic processing unit )上被全部完成了。GPU不仅仅渲染屏幕上的多边形和填充像素,而且执行所有的投射3D数据为2D的转换(如图5-7)。Vertex shader被用来转换所有的3D点到屏幕坐标系,pixel shader随后被用来填充所有屏幕上可视的多边形像素。pixel shader通常是shader中最有趣和最重要的部分,因为它们能很好的操作外观的输出方式(改变颜色、混合纹理、受光线和阴影影响的最终输出等等)。不支持vertex shader 和 pixel shader的旧硬件甚至不被XNA支持,因此你不必担心回落到固定功能管道,或者甚至是进行软件渲染。对于XNA你至少需要Shader Model 1.1,那意味着你至少需要一块GeForce 3 或者 ATI 8000 图形卡。

图5-7

我将在下一页中解释的代码和图片仅仅覆盖3D编程的基础,以及XNA中如何与那些矩阵一起工作。如果你想知道更多我强烈建议你阅读XNA文档,本源的DirectX 文档甚至更好,它有非常好的主题关于3D编程起步、矩阵,以及在framework 中的一切如何被处理(它和XNA非常相似)。

关于所有这些计算,我没有时间深入细节,归功于XNA Framework 的许多辅助类和方法,你不用知道太多的矩阵转换就可以前进了。投影3D数据通常由3步组成:

借助于WorldMatrix,把3D数据带入正确的3D位置。对于这个3D Studio Max中创建的rocket而言,这意味着在你想要的方向上旋转它,相应的缩放它以适应你的3D场景,然后把它定位到你想要放置的地方。你能使用Matrix 方法来帮助你完成;使用CreateRotation、CreateScale,和 CreateTranslation 方法,以及通过结果矩阵的相乘组合它们:

BaseGame.WorldMatrix =Matrix.CreateRotationX(MathHelper.Pi / 2) *Matrix.CreateScale(2.5f) *Matrix.CreateTranslation(rocketPosition);

然后使用WorldMatrix 转换3D模型的每一个单独点,把它带到你的3D世界中的正确位置。在DirectX和固定功能管道中,你不必亲自去做,不过在XNA中你一编写shader,你就不得不亲自转换所有的顶点。正如你所知道的在XNA中一切使用shader渲染,不支持固定功能管道。

屏幕上哪些可视,依赖于你的摄相机或眼睛的位置和定向(如图5-7)。一切被投影到摄像机的位置,不过你也可以旋转世界、倾斜世界,或者用你的摄像机矩阵做更加疯狂的事。你很可能只是想在begin中使用 Matrix.CreateLookAt 方法,创建你的摄像机矩阵,它随后在vertex shader中被用来把3D世界的数据转换到摄像机空间:

BaseGame.ViewMatrix =Matrix.CreateLookAt(cameraPosition, lookTarget, upVector);

最后一步是把投影过的摄像机空间数据带到你的屏幕上。这个操作还没有被完成的原因是 ViewMatrix允许视图矩阵更易于被计算和改变。可视的摄像机空间对于x和y都从-1到+1,z包含关系到摄像机位置的每个点的深度值。ProjectionMatrix 把这些数值转换到你的屏幕分辨率(例如,1024*768),并且它指明了在场景中你能看多深(近平面和远平面的数值)。当你最终pixel shader中渲染多边形时,深度在帮助你找出那个在其它物体前面的物体是非常重要(它就是所有的2D,因为你只在屏幕上渲染2D数据)。projection matrix投影矩阵被这种方式构造:

/// <summary>

/// Field of view and near and far plane distances for the

 /// ProjectionMatrix creation.

/// </summary>

 private const float FieldOfView = (float)Math.PI / 2,NearPlane = 1.0f,FarPlane = 500.0f;…

aspectRatio = (float)width / (float)height;

BaseGame.ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(FieldOfView, aspectRatio, NearPlane, FarPlane);

在XNA中有一个很重要的改变应该被提及到:在DirectX中所有的 Matrix 方法、指南和范例使用了左手系矩阵,不过在XNA中的一切是右手系;没有方法来创建任何左手系矩阵。如果你创建了一个新游戏或者如果你是3D编程的新手,这对你没什么大不了的,但是如果你有旧代码或者旧模型,你可能因为3D数据被用到而发生问题。在右手系环境中使用左手系数据意味着一切看起来转向里边,如果你修改你的矩阵。在XNA中Culling剔除也是其他方式;它工作在逆时针方向,他也是数学上的正确方式。DirectX 默认使用顺时针culling剔除。如果你的模型是从里向外,并且你的选择从顺时针到逆时针,它们将再次看上去正确;现在它们仅仅被镜像了(3D 数据仍然是左手系)。

如果你导入.x文件,这个默认是应用左手系的,以上就尤其正确(如图5-8)。你要么亲自转变所有的矩阵或点,要么就生活在被镜像的事实中。在本书中,你将大量使用.x文件,它们是左手系(像来自于Rocket Commander的Rocket模型)还是右手系(像这里所有的新创建的3D内容文件)没什么关系,因为你不关心左手系数据数据是否被镜像。如果你必须确定所有数据在你的游戏中被正确校直,请确保你有一种可输出格式的所有3D数据,以便于你后面重新导出任何错误数据,而不是必须亲自在你的代码中修改之。

图5-8

 你很可能从学校中记得左手坐标系以左手被命名,左手被用来展示给你X(拇指)、Y(食指)、Z(中指)轴......只要举起你的左手,这三个手指彼此校直90度,你就有了左手坐标系。在学校中,数学和物理都只使用右手坐标系,如果你用右手写字,举起你的左手来展示你这些轴,你会全部弄错,如果你在考试中的整个解决方案正好基于它就一点也不酷了。

总之,右手坐标系要酷得多,因为几乎所有3D模型程序和右手系数据一起工作,除了游戏大多数程序也和右手系数据一起工作。右手坐标系能被你用右手展示X(拇指)、Y(食指)、Z轴(中指)。如图5-9。

图5-9

看上去几乎一样,不过Z轴朝向你始终不自然,如果以前你用左手坐标系。另一个为什么的原因可能是有益于旋转系统,让它有和3D模型程序同样的运转方式,在3D模型程序中Z轴指向上。我也喜欢X和Y躺在地上安置3D数据,以及构建3D地形(如图5-10) 。

图5-10

关于3D到2D的转换,以及坐标系,谈论得够多了。关于基础的渲染3D模型的3D计算,你现在应该知道得够多了。如果你还不自信,并且想学习更多关于3D数学和程序,请拾起一本关于此的好书或者在Internet阅读更多。许多好网站和指南在那里可以获得;只要搜索3D programming。

现在你现在你应该已经把前面讨论过的Rocket.x模型导入到你的工程项目中(使用Content文件夹来维护代码,以及被隔离的内容数据)。

渲染模型数据不是很复杂,不过在XNA的Model类中没有渲染方法,而对于你的单元测试,你想有这样一个方法。要完成这个目的,并且易于以后扩展模型渲染,优化它,以及添加更多的炫酷的特效,你就得编写自己的Model类(类似于你正好写过的Texture 类)。它在内部使用XNA的Model类,给你提供一个Render方法,并且自动为你处理所有的缩放比例问题、设置所有必需的矩阵。

仅仅为你当前正在开发的类编写另一个单元测试,就是一个好的实践,即使另一个单元测试已经包含了基本功能。在这个类中,单元测试能让你得到更多的特别考虑和更多的细节检查。对于快速检查新数据是否合法也是很有用的,新数据正好经由单元测试中的内容加载来交换的。这里是非常简单,但是对于Model类仍然非常有用的单元测试:

public static void TestRenderModel() {

 Model testModel = null;

TestGame.Start("TestRenderModel",delegate {

 testModel = new Model("Rocket");

},

 delegate{

testModel.Render(Matrix.CreateScale(10) );

}

);

} /} // TestRenderModel()

如此,你仅仅是在content系统中加载一个名为Rocket的测试模型,然后每一帧都在3D场景中以10倍的缩放率渲染它。所有其他原料都在别处处理,你不必担心。这包括了设置shader、更新矩阵,确定正在渲染的3D数据是正确的渲染状态,等等。

在Model类唯一有趣的方法(如图5-11)是Render方法。

图5-11

这个构造器只是从content中加载model模型,并且设置一些内部变量来确定你获得当前对象的缩放率和矩阵,让该对象易于适应你的世界:

/// <summary>

/// Create model

/// </summary>

/// <param name="setModelName" /> Set Model Filename

public Model(string setModelName) {

name = setModelName;

xnaModel = BaseGame.Content.Load<XnaModel>(@"Content\" + name);

// Get matrices for each mesh part

transforms = new Matrix[xnaModel.Bones.Count];

xnaModel.CopyAbsoluteBoneTransformsTo(transforms);

// Calculate scaling for this object, used for rendering.

scaling = xnaModel.Meshes[0].BoundingSphere.Radius *transforms[0].Right.Length();

 if (scaling == 0) scaling = 0.0001f;

// Apply scaling to objectMatrix to rescale object to size of 1.0

objectMatrix *= Matrix.CreateScale(1.0f / scaling);

} // Model(setModelName)

从content文件夹中加载xnaModel的实例之后,转换就被预计算了 因为这里不支持任何活动的数据。变换矩阵列表负责每个模型mesh部分的渲染矩阵,这些mesh是你必须渲染的(rocket恰好由一个仅使用了一个effect特效的mesh部分组成)。这个转换是通过在3D创作程序中的3D Modeler 被设置的,并且以3D Modeler 希望的方式排列你的模型。然后你决定缩放率,确定它不为零,因为除以零无意义。最后,你要计算object目标矩阵,这个矩阵将用来混合transform matrice和render matrix 。

Render方法现在只是遍历你模型的所有mesh(为了好的执行效能总是确保你拥有尽可能少的mesh),以及为每一个mesh计算其世界矩阵。然后每一个所使用的特效伴随着当前世界、view视图、投影矩阵值还有一些其他的动态数据,诸如光线方向,而被更新,然后你很好前进了。MeshPart.Draw 方法以你早前进行线条渲染同样的方式调用模型内部的shader。下两个章节更深入细节的谈论这个过程,并且当你以自己的方式重新实现在每一帧渲染许多3D模型的时候,要更有效率得多。

/// <param name="renderMatrix" /> Render matrix

public void Render(Matrix renderMatrix) {

 // Apply objectMatrix

renderMatrix = objectMatrix * renderMatrix;

// Go through all meshes in the model

 foreach (ModelMesh mesh in xnaModel.Meshes)

{

// Assign world matrix for each used effect

BaseGame.WorldMatrix =transforms[mesh.ParentBone.Index] *renderMatrix;

// And for each effect this mesh uses (usually just 1,multimaterials

// are nice in 3ds max, but not efficiently for rendering stuff).

foreach (Effect effect in mesh.Effects) {

// Set technique (not done automatically by XNA framework).

effect.CurrentTechnique = effect.Techniques["Specular20"];

// Set matrices, we use world, viewProj and viewInverse in

// the ParallaxMapping.fx shader

effect.Parameters["world"].SetValue(BaseGame.WorldMatrix);

// Note: these values should only be set once every frame!

// to improve performance again, also we should access them

// with EffectParameter and not via name (which is slower)!

// For more information see Chapter 6 and 7.

effect.Parameters["viewProj"].SetValue(BaseGame.ViewProjectionMatrix);

effect.Parameters["viewInverse"].SetValue(BaseGame.InverseViewMatrix);

// Also set light direction (not really used here, but later)

effect.Parameters["lightDir"].SetValue(BaseGame.LightDirection);

}

// foreach (effect)

// Render with help of the XNA ModelMesh Draw method, which goes

// through all mesh parts and renders them (with vertex and index // buffers).

mesh.Draw();

} // foreach (mesh)

}

// Render(renderMatrix)

正如你能看到的,代码中有相当多的注释,在我写的所有方法中你会发现它有点儿长。当你想改变某事、为了新的需求重构代码,或者只是优化你的代码的时候,我认为快速理解后面代码是很重要的。对于大多数程序员这是艰难的一步;我们都很懒,要理解这些注释多么的有用会花大量的时间,不仅仅如果其他人阅读你的代码这样,而且如果你返回代码的一部分也这样。也请你尝试用英语写下注释,使用一种简化阅读过程的语言,并且在注释中不使用代码。

你从单元测试中传递的render matrix(它仅仅是一个带有因数为10的缩放率矩阵)现在和目标矩阵object matrix做乘法,这样再次通过必须带给对象的因子缩放render matrix渲染矩阵到size 1,所以你的rocket现在是size 10单位 你的3D世界。然后你遍历每个模型mesh(你的rocket只有一个mesh),并且通过使用pre-calculated transforms matrix 列表和render matrix对这个mesh计算世界矩阵。在 vertex shader中通过使用world matrix 你现在能确定你的 rocket中每一个单独的3D 点以你想要的方式被转换。在调用这个方法之前,view matrix 和 projection matrix 很显然也必定被设置,因为 vertex shader 期待所有这些值能转换3D 数据给 pixel shader 在屏幕上渲染它。

对于每一个模型mesh,XNA模型储存所有使用的特效,对于一个单独的mesh你可以有复合的特效,不过对于大多数你将使用的模型通常也只有一个特效。你将稍后看到渲染大量3D数据最有效率的方式,就是一长段时间成串地使用相同的shader渲染它们,然后立刻在屏幕上冲洗一切。这允许你每一帧渲染成千个对象,有一个非常高的帧率。对于更多关于shader的信息,以及如何高效率的和它们工作,请阅读第六章和第七章。

不过在这里你能渲染mesh之前,你必须首先设置使用的技术,不能通过XNA自动去做,即使它被模型艺术师设定了。你将在第七章看到如何围绕这个问题。然后你设置shader期待的世界矩阵、view矩阵、投影矩阵这些值。举个例子,view matrix 从来不被直接需求,你仅仅使用视图逆矩阵view inverse matrix 来算出 camera 位置,并且对于特殊的光照计算使用它。 所有这些 shader 参数以 effect 类的Parameters 属性被设置,Parameters 属性使用这些参数的名称。对于设置 effect 参数这不是一个非常有效率的方式,因为使用字符串来访问数据有一个巨大的效能惩罚 。你将稍后发现做这件事更好的一些方式,不过现在运行正常,在mesh.Draw方法被调用之后,你应该能从单元测试中看到你的小火箭(如图5-12)。

图5-12

 测试其他模型Testing Other Models

如果在你的磁盘有其他.x文件,或者如果你只是想测试来自于Rocket Commander游戏或者它的许多游戏mod之一的其他.x文件,你能通过拖进一个.x文件 到content 文件夹快速测试。并且修改单元测试来加载这个新模型。让我们用来自于Fruit Commander mod 的Apple.x 模型来测试吧:

public static void TestRenderModel() { "TestRenderModel",delegate {

 testModel = new Model("Apple");

},

delegate {

testModel.Render(Matrix.CreateScale(10));

}

);

} // TestRenderModel()

屏幕上的结果显示如图5-13。请注意镜面颜色计算是基于shader中的normal mapping特效,which does not work correctly 这里它工作不正确是因为apple的切线数据不正确 (因为XNA模型不支持切线数据,在第七章你必须用自己的方式实现之)。

图5-13


发布时间:2008/8/30 下午3:05:40  阅读次数:8169

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号