1 /**************************************************************************** 2 Copyright (c) 2008-2010 Ricardo Quesada 3 Copyright (c) 2011-2012 cocos2d-x.org 4 Copyright (c) 2013-2014 Chukong Technologies Inc. 5 Copyright (c) 2012 Pierre-David Bélanger 6 7 http://www.cocos2d-x.org 8 9 Permission is hereby granted, free of charge, to any person obtaining a copy 10 of this software and associated documentation files (the "Software"), to deal 11 in the Software without restriction, including without limitation the rights 12 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 copies of the Software, and to permit persons to whom the Software is 14 furnished to do so, subject to the following conditions: 15 16 The above copyright notice and this permission notice shall be included in 17 all copies or substantial portions of the Software. 18 19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 THE SOFTWARE. 26 ****************************************************************************/ 27 28 /** 29 * the value of stencil bits. 30 * @type Number 31 */ 32 cc.stencilBits = -1; 33 34 cc.setProgram = function (node, program) { 35 node.shaderProgram = program; 36 37 var children = node.children; 38 if (!children) 39 return; 40 41 for (var i = 0; i < children.length; i++) 42 cc.setProgram(children[i], program); 43 }; 44 45 /** 46 * <p> 47 * cc.ClippingNode is a subclass of cc.Node. <br/> 48 * It draws its content (childs) clipped using a stencil. <br/> 49 * The stencil is an other cc.Node that will not be drawn. <br/> 50 * The clipping is done using the alpha part of the stencil (adjusted with an alphaThreshold). 51 * </p> 52 * @class 53 * @extends cc.Node 54 * 55 * @property {Number} alphaThreshold - Threshold for alpha value. 56 * @property {Boolean} inverted - Indicate whether in inverted mode. 57 * @property {cc.Node} stencil - he cc.Node to use as a stencil to do the clipping. 58 */ 59 cc.ClippingNode = cc.Node.extend(/** @lends cc.ClippingNode# */{ 60 alphaThreshold: 0, 61 inverted: false, 62 63 _stencil: null, 64 _godhelpme: false, 65 66 /** 67 * Creates and initializes a clipping node with an other node as its stencil. 68 * The stencil node will be retained. 69 * Constructor of cc.ClippingNode 70 * @param {cc.Node} [stencil=null] 71 */ 72 ctor: function (stencil) { 73 cc.Node.prototype.ctor.call(this); 74 this._stencil = null; 75 this.alphaThreshold = 0; 76 this.inverted = false; 77 78 stencil = stencil || null; 79 cc.ClippingNode.prototype.init.call(this, stencil); 80 }, 81 82 /** 83 * Initializes a clipping node with an other node as its stencil. <br/> 84 * The stencil node will be retained, and its parent will be set to this clipping node. 85 * @param {cc.Node} [stencil=null] 86 */ 87 init: null, 88 _className: "ClippingNode", 89 90 _initForWebGL: function (stencil) { 91 this._stencil = stencil; 92 93 this.alphaThreshold = 1; 94 this.inverted = false; 95 // get (only once) the number of bits of the stencil buffer 96 cc.ClippingNode._init_once = true; 97 if (cc.ClippingNode._init_once) { 98 cc.stencilBits = cc._renderContext.getParameter(cc._renderContext.STENCIL_BITS); 99 if (cc.stencilBits <= 0) 100 cc.log("Stencil buffer is not enabled."); 101 cc.ClippingNode._init_once = false; 102 } 103 return true; 104 }, 105 106 _initForCanvas: function (stencil) { 107 this._stencil = stencil; 108 this.alphaThreshold = 1; 109 this.inverted = false; 110 }, 111 112 onEnter: function () { 113 cc.Node.prototype.onEnter.call(this); 114 this._stencil.onEnter(); 115 }, 116 117 onEnterTransitionDidFinish: function () { 118 cc.Node.prototype.onEnterTransitionDidFinish.call(this); 119 this._stencil.onEnterTransitionDidFinish(); 120 }, 121 122 onExitTransitionDidStart: function () { 123 this._stencil.onExitTransitionDidStart(); 124 cc.Node.prototype.onExitTransitionDidStart.call(this); 125 }, 126 127 onExit: function () { 128 this._stencil.onExit(); 129 cc.Node.prototype.onExit.call(this); 130 }, 131 132 visit: null, 133 134 _visitForWebGL: function (ctx) { 135 var gl = ctx || cc._renderContext; 136 137 // if stencil buffer disabled 138 if (cc.stencilBits < 1) { 139 // draw everything, as if there where no stencil 140 cc.Node.prototype.visit.call(this, ctx); 141 return; 142 } 143 144 // return fast (draw nothing, or draw everything if in inverted mode) if: 145 // - nil stencil node 146 // - or stencil node invisible: 147 if (!this._stencil || !this._stencil.visible) { 148 if (this.inverted) 149 cc.Node.prototype.visit.call(this, ctx); // draw everything 150 return; 151 } 152 153 // store the current stencil layer (position in the stencil buffer), 154 // this will allow nesting up to n CCClippingNode, 155 // where n is the number of bits of the stencil buffer. 156 cc.ClippingNode._layer = -1; 157 158 // all the _stencilBits are in use? 159 if (cc.ClippingNode._layer + 1 == cc.stencilBits) { 160 // warn once 161 cc.ClippingNode._visit_once = true; 162 if (cc.ClippingNode._visit_once) { 163 cc.log("Nesting more than " + cc.stencilBits + "stencils is not supported. Everything will be drawn without stencil for this node and its childs."); 164 cc.ClippingNode._visit_once = false; 165 } 166 // draw everything, as if there where no stencil 167 cc.Node.prototype.visit.call(this, ctx); 168 return; 169 } 170 171 /////////////////////////////////// 172 // INIT 173 174 // increment the current layer 175 cc.ClippingNode._layer++; 176 177 // mask of the current layer (ie: for layer 3: 00000100) 178 var mask_layer = 0x1 << cc.ClippingNode._layer; 179 // mask of all layers less than the current (ie: for layer 3: 00000011) 180 var mask_layer_l = mask_layer - 1; 181 // mask of all layers less than or equal to the current (ie: for layer 3: 00000111) 182 var mask_layer_le = mask_layer | mask_layer_l; 183 184 // manually save the stencil state 185 var currentStencilEnabled = gl.isEnabled(gl.STENCIL_TEST); 186 var currentStencilWriteMask = gl.getParameter(gl.STENCIL_WRITEMASK); 187 var currentStencilFunc = gl.getParameter(gl.STENCIL_FUNC); 188 var currentStencilRef = gl.getParameter(gl.STENCIL_REF); 189 var currentStencilValueMask = gl.getParameter(gl.STENCIL_VALUE_MASK); 190 var currentStencilFail = gl.getParameter(gl.STENCIL_FAIL); 191 var currentStencilPassDepthFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL); 192 var currentStencilPassDepthPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS); 193 194 // enable stencil use 195 gl.enable(gl.STENCIL_TEST); 196 // check for OpenGL error while enabling stencil test 197 //cc.checkGLErrorDebug(); 198 199 // all bits on the stencil buffer are readonly, except the current layer bit, 200 // this means that operation like glClear or glStencilOp will be masked with this value 201 gl.stencilMask(mask_layer); 202 203 // manually save the depth test state 204 //GLboolean currentDepthTestEnabled = GL_TRUE; 205 //currentDepthTestEnabled = glIsEnabled(GL_DEPTH_TEST); 206 var currentDepthWriteMask = gl.getParameter(gl.DEPTH_WRITEMASK); 207 208 // disable depth test while drawing the stencil 209 //glDisable(GL_DEPTH_TEST); 210 // disable update to the depth buffer while drawing the stencil, 211 // as the stencil is not meant to be rendered in the real scene, 212 // it should never prevent something else to be drawn, 213 // only disabling depth buffer update should do 214 gl.depthMask(false); 215 216 /////////////////////////////////// 217 // CLEAR STENCIL BUFFER 218 219 // manually clear the stencil buffer by drawing a fullscreen rectangle on it 220 // setup the stencil test func like this: 221 // for each pixel in the fullscreen rectangle 222 // never draw it into the frame buffer 223 // if not in inverted mode: set the current layer value to 0 in the stencil buffer 224 // if in inverted mode: set the current layer value to 1 in the stencil buffer 225 gl.stencilFunc(gl.NEVER, mask_layer, mask_layer); 226 gl.stencilOp(!this.inverted ? gl.ZERO : gl.REPLACE, gl.KEEP, gl.KEEP); 227 228 // draw a fullscreen solid rectangle to clear the stencil buffer 229 //ccDrawSolidRect(CCPointZero, ccpFromSize([[CCDirector sharedDirector] winSize]), ccc4f(1, 1, 1, 1)); 230 cc._drawingUtil.drawSolidRect(cc.p(0, 0), cc.pFromSize(cc.director.getWinSize()), cc.color(255, 255, 255, 255)); 231 232 /////////////////////////////////// 233 // DRAW CLIPPING STENCIL 234 235 // setup the stencil test func like this: 236 // for each pixel in the stencil node 237 // never draw it into the frame buffer 238 // if not in inverted mode: set the current layer value to 1 in the stencil buffer 239 // if in inverted mode: set the current layer value to 0 in the stencil buffer 240 gl.stencilFunc(gl.NEVER, mask_layer, mask_layer); 241 gl.stencilOp(!this.inverted ? gl.REPLACE : gl.ZERO, gl.KEEP, gl.KEEP); 242 243 if (this.alphaThreshold < 1) { 244 // since glAlphaTest do not exists in OES, use a shader that writes 245 // pixel only if greater than an alpha threshold 246 var program = cc.shaderCache.programForKey(cc.SHADER_POSITION_TEXTURECOLORALPHATEST); 247 var alphaValueLocation = gl.getUniformLocation(program.getProgram(), cc.UNIFORM_ALPHA_TEST_VALUE_S); 248 // set our alphaThreshold 249 cc.glUseProgram(program.getProgram()); 250 program.setUniformLocationWith1f(alphaValueLocation, this.alphaThreshold); 251 // we need to recursively apply this shader to all the nodes in the stencil node 252 // XXX: we should have a way to apply shader to all nodes without having to do this 253 cc.setProgram(this._stencil, program); 254 } 255 256 // draw the stencil node as if it was one of our child 257 // (according to the stencil test func/op and alpha (or alpha shader) test) 258 cc.kmGLPushMatrix(); 259 this.transform(); 260 this._stencil.visit(); 261 cc.kmGLPopMatrix(); 262 263 // restore alpha test state 264 //if (this.alphaThreshold < 1) { 265 // XXX: we need to find a way to restore the shaders of the stencil node and its childs 266 //} 267 268 // restore the depth test state 269 gl.depthMask(currentDepthWriteMask); 270 //if (currentDepthTestEnabled) { 271 // glEnable(GL_DEPTH_TEST); 272 //} 273 274 /////////////////////////////////// 275 // DRAW CONTENT 276 277 // setup the stencil test func like this: 278 // for each pixel of this node and its childs 279 // if all layers less than or equals to the current are set to 1 in the stencil buffer 280 // draw the pixel and keep the current layer in the stencil buffer 281 // else 282 // do not draw the pixel but keep the current layer in the stencil buffer 283 gl.stencilFunc(gl.EQUAL, mask_layer_le, mask_layer_le); 284 gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); 285 286 // draw (according to the stencil test func) this node and its childs 287 cc.Node.prototype.visit.call(this, ctx); 288 289 /////////////////////////////////// 290 // CLEANUP 291 292 // manually restore the stencil state 293 gl.stencilFunc(currentStencilFunc, currentStencilRef, currentStencilValueMask); 294 gl.stencilOp(currentStencilFail, currentStencilPassDepthFail, currentStencilPassDepthPass); 295 gl.stencilMask(currentStencilWriteMask); 296 if (!currentStencilEnabled) 297 gl.disable(gl.STENCIL_TEST); 298 299 // we are done using this layer, decrement 300 cc.ClippingNode._layer--; 301 }, 302 303 _visitForCanvas: function (ctx) { 304 // return fast (draw nothing, or draw everything if in inverted mode) if: 305 // - nil stencil node 306 // - or stencil node invisible: 307 if (!this._stencil || !this._stencil.visible) { 308 if (this.inverted) 309 cc.Node.prototype.visit.call(this, ctx); // draw everything 310 return; 311 } 312 313 var context = ctx || cc._renderContext; 314 // Composition mode, costy but support texture stencil 315 if (this._cangodhelpme() || this._stencil instanceof cc.Sprite) { 316 // Cache the current canvas, for later use (This is a little bit heavy, replace this solution with other walkthrough) 317 var canvas = context.canvas; 318 var locCache = cc.ClippingNode._getSharedCache(); 319 locCache.width = canvas.width; 320 locCache.height = canvas.height; 321 var locCacheCtx = locCache.getContext("2d"); 322 locCacheCtx.drawImage(canvas, 0, 0); 323 324 context.save(); 325 // Draw everything first using node visit function 326 cc.Node.prototype.visit.call(this, context); 327 328 context.globalCompositeOperation = this.inverted ? "destination-out" : "destination-in"; 329 330 this.transform(context); 331 this._stencil.visit(); 332 333 context.restore(); 334 335 // Redraw the cached canvas, so that the cliped area shows the background etc. 336 context.save(); 337 context.setTransform(1, 0, 0, 1, 0, 0); 338 context.globalCompositeOperation = "destination-over"; 339 context.drawImage(locCache, 0, 0); 340 context.restore(); 341 } 342 // Clip mode, fast, but only support cc.DrawNode 343 else { 344 var i, children = this._children, locChild; 345 346 context.save(); 347 this.transform(context); 348 this._stencil.visit(context); 349 context.clip(); 350 351 // Clip mode doesn't support recusive stencil, so once we used a clip stencil, 352 // so if it has ClippingNode as a child, the child must uses composition stencil. 353 this._cangodhelpme(true); 354 var len = children.length; 355 if (len > 0) { 356 this.sortAllChildren(); 357 // draw children zOrder < 0 358 for (i = 0; i < len; i++) { 359 locChild = children[i]; 360 if (locChild._localZOrder < 0) 361 locChild.visit(context); 362 else 363 break; 364 } 365 this.draw(context); 366 for (; i < len; i++) { 367 children[i].visit(context); 368 } 369 } else 370 this.draw(context); 371 this._cangodhelpme(false); 372 373 context.restore(); 374 } 375 }, 376 377 /** 378 * The cc.Node to use as a stencil to do the clipping. <br/> 379 * The stencil node will be retained. This default to nil. 380 * @return {cc.Node} 381 */ 382 getStencil: function () { 383 return this._stencil; 384 }, 385 386 /** 387 * @function 388 * @param {cc.Node} stencil 389 */ 390 setStencil: null, 391 392 _setStencilForWebGL: function (stencil) { 393 this._stencil = stencil; 394 }, 395 396 _setStencilForCanvas: function (stencil) { 397 this._stencil = stencil; 398 var locContext = cc._renderContext; 399 // For texture stencil, use the sprite itself 400 if (stencil instanceof cc.Sprite) { 401 return; 402 } 403 // For shape stencil, rewrite the draw of stencil ,only init the clip path and draw nothing. 404 else if (stencil instanceof cc.DrawNode) { 405 stencil.draw = function () { 406 var locEGL_ScaleX = cc.view.getScaleX(), locEGL_ScaleY = cc.view.getScaleY(); 407 for (var i = 0; i < stencil._buffer.length; i++) { 408 var element = stencil._buffer[i]; 409 var vertices = element.verts; 410 var firstPoint = vertices[0]; 411 locContext.beginPath(); 412 locContext.moveTo(firstPoint.x * locEGL_ScaleX, -firstPoint.y * locEGL_ScaleY); 413 for (var j = 1, len = vertices.length; j < len; j++) 414 locContext.lineTo(vertices[j].x * locEGL_ScaleX, -vertices[j].y * locEGL_ScaleY); 415 } 416 } 417 } 418 }, 419 420 /** 421 * <p> 422 * The alpha threshold. <br/> 423 * The content is drawn only where the stencil have pixel with alpha greater than the alphaThreshold. <br/> 424 * Should be a float between 0 and 1. <br/> 425 * This default to 1 (so alpha test is disabled). 426 * </P> 427 * @return {Number} 428 */ 429 getAlphaThreshold: function () { 430 return this.alphaThreshold; 431 }, 432 433 /** 434 * set alpha threshold. 435 * @param {Number} alphaThreshold 436 */ 437 setAlphaThreshold: function (alphaThreshold) { 438 this.alphaThreshold = alphaThreshold; 439 }, 440 441 /** 442 * <p> 443 * Inverted. If this is set to YES, <br/> 444 * the stencil is inverted, so the content is drawn where the stencil is NOT drawn. <br/> 445 * This default to NO. 446 * </p> 447 * @return {Boolean} 448 */ 449 isInverted: function () { 450 return this.inverted; 451 }, 452 453 454 /** 455 * set whether or not invert of stencil 456 * @param {Boolean} inverted 457 */ 458 setInverted: function (inverted) { 459 this.inverted = inverted; 460 }, 461 462 _cangodhelpme: function (godhelpme) { 463 if (godhelpme === true || godhelpme === false) 464 cc.ClippingNode.prototype._godhelpme = godhelpme; 465 return cc.ClippingNode.prototype._godhelpme; 466 } 467 }); 468 469 var _p = cc.ClippingNode.prototype; 470 471 if (cc._renderType === cc._RENDER_TYPE_WEBGL) { 472 //WebGL 473 _p.init = _p._initForWebGL; 474 _p.visit = _p._visitForWebGL; 475 _p.setStencil = _p._setStencilForWebGL; 476 } else { 477 _p.init = _p._initForCanvas; 478 _p.visit = _p._visitForCanvas; 479 _p.setStencil = _p._setStencilForCanvas; 480 } 481 482 // Extended properties 483 cc.defineGetterSetter(_p, "stencil", _p.getStencil, _p.setStencil); 484 /** @expose */ 485 _p.stencil; 486 487 488 cc.ClippingNode._init_once = null; 489 cc.ClippingNode._visit_once = null; 490 cc.ClippingNode._layer = null; 491 cc.ClippingNode._sharedCache = null; 492 493 cc.ClippingNode._getSharedCache = function () { 494 return (cc.ClippingNode._sharedCache) || (cc.ClippingNode._sharedCache = document.createElement("canvas")); 495 }; 496 497 /** 498 * Creates and initializes a clipping node with an other node as its stencil. <br/> 499 * The stencil node will be retained. 500 * @param {cc.Node} [stencil=null] 501 * @return {cc.ClippingNode} 502 */ 503 cc.ClippingNode.create = function (stencil) { 504 return new cc.ClippingNode(stencil); 505 }; 506