Chinaunix首页 | 论坛 | 博客
  • 博客访问: 588414
  • 博文数量: 208
  • 博客积分: 3286
  • 博客等级: 中校
  • 技术积分: 1780
  • 用 户 组: 普通用户
  • 注册时间: 2007-09-24 20:38
文章分类

全部博文(208)

文章存档

2012年(7)

2011年(28)

2010年(21)

2009年(76)

2008年(65)

2007年(11)

我的朋友

分类: C/C++

2008-02-20 13:01:03

第十章 使用助手类(Using the Helper Classes

 

翻译:clayman

仅供个人学习之用,勿用于任何商业用途,转载请注明作者^_^

绘制直线

         在第四章里我们就讨论过关于绘制直线的问题:使用基本图元里的line listline strip绘制直线。但是这两种直线都不能改变宽度,也没有抗锯齿功能(除非整个场景都使用了抗锯齿)。

         对于不同类型的应用程序来说,绘制直线可能是最普通常见的操作,也可能根本不需要绘制他们。无论如何,有一个方便的Line类能在任何时候满足我们的需要。为了展现绘制线条是多么方便,我们将快速写一个程序来随即绘制一些线条。

         创建一个新工程,为编写Direct3D程序做好准备。不需要再次重复这些简单的操作了吧。

public void InitializeGraphics()(略)

         protected override void OnPaint(PaintEventArgs e)

     {

         device.Clear(ClearFlags.Target,Color.Black,1.0f,0);

         device.BeginScene();

         //Draw some lines

         DrawRandomLines();

         device.EndScene();

         device.Present();

         System.Threading.Thread.Sleep(500); 

         this.Invalidate();

     }

     这里没有什么新内容。只是在最后我们让线程休眠一小段时间,这样可以看清我们所绘制的线,接下来再次开始循环。显然,还没有定义DrawRandomLines方法,添加代码:

private void DrawRandomLines()

{

     Random r = new Random();

     int numberLines = r.Next(50);

     using(Line l = new Line(device))

     {

         for(int i=0; i

         {

              int numVectors = 0;

              while(numVectors < 2)

                   numVectors = r.Next(4);

              Vector2[] vecs = new Vector2[numVectors];

              for(int inner = 0; inner < vecs.Length; inner++)

                   vecs[inner] = new Vector2(r.Next(this.Width),r.Next(this.Height));

              Color c = Color.FromArgb(r.Next(byte.MaxValue),r.Next(byte.MaxValue),r.Next(byte.MaxValue));

int width = 0;

              while(width == 0)

                   width = r.Next(this.Width / 100);

              l.Width = width;

              l.Antialias = r.Next(50) > 25 ? true : false;

              l.Begin();

              l.Draw(vecs,c);

              l.End();

         }

     }

}

     每次调用这个方法的时候,都先创建一个随机数作为所要绘制线条的数量。创建一个line对象来分别绘制每一根线条。可以为每一条线都创建一个line对象,可一个创建一个“全局”的line对象,当然前者让代码更容易看懂。

     接下来,随机选择这条线条中的点。必须保证最少有2个点。在决定了线条中将有几个点之后,根据当先窗口的高度和宽度产生随机数,作为线条中线段的终点和起点。我们还随机选择了线条的宽度以及颜色。当然,线条的宽度也是基于窗口宽度生成的。同样,是否抗锯齿也是随机选择的。可以看到没有抗锯齿的线条(特别是很宽的那种)呈明显锯齿状。最后,绘制直线。Draw方法前后的beginend方法让Direct3D知道所绘制的是直线。

    虽然这里没有提到,但还有一些其他属性可以用来控制如何绘制直线。一个名为GlLines的布尔变量可以用来选择时候绘制OpenGl风格的线条(默认值为false)。还可以使用DrawTransform方法在三维空间里绘制。

 

绘制文本

     同样,绘制文本也是前面讨论过的内容。但只学了一点点而已,这次我们将会讨论的深入一些。在前面几章里,我们知道Microsoft.DirectX.Direct3D名称空间和System.Drawing名称空间下都有一个Font类。使用如下的语句来帮助区别他们:

using Direct3D = Microsoft.DirectX.Direct3D;

这样可以把整个名称空间缩写为Direct3D。创建新工程,添加如下变量:

这里我们声明了将要在屏幕表面绘制的字体,以及一个mesh和相应的材质对象。Mesh将作为一个拉伸的三维文本模型。Angle参数用于控制3维文本的旋转。现在初始化图形:

public void InitializeGraphics()

     {

         PresentParameters presentParams = new PresentParameters ();

         presentParams.Windowed = true;

         presentParams.SwapEffect = SwapEffect.Discard;

         presentParams.AutoDepthStencilFormat = DepthFormat.D16;

         presentParams.EnableAutoDepthStencil = true;

         device = new Device(0,DeviceType.Hardware,this,CreateFlags.HardwareVertexProcessing,presentParams);

         device.DeviceReset +=new EventHandler(this.OnDeviceReset);

         OnDeviceReset(device,null);

         System.Drawing.Font localFont = new System.Drawing.Font("Arial",14.0f,FontStyle.Italic);

         mesh = Mesh.TextFromFont(device,localFont,"Managed DirectX",0.001f,0.4f);

         meshMaterial = new Material();

         meshMaterial.Diffuse = Color.Peru;

         font = new Microsoft.DirectX.Direct3D.Font(device,localFont);

}

我们创建了一个拥有深度缓冲的设备,并为他订阅了DeviceReset事件。因为每次重置设备时,只需要设置灯光和摄像机,所以我们把它放到单独的事件处理程序中。最后,创建了System.Drawing.Font对象作为2维和3维文本的基础。我们选择了14个像素大小的Arial字体。首先使用字体对象拉伸出了三维字体的mesh。我们使用了字符串“Managed DirectX”来拉伸。当然你可以使用其它任何喜欢的字符串。接下来,设置了材质的颜色,创建2维字体。

OnDeviceReset方法中设置摄像机以及灯光:

private void OnDeviceReset(object sender, EventArgs e)

     {

         Device dev = (Device)sender;

         dev.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4, this.Width/this.Height,1.0f,100.0f);

         dev.Transform.View = Matrix.LookAtLH(new Vector3(0,0,9.0f),new Vector3(),new Vector3(0,1,0));

         dev.Lights[0].Type = LightType.Directional;

         dev.Lights[0].Diffuse = Color.White;

         dev.Lights[0].Direction = new Vector3(0,0,1);

         dev.Lights[0].Update();

         dev.Lights[0].Enabled = true;

}

摄像机和灯光都是为了拉伸的三维字体才创建的。二维的字体已经经过变换而起是照亮了的。但是,拉伸的三维字体是真实的模型,所以需要设置灯光和摄像机。添加绘制三维字体的方法:

private void Draw3DText(Vector3 axis, Vector3 location)

     {

         device.Transform.World = Matrix.RotationAxis(axis,angle) * Matrix.Translation(location);

         device.Material = meshMaterial;

         mesh.DrawSubset(0);

         angle += 0.01f;

}

如你所见,我们传入mesh在世界坐标中的位置,以及旋转轴。这个方法和之前的DrawMeshff是很相似的:设置材质,绘制第一个子集。我们还增加了旋转角度,这样做的结果是动画将基于帧速率。接下来添加绘制2为字体的代码:

private void Draw2DText(string text,int x,int y,Color c)

{

     font.DrawText(null,text,new Rectangle(x,y,this.Width,this.Heightk),DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs| DrawTextFormat.WordBreak, c);

}

这里的代码也很简单吧。你可能注意到了我们把使用窗口宽度和高度创建的矩形作为参数。这样做的原因是使用了WordBreak标志,可以在文本超出了绑定的矩形范围之后自动换行。我们也希望文本中的制表符被正确的拉伸,同时,字体不会被裁减了。

有了这两个主要的绘制字体的方法,在OnPaint中添加代码:

protected override void OnPaint(PaintEventArgs e)

{

     device.Clear(ClearFlags.Target,Color.Black,1.0f,0);

     device.BeginScene();

     Draw2DText("Here's some text",10,10,Color.WhiteSmoke);

     Draw2DText("Here's some text\t\nwith\r\nhard\r\nline breaks",100,80,Color.Violet);

     Draw2DText("This\tis\tsome\ttext\twith\ttabs.",this.Width/2,this.Height - 80,Color.RoyalBlue);

     Draw2DText("If you type enough words in a single sentecne you may notice that tha text begins to warp."+

         "Try resizing the window to notice how the text changes as you size it.",this.Width/2+this.Width/4,this.Height/4,Color.Yellow);

     Draw3DText(new Vector3(1.0f, 1.0f, 0.0f), new Vector3(-3.0f, 0.0f, 0.0f));

     Draw3DText(new Vector3(0.0f, 1.0f, 1.0f), new Vector3(0.0f, -1.0f, 1.0f));

     device.EndScene();

     device.Present(); 

     this.Invalidate();

}

这里我们绘制了几种不同的字符串:包含换函符和回车符的,包含制表符的,以及长句。对于长句,我们希望他会正确的换行。(注:调试程序的时候,长句的换行总是不正确,包括作者的源码显示也不正确,书上的结图却是正确的,郁闷了-_-#

特别提示:提高字体性能

Font用于绘制文本的字体是基于纹理的。把这些字体绘制为纹理是通过GDI来完成的,相当缓慢。最好在开始时使用font类的预载方法,保证不会再运行时遇到几次这样的加载。可以调用PreloadCharacters方法来加载指定的字体,或者使用PreloadText方法加载指定的字符串.

Rendering to Surfaces

         你是否玩过那种可以打开一个倒视镜的赛车游戏?或者可以在屏幕表面显示当前赛道的赛车游戏。这些效果都是通过把同一个场景(通常使用不同的摄像机)渲染为一个纹理来实现的。事实上,这虽然听起来很复杂,却相当容易实现。再从第五章的例子开始。

         首先自然先声明将用来渲染的纹理,添加代码:

private Texture renderTexture = null;

private Surface renderSurface = null;

private RenderToSurface rts = null;

private const int RenderSurfaceSize = 128;

这里声明了用于渲染的纹理,实际所要渲染的表面,以及用于绘制表面的助手对象。我们同时还声明了将要创建的纹理大小。在InitializeGraphics方法中,订阅device reste事件来创建纹理以及表面。添加代码:

device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

device.DeviceReset +=new EventHandler(OnDeviceReset);

this.OnDeviceReset();

接下来添加事件处理程序:

private void OnDeviceReset(object sender, EventArgs e)

{

     Device dev = (Device)sender;

     if(dev.DeviceCaps.VertexProcessingCaps.SupportsDirectionalLights)

     {

         uint masLights = (uint)dev.DeviceCaps.MaxActiveLights;

         if(maxLights > 0)

         {

              dev.Lights[0].Type = LightType.Directional;

              dev.Lights[0].Diffuse = Color.White;

              dev.Lights[0].Direction = new Vector3(0,-1,-1);

              dev.Lights[0].Update();

              dev.Lights[0].Enabled = true;

         }

         if(maxLights > 1)

         {

              dev.Lights[1].Type = LightType.Directional;

              dev.Lights[1].Diffuse = Color.White;

              dev.Lights[1].Direction = new Vector3(0,-1,1);

              dev.Lights[1].Update();

              dev.Lights[1].Enabled = true;

         }

     }

     rts = new RenderToSurface(dev,RenderSurfaceSize,RenderSurfaceSize,Format.X8B8G8R8,true,DepthFormat.D16);

     renderTexture = new Texture(dev,RenderSurfaceSize,RenderSurfaceSize,1,Usage.RenderTarget,Format.X8B8G8R8,Pool.Managed);

     renderSurface = renderTexture.GetSurfaceLevel(0);

}

这里对系统作了一些检查。首先看看它是否支持方向光,如果可以,就打开这些灯光,并假设他支持足够的可用灯光。我们使用了2个方法光,分别在模型的前面和后面。

创建了灯光之后,通过之前定义的常量创建助手对象。你可能注意到了,这个构造函数所需的参数大都能从devicepresentaion parameter获得。这里使用了最常用的值,但通过presentParameter结构获得同样的值也是可以的。

最后,创建纹理。注意把Usage设置为RenderTarger,因为我们很快就要在这张纹理上渲染。所有的渲染目标纹理都必须位于默认托管内存池中。同时,通过纹理获得实际的表面。

既然这里设置好的灯光,把选来SetupCamera方法中的代码删除。接下来,添加一个方法来绘制表面。代码如下:

private void RenderIntoSurface()

{

     // Render to this surface

     Viewport view = new Viewport();

     view.Width = RenderSurfaceSize;

     view.Height = RenderSurfaceSize;

     view.MaxZ = 1.0f;

     rts.BeginScene(renderSurface, view);

     device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);

     device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, this.Width / this.Height, 1.0f, 10000.0f);

device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, -580.0f), new Vector3(), new Vector3(0, 1,0));

     DrawMesh(angle / (float)Math.PI, angle / (float)Math.PI * 2.0f, angle / (float)Math.PI / 4.0f, 0.0f, 0.0f, 0.0f);

     DrawMesh(angle / (float)Math.PI, angle / (float)Math.PI / 2.0f, angle / (float)Math.PI * 4.0f, 150.0f, -100.0f, 175.0f);

