批次

[Direct3D] 实现批次渲染、硬件 T&L 的渲染器和 D3DPipeline



在是否从 D3DRender 提供顶点缓存区操作给流水线时做了一些权衡,最后决定暂时使用 IDirect3DDevice9::DrawPrimitiveUP 来渲染,因为它更容易书写,而且开销是一次顶点拷贝,流水线也不用操心对缓存的使用。

D3DPipeline 并不是完整的,其涉及到从场景管理器中传递的静态场景元素列表,这些元素需要事先被整理到各个子容器以便尽可能少地调整渲染状态和写顶点缓存。这些子容器由场景管理器维护,并在适当的时候调用 Render::DrawPrimitive 进行渲染。

大多数的 los-lib 结构与 D3DX 在内存上兼容的,在保持界面独立的同时不影响性能。例如 los::blaze::Material 与 D3DMATERIAL 即是兼容的。灯光定义则存在差异,主要原因在于 los-lib 使用了各个独立的灯光类型,而 D3DLIGHT9 则放置在统一的结构当中,当然,灯光对象通常并不在多个渲染状态间改变,所以执行两种灯光类型数据的转换并不影响效率。一桢通常仅进行一次这样的转换。

另一个容易犯的错误在于几何体法线列表的索引,法线为每个顶点索引设置独立的值,而不再通过顶点列表的索引形式,尝试使用顶点索引来查找法线将得到非预期的结果。

D3DRender:

复制代码
   virtual  int DrawPrimitive( const std::vector& listVertex
        ,  const Matrix& matWorld,  const Matrix& matView,  const Matrix& matProj
        ,  const Material& material)
    {
        ptrDevice->SetTransform(D3DTS_WORLD, (CONST D3DMATRIX*)&matWorld);
        ptrDevice->SetTransform(D3DTS_VIEW, (CONST D3DMATRIX*)&matView);
        ptrDevice->SetTransform(D3DTS_PROJECTION, (CONST D3DMATRIX*)&matProj);

        ptrDevice->SetFVF(D3DFVF_XYZ | D3DFVF_NORMAL);
        ptrDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
        ptrDevice->SetMaterial((CONST D3DMATERIAL9*)&material);

         uint nPrim = ( uint)listVertex.size() /  3;
         uint nBatch = nPrim / _D3DCaps.MaxPrimitiveCount;
         uint nByteBatch =_D3DCaps.MaxPrimitiveCount * ( uint) sizeof(VertexXYZ_N) *  3;

         for ( uint idx =  0; idx < nBatch ; ++idx)
            ptrDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST
            , _D3DCaps.MaxPrimitiveCount
            , &listVertex.front()
            + idx * nByteBatch
            , ( uint) sizeof(VertexXYZ_N));

        ptrDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST, nPrim % _D3DCaps.MaxPrimitiveCount
            , &listVertex.front()
            + nBatch * nByteBatch
            , ( uint) sizeof(VertexXYZ_N));

         return  0;
    }

     virtual  int SetLights( const Lights& lights)
    {
        ptrDevice->SetRenderState(D3DRS_AMBIENT
            , (lights.globalLight.GetColor()
            * lights.globalLight.GetIntensity()).ToColor());

         uint idxLight =  0;
         for (size_t idx =  0; idx < lights.listPointLight.size(); ++idx)
        {
             const PointLight& refLight = lights.listPointLight[idx];
            D3DLIGHT9 lght;
            ::memset(&lght,  0sizeof(D3DLIGHT9));
            lght.Type = D3DLIGHT_POINT;
            lght.Range = refLight.GetDistance();
            lght.Attenuation1 =  1.0f;

            Vector3 vPos = refLight.GetPosition();
            lght.Position.x = vPos.x;
            lght.Position.y = vPos.y;
            lght.Position.z = vPos.z;

            lght.Diffuse = lght.Specular
                = *(D3DCOLORVALUE*)&(refLight.GetColor() * refLight.GetIntensity());

            ptrDevice->SetLight(idxLight, &lght);
            ptrDevice->LightEnable(idxLight++,  true);
        }

         for (size_t idx =  0; idx < lights.listParallelLight.size(); ++idx)
        {
             const ParallelLight& refLight = lights.listParallelLight[idx];
            D3DLIGHT9 lght;
            ::memset(&lght,  0sizeof(D3DLIGHT9));
            lght.Type = D3DLIGHT_DIRECTIONAL;

            Vector3 vDir = refLight.GetDirection();
            lght.Direction.x = vDir.x;
            lght.Direction.y = vDir.y;
            lght.Direction.z = vDir.z;

            lght.Diffuse = lght.Specular
                = *(D3DCOLORVALUE*)&(refLight.GetColor() * refLight.GetIntensity());

            ptrDevice->SetLight(idxLight, &lght);
            ptrDevice->LightEnable(idxLight++,  true);
        }

         for (size_t idx =  0; idx < lights.listSpotLight.size(); ++idx)
        {
             const SpotLight& refLight = lights.listSpotLight[idx];
            D3DLIGHT9 lght;
            ::memset(&lght,  0sizeof(D3DLIGHT9));
            lght.Type = D3DLIGHT_SPOT;
            lght.Range = refLight.GetDistance();
            lght.Attenuation1 =  1.0f;
            lght.Falloff =  1.0f;
            lght.Theta = refLight.GetHotspot().ToRadian();
            lght.Phi = refLight.GetFalloff().ToRadian();

            Vector3 vDir = refLight.GetDirection();
            lght.Direction.x = vDir.x;
            lght.Direction.y = vDir.y;
            lght.Direction.z = vDir.z;

            Vector3 vPos = refLight.GetPosition();
            lght.Position.x = vPos.x;
            lght.Position.y = vPos.y;
            lght.Position.z = vPos.z;

            lght.Diffuse = lght.Specular
                = *(D3DCOLORVALUE*)&(refLight.GetColor() * refLight.GetIntensity());

            ptrDevice->SetLight(idxLight, &lght);
            ptrDevice->LightEnable(idxLight++,  true);
        }

         return  0;
    }
