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 157 // all the _stencilBits are in use? 158 if (cc.ClippingNode._layer + 1 == cc.stencilBits) { 159 // warn once 160 cc.ClippingNode._visit_once = true; 161 if (cc.ClippingNode._visit_once) { 162 cc.log("Nesting more than " + cc.stencilBits + "stencils is not supported. Everything will be drawn without stencil for this node and its childs."); 163 cc.ClippingNode._visit_once = false; 164 } 165 // draw everything, as if there where no stencil 166 cc.Node.prototype.visit.call(this, ctx); 167 return; 168 } 169 170 /////////////////////////////////// 171 // INIT 172 173 // increment the current layer 174 cc.ClippingNode._layer++; 175 176 // mask of the current layer (ie: for layer 3: 00000100) 177 var mask_layer = 0x1 << cc.ClippingNode._layer; 178 // mask of all layers less than the current (ie: for layer 3: 00000011) 179 var mask_layer_l = mask_layer - 1; 180 // mask of all layers less than or equal to the current (ie: for layer 3: 00000111) 181 var mask_layer_le = mask_layer | mask_layer_l; 182 183 // manually save the stencil state 184 var currentStencilEnabled = gl.isEnabled(gl.STENCIL_TEST); 185 var currentStencilWriteMask = gl.getParameter(gl.STENCIL_WRITEMASK); 186 var currentStencilFunc = gl.getParameter(gl.STENCIL_FUNC); 187 var currentStencilRef = gl.getParameter(gl.STENCIL_REF); 188 var currentStencilValueMask = gl.getParameter(gl.STENCIL_VALUE_MASK); 189 var currentStencilFail = gl.getParameter(gl.STENCIL_FAIL); 190 var currentStencilPassDepthFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL); 191 var currentStencilPassDepthPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS); 192 193 // enable stencil use 194 gl.enable(gl.STENCIL_TEST); 195 // check for OpenGL error while enabling stencil test 196 //cc.checkGLErrorDebug(); 197 198 // all bits on the stencil buffer are readonly, except the current layer bit, 199 // this means that operation like glClear or glStencilOp will be masked with this value 200 gl.stencilMask(mask_layer); 201 202 // manually save the depth test state 203 //GLboolean currentDepthTestEnabled = GL_TRUE; 204 //currentDepthTestEnabled = glIsEnabled(GL_DEPTH_TEST); 205 var currentDepthWriteMask = gl.getParameter(gl.DEPTH_WRITEMASK); 206 207 // disable depth test while drawing the stencil 208 //glDisable(GL_DEPTH_TEST); 209 // disable update to the depth buffer while drawing the stencil, 210 // as the stencil is not meant to be rendered in the real scene, 211 // it should never prevent something else to be drawn, 212 // only disabling depth buffer update should do 213 gl.depthMask(false); 214 215 /////////////////////////////////// 216 // CLEAR STENCIL BUFFER 217 218 // manually clear the stencil buffer by drawing a fullscreen rectangle on it 219 // setup the stencil test func like this: 220 // for each pixel in the fullscreen rectangle 221 // never draw it into the frame buffer 222 // if not in inverted mode: set the current layer value to 0 in the stencil buffer 223 // if in inverted mode: set the current layer value to 1 in the stencil buffer 224 gl.stencilFunc(gl.NEVER, mask_layer, mask_layer); 225 gl.stencilOp(!this.inverted ? gl.ZERO : gl.REPLACE, gl.KEEP, gl.KEEP); 226 227 // draw a fullscreen solid rectangle to clear the stencil buffer 228 cc.kmGLMatrixMode(cc.KM_GL_PROJECTION); 229 cc.kmGLPushMatrix(); 230 cc.kmGLLoadIdentity(); 231 cc.kmGLMatrixMode(cc.KM_GL_MODELVIEW); 232 cc.kmGLPushMatrix(); 233 cc.kmGLLoadIdentity(); 234 cc._drawingUtil.drawSolidRect(cc.p(-1,-1), cc.p(1,1), cc.color(255, 255, 255, 255)); 235 cc.kmGLMatrixMode(cc.KM_GL_PROJECTION); 236 cc.kmGLPopMatrix(); 237 cc.kmGLMatrixMode(cc.KM_GL_MODELVIEW); 238 cc.kmGLPopMatrix(); 239 240 /////////////////////////////////// 241 // DRAW CLIPPING STENCIL 242 243 // setup the stencil test func like this: 244 // for each pixel in the stencil node 245 // never draw it into the frame buffer 246 // if not in inverted mode: set the current layer value to 1 in the stencil buffer 247 // if in inverted mode: set the current layer value to 0 in the stencil buffer 248 gl.stencilFunc(gl.NEVER, mask_layer, mask_layer); 249 gl.stencilOp(!this.inverted ? gl.REPLACE : gl.ZERO, gl.KEEP, gl.KEEP); 250 251 if (this.alphaThreshold < 1) { 252 // since glAlphaTest do not exists in OES, use a shader that writes 253 // pixel only if greater than an alpha threshold 254 var program = cc.shaderCache.programForKey(cc.SHADER_POSITION_TEXTURECOLORALPHATEST); 255 var alphaValueLocation = gl.getUniformLocation(program.getProgram(), cc.UNIFORM_ALPHA_TEST_VALUE_S); 256 // set our alphaThreshold 257 cc.glUseProgram(program.getProgram()); 258 program.setUniformLocationWith1f(alphaValueLocation, this.alphaThreshold); 259 // we need to recursively apply this shader to all the nodes in the stencil node 260 // XXX: we should have a way to apply shader to all nodes without having to do this 261 cc.setProgram(this._stencil, program); 262 } 263 264 // draw the stencil node as if it was one of our child 265 // (according to the stencil test func/op and alpha (or alpha shader) test) 266 cc.kmGLPushMatrix(); 267 this.transform(); 268 this._stencil.visit(); 269 cc.kmGLPopMatrix(); 270 271 // restore alpha test state 272 //if (this.alphaThreshold < 1) { 273 // XXX: we need to find a way to restore the shaders of the stencil node and its childs 274 //} 275 276 // restore the depth test state 277 gl.depthMask(currentDepthWriteMask); 278 279 /////////////////////////////////// 280 // DRAW CONTENT 281 282 // setup the stencil test func like this: 283 // for each pixel of this node and its childs 284 // if all layers less than or equals to the current are set to 1 in the stencil buffer 285 // draw the pixel and keep the current layer in the stencil buffer 286 // else 287 // do not draw the pixel but keep the current layer in the stencil buffer 288 gl.stencilFunc(gl.EQUAL, mask_layer_le, mask_layer_le); 289 gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); 290 291 // draw (according to the stencil test func) this node and its childs 292 cc.Node.prototype.visit.call(this, ctx); 293 294 /////////////////////////////////// 295 // CLEANUP 296 297 // manually restore the stencil state 298 gl.stencilFunc(currentStencilFunc, currentStencilRef, currentStencilValueMask); 299 gl.stencilOp(currentStencilFail, currentStencilPassDepthFail, currentStencilPassDepthPass); 300 gl.stencilMask(currentStencilWriteMask); 301 if (!currentStencilEnabled) 302 gl.disable(gl.STENCIL_TEST); 303 304 // we are done using this layer, decrement 305 cc.ClippingNode._layer--; 306 }, 307 308 _visitForCanvas: function (ctx) { 309 // return fast (draw nothing, or draw everything if in inverted mode) if: 310 // - nil stencil node 311 // - or stencil node invisible: 312 if (!this._stencil || !this._stencil.visible) { 313 if (this.inverted) 314 cc.Node.prototype.visit.call(this, ctx); // draw everything 315 return; 316 } 317 318 var context = ctx || cc._renderContext; 319 var canvas = context.canvas; 320 // Composition mode, costy but support texture stencil 321 if (this._cangodhelpme() || this._stencil instanceof cc.Sprite) { 322 // Cache the current canvas, for later use (This is a little bit heavy, replace this solution with other walkthrough) 323 var locCache = cc.ClippingNode._getSharedCache(); 324 locCache.width = canvas.width; 325 locCache.height = canvas.height; 326 var locCacheCtx = locCache.getContext("2d"); 327 locCacheCtx.drawImage(canvas, 0, 0); 328 329 context.save(); 330 // Draw everything first using node visit function 331 cc.Node.prototype.visit.call(this, context); 332 333 context.globalCompositeOperation = this.inverted ? "destination-out" : "destination-in"; 334 335 this.transform(context); 336 this._stencil.visit(); 337 338 context.restore(); 339 340 // Redraw the cached canvas, so that the cliped area shows the background etc. 341 context.save(); 342 context.setTransform(1, 0, 0, 1, 0, 0); 343 context.globalCompositeOperation = "destination-over"; 344 context.drawImage(locCache, 0, 0); 345 context.restore(); 346 } 347 // Clip mode, fast, but only support cc.DrawNode 348 else { 349 var i, children = this._children, locChild; 350 351 context.save(); 352 this.transform(context); 353 this._stencil.visit(context); 354 if (this.inverted) { 355 context.save(); 356 357 context.setTransform(1, 0, 0, 1, 0, 0); 358 359 context.moveTo(0, 0); 360 context.lineTo(0, canvas.height); 361 context.lineTo(canvas.width, canvas.height); 362 context.lineTo(canvas.width, 0); 363 context.lineTo(0, 0); 364 365 context.restore(); 366 } 367 context.clip(); 368 369 // Clip mode doesn't support recusive stencil, so once we used a clip stencil, 370 // so if it has ClippingNode as a child, the child must uses composition stencil. 371 this._cangodhelpme(true); 372 var len = children.length; 373 if (len > 0) { 374 this.sortAllChildren(); 375 // draw children zOrder < 0 376 for (i = 0; i < len; i++) { 377 locChild = children[i]; 378 if (locChild._localZOrder < 0) 379 locChild.visit(context); 380 else 381 break; 382 } 383 this.draw(context); 384 for (; i < len; i++) { 385 children[i].visit(context); 386 } 387 } else 388 this.draw(context); 389 this._cangodhelpme(false); 390 391 context.restore(); 392 } 393 }, 394 395 /** 396 * The cc.Node to use as a stencil to do the clipping. <br/> 397 * The stencil node will be retained. This default to nil. 398 * @return {cc.Node} 399 */ 400 getStencil: function () { 401 return this._stencil; 402 }, 403 404 /** 405 * @function 406 * @param {cc.Node} stencil 407 */ 408 setStencil: null, 409 410 _setStencilForWebGL: function (stencil) { 411 this._stencil = stencil; 412 }, 413 414 _setStencilForCanvas: function (stencil) { 415 this._stencil = stencil; 416 var locContext = cc._renderContext; 417 // For texture stencil, use the sprite itself 418 if (stencil instanceof cc.Sprite) { 419 return; 420 } 421 // For shape stencil, rewrite the draw of stencil ,only init the clip path and draw nothing. 422 else if (stencil instanceof cc.DrawNode) { 423 stencil.draw = function () { 424 var locEGL_ScaleX = cc.view.getScaleX(), locEGL_ScaleY = cc.view.getScaleY(); 425 locContext.beginPath(); 426 for (var i = 0; i < stencil._buffer.length; i++) { 427 var element = stencil._buffer[i]; 428 var vertices = element.verts; 429 430 //cc.assert(cc.vertexListIsClockwise(vertices), 431 // "Only clockwise polygons should be used as stencil"); 432 433 var firstPoint = vertices[0]; 434 locContext.moveTo(firstPoint.x * locEGL_ScaleX, -firstPoint.y * locEGL_ScaleY); 435 for (var j = 1, len = vertices.length; j < len; j++) 436 locContext.lineTo(vertices[j].x * locEGL_ScaleX, -vertices[j].y * locEGL_ScaleY); 437 } 438 } 439 } 440 }, 441 442 /** 443 * <p> 444 * The alpha threshold. <br/> 445 * The content is drawn only where the stencil have pixel with alpha greater than the alphaThreshold. <br/> 446 * Should be a float between 0 and 1. <br/> 447 * This default to 1 (so alpha test is disabled). 448 * </P> 449 * @return {Number} 450 */ 451 getAlphaThreshold: function () { 452 return this.alphaThreshold; 453 }, 454 455 /** 456 * set alpha threshold. 457 * @param {Number} alphaThreshold 458 */ 459 setAlphaThreshold: function (alphaThreshold) { 460 this.alphaThreshold = alphaThreshold; 461 }, 462 463 /** 464 * <p> 465 * Inverted. If this is set to YES, <br/> 466 * the stencil is inverted, so the content is drawn where the stencil is NOT drawn. <br/> 467 * This default to NO. 468 * </p> 469 * @return {Boolean} 470 */ 471 isInverted: function () { 472 return this.inverted; 473 }, 474 475 476 /** 477 * set whether or not invert of stencil 478 * @param {Boolean} inverted 479 */ 480 setInverted: function (inverted) { 481 this.inverted = inverted; 482 }, 483 484 _cangodhelpme: function (godhelpme) { 485 if (godhelpme === true || godhelpme === false) 486 cc.ClippingNode.prototype._godhelpme = godhelpme; 487 return cc.ClippingNode.prototype._godhelpme; 488 } 489 }); 490 491 var _p = cc.ClippingNode.prototype; 492 493 if (cc._renderType === cc._RENDER_TYPE_WEBGL) { 494 //WebGL 495 _p.init = _p._initForWebGL; 496 _p.visit = _p._visitForWebGL; 497 _p.setStencil = _p._setStencilForWebGL; 498 } else { 499 _p.init = _p._initForCanvas; 500 _p.visit = _p._visitForCanvas; 501 _p.setStencil = _p._setStencilForCanvas; 502 } 503 504 // Extended properties 505 cc.defineGetterSetter(_p, "stencil", _p.getStencil, _p.setStencil); 506 /** @expose */ 507 _p.stencil; 508 509 510 cc.ClippingNode._init_once = null; 511 cc.ClippingNode._visit_once = null; 512 cc.ClippingNode._layer = -1; 513 cc.ClippingNode._sharedCache = null; 514 515 cc.ClippingNode._getSharedCache = function () { 516 return (cc.ClippingNode._sharedCache) || (cc.ClippingNode._sharedCache = document.createElement("canvas")); 517 }; 518 519 /** 520 * Creates and initializes a clipping node with an other node as its stencil. <br/> 521 * The stencil node will be retained. 522 * @deprecated 523 * @param {cc.Node} [stencil=null] 524 * @return {cc.ClippingNode} 525 */ 526 cc.ClippingNode.create = function (stencil) { 527 return new cc.ClippingNode(stencil); 528 }; 529