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