复制代码

D3DPipeline:

复制代码
virtual  int ProcessingObject( const Object3D&  object)
    {
        ++_DebugInfo.dynamic_object_counter;

         const Model& refModel =  object.GetModel();
         const Vector3& pos =  object.GetPosition();

        Matrix mat =  object.GetTransform()
            *  object.GetOrientation().ObjectToInertial() *  object.GetAxis()
            * Matrix().BuildTranslation(pos.x, pos.y, pos.z);

         for (size_t gidx =  0; gidx < refModel.listGeometry.size(); ++gidx)
        {
             const Geometry& refGeom = refModel.listGeometry[gidx];
             const Material& refMat = refModel.listMaterial[refGeom.indexMaterial];

             // Triangle triangle;
            
// triangle.bitmap = (DeviceBitmap*)&refModel.listDeviceBitmap[refGeom.indexDeviceBitmap];

            std::vector listVertex;
            listVertex.reserve(refGeom.listIndex.size());

             for (size_t iidx =  0; iidx < refGeom.listIndex.size(); iidx +=  3)
            {
                 const Vector3& vertex0 = refGeom.listVertex[refGeom.listIndex[iidx]];
                 const Vector3& vertex1 = refGeom.listVertex[refGeom.listIndex[iidx +  1]];
                 const Vector3& vertex2 = refGeom.listVertex[refGeom.listIndex[iidx +  2]];

                Vector3 normal0 = refGeom.listNormal[iidx];
                Vector3 normal1 = refGeom.listNormal[iidx +  1];
                Vector3 normal2 = refGeom.listNormal[iidx +  2];

                listVertex.push_back(VertexXYZ_N());
                VertexXYZ_N& refV0 = listVertex.back();
                refV0.x = vertex0.x;
                refV0.y = vertex0.y;
                refV0.z = vertex0.z;
                refV0.normal_x = normal0.x;
                refV0.normal_y = normal0.y;
                refV0.normal_z = normal0.z;

                listVertex.push_back(VertexXYZ_N());
                VertexXYZ_N& refV1 = listVertex.back();
                refV1.x = vertex1.x;
                refV1.y = vertex1.y;
                refV1.z = vertex1.z;
                refV1.normal_x = normal1.x;
                refV1.normal_y = normal1.y;
                refV1.normal_z = normal1.z;

                listVertex.push_back(VertexXYZ_N());
                VertexXYZ_N& refV2 = listVertex.back();
                refV2.x = vertex2.x;
                refV2.y = vertex2.y;
                refV2.z = vertex2.z;
                refV2.normal_x = normal2.x;
                refV2.normal_y = normal2.y;
                refV2.normal_z = normal2.z;

                ++_DebugInfo.polygon_counter;
            }

            _PtrRender->DrawPrimitive(listVertex, mat, _ViewMatrix, _PerspectiveMatrix, refMat);
        }

         return  0;
    }
};
复制代码


这篇看batching段。

Direct3D的一些小贴士收藏



GPU性能调试:

通常来说,使用CPU时间事件来调试GPU是低效并且是不准确的。D3D API在多数命令下会阻塞,甚至是Draw函数。它会在一些时间片上做一些真正的工作,而这往往是不可预知的。因此,GPU的性能调试只能用PIX或者是其他专用产品,例如NVIDIA’s NVPerfHUD来进行。

显卡所用的内存:

显卡所用的内存可以分为两大类:本地的和非本地的(相对于显卡来说)。在显卡处理的某些数据类型的时候,需要本地内存,例如 帧缓冲。 非本地内存,有时也成为AGP卡槽内存(AGP aperture),可以被显卡访问的某些数据类型所在的系统内存,例如顶点缓冲。本地内存要比非本地内存快。

本地内存通常是在显卡内的,但是有些显卡可以共享系统内存,这通常是平衡速度和价格之间的选择。在这种情况下,帧缓存可以存在于系统内存中,而不是在本地内存中。这种技术下,显卡处理某些数据的速度比不使用共享内存的要慢,因为数据必须从I/O Bus(例如PCI-Express)上传输过来。但是这可以使显卡成本大大降低。在NVIDIA,这种技术被称为TurboCache,而ATI称之为HyperMemory。

着色器和着色模型:

Shader是运行在GPU上的,处理一些D3D流水管线上一些任务的程序。有三种类型的shader,他们分别对应三种可编程的stage:

Vertex shader (顶点着色器VS) stage, geometry shader (几何着色器GS) stage, 还有pixel shader(像素着色器PS) stage。其中几何着色器只能在DX10平台上使用。

着色模型(shader model)是在GPU上运行的虚拟机。每个虚拟机定义被称为一种shader profile。并且包含了特定的汇编语言。

着色器的职责:

着色器通常是流水管线中描述物体表面的部分。例如,一种看起来像木头的材质被称为木头着色器(wood shader)。而在D3D中,这些着色语言指令集可以做的事情远不止描述物体表面。他们可以用来计算光照,矩阵转换,顶点动画,进行裁切,动态生成新的几何物体,等等。在Mental ray中,shader按照职责可以划分为surface shader, light shader, shader shader, output shader等等。

在D3D中,这三种着色器的职责划分并不是很明确。例如,光照计算过可以在顶点着色器,或者是像素着色器中完成,这取决于应用程序的需求。因此,包含各种着色器的着色器集合应运而生。他们链接起来定义了一个工作流水线。

关于Direct3D 9 资源和内存类型:

