1. 程式人生 > >cocos2dx的渲染機制

cocos2dx的渲染機制

一、OpenGL基礎


  遊戲引擎是對底層繪圖介面的包裝,Cocos2d-x 也一樣,它是對不同平臺下 OpenGL 的包裝。OpenGL 全稱為 Open Graphics Library,是一個開放的、跨平臺的高效能圖形介面。OpenGL ES 則是 OpenGL 在移動裝置上的衍生版本,具備與 OpenGL 一致的結構,包含了常用的圖形功能。Cocos2d-x 就是一個基於 OpenGL 的遊戲引擎,因此它的繪圖部分完全由 OpenGL 實現。OpenGL 是一個基於 C 語言的三維圖形 API,基本功能包含繪製幾何圖形、變換、著色、光照、貼圖等。除了基本功能,OpenGL還提供了諸如曲面圖元、光柵操作、景深、shader 程式設計等高階功能。


(1)狀態機:


  OpenGL 是一個基於狀態的繪圖模型,我們把這種模型稱為狀態機。為了正確地繪製圖形,我們需要把 OpenGL 設定到合適的狀態,然後呼叫繪圖指令。(繪圖流程和狀態機優勢)。


(2)座標系:OpenGL 是一個三維圖形介面,在程式中使用右手三維座標系。


(3)渲染流水線:


  當我們把繪製的圖形傳遞給 OpenGL 後,OpenGL 還要進行許多操作才能完成 3D 空間到螢幕的投影。通常,渲染流水線過程
有如下幾步:顯示列表、求值器、頂點裝配、畫素操作、紋理裝配、光柵化和片斷操作等。OpenGL 從 2.0 版本開始引入了可程式設計著色器(shader)。


(4)繪圖函式:


(5)矩陣與變換:OpenGL 對頂點進行的處理實際上可以歸納為接受頂點資料、進行投影、得到變換後的頂點資料這 3 個步驟。


在計算機中,座標變換是通過矩陣乘法實現的。


注:詳細參見《cocos2d-x高階開發教程》、《OpenGL程式設計指南》


二、Cocos2d-x繪圖原理


複製程式碼
void CCSprite::draw(void)
{
    //1. 初始準備
    CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");


    CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");


    CC_NODE_DRAW_SETUP();


    //2. 顏色混合函式
    ccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );


    //3. 繫結紋理
    if (m_pobTexture != NULL)
    {
        ccGLBindTexture2D( m_pobTexture->getName() );
    }
    else
    {
        ccGLBindTexture2D(0);
    }
    
    //
    // Attributes
    //
    //4. 繪圖
    ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );


#define kQuadSize sizeof(m_sQuad.bl)
    long offset = (long)&m_sQuad;


    // vertex
    //頂點座標
    int diff = offsetof( ccV3F_C4B_T2F, vertices);
    glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));


    // texCoods
    //紋理座標
    diff = offsetof( ccV3F_C4B_T2F, texCoords);
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));


    // color
    //頂點顏色
    diff = offsetof( ccV3F_C4B_T2F, colors);
    glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));


    //繪製圖形
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);


    CHECK_GL_ERROR_DEBUG();


//5. 除錯相關的處理
#if CC_SPRITE_DEBUG_DRAW == 1
    //除錯模式 1:繪製邊框
    // draw bounding box
    CCPoint vertices[4]={
        ccp(m_sQuad.tl.vertices.x,m_sQuad.tl.vertices.y),
        ccp(m_sQuad.bl.vertices.x,m_sQuad.bl.vertices.y),
        ccp(m_sQuad.br.vertices.x,m_sQuad.br.vertices.y),
        ccp(m_sQuad.tr.vertices.x,m_sQuad.tr.vertices.y),
    };
    ccDrawPoly(vertices, 4, true);
#elif CC_SPRITE_DEBUG_DRAW == 2
    // draw texture box
    //除錯模式 2:繪製紋理邊緣
    CCSize s = this->getTextureRect().size;
    CCPoint offsetPix = this->getOffsetPosition();
    CCPoint vertices[4] = {
        ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
        ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
    };
    ccDrawPoly(vertices, 4, true);
