Skip to content

Consistently use premultiplied alpha throughout the renderer #460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
5 changes: 4 additions & 1 deletion src/BitmapSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
80 changes: 53 additions & 27 deletions src/PenSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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();

Expand Down Expand Up @@ -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<number>} the "native" size, in texels, of this skin. [width, height]
*/
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
Expand All @@ -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));
Expand Down
16 changes: 5 additions & 11 deletions src/RenderWebGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}

Expand Down
32 changes: 22 additions & 10 deletions src/SVGSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>} 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.
Expand All @@ -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);
}
});
}
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions src/ShaderManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
7 changes: 0 additions & 7 deletions src/Skin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
15 changes: 9 additions & 6 deletions src/shaders/sprite.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down
Binary file not shown.