Dynamic 2D Imposters: A Simple, Efficient DirectX 9 Implementation

来源:互联网 发布:在淘宝上怎么搜索发票 编辑:程序博客网 时间:2024/05/19 20:40


Introduction

使用 2 Dimposters 的技术正在世界上电脑游戏变成越来越流行。目标是在没有降低细节的情况下采用将部分场景通过缓存的方式保存到显存里。场景中的三维物体被采集到一些分散的图片中。然后场景中的真实三维物体在渲染的时候被缓存的Impostor替代。通过这种方式Impostor就成为了三维物体简化方式。因为现代的游戏变比较大而且要求远比以往要多的细节,开发者正在逐渐地找寻新、创新的水平LOD算法。Imposters的理念正在得到认同,而且游戏行业很受重视、很多的好文章都有简单的提及,然而我还没有见到关于Impostor系统实现的文章。这篇文章将会讲到一些基本理论,但是大部分关心的内容正如标题所述:一个简单高效的动态生成和渲染Impostor系统。

Impostor可以用在很多情况下,对于远处拥有大量静态物体作为背景的复杂内容丰富的游戏非常适合。在这些游戏了用户注意力大多集中在前景,不怎么会留意背景中的Impostor。游戏在处理和渲染集合体花的时间越少留给逻辑和AI的时间久就更充分。

Impostor的生成方式分为两大类:静态或者离线的Impostor通常是通过美工离线生成的,静态生成的Impostor已经在游戏里作为Sprites,Particle或者Billboard用了很多年了,静态的例子就是经常看到的根据角度生成Impostor构建的Billboard Tree。本文讨论的是动态生成Impostor,它在概念上和静态Impostor很类似,而且仍然是通过Billboard实现的,不同的是它是在运行时将三维物体的图像绘制到贴图上。



本文目标在于为开发者提供一个实现Impostor系统的起点。本文提供的代码都很简单,可以适应不同的环境和需求。开发一个Impostor系统本身是一件很复杂的事情,事实上是一件R&D的任务,需要通过实验来解决随之而来的数学,视觉和性能问题。阅读本文我假设你是有经验的C/ C + +编程语言,并对3D图形概念有一定的理解。你至少应该熟悉的DirectX9 API,你最好手上有SDK帮助。

理论

Impostor将三维物体渲染的图像使用在Billboard上作为对三维物体的监护。使用Impostor目的是为了减少渲染三维场景所需的时间。它的工作原理是缓存三维物体的图像然后将这些图像替换真实的三维物体。使用Impostor降低了每帧的工作量,从而在绘制的时候花更少的时间。在DirectX中,这相当于在多边形和纹理上传的,DP,渲染状态更改降低,更为重要的是,更少的CPU处理时间。



Impostor的生成包括下面这些不住

  • 确定Impostor Billboard的顶点
  • 计算Render-To-Texture需要的View和Projection矩阵
  • 初始化相应的渲染状态将Impostor绘制到贴图里

Impostor Billboard的顶点源于三维物体的包围盒,Billboard在屏幕空间完全包含三维物体。Billboard和当前摄像机的位置被用来计算所需的视图和投影矩阵。在生成一个Impostor的最后阶段是初始化渲染状态和渲染3D物体到纹理。Impostor的贴图必须包含一个alpha通道。Alpha代表在纹理冒名顶替者是:不透明的(alpha =1.0),透明的(alpha =0.0)

 

 


当Impostor被缓存,它将会用于绘制的多个帧。Impostor在最终的3D场景中通过使用alpha test或alpha blend的方式进行绘制,并放置在所替代的3D对象的位置。Impostor带来的性能提升是在尽可能多的帧尽可能地重用缓存的Impostor。因此Impostor应该只当和3D场景产生足够的错觉才有必要进行再生。有一些特定的条件,使Impostor视差变得明显。有全局条件,如Camera的可视角度和光源的颜色和方向。有局部条件,例如动画,位置和三维目标的方位。当这些条件变得如此剧烈,Impostor明显变得很二维,他们必须进行再生。Impostor和3D之间的外观差异是我称之为“Impostor Error。”

Visual QualityConcerns

Impostor要有实用价值如果只是比三维物体渲染更高效是不够的还需要看起来很不错最好是可以以假乱真。通过实践和实验大部分视觉问题都是可以解决的。