#endif // CC_SPRITE_DEBUG_DRAW


    CC_INCREMENT_GL_DRAWS(1);


    CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
}
複製程式碼
   觀察 draw 方法的程式碼可知,它包含 5 部分,其中前 4 個部分較為重要。第 1 部分主要負責設定 OpenGL 狀態,如開啟貼圖等。第 2 部分負責設定顏色混合模式,與貼圖渲染的方式有關。第 3、4 部分分別負責繫結紋理與繪圖。這與 10.1.2 節中提供的繪圖程式碼流程類似,首先繫結紋理,然後分別設定頂點座標、紋理座標以及頂點顏色,最終繪製幾何體,其中頂點座標、紋理座標和頂點顏色需要在呼叫 draw 方法前計算出來。第 5 部分進行一些除錯相關的處理操作。


  同時我們也可以觀察到,在進行一次普通精靈的繪製過程中,我們需要繫結一次紋理,設定一次頂點資料,繪製一次三角形帶。對 OpenGL 的每一次呼叫都會花費一定的開銷,當我們需要大量繪製精靈的時候,效能就會快速下降,甚至會導致幀率降低。因此,針對不同的情況,可以採取不同的策略來降低 OpenGL 呼叫次數,從而大幅提高遊戲效能。這些技巧我們將在後面詳細介紹,現在繼續關注 Cocos2d-x 的繪圖原理。


(1)渲染樹的繪製:


  回顧 Cocos2d-x 遊戲的層次:導演類 CCDirector 直接控制渲染樹的根節點--場景(CCScene),場景包含多個層CCLayer),層中包含多個精靈(CCSprite)。實際上,每一個上述的遊戲元素都在渲染樹中表示為節點(CCNode),遊戲元素的歸屬關係就轉換為了節點間的歸屬關係,進而形成樹結構。


  CCNode 的 visit 方法實現了對一棵渲染樹的繪製。為了繪製樹中的一個節點,就需要繪製自己的子節點,直到沒有子節點可以繪製時再結束這個過程。因此,為了每一幀都繪製一次渲染樹,就需要呼叫渲染樹的根節點。換句話說,當前場景的visit 方法在每一幀都會被呼叫一次。這個呼叫是由遊戲主迴圈完成的,在 cocos2d-x遊戲引擎核心之一中,我們介紹了 Cocos2d-x 的排程原理,在遊戲的每一幀都會執行一次主迴圈,並在主迴圈中實現對渲染樹的渲染。下面是簡化後的主迴圈程式碼,在註釋中標明瞭對當前場景 visit 方法的呼叫:


複製程式碼
void CCDirector::drawSceneSimplified()
{
    _calculate_time();


    if (! m_bPaused)
    m_pScheduler->update(m_fDeltaTime);


    if (m_pNextScene)
    setNextScene();


    _deal_with_opengl();


    if (m_pRunningScene)
    m_pRunningScene->visit();   //繪製當前場景
    
    _do_other_things();
}
複製程式碼
  繪製父節點時會引起子節點的繪製,同時,子節點的繪製方式與父節點的屬性也有關。例如,父節點設定了放大比例,則子節點也會隨之放大;父節點移動一段距離,則子節點會隨之移動並保持相對位置不變。顯而易見,繪製渲染樹是一個遞迴的過程,下面我們來詳細探討 visit 的實現,相關程式碼如下:


