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