自習室

こもります

16bit 浮動小数点のテクスチャできれいにブレンドを行う

はじめに

前回の記事では、WebGL(Three.js)でFBO ping-pongすることで、テクスチャのブレンディングを行い、それによって映像中の動く物体を消す処理をやってみました。

izmiz.hateblo.jp

しかし、問題がありました。

今回の手法だと、背景に対してコントラストが高い動体は、焼きついたように長時間残ることもあります。

f:id:AMANE:20150420184533p:plain

前記事時点では、上の写真のよう焼き付きが起きてしまっていました。手法はあっているはずなのになんでこうなるのか、解明できず気持ち悪かったので、続けて検討したところ、間違いに気がついたので、修正して投稿します。

手法の検証

以下の式で毎回のカメラ画像を足し合わせていくと、動く物は消える(またはブラーがかかる)というのが、前回の記事の内容でした。

 {
Y(n) = a * X(n) + (1.0 - a) * Y(n-1)
}

正しく出来ている場合のシミュレーション

まず、Excelを使って、この計算で狙ったとおりの効果が出るかを確認します。

簡単のためにグレースケールと言うことにして、はじめ入力Xが50の明るさだったところに、10フレームだけ200の明るさが入って、その後また50に戻る、というものです。フィルタの係数はここでは試しにa = 0.1としてみます。(新しいフレームが0.1の重さで足される)

入力X 出力Y
50 50
50 50
50 50
50 50
50 50
200 65
200 78.5
200 90.65
200 101.585
200 111.4265
200 120.28385
200 128.255465
200 135.4299185
200 141.8869267
200 147.698234
50 137.9284106
50 129.1355695
50 121.2220126
50 114.0998113
50 107.6898302
50 101.9208472
50 96.72876245
50 92.05588621
50 87.85029758
50 84.06526783
50 80.65874104
50 77.59286694
50 74.83358025
50 72.35022222
50 70.1152
50 68.10368
50 66.293312
50 64.6639808
50 63.19758272
50 61.87782445
50 60.690042
50 59.6210378
50 58.65893402
50 57.79304062
50 57.01373656
50 56.3123629
50 55.68112661
50 55.11301395
50 54.60171256
50 54.1415413
50 53.72738717
50 53.35464845
50 53.01918361
50 52.71726525
50 52.44553872
50 52.20098485
50 51.98088637
50 51.78279773
50 51.60451796
50 51.44406616
50 51.29965954
50 51.16969359
50 51.05272423
50 50.94745181
50 50.85270663
50 50.76743596
50 50.69069237
50 50.62162313
50 50.55946082
50 50.50351474

200という明るい色が入った後はしばらく画面が明るくなりますが、その後次第にもとの50に戻っていく様子がわかると思います。本当にこうなっていれば、焼き付きのような状態にはならないはずです。

GLSLでの小数丸め

ここからあーだこーだと考えて、はっと、小数の扱いが間違っていることに気がつきました。GLSLのテクスチャは、0-255 の8bit 整数で色が表されますので、 GLSLのmix関数を使って色をブレンドした結果、計算結果が小数だったとしても、最終的に画面には当然整数の値で色が書き込まれます。つまり、どこかで小数の計算結果を整数に直しています。

今回使った THREE.BlendShader では、mix 関数を使って、二枚のテクスチャを混色します。

gl_FragColor = opacity * mix( texel1, texel2, mixRatio );

ここで float mixRatio にfloatの値を入れたとき、計算結果は小数になっているはずですが、 gl_FragColorはもちろん整数です。おそらく、代入の際に四捨五入なり切り捨てなりが行われているはずです。

試しに、毎回計算結果の色が四捨五入されていたとしたら、同じシミュレーションをすると以下の様になります

Excelの計算式はこんな感じ

{
Y(n) = ROUND(a * X(n) + (1.0 - a) * Y(n-1) , 0)
}

入力X 出力Y
50 50
50 50
50 50
50 50
50 50
200 65
200 79
200 91
200 102
200 112
200 121
200 129
200 136
200 142
200 148
50 138
50 129
50 121
50 114
50 108
50 102
50 97
50 92
50 88
50 84
50 81
50 78
50 75
50 73
50 71
50 69
50 67
50 65
50 64
50 63
50 62
50 61
50 60
50 59
50 58
50 57
50 56
50 55
50 55
50 55
50 55
50 55
50 55

途中までは50に戻ろうとしますが、55で下げ止まっています。前回の値が55で今回50が入力されたとき、

 {0.1 * 50 + (1.0 - 0.1) * 55 = 54.5}
 {round(54.5) = 55}

と言うかんじで、四捨五入した結果、前と同じ55になってしまい、これは何回やっても同じだからです。

これが、色の焼き付きの正体だと思われます。

このシミュレーションをしたExcelのファイルをこちらにあげておきます

修正

さっきは {a = 0.1} でやりましたが、blendの係数を下げていくと、より焼き付きが顕著になります。しかし、もともと 長時間の平均画像を求めることで、動体を消す が目的なので、blendの係数は極力下げたいです。

小数の丸めによってblendが途中で止まってしまわないようにするには、Shader内のテクスチャの色計算で小数が使える様にすることが必要です。

浮動小数点テクスチャ

そういえば、いつもお世話になっている wgld.org 様に、このような記事が上がっていました

wgld.org

この記事によると、Three.js も準拠している WebGL 1.0 では、浮動小数点テクスチャはエクステンション扱いですが利用可能のようです。Three.jsで使っているサンプルがないか探してみたところ、例のFBOサンプルで使っていました。

このサンプルに倣って、WebGLRenderTarget を浮動小数点テクスチャ化します

やってみる

まず、Three の rendererからglのコンテキストを取得して、浮動小数点テクスチャのエクステンションを有効化します

gl = renderer.getContext();

if ( !gl.getExtension( "OES_texture_float" )) {
  alert( "No OES_texture_float support for float textures!" );
  return;
}

つぎに、毎フレームテクスチャとして保存される WebGLRenderTarget を THREE.FloatType 指定します

  rt1 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat, type: THREE.FloatType }  );
  rt2 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat, type: THREE.FloatType }  )

以上でOKです。THREE.BlendShaderは既存のままで大丈夫です。

(もっとやることあるかと思ったので拍子抜けしました)

これで、FBO ping-pong で毎回更新されるテクスチャは16bitで計算されるので、小さい係数でblendを行っても四捨五入の際に丸められたりせず、ちゃんと反映されます。

完成品

前回の失敗作

今回修正した物

動体がちゃんと消えて、変な焼き付きがなくなったことがわかると思います。

最後に

前回中途半端な記事を書いてしまい気持ち悪かったのですが、解決してスッキリしました。

また、浮動小数点テクスチャが有効に使えるシーンがわかったのも収穫でした。