自習室

こもります

たき火

f:id:AMANE:20151228191445j:plain

たき火に惚れた

Unity や openFrameworks でたびたびパーティクルを飛ばしたり光る表現をしたりしているけど、

結局は現実の圧勝



そもそも、たき火をつくるのはそんなに簡単ではない

ようやくついた火に少しずつ薪を足して守る

代わる代わる人がやってきて、火に照らされた顔を見たり、火を見たりしながらゆっくりと語る

火がパチパチと音を立て、温かくて、別に話すことがなくても時間が気持ちよく流れていく

子供が枯れ葉を投げ込んで火が大きくなるのに、きゃーきゃー言っている



たき火の熱は服を通り越して直接肌を焼いてくるような感覚がある

冷たい外気と熱い炎のダイナミックレンジ

パチパチという音は、脚色無く現実の音として鳴り、冷たい空気を揺らして直接耳に届く

赤や青の炎のダイナミックレンジ

白飛びした赤から、透明の青

うちわで扇ぐと、遅延無くゆらゆらと反応する光と熱と音

コンピュータでは当分表現できないというか

「ディスプレイ」という概念の無力さを思い知る

見上げると、月と星空

人の目と耳と肌の能力との華麗すぎるコラボレーション



たき火は身に危険を及ぼしうるが、

使いこなすことで、身を守る道具ともなる

食事を作ることも出来る

あかりのおかげで夜も活動できる

熱のおかげで冬を越すことが出来る

危険を管理することで便益を手に入れてきたことを実感する

穏やかで、当たり前すぎるのだけど、

人の能力を高めるもっとも基本的な要素のひとつであることを確認する



6歳の女の子が「たき火は消える直前も良かった」と言った

食事をし、暖を取り、仲間と語り合い、そしてこれから寝る

目に焼きついた光と体に残る暖かさ

たき火は人の生を象徴しているような気がした



Rhizomatiks Research x ELEVENPLAY "border" 体験の感想など

物販で売っていた前公演 “MOSAIC” の CD を聴きながら書いています

f:id:AMANE:20151205191109j:plain

公演の概要

公式をご参考下さい

www.rzm-research.com

感想

  • CGカッコいい
  • 照明カッコいい
  • 音カッコいい
  • ダンスカッコいい (目の前に来るのでドキドキする)
  • 運営が素晴らしい
  • チケット当たって良かった!

以下多大なネタバレが含まれます。間違いとか不理解とかあったらご指摘いただけるとありがたいです。

MIRAGE との対比

もう3年も前になってしまいますが、理研と パフォーマンスグループ GRINDER-MAN の作品「MIRAGE」を体験しました。頭をがつーんとやられたのが今でも生々しく思い出されます。

izmiz.hateblo.jp

似ている部分があったので比較してみます。

  • (同) 遮蔽型の HMD を使い、 HMD に外づけた RGB カメラの現在の映像を表示ししたりしなかったり、透過したりするのは同じ
  • (同) ダンサーが出てきて、目の前まで来たり横切ったり、体を触って実在感を確認し没入感を高める演出は同じ
  • (異) border では、リアルタイムレンダリングされる CG と、RGB カメラの映像が半透明に重ねられたり切り替えられたりしますが、 MIRAGE では、過去に収録された映像と現在の映像が切り替えられたり重ねられたりします。

最後の (異) が重要な気がしました。技術的には、10人同時参加しているすべての人(WHILL)の視点を、 ladybug 等の全天周カメラで移動しながら同時に撮るのは、以下の様な理由で相当困難です

  • ladybug で撮ってしまうと、ダンサーと環境しかなければ良いのですが、同時に体験している他のお客さんの位置に他の全天周カメラが収まってしまう
  • 他の9台にはダミーのお客さんを乗っけて10回全天周カメラで収録する、と言う手もあるけど、他人とは言え性別が入れ替わったりするとさすがにわかる
  • そもそも border ではRGBカムx2でステレオをしているので、 過去映像とのすげ替えは無理

