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