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'))