他にも細かくいろいろありそうですが、総じて MIRAGE ほどの「現実・幻影のすげ替え・重ね合わせ」の作り込みは難しそうです。

border は、 非現実 (CGとダンスの世界、ライゾマ+MIKIKO先生的かっちょいい世界) と現実世界の境界を溶かす体験で、MIRAGE はある種の生々しい幻影を見せられる体験ってことかなーと思いました。

画質話

MIRAGEはまず低画質の世界で現実感をキャリブレーションした後幻影が融けてくる、という演出になっているので、HMDもカメラもある意味低画質で良かったのですが、カッコいいCGの世界が強く押し出されているborderでは、やはり画質がほしいなぁ、と言う感想を持ってしまいました。 Oculusもはその点まだ不足している印象です。

技術的/仕掛け的なはなし

つらつらとメモ。

写真に書き込んじゃいましたが、表に出てて体験者が直接触れるのはこんな感じだと思いました。

f:id:AMANE:20151205185755j:plainf:id:AMANE:20151205184257j:plain

ラッキング周り

  1. OptiでHMDに取り付けてある固有ID登録済みのマーカセットをトラッキング
  2. その位置姿勢情報を各MBPに送信
  3. MBPでレンダリング

という流れだと想像します。 Optiが理想的に取れていれば位置姿勢が取れているので Oculusのジャイロ情報は使わなくても良いのかな。

Optiの場合、(確か)固有の位置関係にある4つのマーカの組みを1つのID付きのトラッキング対象として登録出来るのだけど、それを10組登録してトラッキングするのは結構緊張感あるなーと思いました。距離が近いと入れ替わりとかありそう。意外とだいじょうぶなのかな。。。

車いす自体の制御には、それとは別に車いすの背中につけてある三角形のマーカセットでのトラッキング情報を使う。こちらは平面の仮定を置いている、のかな。

  1. OptiでWHILLの背中につけてあるマーカセットをトラッキング
  2. その位置姿勢情報を各MBPに送信
  3. MBPからWHILLをコントロール(ここはたぶん)

車いすを指定時間で指定位置まで持っていくのとか、結構専門的なプログラムな気がします。映像的なものだけじゃないライゾマさま、やっぱすごすぎ。

ステレオカメラ

公式サイトのキービジュアルではむき出しになっている前面のカメラ、4つのマイクが並んでいる独特のスタイルは、PlayStation Eyeだと思います。本番ではカバーがついていました。以前 @hecomi 先生が記事にもされていましたが、 VGA 60fps で撮れる安価なウェブカムとして超優秀で、おそらくそれが買われて採用されているのでしょう。

tips.hecomi.com

ただ、実際体験した感じだと、あまり立体視している感じは出ていなかったような気がします。他の表現や演出にあっけにとられ過ぎていて、立体化があるかどうかを気にして無すぎたのかもしれません(笑)

照明との連動?

作品中で、白い箱が動き回って、それがさらにAR的な演出で変形したりするのですが、実世界の照明との連動が、されていたようなされていなかったような、微妙な感じでした。(後から鑑賞した際は、確かに白い箱に照明が当てられてカラフルに着色していたのですが、体験したCGの世界では、それほど色がついている印象はなかったです)

照明にColorBlast を使っていたので、照明の入力はすべて楽譜のようにデータ化されているはずです。なので、CGの世界でもそれと連動するように世界に照明を配置して光らせると、より良い感じだったかもなーと思いました。(やってたかもしれません)

ポンチョ

f:id:AMANE:20151205192559j:plain

ポンチョを着せられたときに何でかなーと考えたのですが、体験したあと鑑賞までしてようやくわかりました。

終盤にHMD内ではCGのダンサーが踊っているシーンがあります。重畳しているものだと思って体験中は見ていたのですが、後で鑑賞してみると、実は、ステージで現実のダンサーが踊っているのと完全に同期したCGのダンサー「だけ」がHMDの中では踊っているのです。