複製程式碼
void CCNode::visit()
{
    //1. 先行處理
    if (!m_bIsVisible)
    {
        return;
    }
    //矩陣壓棧
    kmGLPushMatrix();


    //處理 Grid 特效
     if (m_pGrid && m_pGrid->isActive())
     {
         m_pGrid->beforeDraw();
     }


    //2. 應用變換
    this->transform();


    //3. 遞迴繪圖
    CCNode* pNode = NULL;
    unsigned int i = 0;


    if(m_pChildren && m_pChildren->count() > 0)
    {
        //存在子節點
        sortAllChildren();
        // draw children zOrder < 0
        //繪製 zOrder < 0 的子節點
        ccArray *arrayData = m_pChildren->data;
        for( ; i < arrayData->num; i++ )
        {
            pNode = (CCNode*) arrayData->arr[i];


            if ( pNode && pNode->m_nZOrder < 0 ) 
            {
                pNode->visit();
            }
            else
            {
                break;
            }
        }
        // self draw
        //繪製自身
        this->draw();


        for( ; i < arrayData->num; i++ )
        {
            pNode = (CCNode*) arrayData->arr[i];
            if (pNode)
            {
                pNode->visit();
            }
        }        
    }
    else
    {    
        //沒有子節點:直接繪製自身
        this->draw();
    }


    // reset for next frame
    //4. 恢復工作
    m_nOrderOfArrival = 0;


     if (m_pGrid && m_pGrid->isActive())
     {
         m_pGrid->afterDraw(this);
    }
     
     //矩陣出棧
    kmGLPopMatrix();
}
複製程式碼
  (1)visit 方法分為 4 部分。第 1 部分是一些先行的處理,例如當此節點被設定為不可見時,則直接返回不進行繪製等。在這一步中,重要的環節是儲存當前的繪圖矩陣,也就是註釋中的"矩陣壓棧"操作。繪圖矩陣儲存好之後,就可以根據需要對矩陣進行任意的操作了,直到操作結束後再通過"矩陣出棧"來恢復儲存的矩陣。由於所有對繪圖矩陣的操作都在恢復矩陣之前進行,因此我們的改動不會影響到以後的繪製。


   (2)在第 2 部分中,visit 方法呼叫了 transform 方法進行一系列變換,以便把自己以及子節點繪製到正確的位置上。為了理解transform 方法,我們首先從 draw 方法的含義開始解釋。draw 方法負責把圖形繪製出來,但是從上一節的學習可知,draw方法並不關心紋理繪製的位置,實際上它僅把紋理繪製到當前座標系中的原點(如圖 10-7a 所示)。為了把紋理繪製到正確的位置,我們需要在繪製之前調整當前座標系,這個操作就由 transform 方法完成,經過變換後的座標系恰好可以使紋理繪製到正確的位置(如圖 10-7b 所示)。關於 transform 方法,我們稍後將會討論。






  (3)經過第 2 部分的變換後,我們得到了一個正確的座標系,接下來的第 3 部分則開始繪圖。visit 方法中進行了一個判斷:如果節點不包含子節點,則直接繪製自身;如果節點包含子節點,則需要對子節點進行遍歷,具體的方式為首先對子節點按照 ZOrder 由小到大排序,首先對於 ZOrder 小於 0 的子節點,呼叫其 visit 方法遞迴繪製,然後繪製自身,最後繼續按次序把 ZOrder 大於 0 的子節點遞迴繪製出來。經過這一輪遞迴,以自己為根節點的整個渲染樹包括其子樹都繪製完了。


  (4)最後是第 4 部分,進行繪製後的一些恢復工作。這一部分中重要的內容就是把之前壓入棧中的矩陣彈出來,把當前矩陣恢復成壓棧前的樣子。


  以上部分構成了 Cocos2d-x 渲染樹繪製的整個框架,無論是精靈、層還是粒子引擎,甚至是場景,都遵循渲染樹節點的繪製流程,即通過遞迴呼叫 visit 方法來按層次次序繪製整個遊戲場景。同時,通過 transform 方法來實現座標系的變換。


 三、座標變換


在繪製渲染樹中,最關鍵的步驟之一就是進行座標系的變換。沒有座標系的變換,則無法在正確的位置繪製出紋理。同時,座標系的變換在其他的場合(例如碰撞檢測中)也起著十分重要的作用。因此在這一節中,我們將介紹 Cocos2d-x 中的座標變換功能。


複製程式碼
void CCNode::transform()
{    
    kmMat4 transfrom4x4;


    // Convert 3x3 into 4x4 matrix
    //獲取相對於父節點的變換矩陣 transform4x4
    CCAffineTransform tmpAffine = this->nodeToParentTransform();
    CGAffineToGL(&tmpAffine, transfrom4x4.mat);


    // Update Z vertex manually
    //設定 z 座標
    transfrom4x4.mat[14] = m_fVertexZ;


    //當前矩陣與 transform4x4 相乘
    kmGLMultMatrix( &transfrom4x4 );




    // XXX: Expensive calls. Camera should be integrated into the cached affine matrix
    //處理攝像機與 Grid 特效
    if ( m_pCamera != NULL && !(m_pGrid != NULL && m_pGrid->isActive()) )
    {
        bool translate = (m_tAnchorPointInPoints.x != 0.0f || m_tAnchorPointInPoints.y != 0.0f);


        if( translate )
            kmGLTranslatef(RENDER_IN_SUBPIXEL(m_tAnchorPointInPoints.x), RENDER_IN_SUBPIXEL(m_tAnchorPointInPoints.y), 0 );


        m_pCamera->locate();


        if( translate )
            kmGLTranslatef(RENDER_IN_SUBPIXEL(-m_tAnchorPointInPoints.x), RENDER_IN_SUBPIXEL(-m_tAnchorPointInPoints.y), 0 );
    }


}
複製程式碼
  可以看到,上述程式碼用到了許多以"km"為字首的函式,這是 Cocos2d-x 使用的一個開源幾何計算庫 Kazmath。在 10.1.3 節
