Ogre2.1 結合OpenGL3+高效渲染


  在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 = 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.

實例與間接繪制API

  void glDrawArraysInstancedBaseInstance(GLenum mode, GLint first, Glsizei count, GLsizei instanceCount, GLuint baseInstance); 非索引直接繪制

  • 對於通過 mode、 first 和 count 所構成的幾何體圖元集(相當於 glDrawArrays() 函數所需的獨立參數),繪制它的 primCount 個實例。對於每個實例,內置變量 gl_InstanceID都會依次遞增,新的數值會被傳遞到頂點着色器,以區分不同實例的頂點屬性。此外,baseInstance 的值用來對實例化的頂點屬性設置一個索引的偏移值,從而改變 OpenGL 取出的索引位置。

  void glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count,GLenum type, const GLvoid* indices, GLsizei instanceCount, GLuint baseVertex, GLuint baseInstance); 索引直接繪制

  • 對於通過mode、 count、 indices 和 baseVertex所構成的幾何體圖元集(相當於glDrawElementsBaseVertex() 函數所需的獨立參數),繪制它的 primCount 個實例。與glDrawArraysInstanced() 類似,對於每個實例,內置變量 gl_InstanceID 都會依次遞增,新的數值會被傳遞到頂點着色器,以區分不同實例的頂點屬性。此外, baseInstance 的值用來對實例化的頂點屬性設置一個索引的偏移值,從而改變 OpenGL 取出的索引位置。

  void glMultiDrawArraysIndirect(GLenum mode, const void* indirect, Glsizei drawcount, GLsizei stride); 非索引間接繪制

  • 繪制多組圖元集,相關參數全部保存到緩存對象中。在 glMultiDrawArraysIndirect()的一次調用當中,可以分發總共 drawcount 個獨立的繪制命令,命令中的參數與glDrawArraysIndirect() 所用的參數是一致的。每個 DrawArraysIndirectCommand 結構體之間的間隔都是 stride 個字節。如果 stride 是 0 的話,那么所有的數據結構體將構成一個緊密排列的數組。

  void glMultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride); 索引間接繪制

  • 繪制多組圖元集,相關參數全部保存到緩存對象中。在 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 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;
    
}
HLMS產生的頂點着色器代碼

   相關API主要是介紹OpenGL方面的,DX都有對應的API.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM