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 var canvas = context.canvas; 315 // Composition mode, costy but support texture stencil 316 if (this._cangodhelpme() || this._stencil instanceof cc.Sprite) { 317 // Cache the current canvas, for later use (This is a little bit heavy, replace this solution with other walkthrough) 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 if (this.inverted) { 350 context.save(); 351 352 context.setTransform(1, 0, 0, 1, 0, 0); 353 354 context.moveTo(0, 0); 355 context.lineTo(0, canvas.height); 356 context.lineTo(canvas.width, canvas.height); 357 context.lineTo(canvas.width, 0); 358 context.lineTo(0, 0); 359 360 context.restore(); 361 } 362 context.clip(); 363 364 // Clip mode doesn't support recusive stencil, so once we used a clip stencil, 365 // so if it has ClippingNode as a child, the child must uses composition stencil. 366 this._cangodhelpme(true); 367 var len = children.length; 368 if (len > 0) { 369 this.sortAllChildren(); 370 // draw children zOrder < 0 371 for (i = 0; i < len; i++) { 372 locChild = children[i]; 373 if (locChild._localZOrder < 0) 374 locChild.visit(context); 375 else 376 break; 377 } 378 this.draw(context); 379 for (; i < len; i++) { 380 children[i].visit(context); 381 } 382 } else 383 this.draw(context); 384 this._cangodhelpme(false); 385 386 context.restore(); 387 } 388 }, 389 390 /** 391 * The cc.Node to use as a stencil to do the clipping. <br/> 392 * The stencil node will be retained. This default to nil. 393 * @return {cc.Node} 394 */ 395 getStencil: function () { 396 return this._stencil; 397 }, 398 399 /** 400 * @function 401 * @param {cc.Node} stencil 402 */ 403 setStencil: null, 404 405 _setStencilForWebGL: function (stencil) { 406 this._stencil = stencil; 407 }, 408 409 _setStencilForCanvas: function (stencil) { 410 this._stencil = stencil; 411 var locContext = cc._renderContext; 412 // For texture stencil, use the sprite itself 413 if (stencil instanceof cc.Sprite) { 414 return; 415 } 416 // For shape stencil, rewrite the draw of stencil ,only init the clip path and draw nothing. 417 else if (stencil instanceof cc.DrawNode) { 418 stencil.draw = function () { 419 var locEGL_ScaleX = cc.view.getScaleX(), locEGL_ScaleY = cc.view.getScaleY(); 420 locContext.beginPath(); 421 for (var i = 0; i < stencil._buffer.length; i++) { 422 var element = stencil._buffer[i]; 423 var vertices = element.verts; 424 425 //cc.assert(cc.vertexListIsClockwise(vertices), 426 // "Only clockwise polygons should be used as stencil"); 427 428 var firstPoint = vertices[0]; 429 locContext.moveTo(firstPoint.x * locEGL_ScaleX, -firstPoint.y * locEGL_ScaleY); 430 for (var j = 1, len = vertices.length; j < len; j++) 431 locContext.lineTo(vertices[j].x * locEGL_ScaleX, -vertices[j].y * locEGL_ScaleY); 432 } 433 } 434 } 435 }, 436 437 /** 438 * <p> 439 * The alpha threshold. <br/> 440 * The content is drawn only where the stencil have pixel with alpha greater than the alphaThreshold. <br/> 441 * Should be a float between 0 and 1. <br/> 442 * This default to 1 (so alpha test is disabled). 443 * </P> 444 * @return {Number} 445 */ 446 getAlphaThreshold: function () { 447 return this.alphaThreshold; 448 }, 449 450 /** 451 * set alpha threshold. 452 * @param {Number} alphaThreshold 453 */ 454 setAlphaThreshold: function (alphaThreshold) { 455 this.alphaThreshold = alphaThreshold; 456 }, 457 458 /** 459 * <p> 460 * Inverted. If this is set to YES, <br/> 461 * the stencil is inverted, so the content is drawn where the stencil is NOT drawn. <br/> 462 * This default to NO. 463 * </p> 464 * @return {Boolean} 465 */ 466 isInverted: function () { 467 return this.inverted; 468 }, 469 470 471 /** 472 * set whether or not invert of stencil 473 * @param {Boolean} inverted 474 */ 475 setInverted: function (inverted) { 476 this.inverted = inverted; 477 }, 478 479 _cangodhelpme: function (godhelpme) { 480 if (godhelpme === true || godhelpme === false) 481 cc.ClippingNode.prototype._godhelpme = godhelpme; 482 return cc.ClippingNode.prototype._godhelpme; 483 } 484 }); 485 486 var _p = cc.ClippingNode.prototype; 487 488 if (cc._renderType === cc._RENDER_TYPE_WEBGL) { 489 //WebGL 490 _p.init = _p._initForWebGL; 491 _p.visit = _p._visitForWebGL; 492 _p.setStencil = _p._setStencilForWebGL; 493 } else { 494 _p.init = _p._initForCanvas; 495 _p.visit = _p._visitForCanvas; 496 _p.setStencil = _p._setStencilForCanvas; 497 } 498 499 // Extended properties 500 cc.defineGetterSetter(_p, "stencil", _p.getStencil, _p.setStencil); 501 /** @expose */ 502 _p.stencil; 503 504 505 cc.ClippingNode._init_once = null; 506 cc.ClippingNode._visit_once = null; 507 cc.ClippingNode._layer = null; 508 cc.ClippingNode._sharedCache = null; 509 510 cc.ClippingNode._getSharedCache = function () { 511 return (cc.ClippingNode._sharedCache) || (cc.ClippingNode._sharedCache = document.createElement("canvas")); 512 }; 513 514 /** 515 * Creates and initializes a clipping node with an other node as its stencil. <br/> 516 * The stencil node will be retained. 517 * @param {cc.Node} [stencil=null] 518 * @return {cc.ClippingNode} 519 */ 520 cc.ClippingNode.create = function (stencil) { 521 return new cc.ClippingNode(stencil); 522 }; 523