diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 364009f50..5108e19e4 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -182,9 +182,23 @@ class RenderWebGL extends EventEmitter { /** @type {function} */ this._exitRegion = null; + /** @type {object} */ + this._backgroundDrawRegionId = { + enter: () => this._enterDrawBackground(), + exit: () => this._exitDrawBackground() + }; + /** @type {Array.} */ this._snapshotCallbacks = []; + /** @type {Array} + * Don't set this directly-- use setBackgroundColor so it stays in sync with _backgroundColor3b */ + this._backgroundColor4f = [0, 0, 0, 1]; + + /** @type {Uint8ClampedArray} + * Don't set this directly-- use setBackgroundColor so it stays in sync with _backgroundColor4f */ + this._backgroundColor3b = new Uint8ClampedArray(3); + this._createGeometry(); this.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged); @@ -244,7 +258,14 @@ class RenderWebGL extends EventEmitter { * @param {number} blue The blue component for the background. */ setBackgroundColor (red, green, blue) { - this._backgroundColor = [red, green, blue, 1]; + this._backgroundColor4f[0] = red; + this._backgroundColor4f[1] = green; + this._backgroundColor4f[2] = blue; + + this._backgroundColor3b[0] = red * 255; + this._backgroundColor3b[1] = green * 255; + this._backgroundColor3b[2] = blue * 255; + } /** @@ -623,7 +644,7 @@ class RenderWebGL extends EventEmitter { twgl.bindFramebufferInfo(gl, null); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - gl.clearColor.apply(gl, this._backgroundColor); + gl.clearColor.apply(gl, this._backgroundColor4f); gl.clear(gl.COLOR_BUFFER_BIT); this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, this._projection); @@ -739,12 +760,20 @@ class RenderWebGL extends EventEmitter { */ isTouchingColor (drawableID, color3b, mask3b) { const candidates = this._candidatesTouching(drawableID, this._visibleDrawList); - if (candidates.length === 0) { + + let bounds; + if (colorMatches(color3b, this._backgroundColor3b, 0)) { + // If the color we're checking for is the background color, don't confine the check to + // candidate drawables' bounds--since the background spans the entire stage, we must check + // everything that lies inside the drawable. + bounds = this._touchingBounds(drawableID); + } else if (candidates.length === 0) { + // If not checking for the background color, we can return early if there are no candidate drawables. return false; + } else { + bounds = this._candidatesBounds(candidates); } - const bounds = this._candidatesBounds(candidates); - const maxPixelsForCPU = this._getMaxPixelsForCPU(); const debugCanvasContext = this._debugCanvas && this._debugCanvas.getContext('2d'); @@ -805,6 +834,19 @@ class RenderWebGL extends EventEmitter { } } + _enterDrawBackground () { + const gl = this.gl; + const currentShader = this._shaderManager.getShader(ShaderManager.DRAW_MODE.background, 0); + gl.disable(gl.BLEND); + gl.useProgram(currentShader.program); + twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo); + } + + _exitDrawBackground () { + const gl = this.gl; + gl.enable(gl.BLEND); + } + _isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) { this._doExitDrawRegion(); @@ -816,15 +858,8 @@ class RenderWebGL extends EventEmitter { gl.viewport(0, 0, bounds.width, bounds.height); const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1); - let fillBackgroundColor = this._backgroundColor; - - // When using masking such that the background fill color will showing through, ensure we don't - // fill using the same color that we are trying to detect! - if (color3b[0] > 196 && color3b[1] > 196 && color3b[2] > 196) { - fillBackgroundColor = [0, 0, 0, 255]; - } - - gl.clearColor.apply(gl, fillBackgroundColor); + // Clear the query buffer to fully transparent. This will be the color of pixels that fail the stencil test. + gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); let extraUniforms; @@ -836,6 +871,9 @@ class RenderWebGL extends EventEmitter { } try { + // Using the stencil buffer, mask out the drawing to either the drawable's alpha channel + // or pixels of the drawable which match the mask color, depending on whether a mask color is given. + // Masked-out pixels will not be checked. gl.enable(gl.STENCIL_TEST); gl.stencilFunc(gl.ALWAYS, 1, 1); gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); @@ -856,12 +894,25 @@ class RenderWebGL extends EventEmitter { gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); gl.colorMask(true, true, true, true); + // Draw the background as a quad. Drawing a background with gl.clear will not mask to the stenciled area. + this.enterDrawRegion(this._backgroundDrawRegionId); + + const uniforms = { + u_backgroundColor: this._backgroundColor4f + }; + + const currentShader = this._shaderManager.getShader(ShaderManager.DRAW_MODE.background, 0); + twgl.setUniforms(currentShader, uniforms); + twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES); + + // Draw the candidate drawables on top of the background. this._drawThese(candidateIDs, ShaderManager.DRAW_MODE.default, projection, {idFilterFunc: testID => testID !== drawableID} ); } finally { gl.colorMask(true, true, true, true); gl.disable(gl.STENCIL_TEST); + this._doExitDrawRegion(); } } @@ -880,7 +931,8 @@ class RenderWebGL extends EventEmitter { } for (let pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) { - if (colorMatches(color3b, pixels, pixelBase)) { + // Transparent pixels are masked (either by the drawable's alpha channel or color mask). + if (pixels[pixelBase + 3] !== 0 && colorMatches(color3b, pixels, pixelBase)) { return true; } } @@ -1204,7 +1256,7 @@ class RenderWebGL extends EventEmitter { gl.viewport(0, 0, bounds.width, bounds.height); const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1); - gl.clearColor.apply(gl, this._backgroundColor); + gl.clearColor.apply(gl, this._backgroundColor4f); gl.clear(gl.COLOR_BUFFER_BIT); this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, projection); @@ -1292,6 +1344,13 @@ class RenderWebGL extends EventEmitter { // Update the CPU position data drawable.updateCPURenderAttributes(); const candidateBounds = drawable.getFastBounds(); + + // Push bounds out to integers. If a drawable extends out into half a pixel, that half-pixel still + // needs to be tested. Plus, in some areas we construct another rectangle from the union of these, + // and iterate over its pixels (width * height). Turns out that doesn't work so well when the + // width/height aren't integers. + candidateBounds.snapToInt(); + if (bounds.intersects(candidateBounds)) { result.push({ id, diff --git a/src/ShaderManager.js b/src/ShaderManager.js index 8c59d3b7d..37c24346d 100644 --- a/src/ShaderManager.js +++ b/src/ShaderManager.js @@ -176,7 +176,12 @@ ShaderManager.DRAW_MODE = { /** * Draw a line with caps. */ - line: 'line' + line: 'line', + + /** + * Draw the background in a certain color. Must sometimes be used instead of gl.clear. + */ + background: 'background' }; module.exports = ShaderManager; diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index b65fe11a0..6b628592f 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -40,9 +40,15 @@ uniform float u_lineLength; uniform vec4 u_penPoints; #endif // DRAW_MODE_line +#ifdef DRAW_MODE_background +uniform vec4 u_backgroundColor; +#endif // DRAW_MODE_background + uniform sampler2D u_skin; +#ifndef DRAW_MODE_background varying vec2 v_texCoord; +#endif // Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations. // Smaller values can cause problems on some mobile devices. @@ -110,7 +116,7 @@ const vec2 kCenter = vec2(0.5, 0.5); void main() { - #ifndef DRAW_MODE_line + #if !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background)) vec2 texcoord0 = v_texCoord; #ifdef ENABLE_mosaic @@ -216,7 +222,9 @@ void main() gl_FragColor.rgb /= gl_FragColor.a + epsilon; #endif - #else // DRAW_MODE_line + #endif // !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background)) + + #ifdef DRAW_MODE_line // Maaaaagic antialiased-line-with-round-caps shader. // "along-the-lineness". This increases parallel to the line. @@ -235,4 +243,8 @@ void main() // the closer we are to the line, invert it. gl_FragColor = u_lineColor * clamp(1.0 - line, 0.0, 1.0); #endif // DRAW_MODE_line + + #ifdef DRAW_MODE_background + gl_FragColor = u_backgroundColor; + #endif } diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index 90ae4ca5c..6f65cf83d 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -11,7 +11,7 @@ uniform vec4 u_penPoints; const float epsilon = 1e-3; #endif -#ifndef DRAW_MODE_line +#if !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background)) uniform mat4 u_projectionMatrix; uniform mat4 u_modelMatrix; attribute vec2 a_texCoord; @@ -60,6 +60,8 @@ void main() { // Apply view transform position *= 2.0 / u_stageSize; gl_Position = vec4(position, 0, 1); + #elif defined(DRAW_MODE_background) + gl_Position = vec4(a_position * 2.0, 0, 1); #else gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1); v_texCoord = a_texCoord; diff --git a/test/integration/scratch-tests/clear-color.sb3 b/test/integration/scratch-tests/clear-color.sb3 new file mode 100644 index 000000000..42b6d8516 Binary files /dev/null and b/test/integration/scratch-tests/clear-color.sb3 differ