最大的视觉问题包括:

  • 实用alpha
  • 三维物体切换到Impostor时的视觉突变
  • 渲染的贴图分辨率不够会是Impostor看起来像素感很严重
  • 环境或者视窗变化太大导致Imposter error国语明显

常见的DirectX的alpha混合模式“源α-逆源阿尔法”不能在将要作为Impostor的模型上使用。这很容易使用的低细节模型非alpha版本来解决。需要注意的是alpha test可以用于Impostor实现半透明。游戏编程精粹2讨论的预乘Alpha也可以使用。

具有良好的使用时间为基础的alpha混合时视觉突变几乎无法察觉。可以在不同视角之间的Impostor进行渐变,这样几乎察觉不到Impostor正在更新,但是未在本文中实现的,因为它需要一个更大的渲染纹理开销。应该指出的是,当使用alpha blend时,Impostor必须深度排序,并按照从远到近的顺序绘制才能保证半透明正常工作。如果不需要的alpha渐变,可以使用alpha test来代替,这时就不需要进行深度排序。有很多可用的高性能的排序算法。

选择合适的纹理分辨率是很重要的。纹理分辨率过低将意味着Impostor看起来是像素化。此外,如果相机太接近Impostor或Impostor过大则一个低分辨率的纹理变得非常明显。在这篇文章中我曾凭经验选择的纹理分辨率的64由64对每个Impostor。用硬编码的分辨率的问题是,它对不同大小的物体或物体的距离不一样工作的不是很好。举例来说,更大的和更近的Impostor替者将需要更高的分辨率。为了简单起见,本文中,我将一直采用硬编码分辨率,深入讨论部分有关于运行时动态选择贴图分辨率的相关扩展。

当Impostor error变得过于极端,Impostor需要在用户觉察之前重新生成。造成Impostor error的情况或多或少取决于游戏的类型,但通常它们都是基于摄像头的可视角度的变化,对象动画,移动和旋转,以及光线变化。

在这篇文章中的条件通过以下方式处理:

•更改摄像头的可视角度和物体的位置。与阈值角度相机向量之间的角度的简单比较。如果角度大于所述阈值时,Impostor重新生成。

•照明和对象动画的变化。每一个Impostor记录上次重新生成的时间。 ”一旦这个时间已经超过x秒再重新生成。

请注意,改变物体朝向未在示例代码测试。然而,这是一个简单的条件来支持。相机向量之间的角度的测试也可以在模型空间而不是世界空间中进行,这样它考虑物体的朝向。

应当注意的一点是使用的硬件灰雾可以帮助隐藏具有冒名顶替者相关联的视觉问题。雾是你的朋友,只记得你只需要为Impostor考虑雾,而不是3D对象,否则,你可能会得到双倍的雾!


Runtime EfficiencyConcerns

是不是Impostor目的是使渲染更加高效?答案是肯定的,但是,如果一个使用一个原始的Impostor系统你可能会发现,它并没有完全解决您的效率问题。

原始的实现是使用一个纹理渲染一个Impostor的简单的方法。这个方法只能在实现Impostor原型的时候使用,因为它会对性能产生不利影响。原始的实现需要一个渲染目标切换,所产生的每一个Impostor都需要DirectX再绘制一次来替代三维物体。改变渲染目标是一个昂贵的操作。执行一些绘制一个昂贵的操作。为了最多数量的Impostor,多个Impostor纹理需要被打包到每个渲染纹理。越大渲染纹理和Impostor文理的越小越好,因为我们可以在同一个渲染纹理保存更多Impostor。以这种方式打包Impostor纹理意味着只有一个渲染目标的变化并且绘制多个Impostor的时候只需要一个DP.。因此,越多的Impostor被挤压成一个渲染纹理,会获得更好的效率。

使用贴图打包进行Impostor再生虽然很搞笑,但仍是该系统中最昂贵的部分。因此,我们希望Impostor重生尽可能少。重要的是要调整的阈值,这样,当用户明显觉察到Impostor error时才需要对Impostor进行再生。

由于Impostor定期再生,动态顶点缓冲区是用来保存Impostor顶点。动态顶点缓存是生成时加了一个动态标记来告诉DirectX应该每帧更新顶点数据。

