Rendering with Meshes
翻译:clayman
定义Mesh
虽然有很多时候,你需要手动创建顶点和索引数据,但更普遍的情况是从外部的资源加载已有的顶点数据,比如从一个文件。通常我们使用.X文件来保存这些信息。在前一章里,代码的大部分都用来创建几何体了。对于简单的三角形和立方体来说这似乎是完全可行的,但设想假如用相同的方式来创建拥有上万个顶点的物体将,所花费的时间和努力都将是很可怕的。
幸运的是,Managed DirectX里有一个可以封装并且加载顶点和索引数据的对象,这就是Mesh。Mesh可以用来储存任何类型的图形数据,但主要用来封装复杂的模型。Mesh类同样也有一些用来提高渲染物体性能的方法。所有的mesh对象都包含了一个顶点缓冲和一个索引缓存,除此之外,他还包含了一个属性缓冲(attribute buffer)——我们将会在这一章的后面讨论它。
真正的mesh对象包位于Direct3D扩展库(D3DX Direct3D Extensions library)中。添加对Direct3DX.dll程序集的引用,我们将尝试着使用mesh来创建一个旋转的立方体。首先,在声明顶点缓冲和索引缓冲成员之前添加mesh成员:
private Mesh mesh = null;
mesh类有三个构造函数,但现在还不需要用到其中的任何一个。Mesh类有几个静态方法可以用来创建或加载不同的模型。首先需要注意的就是“Box”方法,就像它的名字一样,它将创建包含了一个立方体的mesh。想想看,我们立刻就能渲染这个立方体,简直完美极了^_^(注:呵呵,可以删除之前所有与顶点缓冲、索引缓冲有关的代码了)。在创建device之后添加以下代码:
mesh = Mesh.Box(device,2.0f,2.0f,2.0f);
这个方法创建了一个包含顶点和索引的mesh,并且可以渲染为一个长、宽、高都为2的立方体。它和之前用顶点缓冲手动创建的立方体大小一样。我们已经把创建物体的代码减少为一行了,不能再简单了。虽然已经创建了mesh,但可以用原来的方法来渲染它吗,还是需要另辟途径?之前,在渲染时,我们需要调用SetStreamSource来告诉Direct3D从哪一块顶点缓冲读取数据,同样还必须设置索引以及顶点格式的属性。对于渲染mesh来说,这些都是不需要的。
(tips:mesh已经内置了所有顶点缓冲、索引缓冲以及顶点格式的信息。渲染时会自动设置stream source、索引和顶点格式的属性)
那么如何渲染mesh呢?Mesh会被分为一系列的子集(subsets)(依据属性缓冲的大小来分配),同时使用一个叫做“DrawSubset”的方法来渲染。修改DrawBox方法:
private void DrawBox(float yaw,float pitch,float roll,float x,float y,float z)
{
angle += 0.01f;
device.Transform.World = (Matrix.RotationYawPitchRoll(yaw,pitch,roll) *
Matrix.Translation(x,y,z));
mesh.DrawSubset(0);
}
这里把DeawIndexedPrimitives方法改为了DrawSubset。使用Mesh类创建的普通图元总是只有一个基于0的子集。好了,这就是要让程序再次运行所作的所有改动了,出乎意料的简单。运行看看吧。
Well,再次得到了九个(在源码中是12个)旋转的盒子,但是全部变为了白色对不对?观察一下mesh中顶点的顶点格式(可以通过mesh的VertexFormat属性查看),会发现只有顶点的位置和法线数据储存在mesh中。Mesh中没有关于颜色的数据,灯光也米有打开,自然一切都是白色的。
还记得第一张中提到过,只要顶点数据包含了法线的信息,就可以使用灯光吗,既然盒子有法线数据,也许我们应该吧灯光打开。默认情况下灯光是打开的,现在可以把关闭灯光的代码删了或者设置为true。
呵呵,我们成功把白色的盒子变为黑色了-.-#。 希望你已经猜到了这是因为场景中并没有光源,所以一切都是黑色的。对于指定特定的光源而言,创建一盏能照亮整个场景的灯光将会很不错。欢迎来到环境光(ambient lighting)。
环境光为场景提供了均衡(constant)的光源。场景中所有物体都按同样的方式被照亮,因为环境光并不依赖于其它几种光源需要的因素(比如位置、方向、衰减)。甚至不需要法线数据就可以使用环境光。环境光是最高效的灯光类型,但却不能创造出真实的“世界”。但就现在而言,他就能达到我们满意的效果。在设置RendrState的地方添加如下代码:
device.RenderState.Ambient = Color.Red;
环境光完全是由ambient render state来定义的,接受一个颜色参数。这里,我们希望全局灯光是红色的,这样可以看到明显的效果。运行程序,你希望可以看到9个红色的旋转盒子,不幸的是,它们仍然为黑色。还遗漏了些什么呢?
使用材质和灯光(Using Materials and Lighting)
这里和我们以前使用灯光有什么不同呢?最大的不同点(除了使用的是mesh之外)在于顶点数珠中没有关于颜色的信息。这导致了光照失败。
为了让Direct3D正确的计算3D物体中特定点的颜色,除了灯光的颜色之外,还需要知道物体如何反射灯光的颜色。在真实的世界里,如果把红色的灯光照在淡蓝色的表面,那么它会呈献出柔和的紫色。你还需要描述我们的“表面”(我们的盒子)是如何反光的。
在Direct3D里,材质(materials)描述了这种属性。你可以指定物体如何反射环境光以及散射(diffuse)光线,镜面高光(Specular Highlights)(少后会讨论它)看起来是什么样,以及物体是否完全反射(emit)光线。在DrawBox中添加如下代码(在DrawSubset方法前):
Material boxMaterial = new Material();
boxMaterial.Ambient = Color.White;
boxMaterial.Diffuse = Color.White;
device.Material = boxMaterial;
这里创建了一个新的材质,它的环境颜色(ambient color)(注:环境颜色和环境光的颜色是不同的^_^)和散射颜色值都被设置为白色。使用白色表示它会反射所有的光线。接下来,我们把材质赋予了device的Material属性,这样Direct3D就知道渲染时使用那种材质数据。
运行程序,现在可以看到正确的结果了。修改环境光的颜色可以改变所有盒子的颜色。修改材质的环境颜色元素可以改变灯光如何照亮物体(注:后悔当年没有好好听光学课啊555~~,maya完全手册中是这样说的:环境色(ambient color),当其为黑色时,表示(环境光)不会影响材质的颜色,当环境色变浅时,它就会照亮材质,并将两种颜色混和起来,从而影响材质的颜色。如何场景中有环境光,那么这些光的颜色和亮度就会控制环境色对于最终材质颜色的影响程度)。把材质改为没有红色成分的颜色(比如绿色)会使物体再次变为黑色(注:因为此时物体不会反射红色,红色的光线被物体全部吸收了),改为含一些红色成分的颜色(比如灰色gray)会使物体呈现深灰色。
先前说过,使用这种方式渲染出来的物体不会太真实。甚至看不到每个立方体的“倒角”,好像是一些红色的类立方体斑纹一样。这是因为环境光以同样的方法来计算所有顶点。我们需要一盏真实一点点的灯,在创建环境光之后添加如下代码:
device.Lights[0].Type = LightType.Directional;
device.Lights[0].Diffuse = Color.White;
device.Lights[0].Direction = new Vector3(0,-1,-1);
device.Lights[0].Commit();
device.Lights[0].Enabled = true;
这里创建了一盏白色的方向光,照向摄像机相同的方向。现在可以看到不同方向上光影的变化了。
创建mesh的时候,有一系列物体可以使用。使用以下一种方法来创建mesh(这些方法都要求device作为第一个参数):
(以下均使用左手坐标系)
mesh = Mesh.Box(device,2.0f,2.0f,2.0f);
Width、Height、Depth分别表示盒子在X、Y、Z轴上的尺寸
mesh = Mesh.Cylinder(device,2.0f,2.0f,2.0f,36,36);
Radius1,Radius2 表示圆柱体的下底面和上底面半径,必须为非负;Length 表示圆柱体在Z方向的高度;Slices 表示沿中心轴的片段数量,Stacks 表示沿主轴的“堆数量。(注:类似于由经、纬线分成的水平和垂直方向上的块数)
mesh = Mesh.Polygon(device,2.0f,8);
Length 表示多边形每一边的长度,Sides表示有多少条边
mesh = Mesh.Sphere(device,2.0f,36,36);
Radius表示球体的半径,Slices和Stacks的含义与Cylinder的相同。
mesh = Mesh.Torus(device,0.5f,2.0f,36,18)
InnerRadius 圆环的内径,OutterRadius 圆环的外径,Sides横截面上的面数,Rings横截面上的环数,前两个值必须为非负数,后两个必须大于等于三。
mesh = Mesh.Teapot(device)
创建一个茶壶(对一个茶壶,你没有看错^_^)。
以上每一个方法都有一个能返回阾接信息(adjacency information)的重载,每个面用三个整数来做为阾接信息,指定了相阾的三个面(Adjacency information is returned as three integers per face that specify the three neighbors of each face in the mesh)。
使用Mesh渲染复杂模型
渲染茶壶虽然很有意思,但游戏里不可能只需要渲染茶壶。大量的mesh是通过艺术家使用专业的建模软件来创造的。如果你的建模软件可以导出.X文件那么恭喜你,你很幸运(Direct SDK里包含了常用建模软件的导出转换器)。
可以通过加载x文件里储存的几种数据类型来创建mesh。当然顶点和索引数据是渲染物体的最基本要求。mesh的每个子集都会关联到一种材质。每一个材质组也同样能包含纹理信息。还可以同时使用x文件和High Level Shader Language(HLSL)文件来创建mesh。HLSL是一门高级技术,我们会在后边的内容里深入讨论。
和创建“简单”图原类型的静态方法一样,Mesh类还有两个主要的静态方法可以加载外部模型。这两个方法分别是Mesh.FormFile和Mesh.FromStream。两个方法本质上来说都是一样的,stream方法有更多的重载以适应不同大小的流。最常用的重载方法如下:
public static Mesh FromFile(string filename,MeshFlags options,Device device,out GraphicsStream adjacency,out ExtendedMaterial materials,out EffectInstance effects);
public static Mesh FromStream(Stream stream,int readBytes,MeshFlags options,Device device,out GraphicsStream adjacency,out ExtendedMaterial materials, out EffectInstance effects);
第一个参数是加载为mesh的数据源。对于FromFile方法来说,他是所要加载的文件名;对于FromStream方法来说,它是所使用的流以及要读取的数据字节数。如果使用整个流的话,只要使用没有readBytes参数的重载就可以了。MeshFlags参数控制着去哪里以及如何加载数据。这个参数的值可以是通过以下值组合而来:
Mesh Flags Enumeration Values
PARAMETER VALUE
MeshFlags.DoNotClip Use the Usage.DoNotClip flag for vertex and index buffers.
MeshFlags.Dynamic Equivalent to using both IbDynamic and VbDynamic.
MeshFlags.IbDynamicUse Usage.Dynamic for index buffers.
MeshFlags.IbManaged Use the Pool.Managed memory store for index buffers.
MeshFlags.IbSoftware ProcessingUse the Usage.SoftwareProcessing flag for index buffers.
MeshFlags.IbSystemMem Use the Pool.SystemMemory memory pool for index buffers.
MeshFlags.IbWriteOnly Use the Usage.WriteOnly flag for index buffers.
MeshFlags.VbDynamic Use Usage.Dynamic for vertex buffers.
MeshFlags.VbManaged Use the Pool.Managed memory store for vertex buffers.
MeshFlags.VbSoftwareProcessing Use the Usage.SoftwareProcessing flag for vertex buffers.
MeshFlags.VbSystemMem Use the Pool.SystemMemory memory pool for vertex buffers.
MeshFlags.VbWriteOnly Use the Usage.WriteOnly flag for vertex buffers.
MeshFlags.Managed Equivalent to using both IbManaged and VbManaged.
MeshFlags.Npatches Use the Usage.NPatches flag for both index and vertex buffers. This is required if the mesh will be rendered using N-Patch enhancement.
MeshFlags.Points Use the Usage.Points flag for both index and vertex buffers.
MeshFlags.RtPatches Use the Usage.RtPatches flag for both index and vertex buffers.
MeshFlags.SoftwareProcessing Equivalent to using both IbSoftwareProcessing and VbSoftwareProcessing.
MeshFlags.SystemMemory Equivalent to using both IbSystemMem and VbSystemMem.
MeshFlags.Use32Bit Use 32-bit indices for the index buffer. While possible, normally not recommended.
MeshFlags.UseHardwareOnly Use hardware processing only.
下一个参数是渲染mesh的device。因为资源必须关联到一个device,这是个必选参数。adjacency参数是一个“out”参数,着表示在这个方法结束后adjacency会被分配并且传递出去,它将返回阾接信息。ExtendedMaterial类保存了普通的Direct3D材质和一个加载为纹理的字符串。这个字符串通常是使用的纹理或资源文件名,因为加载纹理是由程序来进行的,它也可以是任何用户提供的字符串。组后,EffectInstance参数描述了用于mesh的HLSL材质文件和值。可以根据需要选择具有不同参数的方法重载。
这里讨论了大量关于加载和渲染mesh的细节,但实际上并没有那么复杂。一开始你可能会有些担心,但看到实际代码之后,确实很简单。现在就来试试吧。首先,要确保有可以用来为不同的子集储存材质和纹理的变量成员。在声明了mesh之后添加如下代码:
private Material[] meshMaterials;
private Texture[] MeshTextures;
因为mesh中可能有许多不同的子集,所以需要分别创建一个材质和纹理的数组以满足每一个子集的需要。好了现在来添加一些真正加载mesh的方法吧,创建一个名为“LoadMesh”的函数,代码如下:
private void LoadMesh(String file)
{
``````(此处代码较多,详见源码)
}
好啦,虽然看起来比我们之前所作的简单工作吓人一点,但实际上却不是这样。首先,我们我们声明了用于保存mesh子集信息的ExtendenMaterial数组。然后,调用FromFile方法加载mesh。我们现在并不关心adjacency或HLSL参数,所以选用了不含这两个参数的重载。
加载mesh之后,需要为大量的子集储存材质和纹理信息。确定了是否有不同的子集之后,我们最终使用子集的大小为材质和纹理成员分配大小。接下来,使用循环把ExtenedMaterial中的数据拷贝到meshMaterials中。如果子集中还包含纹理信息的话,使用TextureLoader.FromFile方法来创建纹理。这个方法接受两个参数,device以及作为纹理的文件名,这个方法可要比以前使用的System.Drawing.Bitmap快许多。
为了绘制mesh,还需要添加如下方法:
private void DrawMesh(float yaw,float pitch,float roll,float x,float y,float z)
{
angle += 0.01f;
device.Transform.World = Matrix.RotationYawPitchRoll(yaw,pitch,roll)*Matrix.Translation(x,y,z);
for(int i=0;i<meshMaterials.Length;i++)
{
device.Material = meshMaterials[i];
device.SetTexture(0,meshTextures[i]);
mesh.DrawSubset(i);
}
}
你可能已经注意到,这个方法保留了DrawBox方法的签名部分。接下来为了绘制mesh,迭代所有材质,并且执行一下步骤:
1, 把保存的材质赋予device;
2, 把纹理赋予device。这里,在没有纹理的情况下,即使值为null也不会出错。
3, 根据子集的ID调用DrawSubset方法
perfect,现在我们已经完成了加载和渲染mesh的工作了。我已经制作了一个名为tiny.x的模型。添加如下代码来加载这个模型吧:
this.LoadMesh(@"..\..\tiny.x");
还需要调整一下摄像机的位置,应为只是模型看起来像除了tiny之外的任何东西。由于模型非常的大,摄像机需要退后一点,修改以下方法:
device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, this.Width / this.Height, 1.0f, 1000.0f);
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 580.0f), new Vector3(), new Vector3(0,1,0));
我们重修调增加了到后裁剪平面的距离,平且把摄像机移动的相当靠后,好了最后的任务:在渲染部分调用DrawMesh方法:
this.DrawMesh(angle / (float)Math.PI,angle / (float)Math.PI*2.0f,angle/(float)Math.PI/4.0f,0.0f,0.0f,0.0f);
最后,你还可以调整一下灯光的颜色试试。
我们又向前迈进了一大步,这可比总看着立方体旋转要有趣多了。
我们又向前迈进了一大步,这可比总看着立方体旋转要有趣多了。
下一章我们将使用Managed DirectX来写一个真正的游戏了,最然它可能看起来很简单,但毕竟是我们的第一个三维游戏^_^,第六章内容比较,大概3、4次才能翻译完^_^。
可以到这个地方去下载源码http://bbs.gameres.com/showthread.asp?threadid=24918