Skip to content

Commit d0d98de

Browse files
authored
Merge pull request #515 from adroitwhiz/premultiply-the-sequel
Use premultiplied alpha everywhere, take 2
2 parents fed02ca + 0cb97a2 commit d0d98de

File tree

11 files changed

+160
-108
lines changed

11 files changed

+160
-108
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: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const DefaultPenAttributes = {
2525
diameter: 1
2626
};
2727

28+
/**
29+
* Reused memory location for storing a premultiplied pen color.
30+
* @type {FloatArray}
31+
*/
32+
const __premultipliedColor = [0, 0, 0, 0];
33+
2834

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

91-
/** @type {WebGLTexture} */
92-
this._texture = null;
93-
9497
/** @type {WebGLTexture} */
9598
this._exportTexture = null;
9699

@@ -123,7 +126,7 @@ class PenSkin extends Skin {
123126

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

128131
/** @type {twgl.ProgramInfo} */
129132
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS);
@@ -154,13 +157,6 @@ class PenSkin extends Skin {
154157
return true;
155158
}
156159

157-
/**
158-
* @returns {boolean} true if alpha is premultiplied, false otherwise
159-
*/
160-
get hasPremultipliedAlpha () {
161-
return true;
162-
}
163-
164160
/**
165161
* @return {Array<number>} the "native" size, in texels, of this skin. [width, height]
166162
*/
@@ -188,7 +184,7 @@ class PenSkin extends Skin {
188184
clear () {
189185
const gl = this._renderer.gl;
190186
twgl.bindFramebufferInfo(gl, this._framebuffer);
191-
187+
192188
/* Reset framebuffer to transparent black */
193189
gl.clearColor(0, 0, 0, 0);
194190
gl.clear(gl.COLOR_BUFFER_BIT);
@@ -317,13 +313,6 @@ class PenSkin extends Skin {
317313

318314
twgl.bindFramebufferInfo(gl, this._framebuffer);
319315

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-
327316
gl.viewport(0, 0, bounds.width, bounds.height);
328317

329318
gl.useProgram(currentShader.program);
@@ -344,8 +333,6 @@ class PenSkin extends Skin {
344333
_exitDrawLineOnBuffer () {
345334
const gl = this._renderer.gl;
346335

347-
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
348-
349336
twgl.bindFramebufferInfo(gl, null);
350337
}
351338

@@ -384,6 +371,13 @@ class PenSkin extends Skin {
384371
const radius = diameter / 2;
385372
const yScalar = (0.50001 - (radius / (length + diameter)));
386373

374+
// Premultiply pen color by pen transparency
375+
const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
376+
__premultipliedColor[0] = penColor[0] * penColor[3];
377+
__premultipliedColor[1] = penColor[1] * penColor[3];
378+
__premultipliedColor[2] = penColor[2] * penColor[3];
379+
__premultipliedColor[3] = penColor[3];
380+
387381
const uniforms = {
388382
u_positionScalar: yScalar,
389383
u_capScale: diameter,
@@ -397,7 +391,7 @@ class PenSkin extends Skin {
397391
twgl.m4.scaling(scalingVector, __modelScalingMatrix),
398392
__modelMatrix
399393
),
400-
u_lineColor: penAttributes.color4f || DefaultPenAttributes.color4f
394+
u_lineColor: __premultipliedColor
401395
};
402396

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

491485
twgl.bindFramebufferInfo(gl, this._framebuffer);
492486

493-
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
494-
495487
this._drawRectangleRegionEnter(this._stampShader, this._bounds);
496488
}
497489

@@ -501,8 +493,6 @@ class PenSkin extends Skin {
501493
_exitDrawToBuffer () {
502494
const gl = this._renderer.gl;
503495

504-
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
505-
506496
twgl.bindFramebufferInfo(gl, null);
507497
}
508498

@@ -661,7 +651,7 @@ class PenSkin extends Skin {
661651
skinImageData.data.set(skinPixels);
662652
skinContext.putImageData(skinImageData, 0, 0);
663653

664-
this._silhouette.update(this._canvas);
654+
this._silhouette.update(this._canvas, true /* isPremultiplied */);
665655

666656
this._silhouetteDirty = false;
667657
}

src/RenderWebGL.js

Lines changed: 9 additions & 18 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

@@ -1902,13 +1895,11 @@ class RenderWebGL extends EventEmitter {
19021895
}
19031896
*/
19041897
Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor);
1905-
// 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);
1898+
// Equivalent to gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
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;

0 commit comments

Comments
 (0)