しかし、HMDで見ている世界には、ほかの9名の鑑賞者もいます。現実の世界からダンサーだけ消すのは難しい気がするのですが、実はこのとき、現実世界と重畳されていると思い込んでみていたHMDのCGの世界は、実はフルCGなんですね。いつの間にかCGの世界に鑑賞者が入っていた!という驚きです。そのためにその日どんな格好で来るかわからない鑑賞者の服装を隠す必要があり、ポンチョを着せられていたわけです。

もしかしたら他のシーンでも、鑑賞者はいつの間にかCGに入れ替わっていたかも知れません。(演出とダンスが格好良すぎてほぼ気がつかない(笑)

オペレーション他

  • 待ち時間に真鍋さん直々に出演されているインストラクション動画を見ました。これで入場から退場まで迷い無く体験することが出来ました。
  • 上演の間の時間WHILLを壁寄せに並べている隙に、MBPの充電とWHILLの充電をしていたようです。ケーブル二本。すごく複雑な装置に見えますが、オペレーションがすごくスムースに行われていて素晴らしかったです

さいごに

とにもかくにもスゴイ体験でした。こんな攻めたステージを一般向けに三日間もやるというのが、すごすぎます。紅白のPerfumeといい、こういう現場での強さは Rhizomatiks さんはさすがですね。惚れます。

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を行っても四捨五入の際に丸められたりせず、ちゃんと反映されます。

完成品

前回の失敗作

今回修正した物

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

最後に

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

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

Three.jsのオフスクリーンレンダリングとping-pongで、リアルタイム動体除去を行う

はじめに

f:id:AMANE:20150420184058p:plain

前回に続き、ブラウザ内だけで画像処理的なことをしよう、というチャレンジです。今回は動体除去というか、モーションブラーというか、画像の時間平均処理をしてみます。

上の写真は、割と短時間の平均画像です。動いているものが透けているのがわかると思います。画像の平均処理をもっと長時間で行うと、ほぼ動体が見えなくなります。アルゴリズムはあとから説明します。

OpenCVC++で使うと、普通にMatとかで前回のフレームを保存しておいたり差分を取ったりとお手軽に出来ますが、おなじことをJavaScriptでやろうとすると、必要な枚数画像を展開して、ピクセルを全走査するなど、かなりヘビーな感じになります。今回はブラウザ内で完結させてみるのが目的なので、WebGLのシェーダを使ってみます。

完成品

完成品はこちらで確認出来ます。ページを開いたら、カメラ利用に許可を与えて下さい。上部のバーで除去具合を調整出来ます。ゼロにするとリアルタイムの動画をそのまま出しますが、大きくしていくと、だんだん動体が薄くなっていくのがわかると思います。

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

動体除去のアルゴリズム

FIRフィルタによる時間平均処理

動画像を時間にわたって足し合わせて平均を取ると、動かないものは残り、動くものは薄ーくなります。これが基本。FIRフィルタ的な感じ。動画像のローパスフィルタですね。

f:id:AMANE:20150420154518p:plain

式で書くとこんな感じ。 X(n) がカメラから得られる最新フレームの画像で、 X(n-1) が一個前のカメラフレーム。 Y(n) が今回表示される絵。

 {
Y(n) = a_{n}X(n) + a_{n-1}X(n-1) + a_{n-2}X(n-2)+\ldots
}

 {
\sum_{k=0}^{N}a_{n-k} = 1.0
}

係数の合計が1になる様にします。1より小さいと全体の画像がグレイがかって薄くなり、1より大きいと、白飛びした画像になります。

この手法でちゃんと動体を消すにはタップ(データを記憶して、演算をする)を多くしなければなりません。ブラウザに大量のframebuffer objectを確保しなければならなくなるので、リーズナブルではありません。

IIRフィルタ(的)な時間平均処理

上記のような問題は、画像処理だけでは無く電気回路におけるノイズ取りなどでも似たようなことがあるようで、解決法としてIIRという手法があります。

大量のタップを用意する代わりに、過去のフィルタ結果をその後も使い回し続けることで、バッファとしてはひとつか少ない数しか用意しないけど、常に平均画像を求め続けているような計算になります。

最もシンプルにバッファ一個だけでやる場合、式で書くとこんな感じ。 X(n) がカメラから得られる最新フレームの画像で、Y(n)が、今回表示される絵。Y(n-1) が、前回表示された絵。

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

Y(n-1) ももちろん上と同じ式で一回前のカメラフレームが係数をかけて足されて出来た画像でY(n-2)も…と続くので、過去のフレームが秘伝のたれのように残りながら足され続け、平均が求められている、という感じです。一回前の描画内容を覚えておくだけで良いので、コストが低いのがポイントです。

実装

上記IIRフィルタ的処理をウェブカムに対してやってみます。

手順としては以下の様な感じになります

  • 新しいカメラ入力と前回の表示内容をテクスチャにしたもの★をブレンドする
  • ブレンド結果をオフスクリーンレンダリングして、テクスチャ★化しておく
  • ブレンド結果を画面にレンダリングする

ここで、★がついている二つの画像が、メモリ上で同一のものである、と言うことが味噌になります。

Three.js でウェブカム画像を利用する

前記事をご参照下さい -> WebRTCの動画にThree.jsのポストプロセスでエフェクトをかける - 自習室

オフスクリーンレンダリング

THREE.WebGLRenderTarget に対してシーンを書き込めば、その画像をテクスチャとして利用する事が出来ます。オフスクリーンレンダリングについては、下記記事様が勉強になります

今回の例では、ブレンドした結果を WebGLRenderTarget に書き込んでいます。

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

// 中略
// ここで、最新のカメラ画像とまえのフレームの描画結果をブレンドした新しいテクスチャを作り、
// それをプレーンに貼って、シーン内に配置する

renderer.render(scene, camera, rt1); // 別途用意した、rt1というレンダーターゲット(フレームバッファを含む) に書き出す
renderer.render(scene, camera);      // 「標準フレームバッファ」つまり画面に書き出される。

ブレンド

Three.js リポジトリexamples/js/shaders に、多数のシェーダのサンプルが含まれています。今回はこの中から、 BlendShader.js を利用します。

videoMaterial = new THREE.ShaderMaterial(THREE.BlendShader);
videoMaterial.uniforms['tDiffuse1'].value = videoTexture; // カメラ画像で毎フレーム更新されるTextureオブジェクト
videoMaterial.uniforms['tDiffuse2'].value = null;         // ブレンドされるもの。初めは無し
videoMaterial.uniforms['mixRatio'].value = 0.0;           // y = x1 * (1-mixRatio) + x2 * mixRatio となります

var planeGeometry = new THREE.PlaneBufferGeometry( vidWidth, vidHeight, 1, 1 );
var plane = new THREE.Mesh( planeGeometry, videoMaterial );  // ブレンドした結果の絵をテクスチャとしてプレーンに描き込む
plane.position.z = 0;

scene.add(plane);  // シーン内に配置する

ちなみに、Three.js における ShaderMaterialの扱いかたについては、下記サイトが勉強になります。

起動時は、ブレンドする2枚目のテクスチャがnullになっていますが、mixRatioも0にしてあるので、1枚目のvideoTextureがそのまま出ることになります。2枚目のテクスチャに適切に画像を当てて、mixRatioを動かすと、ブレンドされます。

FBO ping-pong

IIR的に画像の平均を求め続けるには、前回の描画結果に今回のカメラ入力をブレンドして出力する必要があるのですが、WebGLでは、Aというテクスチャを使ってシーンを作ったあと、Aにrenderする、と言うのは出来ません。メモリに書き込むにつれて絵が変わる、なんてことが起きてしまうから、そりゃそうだ、という感じなのですが。

WebGLRenderTargetを2つ用意し、交互に利用する事で、それを回避します。

var rt1, rt2;
var rtSwitch = true;

// 中略

// 交互に利用する2つの RenderTarget を用意する。
rt1 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat }  );
rt2 = new THREE.WebGLRenderTarget( vidWidth, vidHeight, { minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, format: THREE.RGBFormat }  );

