この記事は、Three.js Advent Calendar 2016 5日目の記事です。
ほんの2ヶ月前・・2016年10月のことでしょうか。PSVRの登場とともに世間を賑わせた、「プレイエリアの外です」現象は。
一番有名になった画像については、「スカートの中を覗こうとした場合に、おぱんてぃの場所を防ぐように表示されている」あの警告でしょう。
実際に警告が表示される条件には、「プレイヤーを外から撮影している子機カメラの撮影範囲外になる」という事だそうですので、別にぱんつを覗く行為が禁止されている訳ではなかった、らしいのですが、画像のインパクトのお陰で広まったようです。
という予防線な駄文はどうでもいいですね。自分がやりたいのは、まさしくアレです。「パンツを覗こうとしたら、パンツの位置に警告が表示される」、それはそれは人類男子にとって何の役にも立たない機能です。
役に立たないことこそ全力。最高ですね。
では、その「自分が実際に実装されていたら腹が立つこと」を実現するために頭を巡らせましょう。
当然、Three.js(javaScriptライブラリ)を使うので、編集はメモ帳、実行はブラウザです。
##この記事で分かること
見る人がいなくなる前に、この記事では何が分かるかを先にご説明します。
1・Three.jsでボーンの変形後の位置(ワールド座標)を知るためには、どうしたらよいか
2・Three.jsでの3D上の座標を、2D画面座標(スクリーン座標)にするにはどうしたよいか
この2点を、やたら長たらしく書かせていただきます。
完成品はこちらになります
音が出ます。注意!
##アプローチそのゼロ。
ぱんつがみえている画像を機械学習させ、Three.jsのrendererが描画した後にぱんつが見えてる判断をかませ、見えていると判断が帰ってきたら、警告を表示する。
おぉ、そのためにパンツが見えている画像が必要です。すぐ用意してください。2次元オンリーでダブリを許可せず1万枚ほどでしょうか。用意ができたらzipに固めて、 こちらまでお送りください。
そんな方法が実現できたら、ココでは書かないですね。Three.jsらしく考えましょう。
##正しいアプローチ(多分)
###そもそも、パンツが見えているとは、なんだろう。
哲学ですか。違います。「パンツが見えている」ということを、プログラムの仕組みの上で検知するには、どうすればよいでしょう。
####ローアングルである、ということ
これは大前提ですね。ローアングルということは、「低い位置から」「見上げる」ということです。
では、「低い位置」とは、どこにあたるでしょう。「空から見た低い位置」なら、地上全部がそうです。「どこかと比べて低い位置」という、基準が必要です。
少なくとも、「目の位置より下(のスカートの中)に、パンツがある」のなら、それはパンツは見えていないでしょう。ならば、「パンツが見える低い位置」とは、どこか!
私は今回、「パンツの履かれている場所よりも、低い地点」から見ていることで、それを実現できそうだと思いました。「位置」はコレでいきましょう。
では次。「見上げる」ということは、どうなるでしょう。
こちらは簡単です。【「目の位置」より、「見ている場所:見ようとしている場所」が「高い」】場合に、「見上げている」といえるのでは無いでしょうか。
「目の位置」は、Three.jsで言えば、「camera.position」にあたります。「見ようとしている位置(注視点といいます)」は、「camera.lookAt(vector3)」で、「セット」することができます・・・・・・
…そう。「セット」なら、コレでできるんです。反対に、「cameraから注視点を得る」のは、実はコレ難しいんです(多分。やったことない)。ただ前もって「注視点が明確になっている」のであれば、この方法が一番ラクで確実です。
ではアプローチを変えて、「見ている場所」ではなく、「見ている角度」で考えましょう。「見上げる」とは、「目線が上を向いている」という言葉にも置き換えることができます。
こちらもやっぱり単純です。 Three.jsのCameraは、視線といえる角度が rotation に入っています。 「見上げる」ための角度は、 rotation.x にあたります。 コレが、水平なら0、真上(90度)なら、約1.57、真下なら約-1.57です。
この約1.57というのは、パイ…PI、円周率から導き出された値です。
1周=360°が2PI、半周=180°が1PI、ならば90°(半周の半分)なら1/2PI=1.57 となります。
このXの値がゼロより大きければ、「水平より上」なので、まさに「見上げている」ことになります。
(上のCamera.lookAtを行うと、ややこしい行列計算が行われ、このrotationの値が更新されます)
さて、これで「カメラの位置と、その目線」は、わかりました。ですが、大切なことが欠けています。
###パンツはどこにある!?
哲学ですか。違います。先程、「ある地点を基準として、低い位置」と書きましたが、その「基準となる地点」・・すなわち、あの日見た「ぱんつ」の位置を、我々はまだ知りません。
きちんとした言葉で言い換えると、「キャラクターの股(腰)の位置は、どう検出したらよいか」です。ここから先は、Three.jsのMMDインポーターのサンプル(webgl_loader_mmd_audio.html)を改変することで進めて行きたいと思います。(Takahiroさんとても良いサンプルを汚してしまってごめんなさい)
このMMDモデルにはボーンが仕組まれており、踊りまくってくれるのは見ての通りです。踊りまくるということは、【動きに合わせて、腰の位置も変わる】ということになります。さぁ困った。動きまくるような物体の、さらに変形された後の位置なんて、知ることができるのでしょうか?
//ボーンの変形後の位置を取得する
var PantuPos = new THREE.Vector3(); //返却用の入れ物、兼少しだけ腰から下げた位置を取りたい
PantuPos.set(0, -3, 0 ),
mesh.skeleton.bones[9].localToWorld(PantuPos); //ボーン9番が、下半身の親ボーンとなる
そのものズバリ、この「localToWorld」というメソッドで、モーションによる変形後の位置、から更に、「ボーンを基準に、とある位置まで進めた位置」を取得することができます。yに対して-3を適用させているのは、あくまでコレが「腰」のボーン位置だからであり、パンツ位置にするには位置が高すぎるための、調整の値です。
これが男達が夢にまで見た、「パンツの位置」です!我々は勝利した!!(何に?)
webgl_loader_mmd_audio.htmlが手元にあるなら、改変してみしましょう。書き換えるのは、 function render() のメソッドの中です
function render() {
if ( ready ) {
var delta = clock.getDelta();
helper.animate( delta );
helper.render( scene, camera );
////ここから書き加え
var pantsPos= new THREE.Vector3(); //返却用の入れ物、兼少しだけ腰から下げた位置を取りたい
pantsPos.set(0, -3, 0 ),
mesh.skeleton.bones[9].localToWorld(pantsPos); //ボーン9番が、下半身の親ボーンとなる
//カメラより高い位置にパンツがあったら、見えたという判断!
if(pantsPos.y > camera.position.y && camera.rotation.x > 0) { /** 見えた!! **/}
} else {
renderer.render( scene, camera );
}
}
##邪魔なものを表示しよう。
さて。目的を見失わないようにしましょう。今回の目的は、「パンツが見えていたら、パンツの位置に警告を表示する」です。残念ながら、我々はこの輝かしいパンツを隠さなくてはいけません。
心苦しいですが、パンツを隠すための物体を作りましょう。作りたくない。作りましょう。
今回はてっとり速くすませるため、HTMLのDIVでやってしまいましょう。
その実現のためには、どうやらもう少しステップがあるようです。
###パンツはどこにある?(スクリーン上で)
先程、私は「パンツの位置がわかった!勝利だ!!」と宣言しました。先程のものを、Firefoxなどのブレークポイントが置ける環境で実行して、pantsPos の位置を確かめてみてください。
きっと、x=0, y=10程度という、非常に小さい値が帰ってきます。
もうお気付きの通り、コレをそのままHTMLのDIVのTopとLeftにセットしても、無駄です。左隅にちょこんと置かれてしまいます。
「仮想3次元空間での位置を、我々が目にしているHTML(2D空間)の位置に戻す」という作業が必要になります。これを「スクリーン座標への変換」と言ったりします。
//3次元位置を2D画面上の位置に変換する
pantsPos.project(camera);
pantsPos.x = (pantsPos.x * window.innerWidth * 0.5) + window.innerWidth * 0.5;
pantsPos.y = -(pantsPos.y * window.innerHeight * 0.5) + window.innerHeight * 0.5;
このメソッドの細かい説明は、14日のカレンダーにて説明させてもらいます。よろしければどうぞ。
ともかく、Vector3.project(camera) を行い、帰ってきた値に画面サイズを乗算すれば、2D画面上での位置に変えることができます。
さぁあと一歩です。2D画面上でのパンツの位置もわかりました!後は、隠すDIVを用意して、パンツの位置に載せましょう!
###DIVをパンツの位置に置こう(仮完成)
three.jsを触ろうと思っている人は、HTMLにも精通していらっしゃると思うので、もうこの辺は細かい説明はいらないと思います。
さくっとDIVを追加して、表示/非表示まで行いましょう。
//上の方のどこか。script読み込みの前あたり
<div id="keikoku" style="position:absolute; width:200px; height:100px; color: #FFFFFF; background-color: #000000; text-align: center;">
<h2 style="line-height:40px;">プレイエリアの外です</h2>
</div>
//上の方のどこか。init()の前あたり
var keikokuDiv = null;
////中略
function render() {
if ( ready ) {
//警告DIVを定義
if(keikokuDiv == null){ keikokuDiv = document.getElementById("keikoku"); }
var delta = clock.getDelta();
helper.animate( delta );
helper.render( scene, camera );
//見えている判断チェック!
var pantLook = false;
var pantsPos= new THREE.Vector3(); //返却用の入れ物、兼少しだけ腰から下げた位置を取りたい
pantsPos.set(0, -3, 0 ),
mesh.skeleton.bones[9].localToWorld(pantsPos); //ボーン9番が、下半身の親ボーンとなる
//カメラより高い位置にパンツがあったら、見えたという判断!
if(pantsPos.y > camera.position.y && camera.rotation.x > 0) {
//3D座標をスクリーン座標に
pantsPos.project(camera);
if(pantsPos.z < 1.0){
pantLook = true;
pantsPos.x = (pantsPos.x * window.innerWidth * 0.5) + window.innerWidth * 0.5;
pantsPos.y = -(pantsPos.y * window.innerHeight * 0.5) + window.innerHeight * 0.5;
keikokuDiv.style.top = pantsPos.y - 50 + "px";
keikokuDiv.style.left = pantsPos.x - 100 + "px";
}
}
if(pantLook){keikokuDiv.style.visibility="visible";}
else {keikokuDiv.style.visibility="hidden";}
} else {
renderer.render( scene, camera );
}
}
うーん!!??
ちょっと判定厳しすぎやしませんかねコレ!!??
明らかに「ここなら見えてない」という感じでも、ボックスが出てきます。疑わしきは罰するです。厳しい世界です。
どうやら、判定が単純過ぎたようです。これでは夢がありません。もう少し良い判断はできないでしょうか。
今回の記事の主題である「ボーンの変形後の位置を得る」と「3D座標を2D座標に変換する」の説明は、以上で終了となります。ここから先は、ぱんつとより向き合う覚悟がある戦士のみ、足を踏み入れてください。
##(Advanced)より良いパンツ判定のために
ここから先は、あくまで一例です。「ぱんつが見えている/見えてない」という事を、もっと深く追求してみましょう。
簡単な解としては、「ローアングルさが足りない!」と思い、カメラとぱんつ位置までのY座標で、大きくぱんつが上に行っていたら良いのではないか、ということが挙げられます。これは単純に、
if(pantsPos.y > camera.position.y + 5 && camera.rotation.x > 0) {
//後略
これは一瞬うまくいったように見えますが、駄目なのです。何が駄目かというと、
「キックで足を上げた時のように、姿勢によって見えるタイミングがある」という時に対応できないのです。足を大きく振り上げ、スカートがめくり上がったときのように、下手をすれば「ぱんつよりカメラが上に有っても、見えるタイミングはある!」ということに、気が付きますでしょうか。
//なんで見えるんだよパンツ・・・隠れろよ・・・見たくなんだよパンツなんて・・・
//・・・はっ!?俺は一体何を!?
ということで、ぱんつとカメラの高さによる判別は、残念ながら欠陥が多そうです。別のアプローチを考えましょう。
こういう時には、ポーズ人形やプラモデルやフィギュアをぐるぐるして考えてみるのがいいでしょう。カメラ位置は当然、自分の目です。これを我々の業界では、マークワン・アイボール(Mk.1 Eyeball)カメラと言います。嘘です言いません。
ふぃぎゅあぐるぐるの結果、私は「ぱんつが見えている判定」を、下記のように変更することにしました
###仮決定版!これが新・パンツ判定だ!
1・カメラとパンツと、左右の「膝」、それぞれの位置を割り出す
2・カメラとパンツ (CP), カメラと左膝 (CL)、 カメラと右膝 (CR) 、それぞれの距離を測定する。
3・CPより、CL、またはCRのどちらかが近いとなった場合、「見えている」判定とする。
「膝」であれば、足を振り上げた時などに、当然くっついて動いてきます。カメラに向かって蹴っているところを思い浮かべればわかると思います。カメラは貴方の目です。蹴ってくれ。俺を蹴ってくれ。ついでに踏んづけて・・・そこまで具体的に思い浮かべる必要はありません。
ともかく、「目」と「パンツ」の間に、左右どちらかの足が膝がある場合に、ぱんつが見えていると言えるのではないか?という仮設を立て、コードを書いてみることにしました。
では実際に試してみましょう
var cp = new THREE.Vector3();
var cl = new THREE.Vector3();
var cr = new THREE.Vector3();
//1・カメラとパンツと、左右の「膝」、それぞれの位置を割り出す
cp.copy(pantsPos); //既にパンツの位置はわかっている!
mesh.skeleton.bones[41].localToWorld(cl); //左ひざ
mesh.skeleton.bones[73].localToWorld(cr); //右ひざ
//2・カメラとパンツ (CP), カメラと左膝 (CL)、 カメラと右膝 (CR) 、それぞれの距離を測定する。
//ぱんつとカメラまでの距離を測定するために、ベクトルの【差】を検出
cp.sub(camera.position);
//同じように、膝に対しても行う
cl.sub(camera.position);
cr.sub(camera.position);
//3・CPより、CL、またはCRのどちらかが近い場合、「見えている」判定とする。
if(cp.length() > cl.length() || cp.length() > cr.length() )
//後略
Vector3.sub はベクトル同士の引き算、 length() で距離を計算して、返してくれます。
んーー、100点とはいきませんが、あきらかに先程より良く見えます。
これで今回は、ひとまず完了とさせてください。
以下、最終版となった、 render() です
function render() {
if ( ready ) {
//警告DIVを定義
if(keikokuDiv == null){ keikokuDiv = document.getElementById("keikoku"); }
var delta = clock.getDelta();
helper.animate( delta );
helper.render( scene, camera );
//見えている判断チェック!
var pantLook = false;
var pantsPos= new THREE.Vector3(); //返却用の入れ物、兼少しだけ腰から下げた位置を取りたい
pantsPos.set(0, -3, 0 ),
mesh.skeleton.bones[9].localToWorld(pantsPos); //ボーン9番が、下半身の親ボーンとなる
var cp = new THREE.Vector3();
var cl = new THREE.Vector3();
var cr = new THREE.Vector3();
//ぱんつとカメラまでの距離を測定するために、ベクトルの【差】を検出
cp.copy(pantsPos);
cp.sub(camera.position);
//同じように、膝に対しても行う
cl.set(0,-1,-4); //膝ぴったりではなく、膝のやや後ろにする
mesh.skeleton.bones[41].localToWorld(cl); //左ひざ
cl.sub(camera.position);
cr.set(0,-1,-4);
mesh.skeleton.bones[73].localToWorld(cr); //右ひざ
cr.sub(camera.position);
//カメラより左右の膝のどちらでも近いほうにあったら、パンツが見えているとする!
if(cp.length() > cl.length() || cp.length() > cr.length() )
{
//3D座標をスクリーン座標に
pantsPos.project(camera);
if(pantsPos.z < 1.0){
pantLook = true;
pantsPos.x = (pantsPos.x * window.innerWidth * 0.5) + window.innerWidth * 0.5;
pantsPos.y = -(pantsPos.y * window.innerHeight * 0.5) + window.innerHeight * 0.5;
keikokuDiv.style.top = pantsPos.y - 50 + "px";
keikokuDiv.style.left = pantsPos.x - 100 + "px";
}
}
if(pantLook){keikokuDiv.style.visibility="visible";}
else {keikokuDiv.style.visibility="hidden";}
} else {
renderer.render( scene, camera );
}
}
#おわりに
いかがだったでしょうか。
貴方の心のなかに、ぱんつは焼き付きましたでしょうか。
今回は、ぱんつが見えている判定に、膝を使いました。貴方ならば、もっと良い方法が浮かぶかもしれません。ぱんつを見るとは、ぱんつが見えているとはどういう事なのか。ぱんつとは何か。
ぱんつ。それは、多くの場合、布に過ぎません。1枚の布切れです。
ですが、私たちは、ただの「パンツ」が見たいのでは、なかったはずです。
私たちが見たかったもの、知りたかったもの。それは「スカートの中にある、ぱんつ」という概念です。
仮想3次元空間で表現された女の子のスカートの中に無ければ、ぱんつに意味はありません。ココまで掘り下げることもなかったでしょう。
ぜひ、貴方だけの「ぱんつが見えている判定」ロジックを作ってみてください。ぱんつと真剣に向き合ってください。ぱんつを想い、ぱんつについて本気出して考えてみてください。そのとき、きっとぱんつは答えてくれます。
そして、これだけは忘れないでください。
ぱんつを覗く時、ぱんつもまた、こちらを覗いてるということを・・・。
この記事は、Pants Calendar 2016 4日目の記事では、ありません。誤解なきよう。