rts.EndScene(Filter.None);

}

这里和一般的渲染方法很类似。调用了BeginScene方法和EndScene方法,设置摄像机变换,绘制mesh。当绘制纹理时,实际上就是把场景中我们需要的对象渲染到纹理上。在这里,你应该注意到我们使用了同一个device,只是把它移动到了模型的另一面而已,这样就可以把模型的背面渲染为纹理。另外我们还在场景中绘制了2个模型。这样可以模拟场景中有两个模型:默认的摄像机之后还有一个模型,只能通过另一个面向相反方向的摄像机才能同时看到2个模型。

注意,BeginScene方法使用了即将要渲染的表面作为参数。因为我们是通过纹理来获得这个表面的,任何对这个表面的更新都将会映射到纹理上。EndScene方法可以把一个mipmap过滤器应用到纹理上。为了避免检测显卡的能力,暂时不使用任何过滤器。最后要注意的一点是我们改天了纹理场景的clear color。这样做可以清楚的显示出“真实”的场景和“其他的”场景。

自然,最后还需要稍微修改一下渲染方法。首先,把纹理渲染到主窗口之前对纹理进行渲染。在OnPaint方法的最前面添加如下代码:

RenderIntoSurface();

最后,可以真正把纹理显示到屏幕上了有一个我们将在以后章节讨论的Sprite类可以完成这个任务,他可以方便的使用屏幕坐标绘制纹理。在EndScene方法之前,添加如下代码:

