blend-mode を使って PexJS の着色処理を若干速くした
以前こんな記事を書いたんで、それを PexJS に応用してみたというお話です。
iOS7 の MobileSafari でもサポートされた Canvas の blend-mode を試してみた - あらびき日記
PexJS はざっくり説明すると Flash Lite 1.1 向けの SWF を再生可能な JavaScript ライブラリです。
っで、変更がこちら (ブランチは https://github.com/abicky/PexJS/tree/feature/use-blend-mode)
関数を使ってもっとキレイに書きたい気持ちはありますが、PexJS があんまり関数に切り出さない思想っぽいので合わせています。
結果
iPhone 6 iOS 8.0.2 MobileSafari と SO-02F Chrome 38 で、512x5121 の画像に対して 16 通りの着色パターンをそれぞれ 100 回レンダリングした時の elapsed time を出してみました。
Flash Player で表示すると次のような着色処理(左上が元画像、星の部分が透過度 0.5 の白色、SWF の背景が紫)で、

PexJS だと次のように表示されます。2 つ明らかに違う表示のものがありますが、PexJS は alpha channel の加算処理を正式にはサポートしていないので仕様です。

cxformList は SWF の仕様書の Color transform with alpha record に出てくる内容に対応していて、[RedMultTerm, GreenMultTerm, BlueMultTerm, AlphaMultTerm, RedAddTerm, GreenAddTerm, BlueMultTerm, AlphaAddTerm] に相当します。
iPhone 6 iOS 8.0.2 MobileSafari
| cxformList | original (ms) | blend-mode (ms) | 
|---|---|---|
| 256,0,0,256,0,0,0,0 | 77 | 24 | 
| 0,256,0,256,0,0,0,0 | 71 | 46 | 
| 0,0,256,256,0,0,0,0 | 43 | 24 | 
| 256,256,256,256,255,0,0,0 | 39 | 27 | 
| 256,256,256,256,0,255,0,0 | 114 | 27 | 
| 256,256,256,256,0,0,255,0 | 58 | 25 | 
| 256,256,256,256,-255,0,0,0 | 44 | 25 | 
| 256,256,256,256,0,-255,0,0 | 30 | 44 | 
| 256,256,256,256,0,0,-255,0 | 54 | 453 | 
| 256,256,256,128,0,0,0,0 | 3 | 12 | 
| 256,256,256,256,0,0,0,255 | 8 | 15 | 
| 256,256,256,256,0,0,0,-100 | 127 | 13 | 
| 256,256,256,256,128,0,-128,0 | 44 | 32 | 
| 48,96,144,176,0,0,0,0 | 49 | 24 | 
| 48,96,144,176,128,0,-128,0 | 94 | 18 | 
SO-02F Chrome 38
| cxformList | original (ms) | blend-mode (ms) | 
|---|---|---|
| 256,0,0,256,0,0,0,0 | 100 | 69 | 
| 0,256,0,256,0,0,0,0 | 91 | 59 | 
| 0,0,256,256,0,0,0,0 | 79 | 56 | 
| 256,256,256,256,255,0,0,0 | 76 | 73 | 
| 256,256,256,256,0,255,0,0 | 88 | 48 | 
| 256,256,256,256,0,0,255,0 | 74 | 67 | 
| 256,256,256,256,-255,0,0,0 | 82 | 64 | 
| 256,256,256,256,0,-255,0,0 | 111 | 58 | 
| 256,256,256,256,0,0,-255,0 | 104 | 49 | 
| 256,256,256,128,0,0,0,0 | 23 | 28 | 
| 256,256,256,256,0,0,0,255 | 15 | 24 | 
| 256,256,256,256,0,0,0,-100 | 23 | 27 | 
| 256,256,256,256,128,0,-128,0 | 74 | 80 | 
| 48,96,144,176,0,0,0,0 | 112 | 53 | 
| 48,96,144,176,128,0,-128,0 | 103 | 83 | 
心なしか全体的に速くなっている気がします。
なお、https://github.com/abicky/PexJS/tree/profile/use-blend-mode で transform_color_movie.swf を再生することで計測しています。
ざっくりした解説
SWF の仕様にあるように着色処理は次のように定義されています。
R’ = max(0, min(((R * RedMultTerm) / 256) + RedAddTerm, 255))
G’ = max(0, min(((G * GreenMultTerm) / 256) + GreenAddTerm, 255))
B’ = max(0, min(((B * BlueMultTerm) / 256) + BlueAddTerm, 255))
A’ = max(0, min(((A * AlphaMultTerm) / 256) + AlphaAddTerm, 255))
この処理と同じことをするように http://dev.w3.org/fxtf/compositing-1/ からどの composite operation を使えばいいか考えることになります。
オリジナルの処理では次のように RGB それぞれの画像を抽出して、それぞれに対して着色処理を行い、それらを重ね合わせることで着色処理を実現しているようです。
var rgbctx = transformImageColor.rgbCtx || (transformImageColor.rgbCtx = []);
var rgbCanvas = transformImageColor.rgbCanvas || (transformImageColor.rgbCanvas = []);
for(var regionIndex = imageRegions.length - 1; regionIndex >= 0; regionIndex--) {
	var region = imageRegions[regionIndex];
	var rx = region[0];
	var ry = region[1];
	var rw = region[2];
	var rh = region[3];
	// create rgb
	for(var i = 0; i < 3; i++) {
		var cCanvas = rgbCanvas[i] || (rgbCanvas[i] = CacheController.getFreeCanvas());
		cCanvas.width = rw;
		cCanvas.height = rh;
		var cctx = rgbctx[i] || (rgbctx[i] = cCanvas.getContext("2d"));
		cctx.drawImage(img, rx, ry, rw, rh, 0, 0, rw, rh);
		cctx.globalCompositeOperation = "darker";
		cctx.fillStyle = colors[i];
		cctx.fillRect(0, 0, rw, rh);
	}
	// rgb transform
	octx.globalCompositeOperation = "lighter";
	for(var i = 0; i < 3; i++) {
		applyColor(rgbctx[i], cxformList, rw, rh, i, colors[i]);
		octx.drawImage(rgbCanvas[i], rx, ry);
	}
}
blend-mode を使った着色処理に関しては以前「Flash の着色処理を実装してみる」で言及しているのでそちらを参照してください。
例えば、色の掛け合わせだけであれば output canvas に draw image で元画像をコピーして、’multiply’ で 1 回 fill rect すればいいだけなので、draw call が格段に減るはずです2。
厄介なのが画像に alpha 値が含まれる場合で、premultiplied alpha 云々を考えるのが面倒なので、次の処理で最初に画像から alpha channel を取り除いています。
octx.drawImage(img, 0, 0);
// Remove transparency to process RGB channel and alpha channel separately
// (blend-mode seems to remove transparency)
octx.globalCompositeOperation = "multiply";
octx.fillStyle = "rgba(255,255,255,1)";
octx.fillRect(0, 0, w, h);
alpha channel に関しては別で処理して、最後に次の処理で destination-in を使うことで alpha を掛け合わせています。これはオリジナルの処理と同様です。
// alpha mask
octx.globalCompositeOperation = "destination-in";
octx.globalAlpha = 1;
octx.drawImage(alphaCanvas, 0, 0);
destination-in の処理は次のように定義されており、αb は最初に alpha channel を除いているので必ず 1 になるので、alpha channel に対する処理のみを施した αs の値がそのまま適用されることになります。
Fa = 0; Fb = αs
co = αb x Cb x αs
αo = αb x αs
おまけ(SWF の作り方)
動作確認用、プロファイル用の SWF は Ming で作りました。
Ming の導入方法についてはMing と Python で SWF を作ってみたを参照してください。
transform_color.py
使っている画像はこれ http://f.hatena.ne.jp/a_bicky/20141026182637
#!/usr/bin/env python
from ming import *
IMAGE_WIDTH = 128
IMAGE_HEIGHT = 128
Ming_useSWFVersion(4)
movie = SWFMovie()
movie.setRate(1)
movie.setDimension(512, 512)
movie.setBackground(255, 0, 255)
img = SWFBitmap('./favicon128.png')
def create_image_rect(x, y, width, height):
    rect = SWFShape()
    # Don't define 'img' here to avoid segmentation fault
    rect.setRightFill(rect.addBitmapFill(img, SWFFILL_BITMAP))
    rect.movePenTo(x, y)
    rect.drawLine(width, 0)
    rect.drawLine(0, height)
    rect.drawLine(-width, 0)
    rect.drawLine(0, -height)
    return rect
transforms = [
    { 'mult': [  1,  1,  1] },  # do nothing
    { 'mult': [  1,  0,  0] },
    { 'mult': [  0,  1,  0] },
    { 'mult': [  0,  0,  1] },
    { 'add':  [ 255,    0,    0] },
    { 'add':  [   0,  255,    0] },
    { 'add':  [   0,    0,  255] },
    { 'add':  [-255,    0,    0] },
    { 'add':  [   0, -255,    0] },
    { 'add':  [   0,    0, -255] },
    { 'mult': [  1,   1,   1, 0.5] },       # with tranceparency
    { 'add':  [   0,    0,     0,  255] },  # with tranceparency
    { 'add':  [   0,    0,     0, -100] },  # with tranceparency
    { 'add':  [ 128,    0, -128,  255] },
    { 'mult': [ 0.2, 0.4, 0.6, 0.7] },
    { 'mult': [ 0.2, 0.4, 0.6, 0.7], 'add': [  128,    0, -128] },
]
for i, transform in enumerate(transforms):
    x = IMAGE_WIDTH * (i % 4)
    y = IMAGE_HEIGHT * (i / 4)
    item = movie.add(create_image_rect(x, y, IMAGE_WIDTH, IMAGE_HEIGHT))
    if transform.has_key('mult'):
        item.multColor(*transform['mult'])
    if transform.has_key('add'):
        item.addColor(*transform['add'])
movie.save(__file__.replace('.py', '.swf'))
transform_color_movie.py
使っている画像はこれ http://f.hatena.ne.jp/a_bicky/20141026182636
#!/usr/bin/env python
from ming import *
IMAGE_WIDTH = 512
IMAGE_HEIGHT = 512
Ming_useSWFVersion(4)
movie = SWFMovie()
movie.setRate(60)
movie.setDimension(512, 512)
movie.setBackground(255, 0, 255)
img = SWFBitmap('./favicon512.png')
def create_image_rect(x, y, width, height):
    rect = SWFShape()
    # Don't define 'img' here to avoid segmentation fault
    rect.setRightFill(rect.addBitmapFill(img, SWFFILL_BITMAP))
    rect.movePenTo(x, y)
    rect.drawLine(width, 0)
    rect.drawLine(0, height)
    rect.drawLine(-width, 0)
    rect.drawLine(0, -height)
    return rect
transforms = [
    { 'mult': [  1,  1,  1] },  # do nothing
    { 'mult': [  1,  0,  0] },
    { 'mult': [  0,  1,  0] },
    { 'mult': [  0,  0,  1] },
    { 'add':  [ 255,    0,    0] },
    { 'add':  [   0,  255,    0] },
    { 'add':  [   0,    0,  255] },
    { 'add':  [-255,    0,    0] },
    { 'add':  [   0, -255,    0] },
    { 'add':  [   0,    0, -255] },
    { 'mult': [  1,   1,   1, 0.5] },      # with tranceparency
    { 'add':  [   0,    0,    0,  255] },  # with tranceparency
    { 'add':  [   0,    0,    0, -100] },  # with tranceparency
    { 'add':  [ 128,    0, -128,  255] },
    { 'mult': [ 0.2, 0.4, 0.6, 0.7] },
    { 'mult': [ 0.2, 0.4, 0.6, 0.7], 'add': [  128,    0, -128] },
]
for transform in transforms:
    item = movie.add(create_image_rect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT))
    if transform.has_key('mult'):
        item.multColor(*transform['mult'])
    if transform.has_key('add'):
        item.addColor(*transform['add'])
    for i in xrange(100):
        movie.nextFrame()
    item.remove()
movie.save(__file__.replace('.py', '.swf'))