自習室

こもります

WebRTCの動画にThree.jsのポストプロセスでエフェクトをかける

やりたいこと

f:id:AMANE:20150414222133p:plain

このブログで何度かにわたって、ブラウザとopenFrameworksを連携させ、oFで加工した映像をWebRTCで扱う、というシリーズをやっていますが、今回は、映像の加工もブラウザ内で完結させてみます。

CSS でもいろいろ出来る

ブラーや明るさ調整は、それぞれ -webkit-filter: blur(100px)-webkit-filter: brightness(0.1) など css としてブラウザで実装されており、非常に高速に動作します。*1

これ以外の、もう少し自分好みのことをしようとしたときも、WebGL の fragment shader を使うことで、やはりブラウザ内で完結させることが可能になります。グリッチとか、ドットとか、歪ませたりとか、そういうの。

そういえば、CSS Shader とかいうのもあったような気もしますが、遠い過去の話ですね。

完成品

こちらで動いています。PCにウェブカムをつないだ状態でこのページにアクセスし、カメラの利用に許可を与えて下さい。

環境

素のWebGLを自力で書くのはしんどいので、Three.js を使います。

動作確認は、以下の環境で行っています。

参考にさせていただいたサイト

特に、一つ目のサイトの、 Bad TV Shader for Three.js と言うサンプルのコードを見てみると、既存の動画ファイルに対してでは無くウェブカムに対してエフェクトを掛けるコードが、コメントアウトされた状態で書かれています。 今回の記事は、ほぼこの内容の焼き直しになります。

やってみる

ポイントだけ追っていきます。

ウェブカムの映像を取り込む

よくあるWebRTCのサンプルのように、 navigator.getUserMedia でストリームを取得し、それを <video> タグに流し込みます。このとき、 <video> タグそのもの(無加工の映像) は使わないので、 style="display:none" しておきます。

<!-- 表示されないのでどこにあっても良い -->
<video id="video" style="display:none"></video>
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia;
window.URL = window.URL || window.webkitURL;

video = document.getElementById('video');
video.autoplay = true;  // これを外すと、スタート時の映像で停止します

var option = {
  video: { mandatory:{ minWidth: 1280 } }, // firefoxでは無効
  audio: false
}
// var option = {video: true};

navigator.getUserMedia(option,
  function(stream) { // for success case
    video.src = window.URL.createObjectURL(stream);  // videoタグにストリームを流し込む
  },
  function(err) { // for error case
    console.log(err);
  }
);

// カメラ画像のサイズを記録しておく。後で使う。
video.addEventListener('loadeddata', function() {
  // Chromeは問題無いが、Firefoxだと、'loadeddata' イベントでvideoWidthらが埋まっていないので、値が得られるまで待機
  (function getVideoResolution() {
    vidWidth = video.videoWidth;
    vidHeight = video.videoHeight;
    if(vidWidth != 0) {
      console.log("video width: " + vidWidth + " height: " + vidHeight);
      setup(); // カメラの映像が流れ始めたら、Three.js内の準備を始める
    } else {
      setTimeout(getVideoResolution, 250);
    }
  })();
});

<video> の映像を Three.js のテクスチャにし、平面なメッシュに貼り付ける

videoTexture = new THREE.Texture(video);  // なんとこれだけでテクスチャとして使える!
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.format = THREE.RGBFormat;
videoMaterial = new THREE.MeshBasicMaterial({  // マテリアルにする。カメラ画像をそのまま使いたいので、ライティング等は無し。
  map: videoTexture
});

var planeGeometry = new THREE.PlaneGeometry( vidWidth, vidHeight, 1, 1 );
var plane = new THREE.Mesh( planeGeometry, videoMaterial );  // 平面に書き込む
plane.position.z = 0;
scene.add( plane );  // シーンに追加

Orthographic なカメラ

パースペクティブなカメラを使ってももちろん良いですが、オルソーなカメラを使うことで、簡単に全画面ぴったりに上記カメラ画像平面を貼り付けることが出来ます。

camera = new THREE.OrthographicCamera(vidWidth/-2, vidWidth/2, vidHeight/2, vidHeight/-2, 1, 2000);
camera.position.z = 500;  // カメラの near/far クリッピングと合わせて設定する

繰り返し描画する

まずは動画にエフェクトを掛けずに、そのまま描画してみます。

HTML の <video> タグのDOMが、描画出来る状態になっているか(つまり、新しいフレームが来ているか)を調べ、その状態になっていたら、 videoTexture を更新するようにしています。

function render() {
  if(video.readyState === video.HAVE_ENOUGH_DATA) {
    if(videoTexture) videoTexture.needsUpdate = true;  // ここがポイント
  }
  // loop
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
render();

取りあえずこれで、ただの <video> タグではなく、 Three.js のつくる <canvas> 内にmeshに貼られた画像として描画されます

EffectComposer

Three.js 内で扱えるようになってしまえばこっちのものです。Three.js で作り上げたシーンをカメラで見てレンダリングした結果に対しポストプロセスをかけていきます。

Three.js は標準でシャレーなポスプロシェーダをたくさん備えています。

公式サンプルの postprocessing/ ディレクトリもおもしろいです

EffectComposerの使い方はこちらで簡潔にまとめられています

自分のものにもかけてみる

DotShaderと RGBShift をかけてみます。

composer = new THREE.EffectComposer( renderer );
composer.addPass( new THREE.RenderPass( scene, camera ) );

var dotScreenEffect = new THREE.ShaderPass( THREE.DotScreenShader );
dotScreenEffect.uniforms[ 'scale' ].value = 2;
composer.addPass( dotScreenEffect );

var rgbShiftEffect = new THREE.ShaderPass( THREE.RGBShiftShader );
rgbShiftEffect.uniforms[ 'amount' ].value = 0.003;
composer.addPass(rgbShiftEffect);

rgbShiftEffect.renderToScreen = true;

// 中略
// 繰り返し描画するルーチンの中

// renderer.render(scene, camera);
composer.render();  // rendererではなく、エフェクトのチェーン後のcomposerが画面に描き出す

本来、ブラウザ(キャンバス)が書き込み先である renderer に対し、 EffectComposer オブジェクトに対して書き込め、と指示し、そこから EffectComposer がいくつかのエフェクトを経由した後、.renderToScreen 指定したところで画面に書き出します。

完成

動作しているものへのリンクを再掲します

最後に

今回はローカルのウェブカムの画像に対してエフェクトを掛けましたが、 対象は <video> タグに描かれている内容でさえあれば良いので、もちろん WebRTC で「受信した」 映像に対してエフェクトを掛けることも出来ます。これを使うことで、さまざまな画像エフェクトが出来るビデオチャットシステムも作ることが出来るようになります。

*1:webkitのコードを読んでみたところ、OpenCLで高速化しているようです https://trac.webkit.org/browser/trunk/Source/WebCore/platform/graphics/filters/FEGaussianBlur.cpp