動画を用いた video テクスチャ
今回のサンプルの実行結果
動画をテクスチャに適用してみよう
前回、前々回と、シェーダを用いた特殊な陰影付けを扱いました。
ちょっとした工夫で、普通にディフューズを計算した場合とは違った雰囲気が演出でき、いかにもシェーダコーディングらしい応用技を体験できたのではないでしょうか。
さて今回ですが、今まで地味に扱っていなかった動画をテクスチャに適用するテクニックについて解説しようと思います。
今までにも何度か触れていましたが、WebGL ではテクスチャの適用元として、画像ファイルだけでなく CANVAS や動画(video エレメント)が利用できます。動画ファイルをテクスチャとして利用すれば、手軽に動きのある映像を利用できますので、ぜひチャレンジしてみてください。
対応状況の確認
WebGL で動画ファイルをテクスチャに扱うことができると言っても、そもそも、動画ファイルを再生するためには HTML5 で採用されたvideo タグが利用できることが前提となります。つまり正確には、WebGL で動画をテクスチャとして利用したければ、まずなによりも video タグが利用できるかどうかが重要となります。
video タグは HTML5 で採用された比較的新しいタグであるため、ブラウザによって実装がまちまちです。
特に、近年のモバイルでのウェブの躍進もあり、実は踏み込むにはそれなりに環境を考慮した実装を行う覚悟が必要です。
たとえば WebGL に既定で対応した iOS 端末、つまり iPhone の safari や、Chrome を用いれば WebGL の実行自体は可能な Android 端末の一部などでは、video エレメントの再生にはなんらかのユーザーのアクション(画面のタップなど)というトリガーが必要です。つまり javascript 側で強制的に動画の再生をスタートすることができません。バックグラウンドで動画を読み込み、アプリケーション側で都合のいいタイミングで任意に再生を開始する、そんな実装は少なくともモバイル端末では現状難しいということになります。
また iOS の場合、それ以外にもまだ罠があります。実は動画の再生をブラウザ上で行おうとすると、強制的に専用のスクリーンが開いてしまう仕様になっておりこれを javascript 側から制御する方法が現状ありません。例えば今回解説するデモもそうですが、video タグの動画をテクスチャとして利用している場合、テクスチャに正しく動画を適用する事自体はできるのですが、その代わり別画面が強制的に開いてフルスクリーンで動画が再生されてしまいます。本来表示したい WebGL のデモが画面から消えてしまうので、正直使い物になりません。
他にも、PC ブラウザにも罠はあります。
動画ファイルには多数の規格があり、これらの対応状況が各ブラウザベンダにより異なります。このことから、javascript 側ではどのブラウザから参照されても問題なく動画が再生されるように事前にチェックする機構を用意するなど、なにかしらの対応が必要となります。
後述しますが、いくつかの異なるファイル形式の動画を用意しておき、ユーザーの環境に合わせて適宜切り替えてやる処理が必要になります。このあたりは現状どうしようもない問題のひとつとして、対応を覚悟しなければなりません。
なお、各種ブラウザがどういった動画形式に対応しているのかは以下のようなページを参考にするのがいいでしょう。
参考:HTML5 の audio 要素と video 要素でサポートされているメディアフォーマット - HTML | MDN
今回のサンプルの仕様
今回解説するサンプルの仕様は、上記で書いたような問題すべてに対応する仕様にはなっていません。特に、モバイルブラウザについては現状完璧な対応が難しいと言わざるを得ないため、非サポートとします。
Android + Chrome であれば動画をテクスチャにあてがうことも可能ですが、どうやらいろいろテストしてみた感じではよくわからないタイミングで突然 WebGL コンテキストがロストしたりすることもあり、端末による動作の違いまで考慮するととても使えないという判断をしました。
また先程も書いたとおり iOS に関しては動画の再生を制御できないので、こちらも対応が難しい。となると、モバイルについてはサポート外とせざるを得ないという判断です。
当テキスト執筆時点(2014年12月現在)では、モバイルでの動画テクスチャは正直難しいです。これは今後どんどんと改善されていくと思いますので、それに期待するよりほかなさそうです。
なお、今回のサンプルでは動画のファイル形式として MP4 と webm の二種類を用意しました。これらの動画ファイルが正しく再生できるのかどうか、そのあたりのチェックの方法も含めて次項より詳しく見ていきましょう。
サンプルの全体の流れ
今回のサンプルでは、従来のテクスチャの処理がそうであったように動的に video エレメントを生成する方法で処理を行います。
現状 Web に出回っているサンプルの多くは、あらかじめ HTML に video タグを含めておくものが多いようですが今回はそのあたり動的にやってみましょう。ちなみに、生成した video エレメントは最後まで DOM に appendChild
などで追加したりはしません。この方法なら iphone で強制的に画面遷移するのを抑止できるかなと思ってやってみましたが、結論を言うとできませんでした。
サンプルの全体の流れとしてはこうです。
まず最初に video エレメントを生成しますが、image エレメントがそうであるように、src 属性に値が設定されるとソースファイルの読み込みが開始されますので事前にイベント関連の処理を記述しておきます。
画像ファイルとは違い、動画ファイルの場合はその容量が大きいこともあって特殊なイベントがいくつかあります。その中に、現在の読み込み速度が維持できるならばストリーミング再生を開始可能である――というなんとも不思議なイベントがあります。これをトリガーにして動画を再生可能かどうかを判断するようにしましょう。
今回のサンプルの場合は、上記のイベントが発生した時点で WebGL のデモをスタートするボタンを有効化するようにします。ユーザーがボタンをクリックすると、動画の再生が開始されると同時に WebGL のサンプルが動き始め、動画がテクスチャに反映されたデモンストレーションが見られるという寸法です。
さて、上記のことを踏まえつつ、実際にどのように処理しているのか見てみます。
まずは window.onload
に登録する処理からです。
window.onload に登録する処理
// オンロードイベント
onload = function(){
// canvasエレメントを取得
c = document.getElementById('canvas');
c.width = 512;
c.height = 512;
// input エレメントへの参照を取得
var button = document.getElementById('playButton');
// input が押されたらレンダリング開始
button.addEventListener('click', function(){
button.value = 'running';
button.disabled = true;
video.play();
render();
}, true);
// ビデオエレメントを生成
video = document.createElement('video');
// ビデオタイプのチェック
var videoExt = checkVideoType();
if(videoExt === null){
alert('not supported');
return;
}
// ビデオエレメントにキャッシュ完了イベントを登録
video.addEventListener('canplaythrough', function(){
if(button.value !== 'running'){
button.value = 'can play video';
button.disabled = false;
}
}, true);
// ビデオエレメントのリピート再生を設定
video.addEventListener('ended', function(){
video.play();
}, true);
// ソースファイルの読み込み
video.src = 'video.' + videoExt;
};
上記の処理のうち、変数 video
はレンダリングを行う render
関数内などでも利用することになるのでグローバルな変数として定義しています。
ポイントになるのは、動画ファイルのキャッシュがある程度まで進み、今の読み込み速度のままなら動画再生が可能であることを通知する canplaythrough
イベントを使っていること。そして、事前にどのタイプの動画が再生可能なのかチェックしていること、この二点です。
動画の再生が可能となる canplaythrough
イベントは、どういうわけか動画の再生が始まったあとなどにも唐突に発生したりすることがあります。ですから上記のコードでは、一度だけしか処理を行わないようにするために、input エレメントの value
属性の値をチェックして処理を行うようにしています。
また、コメントでビデオタイプのチェックとなっているところ。ここでは再生可能な動画の形式を調べています。ここで呼んでいる checkVideoType
という関数は独自の関数で、中身は以下のようになっています。
checkVideoType 関数
function checkVideoType(){
if(video.canPlayType('video/mp4') === 'maybe'){
return 'mp4';
}else if(video.canPlayType('video/webm') === 'maybe'){
return 'webm';
}else{
return null;
}
}
video エレメントが持つ canPlayType
というメソッドに文字列で動画の MIME タイプを渡すと、それが再生可能な動画形式なのかどうかを調べて文字列で結果を返してきます。
ここで戻り値が "maybe"
となる場合には、意味としてはそのままですが「恐らく再生できる」ということになります。もし再生できない動画形式を指定したりすると空の文字列が返却されてきます。この戻り値をチェックして、再生可能と思われる動画形式の拡張子を返却するのが、この checkVideoType
関数の役割というわけですね。
テクスチャの生成と動画の適用
続いてはレンダリング専用の処理をまとめて記述した render
関数の中を見てみましょう。
基本的な WebGL の初期化処理などは、今まで当サイトで何度も登場したような基礎的なもので、特別なことは行っていません。唯一今回のサンプルに特有な処理となるのが、アニメーションのための恒常ループが始まる直前に行っている動画をテクスチャに適用するための処理でしょう。
テクスチャ関連の一連の処理
// テクスチャ関連
var videoTexture = gl.createTexture(gl.TEXTURE_2D);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, videoTexture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, video);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
過去にある程度 WebGL に親しんだことがある方なら比較的見慣れた設定がほとんどだと思います。
簡単に流れを説明すると、まずは空のテクスチャオブジェクトを生成、さらにそれをバインドします。
テクスチャのバインドが済んだら、あとは texImage2D
メソッドの最終引数に video エレメントを渡してやるだけです。
CANVAS などをテクスチャとして利用する際とほぼ同じですね。
ポイントがいくつかあるので、そこに関しては詳細に説明しましょう。まず、上記の例ではコメントアウトしていますが gl.pixelStorei
メソッドを用いて gl.UNPACK_FLIP_Y_WEBGL
フラグを true
にすると、テクスチャに適用された際の動画の上下が反転します。個人的にはこの設定は勝手に上下を反転しますし WebGL に特有のもので他の OpenGL プラットフォームとの親和性がなくなるので嫌いです。
手軽に動画をテクスチャとして扱うためには、コメントアウトを外してフラグを有効化したほうがいい場合もあると思いますので、適宜用途に合わせてコメントアウトを外して使うといいと思います。
また、テクスチャの補間方法を指定するフィルタ系の設定では、ミップマップを生成しているかどうかに注意しましょう。ミップマップは、テクスチャの元となるイメージの一辺の長さが 2 の累乗になっている場合にしか生成できません。そして得てして、動画ファイルというのは正方形ではありませんし、2 の累乗サイズの大きさというわけでもないでしょう。つまり、ミップマップは基本的に生成できない場合が多いと思います。
もし仮に、動画の一辺の大きさを 2 の累乗に合わせることができたのなら、当然ミップマップを生成しておいたほうがフィルタリングの観点からは好ましいのでしょうが、正直なところ動的に再生されている動画の各フレームに対して、毎回ミップマップを生成するような処理は軽いとは思えませんし、基本的にはミップマップ系の設定に関しては無視するのが妥当だと思います。
そして上記のミップマップ問題とも少しだけ関連しますが、本来は一辺の長さが 2 の累乗でなくてはならないテクスチャの元データが、動画ファイルの場合はそう簡単に揃えられないケースが多いと予想できます。そうしたケースでは texParameteri
メソッドで WRAP 系の設定をしっかり行っておく必要があります。
具体的には gl.TEXTURE_WRAP_S
と gl.TEXTURE_WRAP_T
の両方に対して gl.CLAMP_TO_EDGE
を忘れずに設定するようにしましょう。こうすることで、一辺の長さが 2 の累乗とならないような大きさの動画ファイルでも、正しくテクスチャが参照されるようになります。
動画の再生環境としては問題ないのに、うまくテクスチャが表示されないといったケースでは、このような各種の設定も確認してみるといいでしょう。
そもそも動画をテクスチャにするということ
さて、前項で書いたテクスチャのもろもろの設定は、恒常ループが始まる前の段階で行います。つまり、アニメーション処理を行い始める前の段階でやる処理ということですね。
では、実際に恒常ループが始まってからどういうふうに処理すればいいのかというと、これは単に texImage2D
メソッドを呼び出して動的にテクスチャに割り当てることをしてやれば OK です。このことから理解してほしいことは、動画テクスチャを扱う場合に限りませんが、テクスチャに対する texParameteri
メソッドなどを用いた設定はあくまでもテクスチャオブジェクトごとに適用されるものであり、事前に設定をしっかり行っておけば、ループ内では最小のメソッドの呼び出しだけで対応が可能だという見方です。
この考え方がすんなり入ってくるようであれば、動画をテクスチャとして扱うのはそれほど難しくないはずです。
画像をテクスチャに適用する場合には texImage2D
メソッドは画像データをテクスチャオブジェクトに適用する最初の一度しか呼び出しませんね。動画ファイルの場合は、再生に合わせて、その瞬間瞬間の動画の映像を動的にテクスチャに割り当てるようにするわけですね。
注意すべき点としては、あくまでも再生されている動画のその瞬間の映像を拾ってくる形なので、動画の再生が止まっていると当然テクスチャの内容もそれに連動するということです。このあたりは仕組みさえわかってしまえば簡単ですね。
まとめ
動画をテクスチャに適用する方法について解説してきましたがいかがでしたでしょうか。
正直な感想を言うと、現状ではまだまだ動画テクスチャは環境をかなり限定せざるを得ないと言えます。基本的にモバイルでは難しい部分が多いこと、また PC のブラウザに限って言えばほぼ再生やテクスチャへの適用はできるようですが、それでも動画ファイルの形式による問題が残ります。
ただ、この video エレメントを用いて WebGL のテクスチャを使うという方法は何も対象が動画だけとは限らない点で、非常に将来性のあるテクニックかなと思っています。
例えば web カメラの映像を動的に持ってきてフィルタリングしたりといったことも、工夫次第では行うことが可能です。カメラの映像を拾って動的に WebGL で処理するなんて、なんとも面白そうですよね。
まずはとりあえず、普通の動画ファイルを用いてテクスチャに適用する方法をしっかり習得しておいて、それが損になることはないでしょう。将来的にはモバイル環境でも問題なく動作するようになると思いますし、いろいろ可能性の広がる動画テクスチャ。今回のテキストを参考にぜひトライしてみてください。
今回も実際に動作するサンプルを用意してあります。以下のリンクから試してみてください。