D3D支持下列类型的资源:纹理(包括常规的和渲染目标render target),顶点缓冲,索引缓冲,字体,交换链(swap chain),状态组,深度模板缓冲,特效等等。

有四种内存类型(池),资源可以在这里分配:

·         默认Default:在显卡内存中,包括AGP卡槽内存和本地显存。在设备丢失之后,必须被释放,重构。

·         托管Managed:存在于系统内存中,按需拷贝到显存。

·         系统SystemMem:永远存在于系统内存中,并且不能直接用于渲染。可以当作源或者目标拷贝。例如UpdateSurface和UpdateTexture。

·         Scrach: 永远存在于系统内存中,并且不会被设备大小或格式限制,例如纹理的2的幂限制。不能把它放到显存中。

查找资源泄露:

在关闭一个基于D3D的应用程序时,D3D调试运行库会报告内存泄露。按照以下步骤定位泄漏点。

1.       在DirectX Control Panel中(通常在DXSDK安装目录中可以找到),启用“Use Debug Version of Direct3D 9”并且将Debug Output Level设置为”More”。确保Break on Memory Leaks被禁用。点击Apply。

2.       在VS中调试运行应用程序。在关闭应用程序之后,查看VS的输出窗口Direct 3D9: (WARN) : Memory Address:  00xxxxxx,  IAllocID= xx dwSize = xxxxxxxx;(pid = xxxxx)

3.       每条记录对应了一个资源泄漏,查看并记住ID,然后在DirectX Control Panel中输入ID并且点击Apply。

4.       再次运行程序,重复以上步骤。程序会在分配点中断,你可以检查哪里遗忘释放。

5.       当你调试完成之后,别忘了将Break On AllocID设置为0。

处理设备丢失(Device Lost)

一个D3D设备可以在很多情况下丢失,例如从全屏向窗口转换,一个电源管理事件,按CTRL+DEL+ALT返回Windows Security Dialog。

必须采取措施去检查一个设备是否丢失,丢失了之后如何恢复。

方法:在某些地方调用IDirect3DDevice9::TestCooperativeLevel,例如在每帧开始渲染之前调用。当发现设备丢失之后,采取下列措施:

1.       释放所有在Default内存中的资源

2.       释放其他没有和Default, Managed, SystemMem绑定的资源

3.       调用IDirect3DDevice9::TestCooperativeLevel去确认设备是否可以被重置如果能,那么调用IDirect3DDevice9::Reset 如果不能,继续等待,然后再尝试

4.       重新创建需要的资源

渲染目标和交换链(Render Targets and Swap Chains)

一个渲染目标是一个用于保存在图形流水线输出像素的表面。也就是说,它是一个颜色数组。一个设备可以有一个或者多个活动的渲染目标,可以通过SetRenderTarget来启用。一个用于渲染目标的表面只能放在Default池中,有三种渲染目标:

·         渲染目标表面Render target surfaces(通过CreateRenderTarget创建)

·         渲染目标纹理Render target textures(tongguo D3DUSAGE_RENDERTARGET标识来创建)

·         交换链Swap chains 交换链就是后备缓冲的集合,它们能够相继渲染到前缓冲,也就是屏幕上。一个在交换链中的后备缓冲可以当作一个渲染目标赋给一个设备。但是,不像其他的渲染目标,交换链可以渲染到屏幕上,因为交换链是和窗口/全屏大小绑定的。可以创建多个交换链,注意更改默认交换链大小会造成设备丢失,所以窗口程序会忽略默认的交换链,而使用一个附加的交换链来避免这个问题。渲染目标可以被锁定(用来读取),但是当这个渲染目标是活动的话,会影响系统性能。我们可以根据需要用IDirect3DDevice9::GetRenderTargetData来将一个在Default池中的渲染目标拷贝出来。可以使用IDirect3DDevice9::StrechRectangle在两个在显卡内存中的渲染目标中进行高效拷贝。

批处理(Batching)【重剑注:这个是重点

D3D的效率在很大程度上受制于传给API的几何模型数据的批次上。一个批处理就是调用一次DrawPrimitive或者DrawIndexPrimitive。在GPU可以处理数据前,CPU花相当长时间来处理每批数据。现在常见的CPU和GPU,可以参考以下数据:

·         使用DX9,CPU每秒可以处理50,000批次;使用DX10,这个数据是200,000。

·         在DX9中,处理2,000个三角形在CPU和GPU所花的时间大致相等。在DX10中,这个数据是500。简单的着色程序使这个数字增加,复杂的着色程序使这个数字减少。在CPU和GPU在同一个批次上花相同时间的情况下,实例化(Instancing)可以提高三角形的输出能力。因为以上原因,每个批次中处理数据的数量越大越好,这样能够将三角形的吞吐量最大化。

在实践中,具体有两种方式:

·         Consolidation合并:将相同性质的几何元素合并起来,通常是将一些属性进行排序的结果

·         Instancing实例化:将相同的几何物体,经过一些细微的,不同的变换后画出多个实例来。例如世界坐标系的转换和颜色转换。【重剑思考:Q:游戏里角色的护腕部位要同样的模型,不能是一个护腕,一个手套,这个就是为了Instancing?A:非也!两个护腕其实是一个模型,美术画的时候就是画了一对(左右各一个),中间就是断开的】

顶点,索引缓冲Vertex / Index Buffer

顶点和索引缓冲有两种类型:静态和动态的。

一旦创建之后,静态的缓冲使用起来比动态的快一倍。但是,动态缓冲的加锁和解锁要比静态的快,它们是为更改的每一帧设计的,通常被存储在AGP卡槽内存中。经常对静态缓冲加解锁是不明智的,因为只有等驱动完成了所有挂起的命令之后才能返回该缓冲的指针。如果经常这样做,这会导致CPU和GPU很多不必要的同步,这样性能将会变得很差。

