cocos2dx的渲染机制 / OpenGL渲染流程

1 顶点和片段介绍
2 why不同纹理会需要重新渲染 纹理优化---------

–参考文档:
https://blog.csdn.net/u013654125/article/details/79698469 ~ https://blog.csdn.net/sun___shine/article/details/48313749

一、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
在这里插入图片描述
在这里插入图片描述
--------------------------------------------------------------------------------
OpenGL渲染流程及渲染管线,OpenGL ES2.0的渲染管线如下图所示,阴影部分为可编程阶段。

在这里插入图片描述
下面是对图中的每个过程的详细解释:

1 VBO/VAO(顶点缓冲区对象或顶点数组对象)
VBO/VAO是cpu提供给GPU的顶点信息,包括了顶点的位置、颜色、纹理坐标(用于纹理贴图)等顶点信息。
VBO,全名Vertex Buffer Object。它是GPU里面的一块缓冲区,当我们需要传递数据的时候,可以先向GPU申请一块内存,然后往里面填充数据。最后,再通过调用glVertexAttribPointer把数据传递给Vertex Shader。
VAO,全名为Vertex Array Object,它的作用主要是记录当前有哪些VBO,每个VBO里面绑定的是什么数据,还有每一个vertex attribute绑定的是哪一个VBO。

2 VertexShader(顶点着色器)
在这里插入图片描述
顶点着色器是处理VBO/VAO提供的顶点信息的程序。
VBO/VAO提供的每个顶点都执行一遍顶点着色器。
Uniforms(一种变量类型)在每个顶点保持一致,Attribute每个顶点都不同(可以理解为输入顶点属性)。

顶点着色器的输入数据由下面组成:

Attributes:使用顶点数组封装每个顶点的数据,一般用于每个顶点都各不相同的变量,如顶点位置、颜色等。
Uniforms:顶点着色器使用的常量数据,不能被着色器修改,一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的变量,如当前光源的位置。
Samplers:这个是可选的,一种特殊的uniforms,表示顶点着色器使用的纹理。
Shader program:顶点着色器的源码或可执行文件,描述了将对顶点执行的操作。
顶点着色器的输出:

varying:在图元光栅化阶段,这些varying值为每个生成的片元进行计算,并将结果作为片元着色器的输入数据。从分配给每个顶点的原始varying值来为每个片元生成一个varying值的机制叫做插值。
另外,还有gl_postion、gl_FrontFacing和gl_PointSize。
顶点着色器可用于传统的基于顶点的操作,例如:基于矩阵变换位置,进行光照计算来生成每个顶点的颜色,生成或者变换纹理坐标。
另外因为顶点着色器是由应用程序指定的,所以你可以用来进行任意自定义的顶点变换。

3 PrimitiveAssembly(图元装配):
顶点着色器下一个阶段是图元装配,这个阶段,把顶点着色器输出的顶点组合成图元。图元(primitive)是一个能用opengl es绘图命令绘制的几何体,包括三角形、直线或者点精灵等几何对象,绘图命令指定了一组顶点属性,描述了图元的几何形状和图元类型。在图元装配阶段,这些着色器处理过的顶点被组装到一个个独立的几何图元中,例如三角形、线、点精灵。对于每个图元,必须确定它是否位于视椎体内(3维空间显示在屏幕上的可见区域),如果图元部分在视椎体中,需要进行裁剪,如果图元全部在视椎体外,则直接丢弃图元。裁剪之后,顶点位置转换成了屏幕坐标。背面剔除操作也会执行,它根据图元是正面还是背面,如果是背面则丢弃该图元。经过裁剪和背面剔除操作后,就进入渲染流水线的下一个阶段:光栅化。

4 rasterization(光栅化)
这里写图片描述

光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理(片段着色器的输入)。这些二维片段代表着可在屏幕上绘制的像素。用于从分配给每个图元顶点的顶点着色器输出生成每个片段值的机制称作插值(Interpolation)。这句不是人话的话解释了一个问题,就是从cpu提供的分散的顶点信息是如何变成屏幕上密集的像素的,图元装配后顶点可以理解成变为图形,光栅化时可以根据图形的形状,插值出那个图形区域的像素(纹理坐标v_texCoord、颜色等信息)。注意,此时的像素并不是屏幕上的像素,是不带有颜色的。接下来的片段着色器完成上色的工作。总之,光栅化阶段把图元转换成片元集合,之后会提交给片元着色器处理,这些片元集合表示可以被绘制到屏幕的像素。

