在DX10与OpenGL3+以前,两者都是固定管线与可编程管线的混合,其中对应Ogre1.x的版本,也是结合固定与可编程管线设计.转眼到了OpenGL3+与DX10后,固定管线都被移除了,相对应着色器的功能进一步完善与扩充,对应Ogre2.x包装DX11与OpenGL3+,彻底抛弃固定管线的内容,专门针对可编程管线封装. html
Ogre1.x的渲染流程一直是你们吐槽的对象,除开用Ogre1.x自己的实例批次,才能把同材质同模型合并,可是用过的人都知道,这个局限性太大,另外就是每一个Renderable结合一个Pass的渲染方法,致使一是大量的状态切换,二是大量的DrawCall.这二点应该说是Ogre1.x性能一直低的主要缘由.在Ogre2.x中,咱们一是得益于现有流程改进,减小状态切换,二是得益于流程改进与新API的引进,减小DrawCall.编程
前面文档里有提过,不用实例批次,能够把mesh合并,以及是不一样的mesh,当时看到的时候,觉得文档有错,或是本身理解不对,没敢写出来,现查看相关代码,不得不说如今的渲染设计太牛了(结合最新API),同mesh合并不算啥,不一样mesh合到一个DrawCall里,太牛了,而且不要你本身来写是否用实例批次,如Ogre1.x中的手动实例批次,如今是全自动的. 数组
举个例子,在Ogre2.1中,以下代码. 缓存
for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { Ogre::String meshName; if (i == j) meshName = "Sphere1000.mesh"; else meshName = "Cube_d.mesh"; Ogre::Item *item = sceneManager->createItem(meshName, Ogre::ResourceGroupManager:: AUTODETECT_RESOURCE_GROUP_NAME, Ogre::SCENE_DYNAMIC); if (i % 2 == 0) item->setDatablock("Rocks"); else item->setDatablock("Marble"); item->setVisibilityFlags(0x000000001); size_t idx = i * 4 + j; mSceneNode[idx] = sceneManager->getRootSceneNode(Ogre::SCENE_DYNAMIC)-> createChildSceneNode(Ogre::SCENE_DYNAMIC); mSceneNode[idx]->setPosition((i - 1.5f) * armsLength, 2.0f, (j - 1.5f) * armsLength); mSceneNode[idx]->setScale(0.65f, 0.65f, 0.65f); mSceneNode[idx]->roll(Ogre::Radian((Ogre::Real)idx)); mSceneNode[idx]->attachObject(item); } }
如上图,有一个4*4个模型,其中一条对角线上全是球形,余下全是立方体,其中偶数行使用材质Rocks,奇数行使用Marble.调用glDraw…(DrawCall)的次数只须要二次或四次,看硬件支持状况,如何作到的了,在Ogre2.1中,把如上16个模型添加进渲染通道时,会根据材质,模型等生成排序ID,如上顺序大体为Rocks[sphere0-0,sphere2-2,cube0-1,cube0-2,cube0-3,cube2-1…], Marble[sphere1-1,sphere3-3,cube1-2,cube1-3…].其中Rocks中的八个模型只须要一或二次DrawCall,Marble也是同样.Ogre2.1如何作到,请看相关OpenGL3+中新的API.数据结构
实例与间接绘制APIapp
其中连接能够看到Opengl官网中的SDK里的讲解,下面的讲解是红宝书第八版中的.两者对比的看能够更容易理解.第1,2二个是直接绘制版本,3,4是对应1,2的间接绘制版本,若是当前环境支持间接绘制,其中前面所说的就只须要二次DrawCall,一次材质一次DrawCall,不一样mesh也可一次DrawCall.而直接绘制版本须要4次,每次材质二次DrawCall(对应二个类型mesh,每一个类型mesh自动合并).
具体来讲下,渲染时,通道中的模型顺序为Rocks[sphere,sphere,cube,cube…], Marble[sphere,sphere,cube,cube…].应用材质Rocks(就是绑定对应着色器代码)后,绑定VBO,第一个sphere时,生成一次DrawCall,第二次sphere时,只须要DrawCall的实例参数instanceCount加1,到第一个cube时,增长一次DrawCall参数(非索引版本1,3为DrawArraysIndirectCommand结构,索引版本2,4为DrawElementsIndirectCommand结构),在这注意下baseInstance的更改(在相同材质下,模型不一样这个值就会变),在这为2(对应上面函数参数中的baseInstace这个参数,这个和后面的drawID有关).在直接版本中,几回DrawCall参数对应几回DrawCall(上面1,2二个API). 间接绘制直接一次DrawCall(上面3,4二个API)搞定.而后是应用材质Marble,如上步骤同样.
新的Buffer操做
在OpenGL3+,VBO,IBO,UBO,TBO均可以放入同一Buffer里.因此不一样于Ogre1.x中,使用HardwareBuffer,本身生成Buffer.在Ogre2.1中,使用BufferPacked,自己不使用glGenBuffer,只是记录在一块大Buffer中的位置,GPU-CPU数据交互经过BufferInterface.由于VBO,IBO,UBO,TBO如今数据统一管理,因此对应的VertexBufferPacked,IndexBufferPacked, ConstBufferPacked, TexBufferPacked对比原来的HarderwareVertexBuffer, HarderwareIndexBuffer, HardwareUniformBuffer, HardwarePixelBuffer的处理简单太多,生成Buffer交给VaoManager完成,GPU-CPU交互经过BufferInterface完成,而原来HardwareBuffer每一个都本身处理生成Buffer,GPU-CPU数据交互.原来把Buffer分红GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_UNIFORM_BUFFER, GL_TEXTURE_BUFFER等分开处理.在OpenGl3+中,Buffer就是一块存数据的地方,无论类型,你想放啥就放啥.其中UBO与TBO由于要针对不一样着色器中的不一样binding索引,实现与VBO和IBO有点区别,看下ConstBufferPacked,TexBufferPacked相关代码就明白了.
BufferType 对应GPU与CPU的操做权限,不一样的权限对于不一样的实现,简单说下.
其中3,4,5具体可参见博友提高先进OpenGL(三):Persistent-mapped Buffer 中的Buffer Storage,Ogre2.1也使用Buffer Storage来提高效率. Buffer Storage一是只须要一次Map,保留相关指针,无需屡次Map和UnMap,提升效率(因此也称持续映射缓冲区).二是提供更多控制,如上BufferType各枚举.
在Ogre2.1中GL3+的VaoManager中,在初始化中,默认BT_IMMUTABLE与BT_DEFAULT加起来大小为128M,余下BT_DYNAMIC_DEFAULT, BT_DYNAMIC_PERSISTENT, BT_DYNAMIC_PERSISTENT_COHERENT每块分32M,因 BT_IMMUTABLE与BT_DEFAULT其中CPU都没权限,因此统一处理.
在这先假设当前环境支持Buffer Storage,后面VBO,IBO,UBO,TBO都是GL3PlusVaoManager::allocatVbo来分配的,简单说下这个函数,最开始如上给BT_IMMUTABLE与BT_DEFAULT一块儿分配最小128M,余下每种BufferType最小32M.根据不一样的BufferType对应glBufferStorage使用不一样的flags.后面每次不一样BufferType进来时,就找对应块是否还有分配的空间.若是有,分出一块Block,而后对应的BufferPacked记录分配起点. 对应的BufferInterface中的mVboPoolIdx记录在128M Buffer里的Block块的索引,而mVboName就是128M那块Buferr的ID.
当使用CPU端数据更新GPU时,调用BufferInterface::map.其中BT_IMMUTABLE与BT_DEFAULT没有flag-GL_MAP_WRITE_BIT,不能直接Map,使用类StagingBuffer间接完成CPU->GPU->GPU这个转换.经过StagingBuffer::map把数据从CPU->GPU,而后又经过StagingBuffer::unmap,把当前GPU中数据移到最终GPU位置上, 对于缓存之间的复制数据为 GL_COPY_READ_BUFFER 和 GL_COPY_WRITE_BUFFER,如想了解更具体的搜索这二个关键字,从GL3PlusStagingBuffer这个类也能够了解具体用法.从上面得知,这个步骤轻过较多传输,最好不要轻易的去修改BT_IMMUTABLE与BT_DEFAULT类型的Buffer,通常只初始化时传入数据.
余下BufferType类型如BT_DYNAMIC_DEFAULT,如上面所说,采用则使用Buffer Storage,只需Map一次保留指针到mMappedPtr.生面的Map直接使用这个mMappedPtr更新数据,相关更新过程借助类GL3PlusDynamicBuffer,这个类有注释,由于GL3+不能同时mapping(就是没有unmap,都在map)一个buffer.从上面得知,反复更新的BUFFER块应使用这种方式,更新数据很是快速.
若是当前环境不支持Buffer Storage,则相应处理如Ogre1.x.使用glBufferData,当BT_IMMUTABLE与BT_DEFAULT时,对应flag为GL_STATIC_DRAW,不然为GL_DYNAMIC_DRAW.当CPU数据更新GPU时, BT_IMMUTABLE与BT_DEFAULT的处理同上,余下的BufferType由于没有Buffer Storage,每次更新数据须要再次调用glMapBufferRange.
渲染后期相关类与流程
知道了新的Buffer的操做方式,咱们就能够先看以下相关类,而后说明如何经过这些类来渲染.
VertexArraObject(封装VAO):VAO不一样VBO是一块BUFFER,VAO应该说是保存的相应VBO,IBO的绑定信息,以及相应顶点glVertexAttribPointer的状态.在Ogre2.1中,如上面所说VBO,IBO,UBO,TBO都保存在一个BUFFER中,因此通常来讲,建立模型(模型能够有多个SubMesh,一个SubMesh对应一个VAO)对应的VAO时,其中相同的多个SubMesh,通常来讲mVaoName与mRenderQuereID都相同.而不一样的多个SubMesh,通常来讲mVaoName相同,而mRenderQuereID不一样,参见GL3与DX11中的VaoManager实现的createVertexArrayObjectImpl相关renderQueueId的计算.
Renderable:和Ogre1.x同样的是,在渲染通道中关联材质与数据.不一样的是材质再也不是Material(对应固定管线中属性设置),而是HlmsDatablock(主要用于生成对应着色器代码),数据再也不是直接关联对应VBO与IBO对象,而是绑定VAO.其中 mHlmsHash 和上面的mRenderQuereID同样,是个分段数,也是分红二段,前一段是0-8191(占12位),表示在当前HLMS类型的渲染属性组合列表中的索引,其中渲染属性包含如是否骨骼动画,纹理个数,是否启用Alpha测试,是否启用模型,视图,透视矩阵等.后面一段是HLMS的类型,如PBS(基于物理渲染,),Unlit(无光照,用于GUI,粒子,自发光),TOON(卡通着色),Low_level(Ogre1.9材质渲染模式).
QueuedRenderable:原Ogre1.x中,渲染通道中是Renderable和对应pass,如今渲染通道中保存的是QueuedRenderable.其中QueuedRenderable 中的Hash 主要用来在通道中排序,是一个unit64的分段数,在非透明的状况下分红七段,其中纹理占15-25位,meshHash占26-39位, hlmsHash(对应Renderable的mHlmsHash)占到40-49位,是否透明占60-60位(bool类型只用一位),通道ID占用61-64位,更多详情请看RenderQueue::addRenderable这个方法.这样咱们排序后,按照通道ID,而后是透明,材质,模型,纹理排序,这个很重要,后面渲染时,这个顺序能保证模型能正确的组合渲染,而且保证最小的状态切换,提高效率.
HlmsCache:hlms根据Renderable中的mHlmsHash(HLMS中渲染属性组合在列表中的索引)生成对应的各类着色器,详情请看Hlms::createShaderCacheEntry.
经过这几个类,咱们来回顾最初那16个球的问题,如何排序,如何合并,简单说明下渲染流程.
当前摄像机检索场景,检索全部可见的Renderable.根据Renderable的材质(在这是HlmsDatablock,非Ogre1.x中的pass)生成分段数hash(用于排序,其中先材质,再mesh),并把相关Renderable,分段数hash,对应的MovableObject包装成QueuedRenderable添加到线程渲染通道中,合并全部当前线程渲染通道到当前通道中.
而后开始渲染通道中的模型,根据当前Renderable生成HlmsCache,根据Renderable的材质mHlmsHash,找到对应材质全部属性,结合当前类型的HLMS填充HlmsCache里的着色器代码.只需生成一次,相应HlmsCache会缓存起来.
而后如前面所说,vao不一样,通常来讲,材质不一样,须要从新绑定VAO(注释说是DX11/12须要),而后生成一次DrawCall.一个材质下有多个模型,在同材质下(mVaoName相同),若是后来的模型与前面的模型是同一个(mRenderQuereID相同),就只把当前DrawCall的参数中的实例个数加1,若是与前一个不一样(mRenderQuereID不一样),则增长对应DrawCall的参数结构,在这若是环境支持间接绘制,则全部的参数合并成一个结构数组渲染,这样能够多个不一样实例和多个不一样模型一次渲染,不然,仍是每次一个实例多个模型一块儿渲染.
咱们知道,实例中多个模型,他们的局部坐标通常都不一样,这个如何解决?在最开始对应VaoManager初始化时,会生成一块4096个drawID(uint32,存放0,1…4095)的Buffer,经过glVertexAttribDivisor(drawID)与baseInstance(参看前面1,2二个API).咱们把多个实例中的每一个模型参数如局部坐标放入TBO中(咱们假定在PBS材质格式下),这样多个DrawCall都用到这个TBO,因此要用baseInstance来定位每一个模型参数位置,先设置对应顶点属性drawID的glVertexAttribDivisor为1,这样每一个实例中对应每一个DrawID,每一个实例中darwID因里面存放的是从0每一个自加1的数组Buferr,达到和gl_InstanceID相似的效果, baseInstance用来正确产生每次DrawCall的drawID(由于DrawCall都共用TBO,不一样实例的drawID须要增长baseInstance个位移),这样就能经过drawID当索引取得存入在TBO中的模型矩阵,一样也能根据drawID来取共享的TBO中的其余内容(gl_InstanceID相似,可是baseInstance不会影响gl_InstanceID的值),一些下面是一份HlmsPbs产生的顶点着色器代码.
#version 330 core #extension GL_ARB_shading_language_420pack: require out gl_PerVertex { vec4 gl_Position; }; layout(std140) uniform; mat4 UNPACK_MAT4( samplerBuffer matrixBuf, uint pixelIdx ) { vec4 row0 = texelFetch( matrixBuf, int((pixelIdx) << 2u) ); vec4 row1 = texelFetch( matrixBuf, int(((pixelIdx) << 2u) + 1u) ); vec4 row2 = texelFetch( matrixBuf, int(((pixelIdx) << 2u) + 2u) ); vec4 row3 = texelFetch( matrixBuf, int(((pixelIdx) << 2u) + 3u) ); return mat4( row0.x, row1.x, row2.x, row3.x, row0.y, row1.y, row2.y, row3.y, row0.z, row1.z, row2.z, row3.z, row0.w, row1.w, row2.w, row3.w ); } mat4x3 UNPACK_MAT4x3( samplerBuffer matrixBuf, uint pixelIdx ) { vec4 row0 = texelFetch( matrixBuf, int((pixelIdx) << 2u) ); vec4 row1 = texelFetch( matrixBuf, int(((pixelIdx) << 2u) + 1u) ); vec4 row2 = texelFetch( matrixBuf, int(((pixelIdx) << 2u) + 2u) ); return mat4x3( row0.x, row1.x, row2.x, row0.y, row1.y, row2.y, row0.z, row1.z, row2.z, row0.w, row1.w, row2.w ); } in vec4 vertex; in vec4 qtangent; in vec2 uv0; in uint drawId; out block { flat uint drawId; vec3 pos; vec3 normal; vec2 uv0; } outVs; struct ShadowReceiverData { mat4 texViewProj; vec2 shadowDepthRange; vec4 invShadowMapSize; }; struct Light { vec3 position; vec3 diffuse; vec3 specular; }; layout(binding = 0) uniform PassBuffer { mat4 viewProj; mat4 view; mat3 invViewMatCubemap; Light lights[1]; } pass; layout(binding = 0) uniform samplerBuffer worldMatBuf; vec3 xAxis( vec4 qQuat ) { float fTy = 2.0 * qQuat.y; float fTz = 2.0 * qQuat.z; float fTwy = fTy * qQuat.w; float fTwz = fTz * qQuat.w; float fTxy = fTy * qQuat.x; float fTxz = fTz * qQuat.x; float fTyy = fTy * qQuat.y; float fTzz = fTz * qQuat.z; return vec3( 1.0-(fTyy+fTzz), fTxy+fTwz, fTxz-fTwy ); } void main() { mat4x3 worldMat = UNPACK_MAT4x3( worldMatBuf, drawId << 1u); mat4 worldView = UNPACK_MAT4( worldMatBuf, (drawId << 1u) + 1u ); vec4 worldPos = vec4( (worldMat * vertex).xyz, 1.0f ); vec3 normal = xAxis( normalize( qtangent ) ); outVs.pos = (worldView * vertex).xyz; outVs.normal = mat3(worldView) * normal; gl_Position = pass.viewProj * worldPos; outVs.uv0 = uv0; outVs.drawId = drawId; }
相关API主要是介绍OpenGL方面的,DX都有对应的API.