为了得到最好的性能,必须采用动态缓存。这样驱动可以继续进行并行渲染。使用DISCARD或者是NOOVERWRITING标志可以实现这一点,这样驱动可以在更新数据的同时继续处理老的数据。

DISCARD:这个标志说明应用程序不关心当前缓冲的内容。所以在缓冲被渲染的同时,驱动可以给应用程序一个全新的缓冲。这个处理称之为“buffer renaming”。注意,在实践中,驱动倾向于不去释放“缓冲重命名”中所用的内存,因此这个标志必须尽量少用。

NOOVERWRITE:这表示,对于之前添加的,不带这个标志的数据,应用程序不会更改它。例如应用程序只会在现有缓冲之后添加数据。所以驱动可以继续使用现有数据进行渲染。

CPU和GPU的并行处理

D3D runtime会将一堆命令做成命令串传给GPU,这就允许GPU和CPU进行并行处理。这样也是硬件加速渲染这么高效的原因之一。但是,在很多情况下,CPU和GPU必须进行同步之后才能做进一步的处理。通常来说,应该尽量避免这种情况,因为这会导致整个流水管线的刷新,大幅降低性能。例如,对静态缓冲加锁,这要求GPU先处理完所有的命令之后,才能返回被锁缓冲的指针。如果用动态缓冲,就可以避免,就像前面讲过的一样。

有一些同步是不可避免的,例如,CPU可能会需要一些GPU还来不及处理的命令结果。在这种情况下,用户会感到画面延迟Lag。要避免这种情况,可以在GPU落后两三帧的情况下调用Present来强迫CPU等待GPU。因此,调用Present可能比较慢,但是正式它处理了必要的同步。

状态的更换State Changes

不管冗余还是不冗余,状态的转换在到达驱动层的时候,开销总是很大。所以在某些层面,状态转换必须被过滤。一个对状态进行更换的函数调用并不一定会开销很大,因为D3D Runtime很有可能缓冲这些转换请求,在真正调用DrawPrimitive函数之前不会去执行它。多次的状态转换也不会加大开销,因为只使用最后一个状态值。尽管如此,状态转换还是应该尽量避免。某些状态转换会比其他的转换的开销更大。例如,对于更改处于活动状态的顶点缓冲和像素缓冲会导致整个流水管线的刷新。因为在某些显卡上,同一时间每个类型只有一个着色器可以处于活动状态。一个图形流水线可以很长,花一段时间才能完成一个像素的渲染。因此,整个流水线的刷新需要尽量避免。在不同的显卡上,某个状态的更新的花费差别可能会很大。另外,D3D的函数调用个数也必须尽量的少,虽然它的开销不如达到驱动层的状态更改那么大。可以使用状态块来减少D3D API的调用,状态块可以将状态的更改集中在一起,并且可以重用。


注意这段 3.3.4 Batching with the Geometry Instancing API

Inside Geometry Instancing(上)

Inside Geometry Instancing(上)

翻译:clayman
[email protected]
仅供个人学习使用,勿用于任何商业用途,转载请注明作者^_^

注:呵呵,发现我对翻译东西上瘾了。这次翻译了《GPU Gem2》中第三章的内容,大家共同学习^_^

在交互式程序中,丰富用户体验的重要方法之一就是呈现一个充满大量各种有趣物体的世界。从数不清的草丛、树木到普通杂物:所有这些都能提高画面最终的效果,让用户保持“幻想状态(suspension of disbelief)”。只有用户相信并且融入了这个世界,才会对这个世界充满感情——这就是游戏开发的圣杯(Holy Grail)。

         从渲染的观点来看,实现这种效果,无非就是渲染大量小物体,一般情况下,这些物体彼此都很类似,只在颜色、位置以及朝向上有细小的差别。举个例子,比如森林中所有树的几何形状都是很类似的,而在颜色和高度上有很大差别。对用户来说,由外形各异的树组成的森林才真实,才会相信它,从而丰富自己的游戏体验。

         但是,使用当前的GPU和图形库渲染大量由少量多边形组成的小物体会带来很大的性能损失。诸如Direct3D和OpenGL之类的图形API都不是为了每帧渲染只有少数多边形的物体数千次而设计的。本文将讨论如何使用Direct3D把同一几何体渲染为大量独特的实体(instances)。下图是Back & White 2中,使用了这一技术的一个例子:

 

3.1 为何使用Geometry Instancing Why Geometry Instancing

         在Direct3D中,把三角形数据提交给GPU是一个相对很慢的操作。Wloka 2003显示使用Direct3D在1GHz的CPU上,每秒只能渲染10000到400000批次(batches)。对于现代的CPU,可以预测这个值大概在每秒30000到120000批次之间(对FPS为30frame/sec系统来说大概每帧1000到4000批次)。这太少了!这意味着如果我要渲染一片森林,每批次提交一颗树的数据,那么无论每棵树包含多少多边形,都将无法渲染4000棵树以上——因为CPU已经没有时间来处理其他任务了。这种情况当然是我们不想看到的。在应用程序中,我们希望最小化渲染状态和纹理的改变,同时,在Direct3D中使用一次方法调用,在同一批次中对同一三角形进行多次渲染。这样,就能减少CPU提交批次的时间,把CPU资源留给物理、AI等其他系统。

 

