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