毎フレーム、BlendShaderで利用するテクスチャと、オフスクリーンレンダリング対象のレンダーターゲットを切り替えていきます。

if(rtSwitch) renderer.render(scene, camera, rt2); // (フラグonの時)rt2に書き込む
else renderer.render(scene, camera, rt1);         // (フラグoffの時)rt1に書き込む
renderer.render(scene, camera);                   // 同じ内容を画面にも書き出す

if(rtSwitch) videoMaterial.uniforms['tDiffuse2'].value = rt2; // (フラグonの時)次のターンでは、rt2の内容をブレンドに使う
else videoMaterial.uniforms['tDiffuse2'].value = rt1;         // (フラグoffの時)次のターンでは、rt1の内容をブレンドに使う

rtSwitch = !rtSwitch;  // フラグを切り替える

ちなみに、これをミスって、シーン内に使っているテクスチャに書き込もうとすると、以下の様に怒られます

[.WebGLRenderingContext-07E445A0]GL ERROR :GL_INVALID_OPERATION : glDrawElements: Source and destination textures of the draw are the same.

さいごに

改めて、完成品はこちらです

今回の手法だと、背景に対してコントラストが高い動体は、焼きついたように長時間残ることもあります。たとえば下のような例。ラズパイのマークが、白い壁に染みついちゃってます(笑)

