在DX10与OpenGL3+之前,二者都是固定管线与可编程管线的混合,其中对应Ogre1.x的版本,也是结合固定与可编程管线设计.转眼到了OpenGL3+与DX10后,固定管线都被移除了,相对应着色器的功能进一步完善与扩充,对应Ogre2.x包装DX11与OpenGL3+,完全抛弃固定管线的内容,专门针对可编程管线封装.
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 = ; i < ; ++i)
{
for (int j = ; j < ; ++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 % == )
item->setDatablock("Rocks");
else
item->setDatablock("Marble"); item->setVisibilityFlags(0x000000001); size_t idx = i * + 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.
实例与间接绘制API
- 对于通过 mode、 first 和 count 所构成的几何体图元集(相当于 glDrawArrays() 函数所需的独立参数),绘制它的 primCount 个实例。对于每个实例,内置变量 gl_InstanceID都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。此外,baseInstance 的值用来对实例化的顶点属性设置一个索引的偏移值,从而改变 OpenGL 取出的索引位置。
- 对于通过mode、 count、 indices 和 baseVertex所构成的几何体图元集(相当于glDrawElementsBaseVertex() 函数所需的独立参数),绘制它的 primCount 个实例。与glDrawArraysInstanced() 类似,对于每个实例,内置变量 gl_InstanceID 都会依次递增,新的数值会被传递到顶点着色器,以区分不同实例的顶点属性。此外, baseInstance 的值用来对实例化的顶点属性设置一个索引的偏移值,从而改变 OpenGL 取出的索引位置。
- 绘制多组图元集,相关参数全部保存到缓存对象中。在 glMultiDrawArraysIndirect()的一次调用当中,可以分发总共 drawcount 个独立的绘制命令,命令中的参数与glDrawArraysIndirect() 所用的参数是一致的。每个 DrawArraysIndirectCommand 结构体之间的间隔都是 stride 个字节。如果 stride 是 0 的话,那么所有的数据结构体将构成一个紧密排列的数组。
- 绘制多组图元集,相关参数全部保存到缓存对象中。在 glMultiDrawElementsIndirect()的一次调用当中,可以分发总共drawcount个 独立的绘制命令,命令中的参数与glDrawElementsIndirect() 所用的参数是一致的。每个 DrawElementsIndirectCommand结构体之间的间隔都是 stride 个字节。如果 stride 是 0 的话,那么所有的数据结构体将构成一个紧密排列的数组。
其中链接可以看到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的操作权限,不同的权限对于不同的实现,简单说下.
- BT_IMMUTABLE GPU只有读的权限,CPU没权限.一般纹理与模型数据使用.
- BT_DEFAULT GPU有读写的权限,CPU没有任何权限,RTT(FBO等技术), Transform Feedback使用.
- BT_DYNAMIC_DEFAULT GPU可读,CPU可写.一般用于粒子系统,时常更新BUFFER块(如UBO)等使用.
- BT_DYNAMIC_PERSISTENT 同BT_DYNAMIC_DEFAULT,不同的是当Buffer 处理Mapped状态时,还能进行客户端的读写操作,如glDrawElements.
- BT_DYNAMIC_PERSISTENT_COHERENT 同BT_DYNAMIC_PERSISTENT,不同的是当CPU修改数据后,GPU能立即得到最新数据.
其中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的计算.
- mVaoName VAO的ID,对应一个顶点布局,布局是在OpenGL中指渲染类型(点,线,三角形带等),VBO与IBO中的BufferID,索引类型(16bit-32bit),顶点属性(glVertexAttribPointer).如果多个SubMesh用的是相同的顶点布局(在Ogre2.1中,这是很常见的,因为多个VBO,IBO一般共用一个Buffer,那么只要顶点格式一样就是相同的布局),那么可以共用一个VAO,并且这种情况很常见.
- mRenderQuereID 一个uint32的分段数,在这分成二段,前一段是0-511(占8位), 表示当前VaoManager的ID(一个调用createVertexArrayObject后自增ID),后一段是512-uint32.maxValue(占24位),表示对应mVaoName. 这种设计一是能根据段数来排序,如在这,mVaoName不同,二个数值就会相差非常大,而mVaoName相同,创建VertexArraObject 的ID不同,值相差不大,二是这样在创建一个Mesh多SubMesh下(VaoManager的ID加1),同一SubMesh一般排在一起.
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.
- Hash 和前面一样,分段,unit32,前面15位表示当前特定的Hlms类型的HlmsCache中的hash,后面17位表示对应Renderable中的mHlmsHash.
- Type 表示Hlms的类型,如PBS(基于物理渲染,),Unlit(无光照,用于GUI,粒子,自发光),TOON(卡通着色),Low_level(Ogre1.9材质渲染模式)
- Shader:根据特定Hlms类型生成的各种着色器,有顶点,几何,细分曲面,片断.
通过这几个类,我们来回顾最初那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 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 = ) uniform PassBuffer
{ mat4 viewProj;
mat4 view;
mat3 invViewMatCubemap;
Light lights[]; } pass; layout(binding = ) 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; }
HLMS产生的顶点着色器代码
相关API主要是介绍OpenGL方面的,DX都有对应的API.