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 の背景が紫)で、
20141026185539

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

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'))
  1. PexJS のコードを見る限り、width * height > 65536 じゃないと GPU accelerated canvas にならなかったりするみたいなので大きめに設定しています 

  2. 色の掛け合わせだけの場合に格段に速くなるかと思いきやそうでもなかったのでショックです…