f:id:AMANE:20150420184533p:plain

今回の手法は、厳密に背景と動体を分離して消す、というよりは、「動体をうっすらとぼかすことでプライバシーを守りつつ、背景はほぼリアルタイムのものを撮影する」といった用途にむいていると思います。

焼き付きを抑えるには、「動体がない」瞬間を検知して、テクスチャを全部書き換えちゃう、とかすると良いかと思います。

改善案募集

もちっと綺麗に動体を消せる技術ご存じの方、是非とも教えていただきたいです!

(追記) 改善しました

焼き付きのようになってしまう問題を改善しました。追加で投稿していますので、ご参照下さい。

izmiz.hateblo.jp

GPGPU的な用法

FBOのping-pongは、こういった画像の保存という単純な用途以外にも、さまざまな可能性があります。たとえば、大量の点群の

  • 速度
  • 座標
  • 質量

などの「色」ではない情報をテクスチャ風の配列群に仕立ててシェーダに渡し、fragment shaderでガッと計算をした後に、それらを再度オフスクリーンレンダリングという形でテクスチャ的データに書き出して、次のフレームでも利用する、という用法があります。Three.jsの公式サンプルにも作例があります

シェーダでの計算結果を次のフレームで活用するために、FBO ping-pong が使われています。かなり複雑ですが、GPUの計算リソースを活用出来るので、ブラウザでも上記サンプルのようになにやらすごいことが出来ます

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

ofxLibWebsockets で oF とウェブブラウザの間でお話しする

はじめに

相変わらず、ブラウザとopenFrameworks 間で共同作業させるシリーズです。

以前こんなものを書きました。

これらの記事では、ブラウザとoF間にNode.jsのプログラムを挟み、socket.io と OSC を相互に変換することで、データのやりとりをしていました。もともと以下の理由でそういう構成にしていました。

  • ブラウザとnode.js間はsocket.ioでの通信が楽
  • node.jsとoF間では、OSCでの通信が楽
  • それを単につなげればよいので楽

このときの構成は下図のようになります

f:id:AMANE:20150407143947p:plain

しかしこの結果

  • 機能上はほぼ意味の無い node.js の変換コードを書かなければいけない。実装コスト増大

