cocos2d-x遊戲引擎核心之六——繪圖原理和繪圖技巧

1、OpenGL基礎node

  遊戲引擎是對底層繪圖接口的包裝,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)渲染流水線:ide

  當咱們把繪製的圖形傳遞給 OpenGL 後,OpenGL 還要進行許多操做才能完成 3D 空間到屏幕的投影。一般,渲染流水線過程有以下幾步:顯示列表、求值器、頂點裝配、像素操做、紋理裝配、光柵化和片段操做等。OpenGL 從 2.0 版本開始引入了可編程着色器(shader)。函數

(4)繪圖函數:工具

(5)矩陣與變換:OpenGL 對頂點進行的處理實際上能夠概括爲接受頂點數據、進行投影、獲得變換後的頂點數據這 3 個步驟。佈局

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

:詳細參見《cocos2d-x高級開發教程》、《OpenGL編程指南》

2、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 部分負責設置顏色混合模式,與貼圖渲染的方式有關。第 三、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();
     //繪製 zOrder > 0 的子節點
        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 方法來實現座標系的變換。

 3、座標變換

在繪製渲染樹中,最關鍵的步驟之一就是進行座標系的變換。沒有座標系的變換,則沒法在正確的位置繪製出紋理。同時,座標系的變換在其餘的場合(例如碰撞檢測中)也起着十分重要的做用。所以在這一節中,咱們將介紹 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,它是 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)之間。

4、繪圖瓶頸:

(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 也提供了這個功能。

5、繪圖技巧

(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 中更改繪圖相關狀態的規範,能夠保證不對後續繪圖產生影響。

渲染紋理類提供了兩個導出紋理的接口,分別能夠導出紋理爲 CCImage 和文件,它們的定義以下:

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。

:詳細參見《cocos2d-x高級開發教程》11章

OpenGL繪圖技巧(遮罩層和小窗預覽,可編程着色器)

相關文章
相關標籤/搜索