的末尾曾提到過,它是 OpenGL ES 1.0 變換函式的代替,可以為程式編寫提供便利。在這個方法中,首先通過nodeToParentTransform 方法獲取此節點相對於父節點的變換矩陣,然後把它轉換為 OpenGL 格式的矩陣並右乘在當前繪圖矩陣之上,最後進行了一些攝像機與 Gird 特效相關的操作。把此節點相對於父節點的變換矩陣與當前節點相連,也就意味著在當前座標系的基礎上進行座標系變換,得到新的合適的座標系。這個過程中,變換矩陣等價於座標系變換的方式.






"節點座標系"指的是以一個節點作為參考而產生的座標系,換句話說,它的任何一個子節點的座標值都是由這個座標系確定的,通過以上方法,我們可以方便地處理觸控點,也可以方便地計算兩個不同座標系下點之間的方向關係。例如,若我們需要判斷一個點在另一座標系下是否在同一個矩形之內,則可以把此點轉換為世界座標系,再從世界座標系轉換到目標座標系中,此後只需要通過 contentSize 屬性進行判斷即可,相關程式碼如下:


複製程式碼
bool IsInBox(CCPoint point)
{
    CCPoint pointWorld = node1->convertToWorldSpace(point);
    CCPoint pointTarget = node2->convertToNodeSpace(pointWorld);
    CCSize contentSize = node2->getContentSize();
    if(0 <= pointTarget.x && pointTarget.x <= contentSize.width && 0 <= pointTarget.y && pointTarget.y <= contentSize.height)
    return true;
}
複製程式碼
注:上面程式碼中的point座標是相對當前節點(也即相對自己)的座標,比如當前節點A在父節點B中的座標為(50, 100), 當取A的座標為(0, 0)時,取到的是節點A的左下角位置,與節點A在父節點B中的位置無關。 得到在目標節點中的座標同樣也是相對目標節點,所以當要判斷節點A是否在目標節點中的時候,只要判斷轉換得到的座標的x, y是否在目標節點(0, 0)和(width, height)之間。


四、繪圖瓶頸:


(1)紋理過小:OpenGL 在視訊記憶體中儲存的紋理的長寬畫素數一定是 2 的冪,對於大小不足的紋理,則在其餘部分填充空白,這無疑是對視訊記憶體極大的浪費;另一方面,同一個紋理可以容納多個精靈,把內容相近的精靈拼合到一起是一個很好的選擇。


(2)紋理切換次數過多:當我們連續使用兩個不同的紋理繪圖時,GPU 不得不進行一次紋理切換,這是開銷很大的操作,然而當我們不斷地使用同一個紋理進行繪圖時,GPU 工作在同一個狀態,額外開銷就小了很多,因此,如果我們需要批量繪製一些內容相近的精靈,就可以考慮利用這個特點來減少紋理切換的次數。


(3)紋理過大:視訊記憶體是有限的,如果在遊戲中不加節制地使用很大的紋理,則必然會導致視訊記憶體緊張,因此要儘可能減少紋理的尺寸以及色深。


(1-)碎圖壓縮與精靈框幀:使用各自的紋理來建立精靈,由此導致的紋理過小和紋理切換次數過多是產生瓶頸的根源。針對這個問題,一個簡單的解決方案是碎圖合併與精靈框幀。(碎圖合併工具 TexturePacker)


(2-)批量渲染:有了足夠大的紋理圖後,就可以考慮從渲染次數上進一步優化了。如果不需要切換繫結紋理,那麼幾個 OpenGL 的渲染請求是可以批量提交的,也就是說,在同一紋理下的繪製都可以一次提交完成。在 Cocos2d-x 中,我們提供了 CCSpriteBatchNode來實現這一優化。


