clippingNode是利用opengl的裁剪缓冲区实现的,因为最近有使用这个功能需要,顺便把这部分实现看看了看。
opengl的裁剪主要有以下几个步骤:
1、开启裁剪缓冲区
2、设置裁剪缓冲区中的mask。
3、正常绘制图形,这个时候会根据裁剪缓冲区的值和设置好的比较函数进行计算,根据通过与否选择是否会知道framebuffer
4、绘制完成之后关闭裁剪缓冲区
这几个步骤在cocos2dx的clippingNode中体现在以下的这段代码中:
<pre name="code" class="cpp"> _groupCommand.init(_globalZOrder);
renderer->addCommand(&_groupCommand);
renderer->pushGroup(_groupCommand.getRenderQueueID());
_beforeVisitCmd.init(_globalZOrder);
_beforeVisitCmd.func = CC_CALLBACK_0(ClippingNode::onBeforeVisit, this); //1
renderer->addCommand(&_beforeVisitCmd);
if (_alphaThreshold < 1)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC || CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
#else
// since glAlphaTest do not exists in OES, use a shader that writes
// pixel only if greater than an alpha threshold
GLProgram *program = GLProgramCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE_ALPHA_TEST_NO_MV);
GLint alphaValueLocation = glGetUniformLocation(program->getProgram(), GLProgram::UNIFORM_NAME_ALPHA_TEST_VALUE);
// set our alphaThreshold
program->use();
program->setUniformLocationWith1f(alphaValueLocation, _alphaThreshold);
// we need to recursively apply this shader to all the nodes in the stencil node
// FIXME: we should have a way to apply shader to all nodes without having to do this
setProgram(_stencil, program);
#endif
}
_stencil->visit(renderer, _modelViewTransform, flags); //2
_afterDrawStencilCmd.init(_globalZOrder);
_afterDrawStencilCmd.func = CC_CALLBACK_0(ClippingNode::onAfterDrawStencil, this); //3
renderer->addCommand(&_afterDrawStencilCmd);
int i = 0;
bool visibleByCamera = isVisitableByVisitingCamera();
//4
if(!_children.empty())
{
sortAllChildren();
// draw children zOrder < 0
for( ; i < _children.size(); i++ )
{
auto node = _children.at(i);
if ( node && node->getLocalZOrder() < 0 )
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
// self draw
if (visibleByCamera)
this->draw(renderer, _modelViewTransform, flags);
for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
else if (visibleByCamera)
{
this->draw(renderer, _modelViewTransform, flags);
}
_afterVisitCmd.init(_globalZOrder);
_afterVisitCmd.func = CC_CALLBACK_0(ClippingNode::onAfterVisit, this); //5
renderer->addCommand(&_afterVisitCmd);
在这段代码中一共有5步:
1、onBeforeVisit函数:在这个函数中启动了裁剪缓冲去,并且设置好缓冲区比较函数。
2、绘制裁剪图形,这里使用图形的alpha来确定什么区域可以绘制什么区域不绘制,通过设置的比较函数来设置裁剪缓冲区的值。
3、裁剪缓冲设置完毕之后,重新设置裁剪参数(比较函数和ref、mask等)。
4、绘制图形,通过测试的将会知道frambuffer。
5、关闭裁剪缓冲区。上面的第1、2步骤一起完成了裁剪缓冲去mask的设置工作。
3、4步完成了裁剪工作。
5步清楚了裁剪参数配置,关闭裁剪缓冲区,恢复普通绘制。
下面具体看看每一步都是怎么工作的:
第一步:为了方便我直接在代码中进行注释来说明。(中文注释是我自己的理解)
<pre name="code" class="cpp">///////////////////////////////////
// INIT
// increment the current layer
s_layer++; //这个参数是为了可以在clippingNode中嵌入clippingNode使用的,但是通过分析我发现这里有一点点小bug。
// mask of the current layer (ie: for layer 3: 00000100)
GLint mask_layer = 0x1 << s_layer; //这个值作为裁剪缓冲区中的mask。
// mask of all layers less than the current (ie: for layer 3: 00000011)
GLint mask_layer_l = mask_layer - 1;
// mask of all layers less than or equal to the current (ie: for layer 3: 00000111)
_mask_layer_le = mask_layer | mask_layer_l;
// manually save the stencil state
_currentStencilEnabled = glIsEnabled(GL_STENCIL_TEST);
glGetIntegerv(GL_STENCIL_WRITEMASK, (GLint *)&_currentStencilWriteMask);
glGetIntegerv(GL_STENCIL_FUNC, (GLint *)&_currentStencilFunc);
glGetIntegerv(GL_STENCIL_REF, &_currentStencilRef);
glGetIntegerv(GL_STENCIL_VALUE_MASK, (GLint *)&_currentStencilValueMask);
glGetIntegerv(GL_STENCIL_FAIL, (GLint *)&_currentStencilFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, (GLint *)&_currentStencilPassDepthFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, (GLint *)&_currentStencilPassDepthPass);
// enable stencil use
glEnable(GL_STENCIL_TEST);
// check for OpenGL error while enabling stencil test
CHECK_GL_ERROR_DEBUG();
// all bits on the stencil buffer are readonly, except the current layer bit,
// this means that operation like glClear or glStencilOp will be masked with this value
glStencilMask(mask_layer); //设置当前使用的参见缓冲区id
// manually save the depth test state
glGetBooleanv(GL_DEPTH_WRITEMASK, &_currentDepthWriteMask);
// disable depth test while drawing the stencil
//glDisable(GL_DEPTH_TEST);
// disable update to the depth buffer while drawing the stencil,
// as the stencil is not meant to be rendered in the real scene,
// it should never prevent something else to be drawn,
// only disabling depth buffer update should do
glDepthMask(GL_FALSE); //关闭深度检测
///////////////////////////////////
// CLEAR STENCIL BUFFER
// manually clear the stencil buffer by drawing a fullscreen rectangle on it
// setup the stencil test func like this:
// for each pixel in the fullscreen rectangle
// never draw it into the frame buffer
// if not in inverted mode: set the current layer value to 0 in the stencil buffer
// if in inverted mode: set the current layer value to 1 in the stencil buffer
glStencilFunc(GL_NEVER, mask_layer, mask_layer); //设置裁剪缓冲区的比较函数和参数
glStencilOp(!_inverted ? GL_ZERO : GL_REPLACE, GL_KEEP, GL_KEEP); //设置比较失败或者通过之后对裁剪缓冲去的操作。
//这两个函数是操作裁剪缓冲去的最重要的两个函数,后面会做详细介绍
// draw a fullscreen solid rectangle to clear the stencil buffer
//ccDrawSolidRect(Vec2::ZERO, ccpFromSize([[Director sharedDirector] winSize]), Color4F(1, 1, 1, 1));
drawFullScreenQuadClearStencil(); //这里进行一次绘制,因为上面将测试函数设置为了GL_NEVER,表示永远不会通过测试,所以这次绘制不会绘制到裁剪缓冲区,通过上面的glStencilOp函数可知如果不取反则此时缓冲区全为0,否则全为mask_layer。
///////////////////////////////////
// DRAW CLIPPING STENCIL
// setup the stencil test func like this:
// for each pixel in the stencil node
// never draw it into the frame buffer
// if not in inverted mode: set the current layer value to 1 in the stencil buffer
// if in inverted mode: set the current layer value to 0 in the stencil buffer
//这里再次设置比较函数和操作:如果不取反,则缓冲区会被设置为mask_layer,否则设置为0
glStencilFunc(GL_NEVER, mask_layer, mask_layer);
glStencilOp(!_inverted ? GL_REPLACE : GL_ZERO, GL_KEEP, GL_KEEP);
//因为我们是使用图片的alpha进行裁剪,所以对于非gles平台,直接使用alphatest功能:
//对于通过alpha测试的区域,如果取反,则设置为mask_layer,否则设置0.
//如果是opengles平台,因为不支持alphatest,所以使用了一个自定义的shader来实现alphatest,这个shader很简单,就是如果alpha通过则绘制,如果不通过,直接discard。 opengles设置shader的代码在上面的一段代码中,就是修改node的shaderprogram。
// enable alpha test only if the alpha threshold < 1,
// indeed if alpha threshold == 1, every pixel will be drawn anyways
if (_alphaThreshold < 1) {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC || CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
// manually save the alpha test state
_currentAlphaTestEnabled = glIsEnabled(GL_ALPHA_TEST);
glGetIntegerv(GL_ALPHA_TEST_FUNC, (GLint *)&_currentAlphaTestFunc);
glGetFloatv(GL_ALPHA_TEST_REF, &_currentAlphaTestRef);
// enable alpha testing
glEnable(GL_ALPHA_TEST);
// check for OpenGL error while enabling alpha test
CHECK_GL_ERROR_DEBUG();
// pixel will be drawn only if greater than an alpha threshold
glAlphaFunc(GL_GREATER, _alphaThreshold);
#else
#endif
}
//Draw _stencil
从上面的注释可知,在这个函数执行完毕之后,缓冲区已经被设置如下情况:
如果不取反,则缓冲现在全为0
如果取反,则全为mask_layer
并且,设置了新的比较函数和alphatest,如果接下来进行绘制操作,则通过alphatest部分的缓冲区会被重新设置:
如果不取反,则通过alphatest部分的缓冲区为mask_layer
如果取反,则通过alphatest部分的缓冲区为0
第二步:正如上面所说,对裁剪图片进行了绘制,这时缓冲的裁剪mask已经设置完毕。
第三步:重新设置裁剪函数和操作,对node中的子节点进行裁剪:下面看看第三步的参数设置:
glDepthMask(_currentDepthWriteMask);
<span style="white-space:pre"> </span>//恢复之前的深度检测设置
<span style="white-space:pre"> </span>//设置比较函数为GL_EQUAL,这个参数的意义为:mask_layer_le & mask_layer_le == mask & mask_layer_le则通过测试,否则不通过 //mask指的是裁剪缓冲区中对应位置对应的值。
//通过上面的代码中的mask_leyer_le的计算和裁剪缓冲区的设置可知:此时缓冲区中的maks要么是0要么是mask_layer
//果layer大于0,则mask_layer & mask_layer_le = mask_layer
//而mask_layer_le & mask_layer_le = mask_layer_le且mask_layer != mask_layer_le,也就是说如果clippingNode有嵌套,那么第二层嵌套开始所有node都会被裁剪掉,和本义不符,其实这里只需要继续设置mask_layer就可以了,因为每一层都有自己的裁剪缓冲区。这里不知道我的理解是否正确,求指教。</span>
glStencilFunc(GL_EQUAL, _mask_layer_le, _mask_layer_le);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); 这个函数的意义是不管通过或者不通过裁剪测试,都保证裁剪缓冲区不再被修改。
第四步:则是绘制children,在绘制的时候根据上面提到的裁剪测试方式进行裁剪;
如果不取反则通过alphatest部分能通过测试,即裁剪图像透明度大于alpha部分被绘制
如果取反,则没有通过alphatest部分通过测试,即裁剪图像透明度小于alpha部分被绘制。
第五步:恢复保存的裁剪参数(选择上一层的裁剪缓冲区或者关闭裁剪缓冲区,设置上一层的裁减函数配置)
以上就是clippingnode做的事情,下面来详细说明上面用到的两个函数:
一、glStencilFunc(func, ref, mask) 该函数用于设置裁剪缓冲区的比较函数,每一个函数对应的比较方法如下:
GL_NEVER
Always fails.
GL_LESS
Passes if ( ref & mask ) < ( stencil & mask ).
GL_LEQUAL
Passes if ( ref & mask ) <= ( stencil & mask ).
GL_GREATER
Passes if ( ref & mask ) > ( stencil & mask ).
GL_GEQUAL
Passes if ( ref & mask ) >= ( stencil & mask ).
GL_EQUAL
Passes if ( ref & mask ) = ( stencil & mask ).
GL_NOTEQUAL
Passes if ( ref & mask ) != ( stencil & mask ).
GL_ALWAYS
Always passes.
二、glStencilOp(sfail, dpfail, dppass)
这三个参数用于分别在不同情况下对裁剪缓冲区的操作
第一参数表示在裁剪测试失败的情况下所做的操作
第二参数表示在裁剪测试通过,深度测试失败的时候的操作
第三个参数表示在裁剪和深度测试都通过的时候的操作,
操作方法有如下几种:
GL_KEEP
Keeps the current value.
GL_ZERO
Sets the stencil buffer value to 0.
GL_REPLACE
Sets the stencil buffer value to ref, as specified by glStencilFunc.
GL_INCR
Increments the current stencil buffer value. Clamps to the maximum representable unsigned value.
GL_INCR_WRAP
Increments the current stencil buffer value. Wraps stencil buffer value to zero when incrementing the maximum representable unsigned value.
GL_DECR
Decrements the current stencil buffer value. Clamps to 0.
GL_DECR_WRAP
Decrements the current stencil buffer value. Wraps stencil buffer value to the maximum representable unsigned value when decrementing a stencil buffer value of zero.
GL_INVERT
Bitwise inverts the current stencil buffer value.