Skip to content

Use premultiplied alpha everywhere, take 2 #515

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

Merged
merged 2 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions src/BitmapSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ class BitmapSkin extends Skin {
/** @type {!RenderWebGL} */
this._renderer = renderer;

/** @type {WebGLTexture} */
this._texture = null;

/** @type {Array<int>} */
this._textureSize = [0, 0];
}
Expand Down Expand Up @@ -95,22 +92,17 @@ class BitmapSkin extends Skin {
textureData = context.getImageData(0, 0, bitmapData.width, bitmapData.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);
} else {
// TODO: mipmaps?
if (this._texture === null) {
const textureOptions = {
auto: true,
wrap: gl.CLAMP_TO_EDGE,
src: textureData
auto: false,
wrap: gl.CLAMP_TO_EDGE
};

this._texture = twgl.createTexture(gl, textureOptions);
this._silhouette.update(textureData);
}

this._setTexture(textureData);

// Do these last in case any of the above throws an exception
this._costumeResolution = costumeResolution || 2;
this._textureSize = BitmapSkin._getBitmapSize(bitmapData);
Expand Down
3 changes: 3 additions & 0 deletions src/Drawable.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,9 @@ class Drawable {
const localPosition = getLocalPosition(drawable, vec);
if (localPosition[0] < 0 || localPosition[1] < 0 ||
localPosition[0] > 1 || localPosition[1] > 1) {
dst[0] = 0;
dst[1] = 0;
dst[2] = 0;
dst[3] = 0;
return dst;
}
Expand Down
30 changes: 25 additions & 5 deletions src/EffectTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,21 @@ class EffectTransform {
const effects = drawable.enabledEffects;
const uniforms = drawable.getUniforms();

if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
// gl_FragColor.a *= u_ghost
inOutColor[3] *= uniforms.u_ghost;
}

const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;
const enableBrightness = (effects & ShaderManager.EFFECT_INFO.brightness.mask) !== 0;

if (enableColor || enableBrightness) {
// gl_FragColor.rgb /= gl_FragColor.a + epsilon;
// Here, we're dividing by the (previously pre-multiplied) alpha to ensure HSV is properly calculated
// for partially transparent pixels.
// epsilon is present in the shader because dividing by 0 (fully transparent pixels) messes up calculations.
// We're doing this with a Uint8ClampedArray here, so dividing by 0 just gives 255. We're later multiplying
// by 0 again, so it won't affect results.
const alpha = inOutColor[3] / 255;
inOutColor[0] /= alpha;
inOutColor[1] /= alpha;
inOutColor[2] /= alpha;

// vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
const hsl = rgbToHsl(inOutColor);

Expand Down Expand Up @@ -171,6 +177,20 @@ class EffectTransform {
}
// gl_FragColor.rgb = convertHSL2RGB(hsl);
inOutColor.set(hslToRgb(hsl));

// gl_FragColor.rgb *= gl_FragColor.a + epsilon;
// Now we're doing the reverse, premultiplying by the alpha once again.
inOutColor[0] *= alpha;
inOutColor[1] *= alpha;
inOutColor[2] *= alpha;
}

if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
// gl_FragColor *= u_ghost
inOutColor[0] *= uniforms.u_ghost;
inOutColor[1] *= uniforms.u_ghost;
inOutColor[2] *= uniforms.u_ghost;
inOutColor[3] *= uniforms.u_ghost;
}

return inOutColor;
Expand Down
44 changes: 17 additions & 27 deletions src/PenSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const DefaultPenAttributes = {
diameter: 1
};

/**
* Reused memory location for storing a premultiplied pen color.
* @type {FloatArray}
*/
const __premultipliedColor = [0, 0, 0, 0];


/**
* Reused memory location for projection matrices.
Expand Down Expand Up @@ -88,9 +94,6 @@ class PenSkin extends Skin {
/** @type {HTMLCanvasElement} */
this._canvas = document.createElement('canvas');

/** @type {WebGLTexture} */
this._texture = null;

/** @type {WebGLTexture} */
this._exportTexture = null;

Expand Down Expand Up @@ -123,7 +126,7 @@ 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);
Expand Down Expand Up @@ -154,13 +157,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 @@ -188,7 +184,7 @@ class PenSkin extends Skin {
clear () {
const gl = this._renderer.gl;
twgl.bindFramebufferInfo(gl, this._framebuffer);

/* Reset framebuffer to transparent black */
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
Expand Down Expand Up @@ -317,13 +313,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,8 +333,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 @@ -384,6 +371,13 @@ class PenSkin extends Skin {
const radius = diameter / 2;
const yScalar = (0.50001 - (radius / (length + diameter)));

// Premultiply pen color by pen transparency
const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
__premultipliedColor[0] = penColor[0] * penColor[3];
__premultipliedColor[1] = penColor[1] * penColor[3];
__premultipliedColor[2] = penColor[2] * penColor[3];
__premultipliedColor[3] = penColor[3];

const uniforms = {
u_positionScalar: yScalar,
u_capScale: diameter,
Expand All @@ -397,7 +391,7 @@ class PenSkin extends Skin {
twgl.m4.scaling(scalingVector, __modelScalingMatrix),
__modelMatrix
),
u_lineColor: penAttributes.color4f || DefaultPenAttributes.color4f
u_lineColor: __premultipliedColor
};

twgl.setUniforms(currentShader, uniforms);
Expand Down Expand Up @@ -490,8 +484,6 @@ class PenSkin extends Skin {

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 @@ -501,8 +493,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 @@ -661,7 +651,7 @@ class PenSkin extends Skin {
skinImageData.data.set(skinPixels);
skinContext.putImageData(skinImageData, 0, 0);

this._silhouette.update(this._canvas);
this._silhouette.update(this._canvas, true /* isPremultiplied */);

this._silhouetteDirty = false;
}
Expand Down
27 changes: 9 additions & 18 deletions src/RenderWebGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,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 @@ -834,7 +834,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
});

gl.stencilFunc(gl.EQUAL, 1, 1);
Expand Down Expand Up @@ -1554,7 +1555,7 @@ class RenderWebGL extends EventEmitter {
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);

// Draw the stamped sprite onto the PenSkin's framebuffer.
this._drawThese([stampID], ShaderManager.DRAW_MODE.stamp, projection, {ignoreVisibility: true});
this._drawThese([stampID], ShaderManager.DRAW_MODE.default, projection, {ignoreVisibility: true});
skin._silhouetteDirty = true;
}

Expand Down Expand Up @@ -1744,14 +1745,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 Expand Up @@ -1902,13 +1895,11 @@ class RenderWebGL extends EventEmitter {
}
*/
Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor);
// if we are fully transparent, go to the next one "down"
const sampleAlpha = __blendColor[3] / 255;
// premultiply alpha
dst[0] += __blendColor[0] * blendAlpha * sampleAlpha;
dst[1] += __blendColor[1] * blendAlpha * sampleAlpha;
dst[2] += __blendColor[2] * blendAlpha * sampleAlpha;
blendAlpha *= (1 - sampleAlpha);
// Equivalent to gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
dst[0] += __blendColor[0] * blendAlpha;
dst[1] += __blendColor[1] * blendAlpha;
dst[2] += __blendColor[2] * blendAlpha;
blendAlpha *= (1 - (__blendColor[3] / 255));
}
// Backdrop could be transparent, so we need to go to the "clear color" of the
// draw scene (white) as a fallback if everything was alpha
Expand Down
3 changes: 2 additions & 1 deletion src/SVGSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ class SVGSkin extends Skin {
const textureOptions = {
auto: false,
wrap: this._renderer.gl.CLAMP_TO_EDGE,
src: textureData
src: textureData,
premultiplyAlpha: true
};

const mip = twgl.createTexture(this._renderer.gl, textureOptions);
Expand Down
7 changes: 1 addition & 6 deletions src/ShaderManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,7 @@ ShaderManager.DRAW_MODE = {
/**
* Sample a "texture" to draw a line with caps.
*/
lineSample: 'lineSample',

/**
* Draw normally except for pre-multiplied alpha
*/
stamp: 'stamp'
lineSample: 'lineSample'
};

module.exports = ShaderManager;
Loading