最后,应该提及的是,该Impostor渲染纹理不应该被锁定。锁定一个渲染纹理可能会导致图形系统flush命令缓存并且同步CPU和GPU,这极大的影响了性能。

Step-by-stepImplementation

下面的章节详细描述了高效Impostor生成和渲染代码。所包含的代码清单是从随附本文的示例应用程序。示例代码已经编译了DirectX2005年10月9日的SDK,它使用了大量的DirectX的扩展库(D3DX)的。

代码清单按照一步一步的方式罗列了所有相关代码,按顺序介绍了Impostor中最重要的相关技术。第一个步骤涉及的渲染纹理的使用。然后我们看一下构建Impostor Billboard和所需的渲染到纹理矩阵。接下来,以原始的低效Impostor系统作为演示。接着,对使用打包贴图的高效的方法进行了讨论。最后,还有一个部分,演示了如何确定何时冒名顶替需要再生。

1. Render TextureAllocation

Impostor系统的第一件事情是一个渲染纹理。清单1中的代码通过D3DXCreateTexture和D3DXCreateRenderToSurface创建渲染纹理。 DirectX的代码封装在函数RenderTexture:: Initwhich被称为初始化渲染纹理。对于运行效率,当游戏启动时,这个功能才会被调用。Init的参数用来指定纹理的分辨率。纹理采用D3DFMT_A8R8G8B8的格式,其中有8位红,绿,蓝,alpha通道。 Alpha通道是必需的,因为它是定义Impostor的纹理区域的alpha mask。要创建一个渲染纹理,而不是一个正常的贴图,D3DUSAGE_RENDERTARGET被指定为Usage参数。对于Pool参数,D3DPOOL_DEFAULT的将渲染纹理放进显存,这样效率是最高的。

2. Render toTexture

渲染到纹理和渲染普通场景很相似。当渲染DirectX的场景中,渲染调用的IDirect3DDevice9功能需要在BeginScene和EndScene之间被执行。 BeginScene首先被调用。渲染然后进行使用DirectX API函数。当渲染完成EndScene被调用。当渲染到纹理, ID3DXRenderToSurface的BeginScene和EndScene被调用。在清单2中,BeginScene和EndScene功能在RenderTexture类的封装函数中被调用。

现在我们已经覆盖渲染纹理使用的基础知识和创建RenderTexture类來简化渲染纹理的使用。清单3展示了使用的渲染纹理类的一个简单的例子。

3. GeneratingImposter Billboards

Impostor的第一阶段是产生Impostor的几何体Billboard。所需要的Billboard需要在屏幕空间中完全覆盖当前视点的三维对象。



Billboard的几何形状是从三维对象的包围盒得到的。首先,包围盒被投影到屏幕空间。然后,在屏幕空间中生成一个包围3D包围盒的矩形框。这个矩形框是Impostor的几何体在屏幕空间的投影。在确定所述2D边界框时,对每个点的深度值进行比较,以确定所有点的最小深度值。最后,屏幕空间中的点和最小深度值反投影到世界空间和用作Impostor广告牌几何体。

清单4给出了ImposterVertex和Impostor结构。它们保存一个二维imposter需要的数据.ImposterVertex表示了Impostor Billboard的每个顶点。Impostor顶点具有位置,颜色和纹理坐标。该颜色用于从3D对象到Impostor过渡时淡入淡出。Impostor的结构代表一个单独的Impostor Billboard。除了其他有用的数据,它包含构成Billboard的顶点。

清单5包含CreateBillboard,它封装了计算Impostor billboard的功能.D3DXVec3ProjectArray被调用来执行投影到屏幕空间的功能。随后D3DXVec3UnprojectArray被称为从屏幕空间反投影到世界空间。还计算Billboard中心和距包围盒的最近和最远点的距离。这个数据被用于在接下来的部分,用于计算渲染到纹理所需的矩阵。从Billboard中心计算是方向矢量相机的位置。Impostor中心和摄像机方向是用来帮助确定何时Impostor需要再生。

4. Generation Viewand Projection Matrices

