Skip to content

Commit 10a6d87

Browse files
committed
Premultiplied alpha, take 2
1 parent fed02ca commit 10a6d87

File tree

11 files changed

+164
-84
lines changed

11 files changed

+164
-84
lines changed

src/BitmapSkin.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ class BitmapSkin extends Skin {
1818
/** @type {!RenderWebGL} */
1919
this._renderer = renderer;
2020

21-
/** @type {WebGLTexture} */
22-
this._texture = null;
23-
2421
/** @type {Array<int>} */
2522
this._textureSize = [0, 0];
2623
}
@@ -95,22 +92,17 @@ class BitmapSkin extends Skin {
9592
textureData = context.getImageData(0, 0, bitmapData.width, bitmapData.height);
9693
}
9794

98-
if (this._texture) {
99-
gl.bindTexture(gl.TEXTURE_2D, this._texture);
100-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
101-
this._silhouette.update(textureData);
102-
} else {
103-
// TODO: mipmaps?
95+
if (this._texture === null) {
10496
const textureOptions = {
105-
auto: true,
106-
wrap: gl.CLAMP_TO_EDGE,
107-
src: textureData
97+
auto: false,
98+
wrap: gl.CLAMP_TO_EDGE
10899
};
109100

110101
this._texture = twgl.createTexture(gl, textureOptions);
111-
this._silhouette.update(textureData);
112102
}
113103

104+
this._setTexture(textureData);
105+
114106
// Do these last in case any of the above throws an exception
115107
this._costumeResolution = costumeResolution || 2;
116108
this._textureSize = BitmapSkin._getBitmapSize(bitmapData);

src/Drawable.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,9 @@ class Drawable {
685685
const localPosition = getLocalPosition(drawable, vec);
686686
if (localPosition[0] < 0 || localPosition[1] < 0 ||
687687
localPosition[0] > 1 || localPosition[1] > 1) {
688+
dst[0] = 0;
689+
dst[1] = 0;
690+
dst[2] = 0;
688691
dst[3] = 0;
689692
return dst;
690693
}

src/EffectTransform.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,21 @@ class EffectTransform {
130130
const effects = drawable.enabledEffects;
131131
const uniforms = drawable.getUniforms();
132132

133-
if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
134-
// gl_FragColor.a *= u_ghost
135-
inOutColor[3] *= uniforms.u_ghost;
136-
}
137-
138133
const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;
139134
const enableBrightness = (effects & ShaderManager.EFFECT_INFO.brightness.mask) !== 0;
140135

141136
if (enableColor || enableBrightness) {
137+
// gl_FragColor.rgb /= gl_FragColor.a + epsilon;
138+
// Here, we're dividing by the (previously pre-multiplied) alpha to ensure HSV is properly calculated
139+
// for partially transparent pixels.
140+
// epsilon is present in the shader because dividing by 0 (fully transparent pixels) messes up calculations.
141+
// We're doing this with a Uint8ClampedArray here, so dividing by 0 just gives 255. We're later multiplying
142+
// by 0 again, so it won't affect results.
143+
const alpha = inOutColor[3] / 255;
144+
inOutColor[0] /= alpha;
145+
inOutColor[1] /= alpha;
146+
inOutColor[2] /= alpha;
147+
142148
// vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
143149
const hsl = rgbToHsl(inOutColor);
144150

@@ -171,6 +177,20 @@ class EffectTransform {
171177
}
172178
// gl_FragColor.rgb = convertHSL2RGB(hsl);
173179
inOutColor.set(hslToRgb(hsl));
180+
181+
// gl_FragColor.rgb *= gl_FragColor.a + epsilon;
182+
// Now we're doing the reverse, premultiplying by the alpha once again.
183+
inOutColor[0] *= alpha;
184+
inOutColor[1] *= alpha;
185+
inOutColor[2] *= alpha;
186+
}
187+
188+
if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
189+
// gl_FragColor *= u_ghost
190+
inOutColor[0] *= uniforms.u_ghost;
191+
inOutColor[1] *= uniforms.u_ghost;
192+
inOutColor[2] *= uniforms.u_ghost;
193+
inOutColor[3] *= uniforms.u_ghost;
174194
}
175195

176196
return inOutColor;

src/PenSkin.js

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class PenSkin extends Skin {
7979
constructor (id, renderer) {
8080
super(id);
8181

82+
// This silhouette will be updated with data from `gl.readPixels`, which is premultiplied.
83+
this._silhouette.premultiplied = true;
84+
8285
/**
8386
* @private
8487
* @type {RenderWebGL}
@@ -88,9 +91,6 @@ class PenSkin extends Skin {
8891
/** @type {HTMLCanvasElement} */
8992
this._canvas = document.createElement('canvas');
9093

91-
/** @type {WebGLTexture} */
92-
this._texture = null;
93-
9494
/** @type {WebGLTexture} */
9595
this._exportTexture = null;
9696

@@ -123,7 +123,7 @@ class PenSkin extends Skin {
123123

124124
const NO_EFFECTS = 0;
125125
/** @type {twgl.ProgramInfo} */
126-
this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.stamp, NO_EFFECTS);
126+
this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.default, NO_EFFECTS);
127127

128128
/** @type {twgl.ProgramInfo} */
129129
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS);
@@ -317,13 +317,6 @@ class PenSkin extends Skin {
317317

318318
twgl.bindFramebufferInfo(gl, this._framebuffer);
319319

320-
// Needs a blend function that blends a destination that starts with
321-
// no alpha.
322-
gl.blendFuncSeparate(
323-
gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA,
324-
gl.ONE, gl.ONE_MINUS_SRC_ALPHA
325-
);
326-
327320
gl.viewport(0, 0, bounds.width, bounds.height);
328321

329322
gl.useProgram(currentShader.program);
@@ -344,8 +337,6 @@ class PenSkin extends Skin {
344337
_exitDrawLineOnBuffer () {
345338
const gl = this._renderer.gl;
346339

347-
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
348-
349340
twgl.bindFramebufferInfo(gl, null);
350341
}
351342

@@ -490,8 +481,6 @@ class PenSkin extends Skin {
490481

491482
twgl.bindFramebufferInfo(gl, this._framebuffer);
492483

493-
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
494-
495484
this._drawRectangleRegionEnter(this._stampShader, this._bounds);
496485
}
497486

@@ -501,8 +490,6 @@ class PenSkin extends Skin {
501490
_exitDrawToBuffer () {
502491
const gl = this._renderer.gl;
503492

504-
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
505-
506493
twgl.bindFramebufferInfo(gl, null);
507494
}
508495

src/RenderWebGL.js

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ class RenderWebGL extends EventEmitter {
196196
gl.disable(gl.DEPTH_TEST);
197197
/** @todo disable when no partial transparency? */
198198
gl.enable(gl.BLEND);
199-
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
199+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
200200
}
201201

202202
/**
@@ -834,7 +834,8 @@ class RenderWebGL extends EventEmitter {
834834
projection,
835835
{
836836
extraUniforms,
837-
ignoreVisibility: true // Touching color ignores sprite visibility
837+
ignoreVisibility: true, // Touching color ignores sprite visibility,
838+
effectMask: ~ShaderManager.EFFECT_INFO.ghost.mask
838839
});
839840

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

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

@@ -1744,14 +1745,6 @@ class RenderWebGL extends EventEmitter {
17441745
}
17451746

17461747
twgl.setUniforms(currentShader, uniforms);
1747-
1748-
/* adjust blend function for this skin */
1749-
if (drawable.skin.hasPremultipliedAlpha){
1750-
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1751-
} else {
1752-
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1753-
}
1754-
17551748
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
17561749
}
17571750

@@ -1903,12 +1896,10 @@ class RenderWebGL extends EventEmitter {
19031896
*/
19041897
Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor);
19051898
// if we are fully transparent, go to the next one "down"
1906-
const sampleAlpha = __blendColor[3] / 255;
1907-
// premultiply alpha
1908-
dst[0] += __blendColor[0] * blendAlpha * sampleAlpha;
1909-
dst[1] += __blendColor[1] * blendAlpha * sampleAlpha;
1910-
dst[2] += __blendColor[2] * blendAlpha * sampleAlpha;
1911-
blendAlpha *= (1 - sampleAlpha);
1899+
dst[0] += __blendColor[0] * blendAlpha;
1900+
dst[1] += __blendColor[1] * blendAlpha;
1901+
dst[2] += __blendColor[2] * blendAlpha;
1902+
blendAlpha *= (1 - (__blendColor[3] / 255));
19121903
}
19131904
// Backdrop could be transparent, so we need to go to the "clear color" of the
19141905
// draw scene (white) as a fallback if everything was alpha

src/SVGSkin.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ class SVGSkin extends Skin {
9090
const textureOptions = {
9191
auto: false,
9292
wrap: this._renderer.gl.CLAMP_TO_EDGE,
93-
src: textureData
93+
src: textureData,
94+
premultiplyAlpha: true
9495
};
9596

9697
const mip = twgl.createTexture(this._renderer.gl, textureOptions);

src/ShaderManager.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,7 @@ ShaderManager.DRAW_MODE = {
171171
/**
172172
* Sample a "texture" to draw a line with caps.
173173
*/
174-
lineSample: 'lineSample',
175-
176-
/**
177-
* Draw normally except for pre-multiplied alpha
178-
*/
179-
stamp: 'stamp'
174+
lineSample: 'lineSample'
180175
};
181176

182177
module.exports = ShaderManager;

src/Silhouette.js

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,38 @@ const __cornerWork = [
3939

4040
/**
4141
* Get the color from a given silhouette at an x/y local texture position.
42+
* Multiply color values by alpha for proper blending.
4243
* @param {Silhouette} The silhouette to sample.
4344
* @param {number} x X position of texture (0-1).
4445
* @param {number} y Y position of texture (0-1).
4546
* @param {Uint8ClampedArray} dst A color 4b space.
4647
* @return {Uint8ClampedArray} The dst vector.
4748
*/
4849
const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
50+
// 0 if outside bouds, otherwise read from data.
51+
if (x >= width || y >= height || x < 0 || y < 0) {
52+
return dst.fill(0);
53+
}
54+
const offset = ((y * width) + x) * 4;
55+
// premultiply alpha
56+
const alpha = data[offset + 3] / 255;
57+
dst[0] = data[offset] * alpha;
58+
dst[1] = data[offset + 1] * alpha;
59+
dst[2] = data[offset + 2] * alpha;
60+
dst[3] = data[offset + 3];
61+
return dst;
62+
};
63+
64+
/**
65+
* Get the color from a given silhouette at an x/y local texture position.
66+
* Do not multiply color values by alpha, as it has already been done.
67+
* @param {Silhouette} The silhouette to sample.
68+
* @param {number} x X position of texture (0-1).
69+
* @param {number} y Y position of texture (0-1).
70+
* @param {Uint8ClampedArray} dst A color 4b space.
71+
* @return {Uint8ClampedArray} The dst vector.
72+
*/
73+
const getPremultipliedColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
4974
// 0 if outside bouds, otherwise read from data.
5075
if (x >= width || y >= height || x < 0 || y < 0) {
5176
return dst.fill(0);
@@ -78,9 +103,45 @@ class Silhouette {
78103
*/
79104
this._colorData = null;
80105

106+
/**
107+
* Whether or not the color data is premultiplied with its alpha channel.
108+
* If it isn't, it will be multiplied here.
109+
* @type {boolean}
110+
*/
111+
this._isPremultiplied = false;
112+
113+
// By default, silhouettes are assumed not to contain premultiplied image data,
114+
// so when we get a color, we want to multiply it by its alpha channel.
115+
// Point `_getColor` to the version of the function that multiplies.
116+
this._getColor = getColor4b;
117+
81118
this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
82119
}
83120

121+
/**
122+
* @returns {boolean} true if the silhouette color data is premultiplied, false if not.
123+
*/
124+
get premultiplied () {
125+
return this._isPremultiplied;
126+
}
127+
128+
/**
129+
* Set the alpha premultiplication state of this silhouette, to ensure proper color values are returned.
130+
* If set to true, the silhouette will assume it is being set with premultiplied color data,
131+
* and will not multiply color values by alpha.
132+
* If set to false, it will multiply color values by alpha.
133+
* @param {boolean} isPremultiplied Whether this silhouette will be populated with premultiplied color data.
134+
*/
135+
set premultiplied (isPremultiplied) {
136+
this._isPremultiplied = isPremultiplied;
137+
138+
if (isPremultiplied) {
139+
this._getColor = getPremultipliedColor4b;
140+
} else {
141+
this._getColor = getColor4b;
142+
}
143+
}
144+
84145
/**
85146
* Update this silhouette with the bitmapData for a skin.
86147
* @param {*} bitmapData An image, canvas or other element that the skin
@@ -124,7 +185,7 @@ class Silhouette {
124185
* @returns {Uint8ClampedArray} dst
125186
*/
126187
colorAtNearest (vec, dst) {
127-
return getColor4b(
188+
return this._getColor(
128189
this,
129190
Math.floor(vec[0] * (this._width - 1)),
130191
Math.floor(vec[1] * (this._height - 1)),
@@ -151,10 +212,10 @@ class Silhouette {
151212
const xFloor = Math.floor(x);
152213
const yFloor = Math.floor(y);
153214

154-
const x0y0 = getColor4b(this, xFloor, yFloor, __cornerWork[0]);
155-
const x1y0 = getColor4b(this, xFloor + 1, yFloor, __cornerWork[1]);
156-
const x0y1 = getColor4b(this, xFloor, yFloor + 1, __cornerWork[2]);
157-
const x1y1 = getColor4b(this, xFloor + 1, yFloor + 1, __cornerWork[3]);
215+
const x0y0 = this._getColor(this, xFloor, yFloor, __cornerWork[0]);
216+
const x1y0 = this._getColor(this, xFloor + 1, yFloor, __cornerWork[1]);
217+
const x0y1 = this._getColor(this, xFloor, yFloor + 1, __cornerWork[2]);
218+
const x1y1 = this._getColor(this, xFloor + 1, yFloor + 1, __cornerWork[3]);
158219

159220
dst[0] = (x0y0[0] * x0D * y0D) + (x0y1[0] * x0D * y1D) + (x1y0[0] * x1D * y0D) + (x1y1[0] * x1D * y1D);
160221
dst[1] = (x0y0[1] * x0D * y0D) + (x0y1[1] * x0D * y1D) + (x1y0[1] * x1D * y0D) + (x1y1[1] * x1D * y1D);

src/Skin.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class Skin extends EventEmitter {
3333
/** @type {Vec3} */
3434
this._rotationCenter = twgl.v3.create(0, 0);
3535

36+
/** @type {WebGLTexture} */
37+
this._texture = null;
38+
3639
/**
3740
* The uniforms to be used by the vertex and pixel shaders.
3841
* Some of these are used by other parts of the renderer as well.
@@ -171,6 +174,23 @@ class Skin extends EventEmitter {
171174
*/
172175
updateSilhouette () {}
173176

177+
/**
178+
* Set this skin's texture to the given image.
179+
* @param {ImageData|HTMLCanvasElement} textureData - The canvas or image data to set the texture to.
180+
*/
181+
_setTexture (textureData) {
182+
const gl = this._renderer.gl;
183+
184+
gl.bindTexture(gl.TEXTURE_2D, this._texture);
185+
// Premultiplied alpha is necessary for proper blending.
186+
// See http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
187+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
188+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
189+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
190+
191+
this._silhouette.update(textureData);
192+
}
193+
174194
/**
175195
* Set the contents of this skin to an empty skin.
176196
* @fires Skin.event:WasAltered

0 commit comments

Comments
 (0)