動画テクスチャでクロマキー合成
今回のサンプルの実行結果
動画を動的に加工して合成する
前回はウェブカメラの情報をリアルタイムに取得しながら、それを WebGL のテクスチャとして利用する方法について解説しました。
WebRTC と呼ばれるリアルタイムコミュニケーションのための技術を利用することで、PC やモバイル端末に搭載されているウェブカメラからリアルタイムに映像を取得することが可能となります。これを、動画ファイルをテクスチャに適用したときの要領で WebGL のレンダリング結果に合成たわけですね。映像からテクスチャにイメージデータを反映させるという基本的な枠組みはほとんど同じだったので、前々回の動画をテクスチャに適用する処理が理解できていれば、それほど難しくなかったのではないかと思います。
さて、今回はさらにもうひとつ、動画に関係した処理を行ってみます。それが動画テクスチャのクロマキー合成です。クロマキー合成というのは、テレビなどでよく見かける、緑色や青色の背景をすっぱり切り捨てて CG などを合成するアレです。出演者が宇宙空間の中に立っているように見えたりする、比較的昔からある合成技ですね。
今回はこのクロマキー合成のような感じで、動画ファイルの一部を抜き、背景が見えるようにしたうえで WebGL のレンダリング結果に合成してみます。
動画ファイルの情報を WebGL のテクスチャとして扱うことで、再生される動画にリアルタイムにエフェクトを加えるといった、CPU の演算能力だけでは難しいこともできるようになります。今回は特定の色になっている箇所を透過させるだけですが、動画をシェーダでリアルタイムに処理できることのメリットを、今回のサンプルを通じて少しでも感じてもらえたらと思います。
色の合致率
さて、さっそくクロマキー合成の処理を記述していきましょう。
クロマキー合成を行うためには、テクスチャから読みだした色が、透過されるべきなのかそうでないのか、この点をしっかり見極める作業が必要になります。いうなれば「色の合致率」を求める処理といったところでしょうか。たとえば、透過する色を「緑(0x00ff00)」と定めた場合、これから描こうとしている色がどの程度緑っぽいか、という指標が必要になりますよね。これにはいろいろなやり方が考えられると思いますが、今回は GLSL のとあるビルトイン関数を用いて簡易的に実装してみたいと思います。
そのとあるビルトイン関数とは length
関数です。
どうして、この length
関数を使うと色の合致率が求められるのか……もしパッと頭のなかでイメージできたとしたら、数学的な知識をしっかり持っているか、もしくはセンスが良い方なのだと思います。
もしスッとイメージできなくても、悲観する必要はありません。どうして length
関数で色の合致率が求められるのか、次項から詳しく見ていきましょう。
length 関数を利用した合致率の計算
まずは話を見えやすくするために、一度シェーダのソースコードを掲載します。ここで掲載したシェーダは、最終的にクロマキー合成を行うためのシェーダになります。
クロマキー合成を行うシェーダのフラグメントシェーダ
precision mediump float;
uniform sampler2D texture;
uniform float difference;
varying vec2 vTexCoord;
const vec3 chromaKeyColor = vec3(0.0, 1.0, 0.0);
void main(void){
vec4 smpColor = texture2D(texture, vTexCoord);
float diff = length(chromaKeyColor - smpColor.rgb);
if(diff < difference){
discard;
}else{
gl_FragColor = smpColor;
}
}
さてそれでは、上から順番に見ていきましょう。まずこのシェーダが受け取る uniform 変数はふたつ。ひとつ目は sampler2D
型のテクスチャのデータ。そしてふたつ目が difference
という名前の float
型のデータです。
この difference
という変数には、HTML 上に埋め込まれた input エレメントから値が送られてくるようになっています。実際にサンプルを動作させてみるとわかりやすいと思いますが、これがいわゆる「しきい値」に相当します。この difference
の値が小さければ小さいほど、色が合致している率が高くないと透過されなくなります。逆に大きな値にしてやれば、多少の色の違いは無視して透過が行われるようになっていく感じですね。
続いて、定数が宣言されているのがわかるでしょうか。定数 chromaKeyColor
がそれです。
この定数は、どの色を透過する対象とするかの指標となります。上記のコードでは緑 100% になっているのがわかると思います。この定数で指定した色に近い色が透過されることになるわけですね。
さて次は main
関数です。ここでは先程から話題に上がっている length
関数が使われています。
main 関数部分を抜粋
void main(void){
vec4 smpColor = texture2D(texture, vTexCoord);
float diff = length(chromaKeyColor - smpColor.rgb);
if(diff < difference){
discard;
}else{
gl_FragColor = smpColor;
}
}
まずテクスチャ(動画)の情報を普通に texture2D
で取得します。そして、取得した色の情報をもとに下の行で length
関数を使ったなにかの処理を行っているのがわかりますね。
そもそも length
関数はベクトルの長さを返すビルトイン関数です。当然、与えられたベクトルが長いほど大きな数値を返してきます。ここで length
関数に与えられたベクトルに注目してみると、定数として宣言したクロマキー合成の色と、テクスチャから取得した色とで減算処理を行っていますね。ここはぜひ落ち着いて考えてみてください。ここでの計算結果は、両者の RGB がそれぞれ近ければ近いほど、長さの短いベクトルになるはずです。
仮にテクスチャから取得した色が緑 100% だとすると、定数で指定した緑からその色を減算したら、結果は間違いなく 0 です。同じ色で減算処理をするわけですから、当然そうなりますね。ではテクスチャから取得した色が赤 100% だったらどうでしょうか。
緑から赤を減算
緑 = (0.0, 1.0, 0.0)
赤 = (1.0, 0.0, 0.0)
緑 - 赤 = (0.0, 1.0, 0.0) - (1.0, 0.0, 0.0) = (-1.0, 1.0, 0.0)
こうなりますね。これを length
関数に与えると (-1.0, 1.0, 0.0)
の長さを計測することになるので、戻り値は約 1.41421...、つまり、ルート 2 になります。
このように、ビルトイン関数の length
を用いると、色をベクトルと考えて減算した結果から、両者がどの程度似通っているのかを数値化できます。これを指標として、透過するべきかそうでないかを決めているのですね。
シェーダのなかでは最終的に、この指標の大きさをもとに判定を行ったうえで、透過するべきと判断した場合には gl_FragColor
に discard
を設定しています。この discard
は「何も色を出力しない」ということを意味しているため、対象のピクセルはなんの色も塗られることなくごっそりと抜き取られてしまうわけですね。
まとめ
今回のサンプルでは、まず最初に下地となるシーンを一度レンダリングしています。球体と箱が描画されるだけの、簡易なものです。次に、動画テクスチャを用いたクロマキー合成用のシェーダに切り替え、画面全体を覆う一枚のポリゴンを描画します。この一枚の板ポリゴンを描画する際に使うシェーダが、先ほど掲載したものですね。
動画をテクスチャに適用し、そこから取り出した色をリアルタイムに参照しながら discard
とするかそうでないかを画面全体で判定するわけですね。今回のサンプルでは HTML 上から自由にしきい値を変更できるようにしているので、実際にサンプルを動かしながらしきい値を変更してみると、その変化がわかるでしょう。
ただ、今回のやり方では動画テクスチャの色を「採用するか、それとも破棄するか」という二値的な判断しかしていません。ですから、微妙な色合いの部分ではジャギーが目立つ仕上がりになってしまいます。この辺りは、アルファブレンディングをうまく活用する方法で調整すれば、より自然な感じに仕上げることができるでしょう。
今回のサンプルで最も伝えたかったのは、動画ファイルを動的に解析しながらエフェクトを加えることも、WebGL であれば比較的簡単ですよということです。
たとえば、ミュージックビデオのようなコンテンツを WebGL でラップして、ユーザーのアクションに応じて見た目を変えたり、まるで別の映像であるかのごとく雰囲気を変えたりといったことができるのですね。これは工夫次第で、相当いろいろな可能性があると言えるのではないでしょうか。たとえば前回扱ったウェブカメラからの映像を用いる技術と組み合わせれば、ユーザーが撮影している動画に動的にエフェクトを追加する……なんてこともできるわけです。考えただけでも、面白そうですよね。
非常に簡単なサンプルでしたが、工夫して使ってみてください。
今回も実際に動作するサンプルを用意してあります。以下のリンクから試してみてください。