5 FragmentShader(片段着色器)
这里写图片描述

片段着色器为片段(像素)上的操作实现了通用的可编程方法,光栅化输出的每个片段都执行一遍片段着色器,对光栅化阶段生成每个片段执行这个着色器,生成一个或多个(多重渲染)颜色值作为输出。
片元着色器对片元实现了一种通用的可编程方法,它对光栅化阶段产生的每个片元进行操作,需要的输入数据如下:

Varying variables:顶点着色器输出的varying变量经过光栅化插值计算后产生的作用于每个片元的值。
Uniforms:片元着色器使用的常量数据
Samplers:一种特殊的uniforms,表示片元着色器使用的纹理。
Shader program:片元着色器的源码或可执行文件,描述了将对片元执行的操作。
片元着色器也可以丢弃片元或者为片元生成一个颜色值,保存到内置变量gl_FragColor。光栅化阶段产生的颜色、深度、模板和屏幕坐标(Xw, Yw)成为流水线中pre-fragment阶段(FragmentShader之后)的输入。

6Per-Fragment Operations(逐个片元操作阶段)
这里写图片描述
片元着色器之后就是逐个片元操作阶段,包括一系列的测试阶段。一个光栅化阶段产生的具有屏幕坐标(Xw, Yw)的片元,只能修改framebuffer(帧缓冲)中位置在(Xw, Yw)的像素。上图是Opengl es 2.0逐片元操作的过程:

Pixel ownership test:像素所有权测试决定framebuffer中某一个(Xw, Yw)位置的像素是否属于当前Opengl ES的context,比如:如果一个Opengl ES帧缓冲窗口被其他窗口遮住了,窗口系统将决定被遮住的像素不属于当前Opengl ES的context,因此也就不会被显示。
Scissor test:裁剪测试决定位置为(Xw, Yw)的片元是否位于裁剪矩形内,如果不在,则被丢弃。
Stencil and depth tests:模板和深度测试传入片元的模板和深度值,决定是否丢弃片元。
Blending:将新产生的片元颜色值和framebuffer中某个(Xw, Yw)位置存储的颜色值进行混合。
Dithering:抖动可以用来最大限度的减少使用有限精度存储颜色值到framebuffer的工件。
逐片元操作之后,片元要么被丢弃,要么一个片元的颜色,深度或者模板值被写入到framebuffer的(Xw, Yw)位置,不过是否真的会写入还得依赖于write masks启用与否。write masks能更好的控制颜色、深度和模板值写入到合适的缓冲区。例如:颜色缓冲区中的write mask可以被设置成没有红色值写入到颜色缓冲区。另外,Opengl ES 2.0提供从framebuffer中获取像素的接口,不过需要记住的是像素只能从颜色缓冲区读回,深度和模板值不能读回。

参考: 
OpenGL渲染流程 http://www.cnblogs.com/BigFeng/p/5068715.html 
OpenGL ES 2.0渲染管线 http://codingnow.cn/opengles/1504.html (图片来源) 
OpenGL ES 2.0可编程管道 http://www.cnblogs.com/listenheart/p/3292672.html 
OpenGL ES 2.0编程基础 http://blog.csdn.net/iispring/article/details/7649628 
OpenGL-渲染管线的流程(有图有真相) http://www.cnblogs.com/zhanglitong/p/3238989.html

–https://blog.csdn.net/u013654125/article/details/79698469

--------------------------------------------------------------------------------
https://zhuanlan.zhihu.com/p/26077873
面试疑问:
盛大drawCall是什么 游族why不同纹理会增加drawCall
在这里插入图片描述在这里插入图片描述
CreatorPrimer| CustomMaterial.js源码分析

发布了205 篇原创文章 · 获赞 39 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/u013321328/article/details/103079107