diff --git a/src/BitmapSkin.js b/src/BitmapSkin.js index b4b03d3b7..04307b6b7 100644 --- a/src/BitmapSkin.js +++ b/src/BitmapSkin.js @@ -92,14 +92,17 @@ class BitmapSkin extends Skin { if (this._texture) { gl.bindTexture(gl.TEXTURE_2D, this._texture); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); this._silhouette.update(textureData); } else { // TODO: mipmaps? const textureOptions = { auto: true, wrap: gl.CLAMP_TO_EDGE, - src: textureData + src: textureData, + premultiplyAlpha: true }; this._texture = twgl.createTexture(gl, textureOptions); diff --git a/src/PenSkin.js b/src/PenSkin.js index 1e500ba44..3868e78c6 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -112,6 +112,12 @@ class PenSkin extends Skin { exit: () => this._exitDrawLineOnBuffer() }; + /** @type {object} */ + this._toSilhouetteDrawRegionId = { + enter: () => this._enterDrawToSilhouetteBuffer(), + exit: () => this._exitDrawToSilhouetteBuffer() + }; + /** @type {object} */ this._toBufferDrawRegionId = { enter: () => this._enterDrawToBuffer(), @@ -123,10 +129,19 @@ class PenSkin extends Skin { const NO_EFFECTS = 0; /** @type {twgl.ProgramInfo} */ - this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.stamp, NO_EFFECTS); + this._stampShader = this._renderer._shaderManager.getShader( + ShaderManager.DRAW_MODE.default, NO_EFFECTS + ); /** @type {twgl.ProgramInfo} */ - this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS); + this._silhouetteShader = this._renderer._shaderManager.getShader( + ShaderManager.DRAW_MODE.straightAlpha, NO_EFFECTS + ); + + /** @type {twgl.ProgramInfo} */ + this._lineShader = this._renderer._shaderManager.getShader( + ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS + ); this._createLineGeometry(); @@ -154,13 +169,6 @@ class PenSkin extends Skin { return true; } - /** - * @returns {boolean} true if alpha is premultiplied, false otherwise - */ - get hasPremultipliedAlpha () { - return true; - } - /** * @return {Array} the "native" size, in texels, of this skin. [width, height] */ @@ -317,13 +325,6 @@ class PenSkin extends Skin { twgl.bindFramebufferInfo(gl, this._framebuffer); - // Needs a blend function that blends a destination that starts with - // no alpha. - gl.blendFuncSeparate( - gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, - gl.ONE, gl.ONE_MINUS_SRC_ALPHA - ); - gl.viewport(0, 0, bounds.width, bounds.height); gl.useProgram(currentShader.program); @@ -344,9 +345,6 @@ class PenSkin extends Skin { */ _exitDrawLineOnBuffer () { const gl = this._renderer.gl; - - gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE); - twgl.bindFramebufferInfo(gl, null); } @@ -489,11 +487,7 @@ class PenSkin extends Skin { */ _enterDrawToBuffer () { const gl = this._renderer.gl; - twgl.bindFramebufferInfo(gl, this._framebuffer); - - gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - this._drawRectangleRegionEnter(this._stampShader, this._bounds); } @@ -502,9 +496,6 @@ class PenSkin extends Skin { */ _exitDrawToBuffer () { const gl = this._renderer.gl; - - gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE); - twgl.bindFramebufferInfo(gl, null); } @@ -633,6 +624,41 @@ class PenSkin extends Skin { context.lineWidth = diameter; } + /** + * Prepare to draw to the silhouette buffer. + */ + _enterDrawToSilhouetteBuffer () { + const gl = this._renderer.gl; + twgl.bindFramebufferInfo(gl, this._silhouetteBuffer); + this._drawRectangleRegionEnter(this._silhouetteShader, this._bounds); + } + + /** + * Return to a base state from _enterDrawToSilhouetteBuffer. + */ + _exitDrawToSilhouetteBuffer () { + const gl = this._renderer.gl; + twgl.bindFramebufferInfo(gl, null); + } + + /** + * Draw this skin's framebuffer contents to the silhouette buffer. + */ + _drawToSilhouetteBuffer () { + const currentShader = this._silhouetteShader; + const bounds = this._bounds; + + this._renderer.enterDrawRegion(this._toSilhouetteDrawRegionId); + + this._drawRectangle( + currentShader, + this.getTexture(), + bounds, + -this._canvas.width / 2, + this._canvas.height / 2 + ); + } + /** * If there have been pen operations that have dirtied the canvas, update * now before someone wants to use our silhouette. @@ -648,7 +674,7 @@ class PenSkin extends Skin { const bounds = this._bounds; - this._renderer.enterDrawRegion(this._toBufferDrawRegionId); + this._drawToSilhouetteBuffer(); // Sample the framebuffer's pixels into the silhouette instance const skinPixels = new Uint8Array(Math.floor(this._canvas.width * this._canvas.height * 4)); diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index f16b02d45..065544b58 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -197,7 +197,7 @@ class RenderWebGL extends EventEmitter { gl.disable(gl.DEPTH_TEST); /** @todo disable when no partial transparency? */ gl.enable(gl.BLEND); - gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); } /** @@ -828,7 +828,8 @@ class RenderWebGL extends EventEmitter { projection, { extraUniforms, - ignoreVisibility: true // Touching color ignores sprite visibility + ignoreVisibility: true, // Touching color ignores sprite visibility + effectMask: ~ShaderManager.EFFECT_INFO.ghost.mask // Also ignores sprite ghost effect }); gl.stencilFunc(gl.EQUAL, 1, 1); @@ -1117,7 +1118,7 @@ class RenderWebGL extends EventEmitter { gl.clear(gl.COLOR_BUFFER_BIT); try { gl.disable(gl.BLEND); - this._drawThese([drawableID], ShaderManager.DRAW_MODE.default, projection, + this._drawThese([drawableID], ShaderManager.DRAW_MODE.straightAlpha, projection, {effectMask: ~ShaderManager.EFFECT_INFO.ghost.mask}); } finally { gl.enable(gl.BLEND); @@ -1435,7 +1436,7 @@ class RenderWebGL extends EventEmitter { try { gl.disable(gl.BLEND); - this._drawThese([stampID], ShaderManager.DRAW_MODE.stamp, projection, {ignoreVisibility: true}); + this._drawThese([stampID], ShaderManager.DRAW_MODE.default, projection, {ignoreVisibility: true}); } finally { gl.enable(gl.BLEND); } @@ -1621,13 +1622,6 @@ class RenderWebGL extends EventEmitter { twgl.setUniforms(currentShader, uniforms); - /* adjust blend function for this skin */ - if (drawable.skin.hasPremultipliedAlpha){ - gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - } else { - gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - } - twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES); } diff --git a/src/SVGSkin.js b/src/SVGSkin.js index 90e39081e..66f44d25f 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -60,6 +60,24 @@ class SVGSkin extends Skin { super.setRotationCenter(x - viewOffset[0], y - viewOffset[1]); } + /** + * Set this skin's texture from canvas image data. + * @param {ImageData} textureData - The canvas image data to set the texture to. + */ + _setTexture (textureData) { + const gl = this._renderer.gl; + + gl.bindTexture(gl.TEXTURE_2D, this._texture); + // Canvas image data comes un-premultiplied, but premultiplied alpha is necessary for proper blending. + // See http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/ + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData); + // Remember to unset afterwards! + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + + this._silhouette.update(textureData); + } + /** * @param {Array} scale - The scaling factors to be used, each in the [0,100] range. * @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale. @@ -81,10 +99,7 @@ class SVGSkin extends Skin { const context = canvas.getContext('2d'); const textureData = context.getImageData(0, 0, canvas.width, canvas.height); - const gl = this._renderer.gl; - gl.bindTexture(gl.TEXTURE_2D, this._texture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData); - this._silhouette.update(textureData); + this._setTexture(textureData); } }); } @@ -112,19 +127,16 @@ class SVGSkin extends Skin { const textureData = context.getImageData(0, 0, canvas.width, canvas.height); if (this._texture) { - gl.bindTexture(gl.TEXTURE_2D, this._texture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData); - this._silhouette.update(textureData); + this._setTexture(textureData); } else { // TODO: mipmaps? const textureOptions = { auto: true, - wrap: gl.CLAMP_TO_EDGE, - src: textureData + wrap: gl.CLAMP_TO_EDGE }; this._texture = twgl.createTexture(gl, textureOptions); - this._silhouette.update(textureData); + this._setTexture(textureData); } const maxDimension = Math.max(this._svgRenderer.canvas.width, this._svgRenderer.canvas.height); diff --git a/src/ShaderManager.js b/src/ShaderManager.js index 1520d0f5e..013efd05a 100644 --- a/src/ShaderManager.js +++ b/src/ShaderManager.js @@ -169,14 +169,14 @@ ShaderManager.DRAW_MODE = { colorMask: 'colorMask', /** - * Sample a "texture" to draw a line with caps. + * Un-premultiply alpha, for drawable extraction to a canvas. */ - lineSample: 'lineSample', + straightAlpha: 'straightAlpha', /** - * Draw normally except for pre-multiplied alpha + * Sample a "texture" to draw a line with caps. */ - stamp: 'stamp' + lineSample: 'lineSample' }; module.exports = ShaderManager; diff --git a/src/Skin.js b/src/Skin.js index e0e741357..d5fcbb3a8 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -76,13 +76,6 @@ class Skin extends EventEmitter { return false; } - /** - * @returns {boolean} true if alpha is premultiplied, false otherwise - */ - get hasPremultipliedAlpha () { - return false; - } - /** * @return {int} the unique ID for this Skin. */ diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index a781baff5..de21bfa4c 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -155,10 +155,10 @@ void main() gl_FragColor = texture2D(u_skin, texcoord0); - #ifdef ENABLE_ghost - gl_FragColor.a *= u_ghost; - #endif // ENABLE_ghost - + #ifdef ENABLE_ghost + gl_FragColor *= u_ghost; + #endif // ENABLE_ghost + #ifdef DRAW_MODE_silhouette // switch to u_silhouetteColor only AFTER the alpha test gl_FragColor = u_silhouetteColor; @@ -196,9 +196,12 @@ void main() #endif // DRAW_MODE_colorMask #endif // DRAW_MODE_silhouette + #ifdef DRAW_MODE_straightAlpha + gl_FragColor.rgb /= gl_FragColor.a; + #endif //DRAW_MODE_straightAlpha + #else // DRAW_MODE_lineSample - gl_FragColor = u_lineColor; - gl_FragColor.a *= clamp( + gl_FragColor = u_lineColor * clamp( // Scale the capScale a little to have an aliased region. (u_capScale + u_aliasAmount - u_capScale * 2.0 * distance(v_texCoord, vec2(0.5, 0.5)) diff --git a/test/integration/scratch-tests/alpha-multiplication.sb2 b/test/integration/scratch-tests/alpha-multiplication.sb2 new file mode 100644 index 000000000..0f2e632b3 Binary files /dev/null and b/test/integration/scratch-tests/alpha-multiplication.sb2 differ