Rhizomatiks Research x ELEVENPLAY "border" 体験の感想など
物販で売っていた前公演 “MOSAIC” の CD を聴きながら書いています
公演の概要
公式をご参考下さい
感想
- CGカッコいい
- 照明カッコいい
- 音カッコいい
- ダンスカッコいい (目の前に来るのでドキドキする)
- 運営が素晴らしい
- チケット当たって良かった!
以下多大なネタバレが含まれます。間違いとか不理解とかあったらご指摘いただけるとありがたいです。
MIRAGE との対比
もう3年も前になってしまいますが、理研と パフォーマンスグループ GRINDER-MAN の作品「MIRAGE」を体験しました。頭をがつーんとやられたのが今でも生々しく思い出されます。
似ている部分があったので比較してみます。
- (同) 遮蔽型の 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もはその点まだ不足している印象です。
技術的/仕掛け的なはなし
つらつらとメモ。
写真に書き込んじゃいましたが、表に出てて体験者が直接触れるのはこんな感じだと思いました。
トラッキング周り
という流れだと想像します。 Optiが理想的に取れていれば位置姿勢が取れているので Oculusのジャイロ情報は使わなくても良いのかな。
Optiの場合、(確か)固有の位置関係にある4つのマーカの組みを1つのID付きのトラッキング対象として登録出来るのだけど、それを10組登録してトラッキングするのは結構緊張感あるなーと思いました。距離が近いと入れ替わりとかありそう。意外とだいじょうぶなのかな。。。
車いす自体の制御には、それとは別に車いすの背中につけてある三角形のマーカセットでのトラッキング情報を使う。こちらは平面の仮定を置いている、のかな。
車いすを指定時間で指定位置まで持っていくのとか、結構専門的なプログラムな気がします。映像的なものだけじゃないライゾマさま、やっぱすごすぎ。
ステレオカメラ
公式サイトのキービジュアルではむき出しになっている前面のカメラ、4つのマイクが並んでいる独特のスタイルは、PlayStation Eyeだと思います。本番ではカバーがついていました。以前 @hecomi 先生が記事にもされていましたが、 VGA 60fps で撮れる安価なウェブカムとして超優秀で、おそらくそれが買われて採用されているのでしょう。
ただ、実際体験した感じだと、あまり立体視している感じは出ていなかったような気がします。他の表現や演出にあっけにとられ過ぎていて、立体化があるかどうかを気にして無すぎたのかもしれません(笑)
照明との連動?
作品中で、白い箱が動き回って、それがさらにAR的な演出で変形したりするのですが、実世界の照明との連動が、されていたようなされていなかったような、微妙な感じでした。(後から鑑賞した際は、確かに白い箱に照明が当てられてカラフルに着色していたのですが、体験したCGの世界では、それほど色がついている印象はなかったです)
照明にColorBlast を使っていたので、照明の入力はすべて楽譜のようにデータ化されているはずです。なので、CGの世界でもそれと連動するように世界に照明を配置して光らせると、より良い感じだったかもなーと思いました。(やってたかもしれません)
ポンチョ
ポンチョを着せられたときに何でかなーと考えたのですが、体験したあと鑑賞までしてようやくわかりました。
終盤にHMD内ではCGのダンサーが踊っているシーンがあります。重畳しているものだと思って体験中は見ていたのですが、後で鑑賞してみると、実は、ステージで現実のダンサーが踊っているのと完全に同期したCGのダンサー「だけ」がHMDの中では踊っているのです。
しかし、HMDで見ている世界には、ほかの9名の鑑賞者もいます。現実の世界からダンサーだけ消すのは難しい気がするのですが、実はこのとき、現実世界と重畳されていると思い込んでみていたHMDのCGの世界は、実はフルCGなんですね。いつの間にかCGの世界に鑑賞者が入っていた!という驚きです。そのためにその日どんな格好で来るかわからない鑑賞者の服装を隠す必要があり、ポンチョを着せられていたわけです。
もしかしたら他のシーンでも、鑑賞者はいつの間にかCGに入れ替わっていたかも知れません。(演出とダンスが格好良すぎてほぼ気がつかない(笑)
オペレーション他
- 待ち時間に真鍋さん直々に出演されているインストラクション動画を見ました。これで入場から退場まで迷い無く体験することが出来ました。
- 上演の間の時間WHILLを壁寄せに並べている隙に、MBPの充電とWHILLの充電をしていたようです。ケーブル二本。すごく複雑な装置に見えますが、オペレーションがすごくスムースに行われていて素晴らしかったです
さいごに
とにもかくにもスゴイ体験でした。こんな攻めたステージを一般向けに三日間もやるというのが、すごすぎます。紅白のPerfumeといい、こういう現場での強さは Rhizomatiks さんはさすがですね。惚れます。
16bit 浮動小数点のテクスチャできれいにブレンドを行う
はじめに
前回の記事では、WebGL(Three.js)でFBO ping-pongすることで、テクスチャのブレンディングを行い、それによって映像中の動く物体を消す処理をやってみました。
しかし、問題がありました。
今回の手法だと、背景に対してコントラストが高い動体は、焼きついたように長時間残ることもあります。
前記事時点では、上の写真のよう焼き付きが起きてしまっていました。手法はあっているはずなのになんでこうなるのか、解明できず気持ち悪かったので、続けて検討したところ、間違いに気がついたので、修正して投稿します。
手法の検証
以下の式で毎回のカメラ画像を足し合わせていくと、動く物は消える(またはブラーがかかる)というのが、前回の記事の内容でした。
正しく出来ている場合のシミュレーション
まず、Excelを使って、この計算で狙ったとおりの効果が出るかを確認します。
簡単のためにグレースケールと言うことにして、はじめ入力が50の明るさだったところに、10フレームだけ200の明るさが入って、その後また50に戻る、というものです。フィルタの係数はここでは試しにとしてみます。(新しいフレームが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の計算式はこんな感じ
入力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が入力されたとき、
と言うかんじで、四捨五入した結果、前と同じ55になってしまい、これは何回やっても同じだからです。
これが、色の焼き付きの正体だと思われます。
このシミュレーションをしたExcelのファイルをこちらにあげておきます
修正
さっきは でやりましたが、blendの係数を下げていくと、より焼き付きが顕著になります。しかし、もともと 長時間の平均画像を求めることで、動体を消す が目的なので、blendの係数は極力下げたいです。
小数の丸めによってblendが途中で止まってしまわないようにするには、Shader内のテクスチャの色計算で小数が使える様にすることが必要です。
浮動小数点テクスチャ
そういえば、いつもお世話になっている 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で、リアルタイム動体除去を行う
はじめに
前回に続き、ブラウザ内だけで画像処理的なことをしよう、というチャレンジです。今回は動体除去というか、モーションブラーというか、画像の時間平均処理をしてみます。
上の写真は、割と短時間の平均画像です。動いているものが透けているのがわかると思います。画像の平均処理をもっと長時間で行うと、ほぼ動体が見えなくなります。アルゴリズムはあとから説明します。
OpenCVをC++で使うと、普通にMatとかで前回のフレームを保存しておいたり差分を取ったりとお手軽に出来ますが、おなじことをJavaScriptでやろうとすると、必要な枚数画像を展開して、ピクセルを全走査するなど、かなりヘビーな感じになります。今回はブラウザ内で完結させてみるのが目的なので、WebGLのシェーダを使ってみます。
完成品
完成品はこちらで確認出来ます。ページを開いたら、カメラ利用に許可を与えて下さい。上部のバーで除去具合を調整出来ます。ゼロにするとリアルタイムの動画をそのまま出しますが、大きくしていくと、だんだん動体が薄くなっていくのがわかると思います。
動作確認は以下の環境で行っています。
動体除去のアルゴリズム
FIRフィルタによる時間平均処理
動画像を時間にわたって足し合わせて平均を取ると、動かないものは残り、動くものは薄ーくなります。これが基本。FIRフィルタ的な感じ。動画像のローパスフィルタですね。
式で書くとこんな感じ。 がカメラから得られる最新フレームの画像で、 が一個前のカメラフレーム。 が今回表示される絵。
係数の合計が1になる様にします。1より小さいと全体の画像がグレイがかって薄くなり、1より大きいと、白飛びした画像になります。
この手法でちゃんと動体を消すにはタップ(データを記憶して、演算をする)を多くしなければなりません。ブラウザに大量のframebuffer objectを確保しなければならなくなるので、リーズナブルではありません。
IIRフィルタ(的)な時間平均処理
上記のような問題は、画像処理だけでは無く電気回路におけるノイズ取りなどでも似たようなことがあるようで、解決法として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.
さいごに
改めて、完成品はこちらです
今回の手法だと、背景に対してコントラストが高い動体は、焼きついたように長時間残ることもあります。たとえば下のような例。ラズパイのマークが、白い壁に染みついちゃってます(笑)
今回の手法は、厳密に背景と動体を分離して消す、というよりは、「動体をうっすらとぼかすことでプライバシーを守りつつ、背景はほぼリアルタイムのものを撮影する」といった用途にむいていると思います。
焼き付きを抑えるには、「動体がない」瞬間を検知して、テクスチャを全部書き換えちゃう、とかすると良いかと思います。
改善案募集
もちっと綺麗に動体を消せる技術ご存じの方、是非とも教えていただきたいです!
(追記) 改善しました
焼き付きのようになってしまう問題を改善しました。追加で投稿していますので、ご参照下さい。
GPGPU的な用法
FBOのping-pongは、こういった画像の保存という単純な用途以外にも、さまざまな可能性があります。たとえば、大量の点群の
- 速度
- 座標
- 質量
などの「色」ではない情報をテクスチャ風の配列群に仕立ててシェーダに渡し、fragment shaderでガッと計算をした後に、それらを再度オフスクリーンレンダリングという形でテクスチャ的データに書き出して、次のフレームでも利用する、という用法があります。Three.jsの公式サンプルにも作例があります
- http://threejs.org/examples/#webgl_gpgpu_birds
- http://mrdoob.com/lab/javascript/webgl/particles/particles_zz85.html
シェーダでの計算結果を次のフレームで活用するために、FBO ping-pong が使われています。かなり複雑ですが、GPUの計算リソースを活用出来るので、ブラウザでも上記サンプルのようになにやらすごいことが出来ます
WebRTCの動画にThree.jsのポストプロセスでエフェクトをかける
やりたいこと
このブログで何度かにわたって、ブラウザとopenFrameworksを連携させ、oFで加工した映像をWebRTCで扱う、というシリーズをやっていますが、今回は、映像の加工もブラウザ内で完結させてみます。
CSS でもいろいろ出来る
ブラーや明るさ調整は、それぞれ -webkit-filter: blur(100px)
や -webkit-filter: brightness(0.1)
など css としてブラウザで実装されており、非常に高速に動作します。*1
これ以外の、もう少し自分好みのことをしようとしたときも、WebGL の fragment shader を使うことで、やはりブラウザ内で完結させることが可能になります。グリッチとか、ドットとか、歪ませたりとか、そういうの。
そういえば、CSS Shader とかいうのもあったような気もしますが、遠い過去の話ですね。
完成品
こちらで動いています。PCにウェブカムをつないだ状態でこのページにアクセスし、カメラの利用に許可を与えて下さい。
環境
素のWebGLを自力で書くのはしんどいので、Three.js を使います。
動作確認は、以下の環境で行っています。
参考にさせていただいたサイト
HTML の Video タグの描画内容を texture として利用する、Three.js のサンプル
http://threejs.org/examples/#webgl_materials_videoThree.jsを使ったfragment(pixel) shader のチュートリアル記事。中で、ビデオやウェブカム映像に対してエフェクトを掛ける例が出てくる
Intro to Pixel Shaders in Three.js — Airtight InteractiveThree.js のポストプロセッシング (EffectComposer) の解説
three.js wiki - ポストプロセス
特に、一つ目のサイトの、 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 間で共同作業させるシリーズです。
以前こんなものを書きました。
- ycapture で OpenCV での処理結果を Windows のビデオソースとしてブラウザに流し込む - 自習室
- html から node.js を介して oF の画像処理をコントロールし、ブラウザ上で閲覧する - 自習室
これらの記事では、ブラウザとoF間にNode.jsのプログラムを挟み、socket.io と OSC を相互に変換することで、データのやりとりをしていました。もともと以下の理由でそういう構成にしていました。
- ブラウザとnode.js間はsocket.ioでの通信が楽
- node.jsとoF間では、OSCでの通信が楽
- それを単につなげればよいので楽
このときの構成は下図のようになります
しかしこの結果
- 機能上はほぼ意味の無い node.js の変換コードを書かなければいけない。実装コスト増大
という問題も発生しました。
- oFでやろうとしていることを別のプログラムでシミュレートする
- 複数のブラウザやoFのプログラムとやりとりをする
などのケースを想定すると多段構成にする意味もありますが、一対一で通信をするだけなら、ほぼ意味はありません。従って、ブラウザとoFで直接お話しをさせたいです。
いろいろ試した結果、以下の構成が可能であることがわかりました。今回はその方法についてまとめます。
非バイナリデータのみ
WebSocketでバイナリのやりとりも出来る筈ですが、本記事では非バイナリデータ…つまり、数値、文字、bool とそれらをオブジェクト化した物などをやりとりします。いずれバイナリも調査します。
この記事を追うとできるもの
oFの画面でマウスを動かすと、その座標をJSONとしてラップし、WebSocketを介してブラウザに送ります。ブラウザではJSONを復元して、オブジェクトとしてコンソールに出力します。
完成品はこちらにアップしています
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として復元されます。
図にまとめると下のようになります。
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 の使い方
以下のページなどを参考にしました
- jsoncpp を試す - てっく煮ブログ
- c++ - JSONCPP Writing to files - Stack Overflow
- Home · open-source-parsers/jsoncpp Wiki · GitHub
スピードを上げる
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" で再生開始すると、自動的に全画面 + ループになります。
YouTubeだと、お気に入りなりプレイリストに、「繰り返す」ボタンがあります。これをONにしてから全画面再生すれば良い感じです。
それぞれ良いのですが、会社で色んな人に登録してもらうとすると、両方のサイトにアップロードされている動画を扱いたくなります。
おしいやつ
Tumblrで動画を集めておくことはもちろん出来ますが、自動で再生していく機能はありません。
同じようなものがないかなーと探していたところ、dropmark というサービスの存在を知りました。
dropmark
自分も今回の目的以外で使ったことがないのでよくわかっていないのですが、いわゆるブックマークサービスです。その中の特徴的な機能として、ビデオのリンクを貼っておくと自動で再生してくれる機能、と言うのがありました。この記事で知りました
これで解決!と思ったのですが、ひじょーに惜しいことに、リストの最後まで行くと、そこで一覧画面に戻ってしまう仕様です。
そこで、Chrome Extensionを使って、ウェブサービスをハックしてみることにしました。
完成品と使い方
ぎっはぶ
ここに上げてあります
使い方
現状のコードは、すべてのウェブサイトに対して処理をしてしまうような物にになっているので、dropmarkを全画面で再生し続ける専用のPCを用意できる場合は良いのですが、もしご自身のPCで一時的に試してみたい、と言う場合は、利用しない場合は、拡張機能のリスト中で「有効」のチェックをはずしておくと良いかと思います。
ついでに
YouTubeの再生を極力高画質で行いたいので、別途それ用のChrome-extension を入れます。この手の物はたくさんあるのですが、こいつはちゃんと動いていました
Vimeoの方は、ある程度大きくプレイヤーが開いたら、自動的に高画質になるので心配ありません。
設計(?)
主に、以下の三つのことをやっています。
- プレイリストの最後のビデオになったかの判定
- プレイリストの先頭のリンクを取得する
- 最後のビデオの次に、プレイリストの先頭のビデオを再生するようにハック
1. プレイリストの最後のビデオになったかの判定
これは、画面中の "Next Item" ボタンのハイパーリンクを見ることで判別出来ます。動画を指すURLではなく、動画一覧を指すURLだった場合は該当です。
2. プレイリストの先頭のリンクを取得する
プレイリストのHTMLの中身を見て動画の一個目を指すURLを取得すれば良いのですが、実際は「最後の動画を見ている最中に」異なるページのHTMLを参照することになるので、少しトリッキーなことをします。
- jQuery で
<div>
要素を作ってその中にプレイリストのHTMLを丸っと挿入する - その中から該当する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時間くらいで出来て良かったです。会社で運用してみます。
MOVERIOのお出かけ中利用シーン難しさランキング
前説
engadget さんにこんな記事があって
おっ、と思って脊髄反射で申し込んだら、当選したので、行ってきました。そのときのイベントレポート記事が上がっております。
写真のどこかに、私も写ってますね。私もEngadgetさんの記事が上がる前にレポートを書いておいたらリンクを貼ってもらえたはずでしたので、悔やまれます。
そう、そういうわけで EPSON 様から MOVERIO BT-200AV をお借りしてきました。
半年貸してやるから、7本ブログ書け
とのことです。全力で楽しんでいきましょう。開発にもトライしてみる予定です。
うまく撮れました。
MOVERIOについて知る
MOVERIOは、公式には、以下の様なコンセプトの商品のようです。以下公式サイトから抜粋
- 大画面を楽しむ
- いつでもどこでもパーソナルシアター
- レコーダーやスマートフォンの映像を楽しむ
- 高画質・高音質のモバイルビューアー
- 誰にも邪魔されず、自分だけで楽しめる
- 好きな姿勢で、好きな場所で
- 飲食しながらでも楽しめる
などなど。要するに、映像を見る装置です。私もBT-200 を使ってアニメを数本見てみましたが、思ったよりは普通に見られます。
一方で、こうもうたわれています。
Androidなので、この眼鏡スタイルを活かしたアプリ体験はいろいろ探索していけそうです。自分もいくつか作ってみたいアイデアを考え中ですので、このブログで報告できたら良いなと思います。
今回は 開発ネタは置いておいて 、メインの「映像を見る」機能についてちょっと考えてみます。どんな時に使えると嬉しいか。そして、それは実際可能なのか。
MOVERIOのお出かけ中利用シーン難しさランキング
考えてみました。難しそうだなーと思った順。
- 通勤電車
- ショッピングモール
- ディズニーランド
- 飲み屋
- ファストフード店
- 喫茶店(特にスタバ)
- お散歩中
- 公園・河川敷
- ジム
- 大学構内
- 野球場・サッカー場・競馬場
- 飛行機・新幹線・長距離バス
前提
前提として、「出先で映像を見る」という行為をそもそもしそうなシーンを想定しています。言い始めればもっといくらでも候補を挙げられそうですが、
- コンテンツを見る、という行為をスマホ等でもそもそもしないところは除外
- 論理的には上記に含まれますが、「1人で映像を見る」という行為が不適切な場合も除外
としています。MOVERIOのまっとうな使い方を考えてみよう、と言うことです。
解説
簡単そうな順に簡単に考察をしてきます
12. 飛行機・新幹線・長距離バス
公式もおすすめの利用法。手にタブレットを持って持ち上げ続けたり、首を下に向けながら映像を見るのは結構しんどい。自由な姿勢で両手を空けられるモベリオ的には相当妥当な利用法!
車の中でタブレットで動画を見ていると弱い人だと車酔いしますが、「モベリオだったら車酔いしない!」みたいな展開があったりしないでしょうか。これは要調査です。
11. 野球場・サッカー場・競馬場
ラジオを聞きながら野球や競馬を見る、というのは良くある話ですが、それのTV中継版。対象物が遠距離で焦点のズレも問題なさそう!
と思ったのですが、TV放送を見る方法ってあるんでしたっけ…?
まぁその、待ち時間に見るとか…
10. 大学構内
ジャンル的には公園・河川敷に近いですが、大学内、特に理工系のキャンパスなら 「実験かな」と思われるので普通にスルーされること間違いなし!
9. ジム
バイクをこぎながらの動画閲覧は鉄板ユースケースでしょう。ジムによってはスタッフさんに怒られるかもしれないので、真摯な態度で(EPSON社員になったつもりで!)モベリオの良さをアピールしていきましょう。タブレットで動画見てる人なんていくらでもいますしね。 最新のスポーツマンはこれ!くらいの堂々たる態度 を見せられるかがポイントになりそうです。
8. 公園・河川敷
この辺りから、少し攻めたユースケースになります。そもそも、公園でひとりで動画を見るシーンが余り思いつきませんが、「変な人って思われないかな??」と少しドキドキしながら眼鏡を外で掛ける、わりとソフトな入門編という感じでしょう。たまに通りかかる人に、全く気がつかれないか、変わったサングラス?と思われるか、社交的なおじさんに話しかけられるか、くらいで済みそうですね!
7. お散歩中
公園や河川敷でこそこそ見るのにスリルを感じなくなったら、次は出歩いてみるのも良さそうです。ただしおそらく結構危険ですので、がっつり映像を楽しみたい方にはおすすめできません。 普段からアニメを2本同時見したり、プログラミングしながらアニメを見ているような玄人の方向け の用法です。
家でビール飲みながらアニメ見てたらお腹に肉が付く一方ですので、お散歩しながら健康にアニメ鑑賞は、2015年のニューオタクスタイルとしてアピールしていけるかもしれません
6. 喫茶店(特にスタバ)
要Macbook。
スタバにおけるドヤリングはもはや社会現象とも言えるかと思います。たとえTwitterを見ているだけでも、Mac in starbucks is very おしゃれ。ただのドヤリを超える新世代のドヤリとして、 モベリオドヤリング を提唱してきましょう。店員さんと仲良くなれるかも!
5. ファストフード店
公式でも結構頭の方でおすすめしている、食事をしながらの動画鑑賞。両手がふさがっていても動画が見れる!モベリオは 録り溜めた深夜アニメの消化に追われる現代人には欠かせないデバイス と言えそうです。忙しいオタリーマンなあなた(誰)、ためしに、殺伐としている吉野家で、黙々と大盛つゆだくをかっこみながら幸腹グラフィティを見る、というのはいかがでしょうか。
4. 飲み屋
今回のモニターで借りている人みんなで、塚田農場で MOVERIOオフ会 なんかどうでしょうか。良く訓練された浴衣ギャルの塚田農場店員は、きっと笑顔で「えーそれなんですかぁー?」と聞いてくれるに違いありません。 これぞ商機。
3. ディズニーランド
プーさんのハニーハント、2時間待ち!みたいな時こそ、モベリオです。初めの1時間くらいはなんとかたわいもない話で持ちますが、次第に間が持たなくなってきます。こんな時こそモベリオ。なんなら2人で互いにモベリオ。サングラス代わりにもなってちょうど良い感じです。アニメを一話見終わる度に感想を語り合えば、2時間でも3時間でも待てそうなものです。
ディズニーランドはリア充の聖地かと思いきや、コアなネズミファン達の強烈なオタク感はすさまじいものがあります。ちょっと変わった眼鏡を掛けているくらいでとやかく言われる筋合いはありません。どんどんかけましょう。
2. ショッピングモール
いつまでも終わらない奥さんのお買い物に付き合うのに疲れたら、最後の力を振り絞り最高の笑顔で理解ある夫を演じつつ「終わったら呼んでー」と奥様を送り出し、フードコートに腰を下ろしてモベリオを取り出します。
しかしそこは大量の親子連れであふれかえっています。
通りかかった子供「なんかかっこいいめがねだよー」
そのお母さん「しっ、見るんじゃありません」
なんて、ほほえましい風景もあるかもしれませんね。
1. 通勤電車
個人的に最強のユースケースがこれ。
満員電車、周りはおっさんだらけ、顔近いっす、あなたの後頭部なんか見たくないっす。いやおねえさん、別にあなたのこと見つめてないっす偶然そちらしか向けないだけです! 手が挙げられなくてパズドラもできねぇ! なんて日常も、モベリオで一変します。
乗り込む前に再生開始。満員電車の不快な30分が快適アニメタイムに!
というシナリオなのですが、同時に変な人と思われるリスクもひじょーに高い。痴漢えん罪怖いです。あまり世の中に普及しているデバイスというわけでもないので、モベリオかけてるだけで「この人ちかんです!」と言われるリスクが無いわけではありません。この季節、花粉症もあってマスクをしてたりもしますので、より一層怪しいです。
女性ならそんな心配もありませんので、ぜひ試してみていただきたいです。
最後に
途中のディズニーやショッピングモールは冗談として、通勤電車ユースケースは(マスクが取れた頃に)勇気を出してトライしてみたいです。かなり実益があると思われる用途が同時に非常に危険度が高い、というのはなんともモベリオという商品の難しさを表している気がしますが、そこんとこぶち破っていきたいです。
最初に紹介したブロガーイベントでは、エプソンの中の人が電車の中で使っている様子をちゃんと動画にしてレポートしてたりして、がんばるなぁと思ったのですが、そういえば割と美人の女性の社員さんでした(笑)
MOVERIOは電車の中でも使える!(※ただし美人女性に限る) ってわけですね。
冗談はほどほどにして
開発ネタと並行して、「こんなところで使ってみた」レポートも、ブログとして書いていこうと思います。
フリー素材最高
今回はじめて自分のブログでフリー写真素材を使ってみました。フリー素材を使った記事感が見事に出てますね!