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


一、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();
     //繪制 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 方法來實現坐標系的變換。

 三、坐標變換

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

四、繪圖瓶頸:

(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 中更改繪圖相關狀態的規范,可以保證不對后續繪圖產生影響。

渲染紋理類提供了兩個導出紋理的接口,分別可以導出紋理為 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繪圖技巧(遮罩層和小窗預覽,可編程着色器)


免責聲明!

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



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