现在手头上负责的一个项目,基于cocos2d-x做的一个手机游戏。
里面有个背包界面。很早以前出现一个bug。
外层界面一个ScrollView,就是滚动界面,底层基于两套机制实现裁剪的。
1.基于模板缓冲机制做的。
2.基于可视区域裁剪的。
系统默认都是基于模板裁剪,最早以前我发现这个界面非常的卡顿,后面发现同事在里面的背包Item为了实现Mask用RTT(渲染到纹理)。
每次进入界面都创建多个RT。这对性能影响是非常大的。后面我修改这段代码改成用 cocos2d-x提供一个类叫 ClipNode(也是基于模板缓冲+ALPHA_TEST做的)实现。
但是放入之后,竟然出现上层模板缓冲失效的问题。
先看代码,cocos2d-x怎么实现裁剪的。
static GLint s_layer = -1;
1 void Layout::onBeforeVisitStencil() 2 { 3 s_layer++; 4 GLint mask_layer = 0x1 << s_layer; 5 GLint mask_layer_l = mask_layer - 1; 6 _mask_layer_le = mask_layer | mask_layer_l; 7 _currentStencilEnabled = glIsEnabled(GL_STENCIL_TEST); 8 glGetIntegerv(GL_STENCIL_WRITEMASK, (GLint *)&_currentStencilWriteMask); 9 glGetIntegerv(GL_STENCIL_FUNC, (GLint *)&_currentStencilFunc); 10 glGetIntegerv(GL_STENCIL_REF, &_currentStencilRef); 11 glGetIntegerv(GL_STENCIL_VALUE_MASK, (GLint *)&_currentStencilValueMask); 12 glGetIntegerv(GL_STENCIL_FAIL, (GLint *)&_currentStencilFail); 13 glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, (GLint *)&_currentStencilPassDepthFail); 14 glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, (GLint *)&_currentStencilPassDepthPass); 15 16 glEnable(GL_STENCIL_TEST); 17 CHECK_GL_ERROR_DEBUG(); 18 glStencilMask(mask_layer); 19 glGetBooleanv(GL_DEPTH_WRITEMASK, &_currentDepthWriteMask); 20 glDepthMask(GL_FALSE); 21 glStencilFunc(GL_NEVER, mask_layer, mask_layer); 22 glStencilOp(GL_ZERO, GL_KEEP, GL_KEEP); 23 24 this->drawFullScreenQuadClearStencil(); 25 26 glStencilFunc(GL_NEVER, mask_layer, mask_layer); 27 glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP); 28 }
void Layout::onAfterDrawStencil() { glDepthMask(_currentDepthWriteMask); glStencilFunc(GL_EQUAL, _mask_layer_le, _mask_layer_le); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); }
void Layout::onAfterVisitStencil() { glStencilFunc(_currentStencilFunc, _currentStencilRef, _currentStencilValueMask); glStencilOp(_currentStencilFail, _currentStencilPassDepthFail, _currentStencilPassDepthPass); glStencilMask(_currentStencilWriteMask); if (!_currentStencilEnabled) { glDisable(GL_STENCIL_TEST); } s_layer--; }
UILayout和ClipNode实现模板裁剪的方式都是一样的,通过代码了解下。
我这里用gDebugger来显示模板缓冲值,断点断在 glStencilFunc。
首先是断在UILayout onBeforeVisitStencil函数
glStencilFunc(GL_NEVER, mask_layer, mask_layer);
glStencilOp(GL_ZERO, GL_KEEP, GL_KEEP);
this->drawFullScreenQuadClearStencil();
这三行的作用是把全屏幕的模板值设置成0,永远不通过,glStencilOp(GL_ZERO,打到清屏的作用。我们断点走一下就可以看到效果
后台的值都是清空的,都是0。下一步就是渲染要遮罩的区域
glStencilFunc(GL_NEVER, mask_layer, mask_layer);
glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);
测试函数也是这是成,永远不通过。但是失败操作设置成替换,GL_REPLACE,这样遮罩区域渲染就会在模板缓冲相同的区域值设置成1.我们来看下效果。
断点走一步
跟我们预想的一样,可以见区域的模板值设置成1,不可见区域设置成0,那么后面的渲染部分只要比对模板是否是1可以了。
如果不是1就是不可见,如果是1就是可见。
glStencilFunc(GL_EQUAL, _mask_layer_le, _mask_layer_le);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
所以的子节点都是这样渲染。等节点遍历完,
void Layout::onAfterVisitStencil() { glStencilFunc(_currentStencilFunc, _currentStencilRef, _currentStencilValueMask); glStencilOp(_currentStencilFail, _currentStencilPassDepthFail, _currentStencilPassDepthPass); glStencilMask(_currentStencilWriteMask); if (!_currentStencilEnabled) { glDisable(GL_STENCIL_TEST); } s_layer--; }
还原原来的值。
代码里面实现这样,没错。如果一般情况下工作正常。
我们可以看到Icon的边界被裁剪了。
我前面说过了,里面有些Icon是需要裁剪的,用到ClipNode,但是ClipNode里面的代码实现跟UILayout一模一样。这样就造成一种情况。cocos2d-x是树形节点遍历的。渲染从后向前一层层画上去的,如果ScrollView里面放个ClipNode。会造成什么情况呢
bool ClippingNode::isInverted() const { return _inverted; } void ClippingNode::setInverted(bool inverted) { _inverted = inverted; } void ClippingNode::onBeforeVisit() { /////////////////////////////////// // INIT // increment the current layer s_layer++; // mask of the current layer (ie: for layer 3: 00000100) GLint mask_layer = 0x1 << s_layer; // 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); // 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(); /////////////////////////////////// // 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 glStencilFunc(GL_NEVER, mask_layer, mask_layer); glStencilOp(!_inverted ? GL_REPLACE : GL_ZERO, GL_KEEP, GL_KEEP); // 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 }
在这里面跟UILayout一样先是清理了,整个后台设置成0值,后面渲染一个Mask,往后台写个遮罩值。看模板缓冲的变化。
看下清理代码的效果
后面渲染一个遮罩的mask值到模板缓冲
ClipNode里面遍历子节点跟UILayout一样,都是判断是否Mask为1,
void ClippingNode::onAfterDrawStencil() { // restore alpha test state if (_alphaThreshold < 1) { #if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC || CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_LINUX) // manually restore the alpha test state glAlphaFunc(_currentAlphaTestFunc, _currentAlphaTestRef); if (!_currentAlphaTestEnabled) { glDisable(GL_ALPHA_TEST); } #else // FIXME: we need to find a way to restore the shaders of the stencil node and its childs #endif } // restore the depth test state glDepthMask(_currentDepthWriteMask); //if (currentDepthTestEnabled) { // glEnable(GL_DEPTH_TEST); //} /////////////////////////////////// // DRAW CONTENT // setup the stencil test func like this: // for each pixel of this node and its childs // if all layers less than or equals to the current are set to 1 in the stencil buffer // draw the pixel and keep the current layer in the stencil buffer // else // do not draw the pixel but keep the current layer in the stencil buffer glStencilFunc(GL_EQUAL, _mask_layer_le, _mask_layer_le); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // draw (according to the stencil test func) this node and its childs }
所以小Icon遮罩是正常的,因为模板测试区域是对了,但是当Icon渲染完成退出,状态还原到UILayout,去渲染兄弟节点的时候。状态还原了,但是模板缓冲可没法还原。
所以只为了1的区域只有那么一小块。所以后面整个滚动区域裁剪就出错了。
好了,说明就到这里,问题也找到了。怎么改呢?简单的做法,我前面提过了,因为UILayout裁剪有两种,一种是模板一种可视区域。
改成可视区域裁剪,就没问题。
最后买个关子问个问题。先说下Icon这个是三个Layer就是三层合在一起的,ClipNode是最上层。请问为什么第一个渲染都正常,就是红色框框那个,后面几个渲染底下外框都不见了呢。
只剩下ClipNode呢?