(3-)色彩深度優化:預設情況下,我們匯出的紋理圖片是 RGBA8888 格式的,它的含義是每個畫素的紅、藍、綠、不透明度 4 個值分別佔用 8 位元(相當於 1 位元組),因此一個畫素總共需要使用 4 個位元組表示。若降低紋理的品質,則可以採用 RGBA4444 格式來儲存圖片。RGBA4444 圖片的每一個畫素中每個分量只佔用 4 位元,因此一個畫素總共佔用 2 位元組,圖片大小將整整減少一半。對於不透明的圖片,我們可以選擇無 Alpha 通道的顏色格式,例如 RGB565,可以在不增加尺寸的同時提高影象品質。各種影象編輯器通常都可以修改圖片的色彩深度,TexturePacker 也提供了這個功能。


五、繪圖技巧


(1)遮罩效果


  遮罩效果又稱為剪刀效果,允許一切的渲染結果只在螢幕的一個指定區域顯示:開啟遮罩效果後,一切的繪製提交都是正常渲染的,但最終只有螢幕上的指定區域會被繪製。形象地說,我們將當前螢幕截圖成一張固定的畫布蓋在螢幕上,只挖空指定的區域使之能活動,而螢幕上的其他位置儘管如常更新,但都被掩蓋住了。 於是,我們可以在錶盤上順序排列所有的數字,不該顯示的部分用遮罩效果蓋住,滾動的錶盤效果可以藉助遮罩得到快速的實現。


我們在數字類中新增遮罩效果,將不應該出現的數字隱藏起來。過載NumberScrollLabel::visit 方法,相關程式碼如下所示:


複製程式碼
void visit()
{
    //啟動遮罩效果
    glEnable(GL_SCISSOR_TEST);
    CCPoint pos = CCPointZero;
    pos = visibleNode->getParent()->convertToWorldSpace(pos); //獲取螢幕絕對位置
    CCRect rect = CCRectMake(pos.x, pos.y, m_numberSize, m_numberSize);
    //設定遮罩效果
    glScissor(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
    CCNode::visit();
    //關閉遮罩效果
    glDisable(GL_SCISSOR_TEST);
}
複製程式碼
  這裡我們選擇重寫 visit 函式來設定遮罩效果,對於"僅在 draw 中設定繪圖效果"原則是個小小的破例。這樣做是為了能成功遮擋所有子節點的無效繪圖。回想一下引擎中渲染樹的繪製過程,draw 方法並不是遞迴呼叫的,而 visit 方法是遞迴的,並且 visit 方法通過呼叫 draw 來實現繪圖。因此,我們在設定了遮罩效果後呼叫了父類的 visit,使繪製流程正常進行下去,最後在繪製完子節點後關閉遮罩效果。(詳細參見《cocos2d-x高階開發教程》11章)


(2)小窗預覽(截圖功能)


  我們再為遊戲新增一個小小的截圖功能,藉此討論遊戲中涉及的底層的資料交流。底層的資料交流必須介紹兩個類:CCImage 和 CCTexture2D,這是引擎提供的描述紋理圖片的類,也是我們和顯示卡進行資料交換時主要涉及的資料結構。


  CCImage 在"CCImage.h"中定義,表示一張載入到記憶體的紋理圖片。在其內部的實現中,紋理以每個畫素的顏色值儲存在記憶體之中。CCImage 通常作為檔案和顯示卡間資料交換的一個工具,因此主要提供了兩個方面的功能:一方面是檔案的載入與儲存,另一方面是記憶體緩衝區的讀寫。


  我們可以使用 CCImage 輕鬆地讀寫圖片檔案。目前,CCImage 支援 PNG、JPEG 和 TIFF 三種主流的圖片格式。下面列舉與檔案讀寫相關的方法:


bool initWithImageFile(const char* strPath, EImageFormat imageType = kFmtPng);
bool initWithImageFileThreadSafe(const char* fullpath, EImageFormat imageType = kFmtPng);
bool saveToFile(const char* pszFilePath, bool bIsToRGB = true);
  CCImage 也提供了讀寫記憶體的介面。getData 和 getDataLen 這兩個方法提供了獲取當前紋理的緩衝區的功能,而
initWithImageData 方法提供了使用畫素資料初始化圖片的功能。相關的方法定義如下:


複製程式碼
unsigned char* getData();
int getDataLen();
bool initWithImageData(void* pData,
                        int nDataLen,
                        EImageFormat eFmt = kFmtUnKnown,
                        int nWidth = 0,
                        int nHeight = 0,
                        int nBitsPerComponent = 8);
複製程式碼
注意,目前僅支援從記憶體中載入 RGBA8888 格式的圖片。


另一個重要的類是 CCTexture2D,之前已經反覆提及,它描述了一張紋理,知道如何將自己繪製到螢幕上。通過該類還可以設定紋理過濾、抗鋸齒等引數。該類還提供了一個介面,將字串建立成紋理。


這裡需要特別重提的兩點是:該類所包含的紋理大小必須是 2 的冪次,因此紋理的大小不一定就等於圖片的大小;另外,有別於 CCImage,這是一張存在於視訊記憶體中的紋理,實際上並不一定存在於記憶體中。


瞭解了 CCImage 和CCTexture2D 後,我們就可以新增截圖功能了。截圖應該是一個通用的功能,不妨寫成全域性函式放在 MTUtil
庫中,使其不依賴於任何一個類。首先,我們使用 OpenGL 的一個底層函式 glReadPixels 實現截圖:


void glReadPixels (GLint x, GLint y,GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);
這個函式將當前螢幕上的畫素讀取到一個記憶體塊 pixels 中,且 pixels 指標指向的記憶體必須足夠大。為此,我們設計一個函式 saveScreenToCCImage 來實現截圖功能,相關程式碼如下:


複製程式碼
unsigned char screenBuffer[1024 * 1024 * 8];
CCImage* saveScreenToCCImage(bool upsidedown = true)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSizeInPixels();
    int w = winSize.width;
    int h = winSize.height;
    int myDataLength = w * h * 4;
    GLubyte* buffer = screenBuffer;
    glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    CCImage* image = new CCImage();
    if(upsidedown) {
        GLubyte* buffer2 = (GLubyte*) malloc(myDataLength);
        for(int y = 0; y <h; y++) {
            for(int x = 0; x <w * 4; x++) {
                buffer2[(h - 1 - y) * w * 4 + x] = buffer[y * 4 * w + x];
            }
        }    
        bool ok = image->initWithImageData(buffer2, myDataLength,
        CCImage::kFmtRawData, w, h);
        free(buffer2);
    }
    else {
        bool ok = image->initWithImageData(buffer, myDataLength,
                                            CCImage::kFmtRawData, w, h);
    }
    return image;
}
複製程式碼
  這裡我們使用 glReadPixels 方法將當前繪圖區的畫素都讀取到了一個記憶體緩衝區內,然後用這個緩衝區來初始化 CCImage並返回。注意,我們設定了一個引數 upsidedown,當這個引數為 true 時,我們將所有畫素倒序排列了一次。這是因為 OpenGL的繪製是從上到下的,如果直接使用讀取的資料,再次繪製時將上下倒置。


  在這個函式的基礎上,我們在遊戲選單層中新增相關按鈕和響應操作就完成了截圖功能,相關程式碼如下:


複製程式碼
void GameMenuLayer::saveScreen(CCObject* sender)
{
    CCImage* image = saveScreenToCCImage();
    image->saveToFile("screen.png");
    image->release();
}
複製程式碼
  實際上,引擎還提供了另一個很有趣的方法讓我們完成截圖功能。在 Cocos2d-x 中,我們實現了一個渲染紋理類CCRenderTexture,其作用是將繪圖從裝置螢幕轉移到一張紋理上,從而使得一段連續的繪圖被儲存到紋理中。這在 OpenGL的底層中並不罕見,有趣的地方就在於,我們可以使用這個渲染紋理類配合主動呼叫的繪圖實現截圖效果。下面的函式saveScreenToRenderTexture 同樣實現了截圖功能:


複製程式碼
CCRenderTexture* saveScreenToRenderTexture()
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
    CCRenderTexture* render = CCRenderTexture::create(winSize.height, winSize.width);
    render->begin();
    CCDirector::sharedDirector()->drawScene();
    render->end();
    return render;
}
複製程式碼
  在上述程式碼中,CCRenderTexture 的 begin 和 end 介面規定了繪圖轉移的時機,在這兩次函式呼叫之間的 OpenGL 繪圖都會
被繪製到一張紋理上。注意,這裡我們主動呼叫了導演類的繪製場景功能。但是根據引擎的介面規範,我們不建議這樣做,因為每次繪製都產生了 CCNode 類的 visit 函式的呼叫,但只要遵守不在 visit 中更改繪圖相關狀態的規範,可以保證不對後續繪圖產生影響。


  渲染紋理類提供了兩個匯出紋理的介面,分別可以匯出紋理為 CGImage 和檔案,它們的定義如下:


