本文的主要內容是根據跑馬燈解析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 深入分析