3.2 定义(Definitions

         我们先来定义一系列与geometry instancing相关的概念。

3.2.1 几何包(Geometry Packet)

         A geometry packet is a description of a packet of geometry to be instanced, a collection of vertices and indices。一个几何包可以使用顶点——包括他的位置、纹理坐标、法线、切线空间(tangent space)以及用于skinning的骨骼信息——以及顶点流中的索引信息来描述。这样的描述,可以直接映射为一个高效的提交几何体的方法。

         几何包是对一个几何体在模型空间进行的抽象描述, 从而可以独立于当前的渲染环境。

         下面是对几何包的一种可能的描述,它不但包含了几何体的信息,同时还包含了物体的边界球体信息:

struct GeometryPacker

{

         Primitive mPrimType;

         void* mVertice;

         unsigned int mVertexStride;

 

         unsigned short* mIndices;

         unsigned int mVertexCount;

         unsigned int mIndexCount;

 

         D3DXVECTOR3 mSphereCentre;

         float mSphereRadius;

}

 

3.2.2 实体属性(Instance Attribute

         对每个实体来说,典型的属性包括模型到世界的坐标变换矩阵,实体颜色以及由animation player提供的用于对几何包进行skin的骨骼。

struct InstanceAttributes

{

         D3DXMATRIX mModelMatrix;

         D3DCOLOR mInstanceColor;

         AnimationPlayer* mAnimationPlayer;

         unsigned int mLOD;

}

 

3.2.3 几何实体(Geometry Instance

         几何实体就是一个几何包与特定属性的集合。他直接联系到一个几何包以及一个将要用于渲染的实体属性,包含了将要提交给GPU的关于实体的完整描述。

struct GeometryInstance

{

         GeometryPacket* mGeometryPacket;

         InstanceAttributes mInstanceAttributes;

}

 

3.2.4 渲染及纹理环境(Render and Texture Context

         渲染环境指的是当前的GPU渲染状态(比如alpha blending, testing states, active render target等等)。纹理环境指的则是当前激活(active) 的纹理。通常使用类来对渲染状态和纹理状态进行模块化。

class RenderContext

{

         public:

         //begin the render context and make its render state active

         void Begin(void);

         //End the render context and restore previous render states if necessary

         void End(void);

        

         private:

         //Any description of the current render state and pixel and vertex shaders.

         //D3DX Effect framework is particularly useful

         ID3Deffect* mEffect;

         //Application-specific render states

         //….

};

class TextureContext

{

         public:

         //set current textures to the appropriate texture stages

         void Apply(void) const;

        

         private :

         Texture mDiffuseMap;

         Texture mLightMap;

         //……..

}

 

3.2.5 几何批次(Geometry Batch

         几何批次是一系列几何实体的集合,以及用来渲染这个集合的渲染状态和纹理环境。为了简化类的设计,通常直接映射为一次DrawIndexedPrimitive()方法调用。以下是几何批次类的一个抽象接口:

 

class GeometryBatch

{

         public:

         //remove all instances form the geometry batch

         virtual void ClearInstances(void);

         //add an instance to the collection and return its ID. Return -1 if it can’t accept more instance.

         virtual int AddInstance(GeometryInstance* instance);

         //Commit all instances, to be called once before the render loop begins and after every change to the instances collection

         virtual unsigned int Commit(void) = 0;

         //Update the geometry batch, eventually prepare GPU-specific data ready to be submitted to the driver, fill vertex and

         //index buffers as necessary , to be called once per frame

         virtual void Update(void) = 0;

         //submit the batch to the driver, typically impemented eith a call to DrawIndexedPrimitive

         virtual void Render(void) const = 0;

        

         private:

         GeometryInstancesCollection mInstances;

}

3.3 实现(Implementation

         引擎的渲染器只能通过GeometryBatch的抽象接口来使用geometry instancing,这样能很好隐藏具体的实体化(instancing)实现,同时,提供管理实体、更新数据、以及渲染批次的服务。这样引擎就能集中于分类(sorting)批次,从而最小化渲染和纹理状态的改变。同时,GeometryBatch完成具体的实现,并且与Direct3D进行通信。

         下面使用的伪代码实现了一个简单的渲染循环:

//Update phase

Foreach GeometryBatch in ActiveBatchesList

         GeometryBatch.Update();

 

//Render phase

Foreach RenderJContext

Begin

         RenderContext.BeginRendering();

         RenderContext.CommitStates();

        

         Foreach TextureContext

         Begin

                   TextureContext.Apply();

                   Foreach GeometryBatch in the texture context

                            GeometryBatch.Render();

         End

End

 

         为了能一次更新所有批次并且进行多次渲染,更新和渲染阶段应该分为独立的两部分:这种方法在渲染阴影贴图或者水面的反射以及折射时特别有用。这里我们将讨论4种GeometryBatch的实现,并且通过比较内存占用量、可控性来分析各种技术的性能特性。

         这里是一个大概的摘要:

l          静态批次(static batching:执行instance geometry最快的方法。每个实体通过一次变换移动到世界坐标,附加上属性值,然后就提交给GPU。静态批次很简单,但也是可控性最小的一种。

l          动态批次(Dynamic batching:执行insance geometry最慢的方法。每一帧里,每个经过变换,附加了属性的实体都以流的形式传入GPU。动态批次可以完美的支持skinning,也是可控性最强的。

l          Vertex constants instancing:一种混合的实现方法。每个实体的几何信息都被复制多次,并且一次性把他们复制到GPU的缓存中。通过顶点常量,每一帧都重新设置实体属性,使用一个vertex shader完成gemetry instancing。

l          Batching with Geometry Instancing API。使用DirectX 9提供的Geometry Instancing API,可以获得GeForce 6系列显卡完全的硬件支持,这是一种高效而又具有高度可控性的gemetry instancing方法。与其他几种方法不同的是它不需要把几何包复制到Direct3D的顶点流中。

 

3.3.1 静态批次(Static Batching

         对静态批次来说,我们希望对所有实体进行一次变换之后,复制到一块静态顶点缓冲中。静态批次最大的优点就是高效,同时几乎市场上所有的GPU都能支持这个特性。

         为了实现静态批次,先创建一个用来填充经过变化后的几何体的顶点缓冲对象(当然也包括索引缓冲)。需要保证这个这个缓冲足够大,足以储存我们希望处理的所有实体。由于我们只对缓冲进行一次填充,并且不再做修改,因此,可以使用Direct3D中的D3DUSAGE_WRITEONLY标志,提示驱动程序把缓冲放到速度最快的可用显存中:

HRESULT res;

res = lpDevice -> CreateVertexBuffer( MAX_STATIC_BUFFER_SIZE, D3DUSAGE_WRITE, 0, D3DPOOL_MANAGED, &mStaticVertexStream, 0 );

ENGINE_ASSERT(SUCCEEDED(res));

         根据应用程序的类型或者引擎的内存管理方式,可以选择使用D3DPOOL_MANAGED或D3DPOOL_DEFAULT标志来创建缓冲。

         接下来实现Commit()方法。它将把需要渲染的经过坐标变换的几何体数据填充到顶点和索引缓冲中。以下是Commit方法的伪代码实现:

Foreach GeometryInstance in Instances

Begin

         transform geometry in mGeometryPack to world space with instance mModelMatrix

         Apply other instnce attributes(like instace color)

         Copy transformed geometry to the Vertex Buffer

         Copy indices ( with the right offset) to the Index Buffer

         Advance current pointer to the Vertex Buffer

         Advance currect pointer to the Index Buffer

End

         好了,接下来就只剩使用DrawIndexedPrimitive()方法,提交这些准备好的数据了。Update()方法和Render()方法的实现都很简单,这里不具体讨论。

         静态批次是渲染大量实体最快的方法,它可以在一个批次中包含不同类型的几何包,但也有一些严重的限制:

l          大内存占用(Large memory footprint:根据几何包大小和希望渲染的实体数量,内存占用量可能会变的很大。对于大场景来说,应该预留出几何体所需的空间。Falling back to AGP memory is possible(注:这里应该指的是当显存不够用时,需要把数据分页存放到AGP memory中),但这会降低效率,因此,应该尽量避免。

l          不支持多种LODNo support for different level of detal:由于在提交数据时,所有实体都被一次性复制到顶点缓冲中,因此很难对每种环境都选择一个有效的LOD层次,同时,还会导致对多边形数量的预算不正确。可以使用一种半静态的方法来解决这个问题,把特定实体的所有LOD层次都放在顶点缓冲中,每一帧选择不同的索引值,来选择实体的正确LOD。但这样会让实现看起来很笨拙,违反了我们使用这种方法最初的目的:简单并且高效。

l          No support for skinning

l          不直接支持实体移动(No direct support for moving instances:由于效率的原因,实体的移动应该使用vertex shader逻辑和动态批次来实现。最终的解决方案其实就是vertex constants instancing。

接下来的一种方法将解除这些限制,以牺牲渲染速度换取可控性。

 

3.3.2 动态批次(DynamicBatching

         动态批次以降低渲染效率为代价,克服了静态批次方法的限制。动态批次最大的优点和静态批次一样,也能在不支持高级编程管道的GPU上使用。

         首先使用D3DUSAGE_DYNAMIC和D3DPOOL_DEFAULT标志创建一块顶点缓冲(同样也包括相应的索引缓冲)。这些标志将保证缓冲处于最容易进行内存定位的地方,以满足我们动态更新的要求

HRESULT res;

res = lpDevice->CreateVertexBuffer(MAX_DYNAMIC_BUFFER_SIZE, D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, 0 , D3DPOOL_DEFAULT, &mDynamicVertexStream, 0)

这里,选择正确的MAX_DYNAMIC_BUFFER_SIZE值是很重要的。有两种策略来选择这个值:

l          选择一个可以容纳每一帧里所有可能实体的足够大值。

l          选择一个足够大的值,以保证可以容纳一定量的实体。

第一种策略在一定程度上保证了更新和渲染批次的独立。更新批次意味着对动态缓冲中的所有数据进行数据流化(streaming);而渲染则只是使用DrawIndexedPrimitive()方法提交几何数据。当这种方法将会占用大量的图形内存(显存或者AGP memory),同时,在最差的情况下,这种方法将变的不可靠,因为我们无法保证缓冲在整个应用程序生命期中都足够大。

第二种策略则需要在几何体信息数据流化和渲染之间进行交错:当动态缓冲被填满时,提交几何体进行渲染,同时丢弃缓冲中的数据,准备好填充更多将被数据流化的实体。为了优化性能,使用正确的标志是很重要的,换句话说就是,在每一批实体开始时都使用D3DLOCK_DISCARD标志锁定(locking)动态缓冲,此外,对每个将要数据流化的新实体都使用D3DLOCK_WRITEONLY标志。这个方法的缺点是每次当批次需要进行渲染时,都需要重新锁定缓冲,以数据流化几何体信息,比如实现阴影影射时。

应该根据应用程序的类型和具体要求来选择不同方法。这里,由于简单和清楚的原因,我们选择了第一种方法,但是也添加了一点点复杂度:动态批次天生支持skinning,我们顺便对他进行了实现。

Update方法与之前在3.3.1讨论的Commit()方法很类似,但它需要在每一帧都执行。这里是伪代码的实现;

Foreach GeometryInstance in Instances

Begin

         Transform geometry in mGeometryPacket to world space with instance mModelMatrix

         if instance nedds skinning, request a set of bones from mAnimationPlayer and skin geometry

         Apply other instance attributes(like instance color)

         Copy transformd geometry to the Vertex Buffer

         Copy indices (with the right offset) to the Index Buffer

         Advance current pointer to the Vertex Buffer

         Advance current pointer to the Index Buffer

End

         这种情况下,Render()方法只是简单的调用DrawIndexedPrimitive()方法而已。


Inside Geometry Instancing(下)
Inside Geometry Instancing(下)
此教程版权归我所有,仅供个人学习使用,请勿转载,勿用于任何商业用途。商业应用请同我联系。
由于本人水平有限,难免出错,不清楚的地方请大家以原著为准。也欢迎大家和我多多交流。
其中部分图片来自网络,尽量保证了和原书中插图一致。
特别感谢mtt重现了文章中的流程图^_^
翻译:clayman
Blog:http://blog.csdn.net/soilwork
 
 

3.3.3 Vertex Constants Instancing

         vertex constants instancing方法中,我们利用顶点常量来储存实体属性。就渲染性能而言,顶点常量批次是非常快的,同时支持实体位置的移动,但这些特点都是以牺牲可控性为代价的。

         以下是这种方法主要的限制:

l          根据常理数值的大小,每批次的实体数量是受限制的;通常对一次方法调用来说,批次中不会超过50100个实体。但是,这足以满足减少CPU调用绘图函数的负载。

l          不支持skinning;顶点常量全部用于储存实体属性了

l          需要支持vertex shaders的硬件

首先,需要准备一块静态的顶点缓冲(同样包括索引缓冲)来储存同一几何包的多个副本,每个副本都以模型坐标空间保存,并且对应批次中的一个实体。

必须更新最初的顶点格式,为每个顶点添加一个整数索引值。对每个实体来说,这个值将是一个常量,标志了特定几何包属于哪个实体。这和palette skinning有些类似,每个顶点都包含了一个索引,指向将会影响他的一个或多个骨骼。

更新之后的顶点格式如下:

Stuct InstanceVertex

{

         D3DVECTOR3  mPosition;

         //other properties……

         WORD     mInstanceIndex[4];  //Direct3D requires SHORT4

};

在所有实体数据都添加到几何批次之后,Commit()方法将按照正确的设计,准备好顶点缓冲。

接下来就是为每个需要渲染的实体加载属性。我们假设属性只包括描述实体位置和朝向的模型矩阵,以及实体颜色。

对于支持DirectX9系列的GPU来说,最多能使用256个顶点常量:我们使用其中的200个来保存实体属性。在我们所举的例子中,每个实体需要4个常量储存模型矩阵,1个常量储存颜色,这样每个实体需要5个常量,因此每批次最多包含40个实体。

以下是Update()方法。实际的实体将在vertex shader进行处理。

D3DVECTOR4  instancesData[MAX_NUMBER_OF_CONSTANTS];

unsigned int count = 0;

for(unsigned int i=0; i

{

         //write model matrix

instancesData[count++] = *(D3DXVECTOR4*) & mInstances[i].mModeMatrix.m11;

instancesData[count++] = *(D3DXVECTOR4*) & mInstances[i].mModelMatrix.m21;

instancesData[count++] = *(D3DXVECTOR4*) & mInstances[i].mModelMatrix.m31;

instancesData[count++] = *(D3DXVECTOR4*) & mInstances[i].mModelMatrix.m41;

//write instance color

instaceData[count++] = ConverColorToVec4(mInstances[i].mColor);

         }

         lpDevice->SetVertexConstants(INSTANCES_DATA_FIRST_CONSTANT, instancesData, count);

         下面是vertex shader

         //vertex input declaration

         struct vsInput

         {

                   float4 postion : POSITON;

                   float3 normal : NORMAL;

                   //other vertex data

                   int4 instance_index : BLENDINDICES;

         };

 

         vsOutput VertexConstantsInstancingVS( in vsInput input)

         {

                   //get the instance index; the index is premultiplied by 5 to take account of the number of constants used by each instance

                   int instanceIndex = ((int[4])(input.instance_index))[0];

                   //access each row of the instance model matrix

                   float4 m0 = InstanceData[instanceIndex + 0];

                   float4 m1 = InstanceData[instanceIndex + 1];

                   float4 m2 = InstanceData[instanceIndex + 2];

                   float4 m3 = InstanceData[instanceIndex + 3];

                   //construct the model matrix

                   float4x4 modelMatrix = {m0, m1, m2, m3}

                   //get the instance color

                   float instanceColor = InstanceData[instanceIndex + 4];

                   //transform input position and normal to world space with the instance model matrix

                   float4 worldPostion = mul(input.position, modelMatrix);

                   float3 worldNormal = mul(input.normal, modelMatrix;

                   //output posion, normal and color

                   output.position = mul(worldPostion, ViewProjectionMatrix);

                   output.normal = mul(worldPostion,ViewProjectionMatrix);

                   output.color = instanceColor;

                   //output other vertex data

         }

         Render()方法设置观察和投影矩阵,并且调用一次DrawIndexedPrimitive()方法提交所有实体。

         实际代码中,可以把模型空间的旋转部分储存为一个四元数(quaternion,从而节约2个常量,把最大实体数增加到70左右。之后,在vertex shader中重新构造矩阵,当然,这也增加了编码的复杂度和执行时间。

 

3.3.4 Batching with the Geometry Instancing API

         最后介绍的一种方法就是在DirectX9中引入的,完全可由Geforce 6系列GPU硬件实现的几何实体API批次。随着原来越多的硬件支持几何实体API,这项技术将变的更加有趣,它只需要占用非常少的内存,另外也不需要太多CPU的干涉。它唯一的缺点就是只能处理来自同一几何包的实体。

         DirectX9提供了以下函数来访问几何实体API

         HRESULT SetStreamSourceFreq( UINT StreamNumber, UINT FrequencyParameter);

         StreamNumber是目标数据流的索引,FrequencyParameter表示每个顶点包含的实体数量。

         我们首先创建2快顶点缓冲:一块静态缓冲,用来储存将被多次实体化的单一几何包;一块动态缓冲,用来储存实体数据。两个数据流如下图所示:

         Commit()必须保证所有几何体都使用了同一几何包,并且把几何体的信息复制到静态缓冲中。

         Update()只需简单的把所有实体属性复制到动态缓冲中。虽然它和动态批次中的Update()方法很类似,但是却最小化了CPU的干涉和图形总线(AGP或者PCIE)带宽。此外,我们可以分配一块足够大的顶点缓冲,来满足所有实体属性的需求,而不必担心显存消耗,因为每个实体属性只会占用整个几何包内存消耗的一小部分。

         Render()方法使用正确流频率(stream frequency)设置好两个流,之后调用DrawIndexedPrimitive()方法渲染同一批次中的所有实体,其代码如下:

         unsigned int instancesCount = GetInstancesCount();

         //set u stream source frequency for the first stream to render instancesCount instances

         //D3DSTREAMSOURCE_INDEXEDDATA tell Direct3D we’ll use indexed geometry for instancing

         lpDevice->SetStreamSourceFreq(0, D3DSTREAMSOURCE_INDEXEDDATA | instancesCount);

         //set up first stream source with the vertex buffer containing geometry for the geometry packet

         lpDevice->setStreamSource(0, mGeometryInstancingVB[0], 0, mGeometryPacketDeck);

         //set up stream source frequency for the second stream; each set of instance attributes describes one instance to be rendered

         lpDevice->SetstreamSouceFreq(1, D3DSTREAMSOURCE_INDEXEDDATA | 1);

         // set up second stream source with the vertex buffer containing all instances’ attributes

         pd3dDevice->SetStreamSource(1, mGeometryInstancingVB[0], 0, mInstancesDataVertexDecl);

         GPU通过虚拟复制(virtually duplicating)把顶点从第一个流打包到第二个流中。vertex shader的输入参数包括顶点在模型空间下的位置,以及额外的用来把模型矩阵变换到世界空间下的实体属性。代码如下:

         // vertex input declaration

         struct vsInput

         {

                   //stream 0

                   float4 position : POSITION;

                   float3 normal  : NORMAL;

                   //stream 1

                   float4 model_matrix0   :  TEXCOORD0;

                   float4 model_matrix1   :  TEXCOORD1;

float4 model_matrix2   :  TEXCOORD2;

float4 model_matrix3   :  TEXCOORD3;

 

float4 instance_color    :  D3DCOLOR;

         };

 

         vsOutput geometryInstancingVS(in vsInput input)

         {

                   //construct the model matrix

                   float4x4 modelMatrix =

                   {

                            input.model_matrix0,

                            input.model_matrix1,

                            input.model_matrix2,

                            input.model_matrix3,

                   }

                   //transform inut position and normal to world space with the instance model matrix

                   float4 worldPosition = mul(input.position, modelMatrix);

                   float3 worldNormal = mul(input.normal,modelMatrix);

                   //output positon, normal ,and color

                   output.positon = mul(worldPostion,ViewProjectionMatrix);

                   output.normal = mul(worldNormal,ViewProjectionMatrix);

                   output.color = int.instance_color;

                   //output other vertex data…..

         }

         由于最小化了CPU负载和内存占用,这种技术能高效的渲染同一几何体的大量副本,因此,也是游戏中理想的解决方案。当然,它的缺点在于需要硬件功能的支持,此外,也不能轻易实现skinning

         如果需要实现skinning,可以尝试把所有实体的所有骨骼信息储存为一张纹理,之后为相应的实体选择正确的骨骼,这需要用到Shader Model3.0中的顶点纹理访问功能。如果使用这种技术,那么访问顶点纹理带来的性能消耗是不确定的,应该实现进行测试。

 

3结论

         本文描述了几何实体的概念,并且描述了4中不同的技术,来达到高效渲染同一几何体多次的目的。每一种技术都有有点和缺点,没有哪种单一的方法能完美解决游戏场景中可能遇到的问题。应该根据应用程序的类型和渲染的物体种类来选择相应的方法。

         一下是一些场景中建议使用的方法:

l          对于包含了同一几何体大量静态实体的室内场景,由于他们很少移动,静态批次是最好的选择。

l          包含了大量动画实体的户外场景,比如包含了数百战士的即时战略游戏,动态批次也许是最好的选择。

l          包含了大量蔬菜和树木的户外场景,通常需要对他们的属性进行修改(比如实现随风而动的效果),以及一些粒子系统,几何批次API也许就是最好的选择。

通常,同一应用程序会用到两个以上的方法。这种情况下,使用一个抽象的几何批次接口隐藏具体实现,能让引擎更容易进行模块化和管理。这样,对整个程序来说,几何实体化的实现工作也能减少很多。

(图中,静态的建筑使用了静态批次,而树则使用了几何实体API)

点击这里可以下载完整的PDF文档,完整的demo大家可以参考NVIDIA SDK中的示例Instancing,也可以直接在这里下载。另外也可参考DirectX SDK中的示例Instancing。




注意这个

GPU Gems 2  Chapter 3. Inside Geometry Instancing



3.5 References

Wloka, Matthias. 2003. "Batch, Batch, Batch: What Does It Really Mean?" Presentation at Game Developers Conference 2003. http://developer.nvidia.com/docs/IO/8230/BatchBatchBatch.pdf



看完这些,理解得差不多了。再结合现在的引擎代码具体理解下。理解了才好办事!
标签:  d3d,  instancing,  batch
好文要顶  关注我  收藏该文   
CG迷途大熊猫
关注 - 5
粉丝 - 73
+加关注
1
0
« 上一篇: CRY ENGINE 3 引擎详解
» 下一篇: 理解D3D--(1)实例化Instancing
posted @  2009-09-24 06:53  CG迷途大熊猫 阅读( 4565) 评论( 3)  编辑  收藏

你可能感兴趣的:(Ogre)