CCImage* newCCImage();
bool saveToFile(const char *name, tCCImageFormat format);
感興趣的讀者可以檢視 CCRenderTexture 的內部實現,其匯出紋理的過程實際上也是利用 glReadPixels 函式來獲取畫素資訊。因此,匯出紋理這一步的效率和我們自己編寫的 saveScreenToCCImage 函式是一致的。然而如果採用重新繪製的方式來匯出紋理則與此不同,一次螢幕的過程較為費時,尤其在佈局比較複雜的場景上。重新繪製的強大之處在於繪製結果可以迅速被重用,非常適合做即時小窗預覽之類的效果。下面的 saveScreen 方法實現了實時的截圖功能:


複製程式碼
void GameMenuLayer::saveScreen(CCObject* sender)
{
    //我們註釋掉了舊的程式碼,改用 saveScreenToRenderTexture 方法來實現截圖
    //CCImage* image = saveScreenToCCImage();
    //image->saveToFile("screen.png");
    //image->release();
    CCRenderTexture* render = saveScreenToRenderTexture();
    this->addChild(render);
    render->setScale(0.3);
    render->setPosition(ccp(CCDirector::sharedDirector()->getWinSize().width, 0));
    render->setAnchorPoint(ccp(1,0));
}
複製程式碼
CCRenderTexture 繼承自 CCNode,我們把它新增到遊戲之中,就可以在右下角看到一個動態的螢幕截圖預覽了,如下圖所示:






(3)可程式設計管線:


  正如本章開始所說的那樣,在 Cocos2d-x 中,最大的變革就是引入了 OpenGL ES 2.0 作為底層繪圖,這意味著渲染從過去
的固定管線升級到了可程式設計管線,我們可以通過著色器定義每一個頂點或畫素的著色方式,產生更豐富的效果。著色器實際上就是一小段執行渲染效果的程式,由圖形處理單元執行。之所以說是"一小段",是因為圖形渲染的執行週期非常短,不允許過於臃腫的程式,因此通常都比較簡短。


在渲染流水線上,存在著兩個對開發者可見的可程式設計著色器,具體如下所示。


  頂點著色器(vertex shader)。對每個頂點呼叫一次,完成頂點變換(投影變換和檢視模型變換)、法線變換與規格化、紋理座標生成、紋理座標變換、光照、顏色材質應用等操作,並最終確定渲染區域。在 Cocos2d-x 的世界中,精靈和層等都是矩形,它們的一次渲染會呼叫 4 次頂點著色器。


  段著色器(fragment shader,又稱片段著色器)。這個著色器會在每個畫素被渲染的時候呼叫,也就是說,如果我們在螢幕上顯示一張 320×480 的圖片,那麼畫素著色器就會被呼叫 153 600 次。所幸,在顯示卡中通常存在不止一個圖形處理單元,渲染的過程是並行化的,其渲染效率會比用序列的 CPU 執行高得多。


  這兩個著色器不能單獨使用,必須成對出現,這是因為頂點著色器會首先確定每一個顯示到螢幕上的頂點的屬性,然後這些頂點組成的區域被化分成一系列畫素,這些畫素的每一個都會呼叫一次段著色器,最後這些經過處理的畫素顯示在螢幕上,二者是協同工作的。


引擎提供了 CCGLProgram 類來處理著色器相關操作,對當前繪圖程式進行了封裝,其中使用頻率最高的應該是獲取著色器程式的介面:


const GLuint getProgram();
  該介面返回了當前著色器程式的識別符號。後面將會看到,在操作 OpenGL 的時候,我們常常需要針對不同的著色器程式作設定。注意,這裡返回的是一個無符號整型的識別符號,而不是一個指標或結構引用,這是 OpenGL 介面的一個風格。物件(紋理、著色器程式或其他非標準型別)都是使用整型識別符號來表示的。


  CCGLProgram 提供了兩個函式匯入著色器程式,支援直接從記憶體的字串流載入或是從檔案中讀取。這兩個函式的第一個參
數均指定了頂點著色器,後一個引數則指定了畫素著色器:


bool initWithVertexShaderByteArray(const GLchar* vShaderByteArray,const GLchar* fShaderByteArray);
bool initWithVertexShaderFilename(const char* vShaderFilename,const char* fShaderFilename);
  僅僅載入肯定是不夠的,我們還需要給著色器傳遞執行時必要的輸入資料。在著色器中存在兩種輸入資料,分別被標識為
attribute 和 uniform。