拖放窗口时自动重置Device
任何曾经使用C++或VB开发DirectX3D的人都知道,在改变窗口大小时,需要重新设置devicd,否则,DirectX3D会按原来的分辨率继续渲染场景,并且把结果拷贝到(通过拉伸)新的窗口。当通过Windows Form控件创建device时,聪明的Mamaged DirectX能发现你改变了窗口的大小,并且重置device。毫无疑问,程序总是能在正常的行为下运行,同时,你也能方便的自己重置device。在自动重置device之前,会引发一个叫做DeviceResizing的事件。捕获这个事件,把EventArgs类的Cancel成员设置为true,就能回到默认的行为,在创建device之后加上如下代码
private void CancelResize(object sender, CancelEventArgs e)
{
e.Cancel = true;
}
如你所见,这个方法只是简单的say yes,我们确实想要取消这个操作。现在订阅事件处理程序,让device知道不进行这种操作 :
device.DeviceResizing += new CancelEventHandler(this.CancelResize);(注:CancelEventHandle委托在System.ComponentModel名称空间)
运行程序,最大化窗口。三角的位置还和原来一样,不过这次看起来可怕极了。边缘都是锯齿,看起来糟糕透了。可以删除我们刚添加的代码了。Managed DirectX默认操作已经帮我们完成了这个任务,可以直接利用它。
我说:“要有光”,于是场景就有了光
我们绘制了三角形并且让他转起来了,怎样才能让他更好呢?当然是灯光。在前面曾简要的提到过它,事实上,那个时候我们完全关闭了灯光。首先要做的就是先回到那个黑暗的场景:
deviceRenderState.Lighting = true;
其实你甚至可以把整行都删了,device的默认行为是打开灯光的;只是为了让代码更清楚才保留它。现在获得了一个黑色的旋转三角。或许我们应该先定义一盏灯,再来打开它。你可能已经注意到有一个灯光数组连接到了device类上,并且这个数组的每一个元素都保存了有关灯光的大量属性。我们希望定义场景里的第一盏灯并且打开它,So,在OnPaint方法定义了三角形之后的地方(注:与sdk中有区别,不过都是一样的效果^_^)添加如下代码:
device.Lights[0].Type = LightType.Point;
device.Lights[0].Positon = new Vector3();
device.Lights[0].Diffuse = System.Drawing.Color.White;
device.Lights[0].Attenuation = 0.2f;
device.Lights[0].Range = 1000.0f;
device.Lights[0].Commit();
device.Lights[0].Enabled = true;
这些代码什么意思呢?首先申明了要创建的灯光类型,我们选择了一个在所有方向上辐射强度都一样的point light,创造了一个灯泡般的世界。当然,也有灯光沿着指定方向传播的direction light。direction light只会产生方向和颜色上的效果,忽略其他的灯光要素(比如光线的削弱(attenuation)和范围(range)),因此它也是计算量最小的灯光。最后一种能用的就是spot light了,类似于剧场里用来照亮舞台上人物的灯光。有许多的要素来描述spot light(位置,方向,角度,等等),所以它是系统里所需计算量最大的灯光。
在对灯光类型简单的讨论之后,我们继续。接下来设置灯光的位置。因为三角形的中心在(0,0,0),所以我们把灯光也放到那个位置。Vector3无参数的构造函数完成了这个任务。把灯光的漫射颜色设置为白色,这样可以正常的照亮表面。接下来设置控制灯光强度在空间改变的削弱属性。范围是灯光能产生效果的最远距离。例子里的范围已经远远超过了我们所需要的。请查阅sdk寻找有关灯光的更多内容。
最后我们把灯光提交给了device,并使它可用。如果你浏览灯光的属性,会注意到一个叫做“Deferred”的布尔值。默认情况下,这个值是false,所以你需要在准备使用灯光之前调用Commit函数。把这个值设为true,可以取消对Commit的调用,但会带来一定的性能损失。在观看灯光的效果前一定要确定它是enable和committed的。
回到程序,你发现即使我们为场景定义了灯光,三角也还是黑色的!打开了灯,却看不到光,Direct3D一定没有照亮我们的三角形,事实上,它确实没有。只有在几何体的每一个面都有一条法线(normal)时,才会进行灯光的计算。知道了这点,我们来为三角添加法线吧,这样就能在场景里看到它了。最简单的方法就是把顶点格式改为一种包含了法线的格式。碰巧我们也有这样一个结构了,改变创建三角形的代码:
CustomVertex.PositionNormalColored[] verts = new CustomVertex.PositionNormalColored[3];
verts[0].SetPositon(new Vector3(0.0f,1.0f,1.0f));
verts[0].SetNormal(new Vector3(0.0f,0.0f,-1.0f));
verts[0].Color = Ststem.Drawing.Color.White.ToArgb();
verts[1]``````
`````````
更新顶点格式来适应新的数据:
device.VertexFormat = CustomVertex.PositionNormalColored.Format;
这次最大的改变就是使用了一组包含法线的数据,并且把三角形的颜色改为白色。可以看到,我们把垂直于顶点指向外的方向定义为法矢量。因为点只是在Z平面内移动,所以沿着Z轴的负方向即是法线矢量的方向。现在程序就一切正常了。可以试着改变一下灯光的漫射颜色,看看会有怎样的变化。
还有一件应该记住的事:灯光是按照每一个顶点来计算,所以在low polygon模型(就像我们简单的三角形)的情况下,灯光可能会不太真实。我们会在后边的章节里讨论一些高级灯光技术,比如per pixel linghting。这些灯光能创造一个真实的世界。
Device State and Transforms
至今为止,示例代码里还有两项没有讨论过:设备状态(device state)以及变换(transform)。对一个设备来说,有三种不同方式的设备状态:the render state,The sampler states,和 the texture state。我们仅仅使用过the render state中的几种类型;后边的两种类型是用来处理纹理的。不要担心我们很快就会谈到纹理。The render state类规定了DirectX3D怎样来对场景进行光栅化。可以使用这个类来改变很多属性,包括我们已经使用过的灯光以及剔除。其他render state可用的选项有填充模式(fill mode) (比如wire frame mode)和各种雾化参数。我们也会来接下来的几章深入讨论。
前面提到过,变换就是用来把几何体位置从一个坐标系转到另一个坐标系的一系列矩阵。用于device上的三个主要变换就是world,view以及projection变换,但是也有一些其他的变换。比如用来控制texture stages的变换,就依赖于一个255的世界矩阵(There are transforms thst are used to modify texture stages,as well as up to 255 world matrices??).
Swapchains and RenderTargets
Device到底作了些什么工作来绘制这些三角形呢?device有一些固定的方法来处理在哪绘制并且如何绘制对象。每一个device都有一个交换链(swap chain)以及一个渲染目标(render target)。
一条交换链实际上就是一系列被控制着用来渲染的缓冲区。所有绘图过程都是在交换链中的后备缓冲区发生。当使用SwapEffect.Flip来创建一条交换链时,后备缓冲区翻转(flipped)为真正被图形卡用于读取数据的前缓冲(front buffer)。同时,三号缓冲区变为新的后备缓冲,而先前的前缓冲变为未使用过的三号缓冲区。
真正的翻转操作是通过改变图形卡当前所读的数据区、刚读过的数据区以及后备缓冲区之间的地址来实现。只有在全屏模式下,才会发生真正的翻转操作。而在窗口模式,翻转实际上只是数据的拷贝而已,因为device并没有控制着整个显示器,仅仅是一小部分而已。虽然两种模式下结果都一样。全屏模式下,有一些驱动程序也会使用翻转操作来实现SwapEffect.Discard 或者 SwapEffect.Copy。
如果使用SwapEffect.Copy或SwapEffect.Flip来创建交换链,可以确保presen()之后不会影响后备缓冲中的内容。运行时会在需要时强制创建额外的隐藏缓冲。建议使用SwapEffect.Discard来避免这种潜在的损失。这种模式允许驱动程序选择最高效的方法分配后备缓冲。使用SwapEffect.Discard时,不值得(???)在绘制新的图形前检查你是否清除了整个后备缓冲。调试模式下的运行时将会使用随机的数据来填充(刚刚使用过的)后备缓冲,让开发者检查是否忘了调用clear()。(it is worth nothing that when usuing SwapEffect.Discardyou will want to ensure that you clear the entire back buffer before starting new drawing operations. the runtime will fill the the back buffer with random data in the debug runtime so developers can see if they forget to call clear)(注:这一段内容看的不是太明白,所以把原文也给出来。Sdk中对SwapEffect枚举的解释也不是太清除。参考sdk:交换效果明确定义了调用present()之后,后备缓冲的状态。Flip交换链是一个循环的队列,可以有0~(n-1)块后备缓冲, discard交换链是一个队列, copy交换链只有一块后备缓冲。Flip中的后备缓冲在present()之后内容不会改变,所以系统需要额外内存作为后备缓冲,带来性能损失。既然后备缓冲中的内容不改变,如何构成循环队列来使用?? Discard后备缓冲中队列的长度以及怎样变化也没有明确说明,只有“The swap chain is essentially a queue where 0 always indexes the back buffer that will be displayed by the next Present operation and from which buffers are discarded once they have been displayed. An application that uses this swap effect should update an entire back buffer before invoking a Present operation that displays it.The debug version of the runtime overwrites the contents of discarded back buffers with random data, to enable developers to verify that their applications are updating the entire back buffer surface correctly.” 随机数据能帮助检查是否更新了整个后备缓冲区??既然会丢弃数据还需要调用clear??)
交换链的后备缓冲区也同样能作为渲染目标。毫无疑问,当创建了device,创建了交换链之后,渲染目标就被设置为链的后备缓冲。一个渲染目标就是能保存所执行的绘制任务的输出的表面(a surface that will hold the output of the drawing operations that you perform)。如果你创建了多个交换链的话,就必须确定预先更新了device的渲染目标。后边我们会稍后讨论这点。
(第三部分完)