渲染到纹理需要视图和投影矩阵。这些矩是通过清单6中的CreateMatrices计算得到的。使用D3DXMatrixLookAtLH andD3DXMatrixPerspectiveLH生成矩阵。视图矩阵的计算与当前相机位置和相机重新定位,直接看Impostor Billboard的中心。投影矩阵是使用Billboard作为投影平面,并用清单5中的计算出来的近和远平面一起计算的。

5. Rendering aMesh to the Imposter Texture

有了Impostor Billboard和正确的矩阵,我们现在可以看一看如何渲染Impostor到纹理中。本节介绍了Impostor生成的原始方法。这里介绍的方法对于Impostor原型系统已经足够了,但它是低效的,因为它为每个Impostor都切换了渲染目标。

清单7给出了如何将一个模型渲染到Impostor贴图里。 开始渲染到渲染纹理时BeginScene被调用,然后将Impostor渲染到纹理中,接着EndScene被调用。要注意,这是低效的,因为对每个Impostor都切换了一次渲染目标。当渲染Impostor到纹理时,首先通过调用CreateImposterBillboard构建Billboar,下一步CreateMatrices被调用来计算视图​​和投影矩阵。这些矩阵通过调用一次SetTransform传给DirectX 。在渲染之前,渲染状态被初始化和背景被清除。要注意的是雾被禁用,因为它是是Impostor的Billboard要被雾化而不是渲染到纹理的模型 。

6. Rendering anImposter Billboard

继续原始的Impostor实现方法,清单8中给出呈现一个Billboard的代码。 调用SetTexture绑定渲染纹理调用DrawPrimitiveUP渲染广告牌。

在调用DrawPrimitveUP前渲染状态被初始化。应注意的是,雾在此处启用。被渲染到纹理的模型不计算雾,所以雾需要在绘制Impostor Billboard的时候被启用。还要注意使用alpha测试,使渲染的纹理,其中存在Impostor(其中的α等于1.0)的部分和将被混合到场景中。 alpha混合用来作为三维物体和二维Impostor的过渡。

同样,这种方法是非常低效的,但对于演示和原型非常有用。这是低效的,不仅由于调用DrawPrimitiveUP,最大的开销是有需每个Impostor需要一个DP。

7. Using TexturePacking for Efficient Imposter Generation and Rendering

到目前为止,我们已经开发了一些有用的功能,生成和渲染Impostor。在前面两节,展示了一个简单和原始的技术。每个Impostor使用一个纹理是非常昂贵的。当每帧生成和渲染多个Impostor,性能成本迅速增加。本节介绍的高效实现将多个Impostor渲染到一个纹理中。纹理打包有降低渲染目标切换和减少DP的双重效果。

渲染纹理被划分成多个区域每一块作为Impostor纹理。当生成多个Impostor的时候只需要一次渲染目标切换。当渲染Impostor Billboard的时候,我们都可以将所有的Billboard顶点复制到一个动态的DirectX顶点缓冲。包含在相同的纹理的Impostor Billboard可以通过一个DP完成绘制。

第一步是分割渲染纹理并每个Impostor产生纹理坐标的。纹理坐标将Impostor映射到它占据的区域。清单9给出了AllocImposters功能。函数的参数指定纹理U和V轴的Impostor数目。纹理被切分并且为每个Impostor分配纹理坐标。注意,Impostor对象是一个预分配数组。

清单9中生成的纹理坐标将被同时使用生成和渲染Impostor。当生Impostor时,我们需要调用的IDirect3DDevice9功能SetViewport设置渲染纹理的渲染区域。清单10中的函数InitImposterViewport演示了如何在纹理坐标用于设置视口。调用SetViewport后渲染结果才会被输出到渲染纹理的指定区域。

有了渲染到纹理指定区域的能力,我们现在能将多个Imposotr渲染到一个单一的纹理。要做到这一点,我们把清单7中给出的GenerateImposter功能进行调整来处理多个Impostor。清单11中给出的函数GenerateImposters是修改后的版本。需要注意的重要一点是,在执行一个生成多个Impostor循环之后,只有一个DP。注意,绘制3D物体到纹理志强InitImposterViewport被调用来设置正确区域。