using (Sprite s = new Sprite(device))

{

      s.Begin(SpriteFlags.None);

      s.Draw(renderTexture, new Rectangle(0, 0, RenderSurfaceSize, RenderSurfaceSize),new Vector3(0, 0, 0), new Vector3(0, 0, 1.0f), Color.White);

      s.End();

}

这段代码把纹理渲染到了屏幕的左上方,现在运行程序来看看吧。

 

渲染环境贴图(Rendering Environment Maps

         环境贴图是用来模拟反光很强烈的表面的一门技术。你因该在赛场游戏里见过这种效果,赛车表面反映出天上的云,或者冰面上映出冰球选手的影子。实现环境贴图最常见的方法就是使用立体纹理(cube texture)也称天空盒(一个有六面的立方体纹理),现在就来学习这门技术吧。

         在开始编写代码之前,创建一个新工程,做好各种必要准备。我们将用到SDK中的两个模型:一个赛车模型,一个天空盒模型(带有纹理)。接下来就可以写代码了,先添加变量:

private Mesh skybox = null;

private Material[] skyboxMaterials;

private Texture[] skyboxTexture;

private Mesh car = null;

private Material[] carMaterials;

private Texture[] carTexture;

private CubeTexture environment = null;

private RenderToEnvironmentMap rte = null;

private const int CubeMapSize = 128;

private readonly Matrix ViewMatrix = Matrix.Translation(0.0f,0.0f,13.0f);

         这里声明了将要绘制的两个模型:天空盒(“环境”)与我们希望环境所反射到的对象——车。同时还需要cube texture对象来保存环境,以及用于渲染环境贴图的助手类。

         由于并不是所有图形卡都支持立体纹理,需要对显卡做一点点检测。在InitializeGraphics方法中完成这个任务,添加代码:

public bool InitializeGraphics()

{

     // Set our presentation parameters

     PresentParameters presentParams = new PresentParameters();

     presentParams.Windowed = true;

     presentParams.SwapEffect = SwapEffect.Discard;

     presentParams.AutoDepthStencilFormat = DepthFormat.D16;

     presentParams.EnableAutoDepthStencil = true;

     // Create our device

     device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);

     device.DeviceReset += new EventHandler(OnDeviceReset);

     OnDeviceReset(device,null);

     if(!device.DeviceCaps.TextureCaps.SupportsCubeMap)

         return false;

     //load our mesh

     skybox = LoadMesh(@"..\..\skybox2.x",ref skyboxMaterials,ref skyboxTextures);

     car = LoadMesh(@"..\..\car.x",ref carMaterials,ref carTexture);

     return true;         

}

        

阅读(1070) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~