という問題も発生しました。

  • oFでやろうとしていることを別のプログラムでシミュレートする
  • 複数のブラウザやoFのプログラムとやりとりをする

などのケースを想定すると多段構成にする意味もありますが、一対一で通信をするだけなら、ほぼ意味はありません。従って、ブラウザとoFで直接お話しをさせたいです。

いろいろ試した結果、以下の構成が可能であることがわかりました。今回はその方法についてまとめます。

f:id:AMANE:20150407143959p:plain

非バイナリデータのみ

WebSocketでバイナリのやりとりも出来る筈ですが、本記事では非バイナリデータ…つまり、数値、文字、bool とそれらをオブジェクト化した物などをやりとりします。いずれバイナリも調査します。

この記事を追うとできるもの

oFの画面でマウスを動かすと、その座標をJSONとしてラップし、WebSocketを介してブラウザに送ります。ブラウザではJSONを復元して、オブジェクトとしてコンソールに出力します。

f:id:AMANE:20150408215316j:plain

完成品はこちらにアップしています

github.com

ofxLibWebsockets の基本

addon単体としてのドキュメントがほとんど無いため、使いこなしに少々苦労しました。サンプルを読んで探していくスタイル。

プロジェクトへの追加

oFプロジェクトへの追加の方法は、ofxLibWebsockets のgithubにまとめられています。こちらを参考に追加もしくはoFプロジェクトの作成を行ってください。

自分はWindows(VS2012) で試しているのですが、配布されている ofxLibWebsockets のオプションに間違いを見つけました。以下修正してください。

プロジェクトプロパティ > リンカー > 全般 > 追加のライブラリディレクトリ で、構成が Debug の際に、