随着多个Impostor打包成一个单一的纹理,我们现在来看看渲染Impostor Billboard更有效的方法。首先,动态的DirectX顶点缓存被创建。动态顶点缓冲区是Impostor渲染正确的选择。因为我们打算按照深度对Impostor进行排序,他们有可能在每一帧的渲染顺序是不一样的。因此,我们需要每帧复制顶点到一个动态顶点缓冲区中的。值得注意的是,如果你不使用深度排序,你可以尝试使用一个静态顶点缓冲的Impostor定期再生,这只是更新,但是所需要的缓存代码是超出了本文的范围。创建动态顶点缓存示例代码呈现在Listing 12 。

清单13给出了RenderImposterBillboards函数。这个函数渲染纹理中打包的所有Impostor。为了alpha混合能正确工资,Impostor会先排序,使它们从后到前绘制。将排序后的Billboard顶点,应用动态顶点缓冲。最后,渲染状态被初始化调用DrawPrimitive一个渲染Billboard。

8. Testing forImposter Regeneration

最后,我们需要确定什么时候Impostor需要再生。每一帧重新生成所有Impostor是不切实际的。这将比渲染的3D对象更加低效。缓存的Impostor应该被尽可能多的帧重复使用,尽量不重新生成,直到用户觉得Impostor error变得明显。清单14提供了两个测试,确定何时Impostor需要再生。第一个测试检查从上一次再生到现在的时间。如果这个时间大于所述阈值,则该Impostor需要再生。第二个测试检查当前的相机矢量和当前Impostor最后生成时相机矢量之间的角度。如果这个角度大于所述阈值角度,然后Impostor需要再生。在任一情况下, requiresRegenerate被设置为真。


Taking ImpostersFurther

有很多方法可以提高这里展示的Impostor的系统imposter system presented here can beimproved.

Viewing AngleTests

该代码示例不考虑改变3D对象的方向。如果考虑这一点,视角测试应在模型空间中计算,而不是世界空间。然后测试将考虑到不仅在相机视场角和模型的位置,而且该模型的方位。一个更好的测试是从相机位置向量之间的角度比较近,远点在对象的边界框。这个测试是比较昂贵,但占不仅摄像机视角,而且大小,位置,方向和距离的物体。这个测试是记录在实时渲染。

CalculatingTexture Resolution at Runtime

对于示例代码中,我选择了硬编码Impostor的纹理分辨率到应用程序中。一个更通用的Impostor系统可能要在运行时根据3D对象和屏幕分辨率的大小和距离计算的纹理分辨率。更小,更远处的物体将使用一个较小的纹理分辨率,更大和更密切的对象都将使用更大的纹理分辨率。实时渲染提出了用于计算纹理分辨率有用的公式。

支持任意Impostor纹理分辨率将有助于使Impostor有更好的视觉质量,但为任意的纹理分辨率的编码是复杂的,代码难以优化。我们不想让事情更难,所以我提出一个简单的方法。选择多个离散的纹理分辨率,例如32×32 , 64×64和128×128 。每个离散解决方案创建一个渲染纹理。在运行时,计算所需的任意分辨率,然后映射到这一点的预先定义的离散解决方案之一。

Load BalancingImposter Regeneration

当很多甚至所有Impostor,需要在一帧里重新生成时会发生什么?答案很简单,最简单的答案是使用负载均衡,来保证一帧顶多只有X个Impostor被重建。 X的值可以根据经验或通过游戏的其余部分的量来确定。正是由于这个原因,在示例代码中,它测试的再生和执行再生代码的代码解耦。通过分离这些,它是可以测试每一帧生成所有Impostor的,以确定他们是否需要再生,那么,那些需要再生的,只有过程X个被处理。

这种方式负载平衡引入了一个问题。这是可能有被标记为需要再生,但仍在等待重新生成。在某些时候,正在等待Impostor再生可能会导致Impostor error变相当明显的,并可能干扰用户沉浸在场景中。这些影响通过上次再生时相机间的距离和时间排序会有所减少。这使得最接近和最古老的Impostor得到首先再生。这确实让问题不太明显,但仍然有机会,一个用户可能会看到明显的Impostor error。对于Impostor error已经很明显的,唯一的选择就是完全恢复到3D对象。这可以通过视角测试来实现。当视角变得大于所述阈值的冒名顶替α-变淡回3D对象。

0 0
原创粉丝点击