本文的主要内容是根据跑马灯解析ClippingNode实现原理。本文涉及到cocos2dx 3.x的渲染机制以及部分opengl的知识。
首先看看上一篇文章中说到的跑马灯的简单实现:
//设置模板
auto stencil = Sprite::create();
//设置显示区域大小
stencil->setTextureRect(Rect(0, 0, 50, 30));
//设置跑马灯文字
auto text = Label::createWithSystemFont("-1dasdasdasd efadaewfevgds dfhrthrbgrg1-", "", 24);
//设置锚点
text->setAnchorPoint(Vec2::ANCHOR_MIDDLE_LEFT);
//创建裁剪节点
auto clippingNode = ClippingNode::create(stencil);
//设置节点位置
clippingNode->setPosition(Vec2(700, 400));
//显示模板内的内容
clippingNode->setInverted(false);
//添加显示内容
clippingNode->addChild(text, 2);
//加入到UI树
addChild(clippingNode);
1.跑马灯代码分析
从创建裁剪节点这里开始分析
//创建裁剪节点
auto clippingNode = ClippingNode::create(stencil);
这一句话实现了怎么样的功能呢?走进去看看(create函数中,实际上调用的是init函数,所以这里就直接展示init函数了)
bool ClippingNode::init(Node *stencil)
{
CC_SAFE_RELEASE(_stencil);
_stencil = stencil;
CC_SAFE_RETAIN(_stencil);
_alphaThreshold = 1;
_inverted = false;
// get (only once) the number of bits of the stencil buffer
static bool once = true;
if (once)
{
glGetIntegerv(GL_STENCIL_BITS, &g_sStencilBits);
if (g_sStencilBits <= 0)
{
CCLOG("Stencil buffer is not enabled.");
}
once = false;
}
return true;
}
首先设置_stencil ,这里实际上与setStencil方法一样的(在上文有说过这个方法的作用)
void ClippingNode::setStencil(Node *stencil)
{
CC_SAFE_RETAIN(stencil);
CC_SAFE_RELEASE(_stencil);
_stencil = stencil;
}
然后设置_alphaThreshold ,实际上与setAlphaThreshold是一样的(在上文有说过这个方法的作用)
void ClippingNode::setAlphaThreshold(GLfloat alphaThreshold)
{
_alphaThreshold = alphaThreshold;
}
其次设置_inverted ,实际上与setInverted方法是一样的(在上文有说过这个方法的作用)
void ClippingNode::setInverted(bool inverted)
{
_inverted = inverted;
}
然后调用一个opengl的API,
glGetIntegerv(GL_STENCIL_BITS, &g_sStencilBits);
从逻辑上可以看到这个API在类第一次被创建的时候才会调用。那么这个方法的作用是什么呢,他的作用是将模板缓存中的每一个像素的位数赋值给g_sStencilBits,如果这个值小于0,说明不支持模板缓存。模板缓存的作用就是将绘图的范围限定在屏幕的固定区域,这个区域可以是一个复杂的图形,这个区域称之为绘图模板。
此时,已经将跑马灯的实现代码分析得差不多了,接下来看看具体的渲染部分。
2.渲染代码分析
根据cocos2dx的渲染机制,渲染部分从visit函数开始分析。以下是visit的实现代码
void ClippingNode::visit(Renderer *renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
if(!_visible)
return;
//父节点座标转换
uint32_t flags = processParentFlags(parentTransform, parentFlags);
//设置gl使用的矩阵类型
Director* director = Director::getInstance();
CCASSERT(nullptr != director, "Director is null when seting matrix stack");
director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);
//使用组渲染指令
_groupCommand.init(_globalZOrder);
renderer->addCommand(&_groupCommand);
//下面的渲染指令都会加入到组渲染指令中,一起渲染
renderer->pushGroup(_groupCommand.getRenderQueueID());
//渲染前 _beforeVisitCmd是一个自定义渲染指令(CustomCommand),
//作用就是在渲染的这个指令的时候,实际上执行的是ClippingNode::onBeforeVisit这个函数
_beforeVisitCmd.init(_globalZOrder);
_beforeVisitCmd.func = CC_CALLBACK_0(ClippingNode::onBeforeVisit, this);
renderer->addCommand(&_beforeVisitCmd);
//alpha测试(上文说到:Alpha测试的作用通过一句话解释就是:所有像素的透明度值低于某个阀值的统统抛弃,不绘制到屏幕上。)
if (_alphaThreshold < 1)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC || CC_TARGET_PLATFORM == CC_PLATFORM_WINDOWS || CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
#else
//引擎提供GLProgram类来处理着色器相关操作
//下述代码的具体作用就是将_alphaThreshold与alphaValueLocation关联,然后将_stencil与着色器程序program关联,
//着色器程序program的作用就是执行ALPHA_TEST
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);
program->use();
program->setUniformLocationWith1f(alphaValueLocation, _alphaThreshold);
setProgram(_stencil, program);
#endif
}
//渲染模板
_stencil->visit(renderer, _modelViewTransform, flags);
//渲染模板后
//作用就是在渲染的这个指令的时候,实际上执行的是ClippingNode::onAfterDrawStencil
_afterDrawStencilCmd.init(_globalZOrder);
_afterDrawStencilCmd.func = CC_CALLBACK_0(ClippingNode::onAfterDrawStencil, this);
renderer->addCommand(&_afterDrawStencilCmd);
//下面是渲染所有的子节点
int i = 0;
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
this->draw(renderer, _modelViewTransform, flags);
for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
else
{
this->draw(renderer, _modelViewTransform, flags);
}
//渲染后
//作用就是在渲染的这个指令的时候,实际上执行的是ClippingNode::onAfterVisit
_afterVisitCmd.init(_globalZOrder);
_afterVisitCmd.func = CC_CALLBACK_0(ClippingNode::onAfterVisit, this);
renderer->addCommand(&_afterVisitCmd);
//将组渲染命令推出栈,作用是让后续的渲染命令不加入到组渲染命令中
renderer->popGroup();
director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}
代码中已经写了比较多的解释了,在这里就只看代码中出现了三个比较特殊的函数,分别是
ClippingNode::onBeforeVisit //渲染前 首先执行
ClippingNode::onAfterDrawStencil //渲染模板后 其次
ClippingNode::onAfterVisit //渲染后 最后执行
首先开看看onBeforeVisit的实现
void ClippingNode::onBeforeVisit()
{
// 静态变量,用于记录当前程序一共用到的模版缓冲遮罩数量。 每次渲染都会加1
s_layer++;
// 当前模版缓冲位的参数值
GLint mask_layer = 0x1 << s_layer;
// 模版缓冲位的参数值的掩码
GLint mask_layer_l = mask_layer - 1;
// 上面两个值做或运算的结果值
_mask_layer_le = mask_layer | mask_layer_l;
// 获取是否使用模版缓冲
_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); //当前模板测试通过,深度测试通过后的操作
// 开启模板测试
glEnable(GL_STENCIL_TEST);
// 检测开启模板测试的时候是否存在OpenGL错误
CHECK_GL_ERROR_DEBUG();
// 设置模版缓冲的掩码值
glStencilMask(mask_layer);
//取得是否可以写入模版掩码参数
glGetBooleanv(GL_DEPTH_WRITEMASK, &_currentDepthWriteMask);
// 禁止写入深度缓冲
glDepthMask(GL_FALSE);
// 永远不能通过测试
glStencilFunc(GL_NEVER, mask_layer, mask_layer);
// 根据是否反向运算来决定如果测试不能通过时是否将相应像素位置的模版缓冲位的值设为0。
glStencilOp(!_inverted ? GL_ZERO : GL_REPLACE, GL_KEEP, GL_KEEP);
// 用白色绘制一下屏幕矩形,因为都不能通过嘛,所以就全屏的模版缓冲位的值都被设为0
drawFullScreenQuadClearStencil();
// 永远不能通过测试
glStencilFunc(GL_NEVER, mask_layer, mask_layer);
// 根据是否反向运算来决定如果测试不能通过时是否将相应像素位置的模版缓冲位的值设为当前参数值
glStencilOp(!_inverted ? GL_REPLACE : GL_ZERO, GL_KEEP, GL_KEEP);
// 如果需要alpha测试,并且是下述平台,则在这里执行alpha测试,否则在visit函数中执行
if (_alphaThreshold < 1) {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC || CC_TARGET_PLATFORM == CC_PLATFORM_WINDOWS || CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
// 获取是否允许alpha测试
_currentAlphaTestEnabled = glIsEnabled(GL_ALPHA_TEST);
// 获取一些参数
glGetIntegerv(GL_ALPHA_TEST_FUNC, (GLint *)&_currentAlphaTestFunc); //当前alpha测试函数
glGetFloatv(GL_ALPHA_TEST_REF, &_currentAlphaTestRef); //当前alpha参考值
// 开启alpha测试
glEnable(GL_ALPHA_TEST);
// 检测开启alpha测试的时候是否存在OpenGL错误
CHECK_GL_ERROR_DEBUG();
// 执行并且传入参数
glAlphaFunc(GL_GREATER, _alphaThreshold);
#else
#endif
}
//Draw _stencil
}
然后是onAfterDrawStencil 的实现:
void ClippingNode::onAfterDrawStencil()
{
// 还原alpha测试状态
if (_alphaThreshold < 1)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC || CC_TARGET_PLATFORM == CC_PLATFORM_WINDOWS || CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
glAlphaFunc(_currentAlphaTestFunc, _currentAlphaTestRef);
if (!_currentAlphaTestEnabled)
{
glDisable(GL_ALPHA_TEST);
}
#else
// 在其他平台暂时没有找到解决方法去还原状态 233
#endif
}
// 还原深度测试写入状态
glDepthMask(_currentDepthWriteMask);
// 这里设置如果当前模版缓冲中的模版值与运算结果相等则保留相应像素
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
}
最后是onAfterVisit的实现
void ClippingNode::onAfterVisit()
{
// 还原模板状态
glStencilFunc(_currentStencilFunc, _currentStencilRef, _currentStencilValueMask);
glStencilOp(_currentStencilFail, _currentStencilPassDepthFail, _currentStencilPassDepthPass);
glStencilMask(_currentStencilWriteMask);
if (!_currentStencilEnabled)
{
glDisable(GL_STENCIL_TEST);
}
// 结束使用当前模版缓冲位数,就减1.以保证下次还能正常使用
s_layer--;
}
本文部分注释来自:Cocos2d-x2.1.1-ClippingNodeTest 深入分析