..\..\..\addons\ofxLibwebsockets\libs\libwebsockets\lib\win32\Release` となっているものを
..\..\..\addons\ofxLibwebsockets\libs\libwebsockets\lib\win32\Debug に修正。 

oF側をサーバとしてセットアップ

ofxLibWebsockets のサンプル "example_server_echo" を参考に。最低限の構成は以下、だと思います

void ofApp::setup(){
    ofxLibwebsockets::ServerOptions options = ofxLibwebsockets::defaultServerOptions();
    options.port = 9092;
    options.bUseSSL = false; // you'll have to manually accept this self-signed cert if 'true'!
    bSetup = server.setup( options );

    // this adds your app as a listener for the server
    server.addListener(this);
}

メッセージの受け取り

こちらも、ofxLibWebsockets のサンプル "example_server_echo" を参考に。ただ受け取ったメッセージを標準出力します。

void ofApp::onMessage( ofxLibwebsockets::Event& args ){
    cout << "got message " << args.message << endl;
}

試しにブラウザから送ってみる

先に "example_server_echo" を起動してから、Chromeで適当なページを開き、developer tool のコンソール上で以下の様に順に打ち込みます

ws = new WebSocket('ws://localhost:9092'); // 接続
ws.send('hoge'); // 文字列を送る

一行目をenterした時点で、oFのコンソールには "new connection open" と表示されます。
更に二行目をenterすると、oFのコンソールに "got message hoge" を表示されます。

JSON形式でやりとりしてみる

ブラウザ側から送ってくるデータは、それが元々オブジェクトであった場合も、message = JSON.stringify(data) したのちに送り出し、ofxLibWebsockets で受け取ると、jsonとして復元されます。

逆に、ブラウザ側でJSON形式で受け取りたい場合は、oF側でJSON形式でまとめ上げたものを文字列として送り出し、それをdata = JSON.parse(message) することで、jsonとして復元されます。

図にまとめると下のようになります。

f:id:AMANE:20150407162026p:plain

ofxLibWebsockets は内部でjsoncpp というライブラリを利用しているので、json <-> 文字列 の変換が出来ます。新しいjson形式のデータ作成も可能です。

ブラウザからobjectデータを送り出し、oFで受け取るケース

// 送出側 (js)
var ws = new WebSocket('ws://localhost:9092/');
var pos = {'x':0, 'y':100};
var message = JSON.stringify(pos);
ws.send(message);
// 受け取り側 (cpp)
void ofApp::onMessage( ofxLibwebsockets::Event& args ){
    // args.messageには、 js側で message = JSON.stringify(pos) として作られた文字列"message" が入っている
    cout<<"got message "<<args.message<<endl;
    
    // 送られてきた args.message がjsonにパースできる形式である場合、
    // 自動的にパースされ args.json に Json::Value 形式で収められる
    if ( !args.json.isNull() ){
        cout << "New message: " + args.json.toStyledString() + " from " + args.conn.getClientName();
    }
}

oFでJSON形式にラップしたものをブラウザで受け取るケース

マウスを動かすと、最新の座標をWebSocketで送り出す、というもの

// 送出側 (cpp)
void ofApp::mouseMoved(int x, int y ){
    Json::FastWriter    writer;
    Json::Value     value;
    value["x"] = x;
    value["y"] = y;
    std::string message =  writer.write(value);
    server.send(message);
    cout << "mouseMoved : " << message << endl;
}
// 受け取り側 (js)
var ws = new WebSocket('ws://localhost:9092/');
ws.onmessage = function(event) {
  var data = JSON.parse(event.data); //event.dataは文字列。これをJSON形式にパース
  console.log(data);
}

JSONCPP の使い方

以下のページなどを参考にしました

スピードを上げる

ofxLibWebsocket (libwebsocket)では、WebSocketの送り出し受け取りを、専用のスレッドを立てて行っています。一つの処理をコンテキストとしてまとめ、それをキューに積んでいって、前から順に処理していきます。その辺りはこちらのページで勉強させて頂きました。

ofxLibWebsocketでは、そのスレッド処理に、何故か標準で 50ms の待機時間が挿入されているため、毎秒 20 回までしか通信の処理が出来ません。マウスの動きを伝えるには遅すぎます。ここを変更します。

// addons/ofxLibwebsockets/ofxLibWebsockets/Server.cpp 19行目
    Server::Server(){
        context = NULL;
        waitMillis = 10; //★ここ 50 -> 10 など。
        reactors.push_back(this);
        
        defaultOptions = defaultServerOptions();
    }

これで、最大100回/秒 通信できるようになります。

waitMillis を変更するための public な関数をServer.cpp か 親クラスの Reactor.cpp に追加しても良いかもしれません。

さいごに

oFで画像を作ってブラウザ内で利用する、と言ったことも出来るかもしれないので、調査してみます。

dropmark のビデオプレイリストの再生をループさせるChrome Extension を作った

動機

こんなことがしたい、と思いました

  • 余ってるPC + ディスプレイで、様々な動画を垂れ流しにしたい(会社でシェアしたい)
  • 極力簡単にプレイリストを管理したい
  • YouTube と Vimeo のリンクを貼るだけでOKみたいな感じがベスト
  • 全画面再生
  • ループ

イケてる動画をみんなであつめてそれを垂れ流しておくことで、ふとした瞬間に素敵な動画に出会えるたら良いなーというのが狙いです。

調査

公式

Vimeo だと、Likesなり Watch Laterなり Channel なりを、"Watch in Couch Mode" で再生開始すると、自動的に全画面 + ループになります。

f:id:AMANE:20150301201100p:plain

YouTubeだと、お気に入りなりプレイリストに、「繰り返す」ボタンがあります。これをONにしてから全画面再生すれば良い感じです。

f:id:AMANE:20150301201113p:plain

それぞれ良いのですが、会社で色んな人に登録してもらうとすると、両方のサイトにアップロードされている動画を扱いたくなります。

おしいやつ

Tumblrで動画を集めておくことはもちろん出来ますが、自動で再生していく機能はありません。

同じようなものがないかなーと探していたところ、dropmark というサービスの存在を知りました。

dropmark

自分も今回の目的以外で使ったことがないのでよくわかっていないのですが、いわゆるブックマークサービスです。その中の特徴的な機能として、ビデオのリンクを貼っておくと自動で再生してくれる機能、と言うのがありました。この記事で知りました

これで解決!と思ったのですが、ひじょーに惜しいことに、リストの最後まで行くと、そこで一覧画面に戻ってしまう仕様です。

そこで、Chrome Extensionを使って、ウェブサービスをハックしてみることにしました。

完成品と使い方

ぎっはぶ

ここに上げてあります

使い方

  • コードをPC内のどこかにおく
  • chromeの設定 > 拡張機能 > パッケージ化されていない拡張機能を読みこむ
  • フォルダを指定して開く

現状のコードは、すべてのウェブサイトに対して処理をしてしまうような物にになっているので、dropmarkを全画面で再生し続ける専用のPCを用意できる場合は良いのですが、もしご自身のPCで一時的に試してみたい、と言う場合は、利用しない場合は、拡張機能のリスト中で「有効」のチェックをはずしておくと良いかと思います。

f:id:AMANE:20150301205851p:plain

ついでに

YouTubeの再生を極力高画質で行いたいので、別途それ用のChrome-extension を入れます。この手の物はたくさんあるのですが、こいつはちゃんと動いていました

Vimeoの方は、ある程度大きくプレイヤーが開いたら、自動的に高画質になるので心配ありません。

設計(?)

主に、以下の三つのことをやっています。

  1. プレイリストの最後のビデオになったかの判定
  2. プレイリストの先頭のリンクを取得する
  3. 最後のビデオの次に、プレイリストの先頭のビデオを再生するようにハック

1. プレイリストの最後のビデオになったかの判定

これは、画面中の "Next Item" ボタンのハイパーリンクを見ることで判別出来ます。動画を指すURLではなく、動画一覧を指すURLだった場合は該当です。

f:id:AMANE:20150301202152p:plain

2. プレイリストの先頭のリンクを取得する

プレイリストのHTMLの中身を見て動画の一個目を指すURLを取得すれば良いのですが、実際は「最後の動画を見ている最中に」異なるページのHTMLを参照することになるので、少しトリッキーなことをします。

  1. jQuery<div> 要素を作ってその中にプレイリストのHTMLを丸っと挿入する
  2. その中から該当するHTML要素を見つける
$.get(playlisturl, function(data) {
  var playlisthtml = $('<div>').html(data);

jQueryのgetコマンドで動画一覧のページのHTMLを引っこ抜いてきて、それを <div> タグの中に展開することで、jQueryで解析可能なものにします。ここでこの <div>.append とかしなければ、その要素は表示されたりはしません。あくまでHTMLデータの中身を解析するためだけに一時的に存在するものになります。

プレイリストのhtmlを取得できたら、その中から、お目当ての「先頭の動画」のURLを見つけてきます

var firsturl = playlisthtml.find('.item-preview:first-child').attr('href');

3. 最後のビデオの次に、プレイリストの先頭のビデオを再生するようにハック

1.で、最後のビデオか判定するのに使った "Next Item" の href に、先ほど見つけてきた先頭動画のURLを代入すれば完了です。

まじで?

「えっ…?」って感じでした。次の動画への飛ばし方はちゃんと本家のjs読めばわかるかも知らん、気合い入れて解析してやろう!と意気込んで始めた直後、まぁそんなわけはあるまいと思いつつもものは試しでURL書き換えてやろーと適当こいたところ、出来てしまった、という感じです。結果オーライ、簡単で良かった。やったー

さいごに

と、説明しましたとおり、dropmarkの現状の実装を、偶然も織り交ぜつつ勝手にハックした感じですので、動作保証など一切できません。万が一ご入り用の方がいらっしゃった場合は十分ご理解の上ご利用ください。

3時間くらいで出来て良かったです。会社で運用してみます。