はじめに
みなさんどうもこんにちは。XR Interaction Engineer のharuya_i と申します 😀
ここでは、いま東京タワーで絶賛開催中🗼、そして12月26日(土)から新宿小田急百貨店で開催予定のcode name: WIZARDで体験できる魔法の一部である、妖精魔法をMagic Leap でどのように実装したかを書かせていただいております。
この記事は、Magic Leap Advent Calendar 2020 の9日目でございます。
目次
1. code name: WIZARD について
2. プレイヤーを魔法使いにさせる術
2-1. あなたが魔法を当てたい物に手を伸ばしたその時、果たしてどのような線が描かれる?
2-2. オブジェクトの選択はコライダー? それとも 、 、 、
2-3. 「無意識で使えてしかも楽しい」を目指した魔法のデザイン
3. さいごに
#1. code name: WIZARD について
code name: WIZARD について
「code name: WIZARD ってなんぞや」と思われる方がいるかと思いますのでサラリと説明させていただきますと、以下公式サイトの言葉をお借りしますと
「現実世界で、魔法使いになれる!?」がコンセプトの、XR謎解きエンターテインメント!!
会場を実際に歩いて本格的な謎解きに挑戦するアナログ謎解きパートと、Magic Leap1を使ったMR謎解きパートをお楽しみいただけます!
と、いうものでして「プレイヤーに魔法使いになって欲しい」という思いで作られたXR謎解きコンテンツです。
そしてこのcode name: WIZARD で使っているデバイスがMagic Leap です。
👇 のGIF画像が実際の体験イメージです
プレイヤーは各妖精から提示される謎や試練を、妖精から与えられる魔法を駆使しながらクリアしていきます。
本記事でお話しするのは、イメージの途中にも出てくる妖精魔法(以下画像)の実装方法についてです
このピンクとかグリーンのパーティクル状の魔法です。
魔法陣の出ているプレイヤーの手から魔法が発射されます ✋
#2. プレイヤーを魔法使いにさせる術
さて、ここからはどのように妖精魔法を実装したかを具体的にお話しします 😀
code name: WIZARD の妖精魔法では、プロトタイプ作成時のさまざまな苦節を経た結果、魔法を飛ばすオブジェクトの選択と魔法の実行を同時 におこなっています。
そのため、妖精魔法を構成している要素は大きく以下の3つとなります。
① オブジェクト選択用のベクトルを計算する
② 魔法がオブジェクトに当たるか否かおよびどのオブジェクトを選択するかを計算する
③ 目標の場所に向かってどのような魔法を飛ばすか
これから話す2-1, 2-2, 2-3 の3章は、これらの構成要素にそれぞれ紐付けてお話ししています。
また、「プロトタイプ作成時のさまざまな苦節」につきましては、3. さいごにで説明させていただくMagic Leap Meetup vol.2 in Japan にてその詳細もお話しできればと思っております。
##2-1. あなたが魔法を当てたい物に手を伸ばしたその時、果たしてどのような線が描かれる?
例えば、あなたが「どんな物でも引き寄せることのできる魔法」を使えるとしましょう。
手のひらをかざすだけで、どんな物でもあなたの近くに引き寄せることができます。
あなたがテレビを観ようとリモコンに魔法を当てたい時、あなたの手とリモコンの間にはどのような線が描かれるでしょうか?
もっと端的に言えば、あなたの手からどのような線を計算すればリモコンに当てることができるでしょうか?
この疑問に対する1つの最適解が、Leap Motion社のブログに書かれた1つの記事で紹介されています。(この記事をはじめ、このブログで書かれている記事はとても面白く参考になることが多いので、XRに関わる方々には特にオススメいたします 😌)
この記事では「VRにおける離れた物体とのインタラクションに関するデザイン」について書かれています。
そのなかで、手を入力にして遠くのものを選択する場合「単に手の向きを使うのではなく、頭(HMD)からおおよその肩の位置を推測してその肩から手のひらを通る線を使う」という旨の記述があります。
(https://blog.leapmotion.com/summoning-superpowers-designing-vr-interactions-distance/ より)
実際に、手の向きや頭(Magic Leap)の向きをもとに計算したことがありますが「手の向きだとノイズが多すぎる上に全然狙ったところに飛ばない」「頭の向きだとある程度狙ったところに飛ぶが、狙ったオブジェクトが体に対して正面にないときに当たらない」
といった問題がありました。
code name: WIZARD でもLeap Motion社の記事で紹介されているように、頭の位置からおおよその肩の位置を推測してその肩と手を結ぶベクトルを利用することで、狙った物と手を結ぶおよそ理想的なベクトルを得ることができました。
##2-2. オブジェクトの選択はコライダー? それとも 、 、 、
さて、プレイヤーの肩の位置を推定することで① オブジェクト選択用のベクトルを計算する ことができました。
次は、② 魔法がオブジェクトに当たるか否かおよびどのオブジェクトを選択するかを計算する 方法についてです。
code name: WIZARD では当初、先ほどのLeap Motion社の記事と同じように、手から出る選択用のベクトルの方向にレイを飛ばし、そのレイに当たったコライダーを持つオブジェクトを選択するという方法を使っていました。
しかし、この方法だといくつか問題があったのです。
1つは、オブジェクトが重なった時に奥のオブジェクトに当てにくくなるということです
図で説明します。
以下のようにcode name: WIZARD を体験中のマジクリくんの前に、コライダーを持つオブジェクトAとオブジェクトBがあったとします。(以下図1参照)
図は上から見た図であり、XZ平面になります。
コライダーを当たり判定とした場合、コライダーが小さすぎると魔法を当てることが困難になるため、オブジェクトがよほど大きくない限り実際のオブジェクトのメッシュよりもマージンをとって大きくする必要があるでしょう。
その際、マジクリくんの手に対し、狙っているオブジェクトBの手前にオブジェクトAが重なるケースが起こりえます。
この場合、たとえマジクリくんが魔法の天才で完璧にオブジェクトBの中心に魔法を飛ばせたとしても、当たり判定はオブジェクトAになり、魔法はオブジェクトAに飛んでしまいます。(以下図2参照)
このように、狙っているオブジェクトの手前に他のオブジェクトがあると、たとえコライダーが完全に重なっていなかったとしても、手前のオブジェクトの方が当たりやすくなります。場合によっては、奥にあるオブジェクトに当てることができなくなる可能性が高くなります。
これではプレイヤーの魔法の放ち方の習得に影響が出るでしょう。
原因はコライダーの大きさにマージンを持たせていることにありますが、マージンが小さくなることはすなわち魔法が当たりにくくなることにつながります。
もう1つ、オブジェクトの選択でコライダーを使う問題は**オブジェクトが遠い場合、当てるのが一気に難しくなる**ということです。
こちらも、マジクリくんに実践してもらいましょう。
オブジェクトとの距離が近い場合、オブジェクトに魔法を当てることは簡単です。
魔法を当てることのできる範囲は大きく、マジクリくんも安心です。(以下図3参照)
しかし、オブジェクトとの距離が遠い場合、魔法を当てることのできる範囲は非常に小さくなります。(以下図4参照)
プレイヤーは体験中に動き回ることができるため、狙いたいオブジェクトが他のオブジェクトの影に隠れることやオブジェクトが遠くなることは十分に考えられます。
他にも、コライダーの内側で魔法を放ったときの処理などが厄介になりそうです。
オブジェクトAのコライダーの内側だけどオブジェクトAを狙ったときや、オブジェクトAのコライダーの内側だけどオブジェクトBを狙ったときの区別など。処理が複雑になりそうな予感がプンプンします 😶
結論として、オブジェクトを選択する方法に内積を使いました。
内積を用いたオブジェクトの選択方法の流れは以下になります。
① オブジェクト選択用のベクトルと妖精魔法の効くオブジェクトと手を結ぶベクトルを正規化して内積をとる
↓
② 内積の値が当たり判定の閾値よりも大きいか小さいかを比べる
↓
③ 閾値より大きいオブジェクトのなかで一番値の大きいオブジェクトに妖精魔法を飛ばす。全て閾値より小さい場合外れの妖精魔法を飛ばす
実際のコードを一部抜粋すると以下になります。
// ①〜③ のうち、実際に魔法を飛ばす前の部分
// 魔法が当たっているか否かと、当たったオブジェクトを返す関数
private ( bool, ITickyMagicableObject ) CalculateMagicBeamBasedInnerProduct( MagicBeamStatus status, float angleThresholdToTickyMagic )
{
MLHandTracking.HandType whichHand = status.whichHandVisualizer.handType;
List<ITickyMagicableObject> shouldTickyMagicTickyMagicableObjects = TickyMagicableObjectsManager.Instance.tickyMagicableObjectsInScene
.Where( tickyMagicableObjectInScene => Vector3.Dot( status.direction, (tickyMagicableObjectInScene.GetMyTransform().position - status.startPoint).normalized ) >= angleThresholdToTickyMagic )
.ToList();
bool isMagicBeamHit = shouldTickyMagicTickyMagicableObjects.Count() != 0;
ITickyMagicableObject shouldTickyMagicTickyMagicableObject = null;
if (isMagicBeamHit)
{
List<float> shouldTickyMagicTickyMagicableObjectsInnerProduct = shouldTickyMagicTickyMagicableObjects
.Select( tickyMagicableObject => Vector3.Dot( status.direction, (tickyMagicableObject.GetMyTransform().position - status.startPoint).normalized ) )
.ToList();
int index = shouldTickyMagicTickyMagicableObjectsInnerProduct.IndexOf( shouldTickyMagicTickyMagicableObjectsInnerProduct.Max() ); // 一番内積の値が大きいやつ -> 一番魔法ビームの角度に近いやつ
shouldTickyMagicTickyMagicableObject = shouldTickyMagicTickyMagicableObjects[index];
}
return ( isMagicBeamHit, shouldTickyMagicTickyMagicableObject );
}
内積を使うことで先ほどコライダーを用いたオブジェクトの選択方法の2つの問題も解決できます。
先ほど問題になっていた、狙っているオブジェクトが他のオブジェクトの奥にある場合については、もし以下図5のようにオブジェクトAが内積の閾値の範囲内(魔法の有効範囲内)に入っていたとしても、③でおこなっている内積が一番大きいオブジェクトを1つ選択することで解決します。
マジクリくんの魔法が正確である限り、魔法は狙ったオブジェクトに当たります。
次に、オブジェクトの距離が遠い場合の問題も解決できます。
コライダーをもとに魔法を飛ばした場合は、魔法の有効範囲自体がコライダーに左右される一方、内積をもとに魔法を飛ばすと、魔法の有効範囲を固定することができます。
無論、遠い位置からそれぞれのオブジェクトを狙い分けることは依然として難しいですが、魔法が当たらないということは少なくなります。
しかし一方で、コライダーの時にはなかった問題も起こりました。
それは、オブジェクトが十分大きい場合にかえって上手く狙えないことです。
オブジェクト選択用のベクトルと妖精魔法の効くオブジェクトと手を結ぶベクトルの内積をとる際、妖精魔法の効くオブジェクトの中心座標を参照します。
そのため、オブジェクトの中心座標から離れた場所を狙った場合「魔法を当ててるはずなのに当たらない」という現象が起こります。
この問題に関しては、的専用のオブジェクトを用意することで解決しました。
オブジェクトが非常に大きくなるケースは限られているため、この方法で対処できました 😉
このように、code name: WIZARD で魔法を使うために内積を使ったオブジェクトの選択を採用しました。
もちろん、コライダーを用いた方法も内積を用いた方法もお互いメリットデメリットはあるかと思いますが、結果として複雑な処理になっていない内積を用いた方法は、魔法を使うのに合っていたのだと思います。
「sin、cos とかいつ使うんだよ(笑)」「ベクトルとか社会人になってからいらなくね?」と思ってる学生の皆さん。気持ちはよくわかります。僕も昔そう思っていた時期がありました。
でもそれ、「魔法で使うんです」
##2-3. 「無意識で使えてしかも楽しい」を目指した魔法のデザイン
さて、ようやくおおよそ思った通りのところに魔法を飛ばすことができるようになりました。
残るは、③ 目標の場所に向かってどのような魔法を飛ばすか です。
プレイヤーが魔法を使って楽しむためには、魔法が**「無意識でつかえてしかも楽しい」**ものであるべきだと考えています。
それを実現するには、プレイヤーに「魔法を自分の意のままに使いこなせている」と感じさせることがきっと重要になるでしょう。渡邊恵太さん著作の「融けるデザイン」のお言葉を借りるとすれば「自己帰属感」を意識して魔法もデザインするべきだと僕は考えています。
そのためには、プレイヤーの体の一部のように魔法が振る舞うべきでしょう。
魔法をプレイヤーの体の一部のように振る舞わせるための工夫の1つとして、ベジェ曲線を用いて魔法を曲げました。
2. プレイヤーを魔法使いにさせる術 で述べたように、code name: WIZARD の妖精魔法では、プロトタイプ作成時のさまざまな苦節を経た結果、魔法を飛ばすオブジェクトの選択と魔法の実行を同時 におこなっているため、魔法が放たれた瞬間にどのオブジェクトに当たるのかがわかっています。
そのため、オブジェクトに魔法が当たる場合、魔法が放たれた時点でベジェ曲線に必要な始点と終点は求まっているのです。あとは制御点さえ求めてあげれば、魔法を曲げることができます。
制御点は、始点(手の座標)から2-1. あなたが魔法を当てたい物に手を伸ばしたその時、果たしてどのような線が描かれる? で求めたオブジェクト選択用のベクトルの方向に対して、始点(手の座標)と終点(魔法が当たるオブジェクトの座標)の距離の4分の3だけ進ませた点としています。
魔法が放たれてから後半でグイッと曲がる方が物理的に自然な動きに見えるため後ろ半分という意味で4分の3にしています。(以下図7参照)
実際のコードは以下になります。
private void RayMagicBeam( MLHandTracking.HandType whichHand, GameObject KIKOHAObject, Vector3 endPoint, float duration, Action onCompleted )
{
float startToEndDistance = (KIKOHAObject.transform.position - endPoint).magnitude;
Vector3 firstControlPoint = KIKOHAObject.transform.position + KIKOHAObject.transform.forward * startToEndDistance * 0.75f;
KIKOHAObject.transform
.DOPath( new Vector3[]{ endPoint, firstControlPoint, firstControlPoint }, duration, PathType.CubicBezier )
.SetEase(Ease.InQuad)
.SetLookAt(0.0001f, KIKOHAObject.transform.forward)
.OnComplete( () => onCompleted() );
}
この関数が呼ばれる前のKIKOHAObject の座標は手の座標と同じです。
DOTween のDOPath の第1引数に終点を1つと同じ制御点を2つ代入することで、2次ベジェ曲線を描いて魔法を遷移させています。
制御点を増やした3次ベジェ曲線でも試したのですが、2次ベジェ曲線の方がシンプルで軌跡がキレイに見えたためこの方法を採用しました。
以下のGIF画像のように、魔法が曲がります。
魔法は本来飛ぶ方向に飛び始めてから徐々にオブジェクトに向かって曲がっていきます。そのため、プレイヤーが狙った方向と実際に魔法が飛んだ方向が認識しやすくなり、プレイヤーの魔法の放ち方の学習を補助する役割も期待できます。
また、曲げることで魔法の動きに幅ができるので、魔法が単調になりにくく、オブジェクトに向かって真っ直ぐ直線的に飛ぶよりも活き活きと感じられるかと思います。
魔法がオブジェクトに当たらなかった場合は、外れた魔法として放たれます。
外れた魔法の場合は、魔法が放たれた瞬間の手の速さに応じて変わる終点をもとにベジェ曲線を描きます。
そのため、以下のGIF画像のように魔法が放たれる時に手を動かしていると、その方向へ魔法が曲がります。
これも魔法を活き活きさせるためのちょっとした工夫です。
また、魔法のねじれ具合やエフェクトの揺らめきなどにもランダム性や三角関数を用いて同じ動きにならないようにして、できるだけ魔法を単調に感じさせないようにしています。
あと、GIF画像を見ても確認できますが、魔法がオブジェクトに当たる場合と外れた場合とでエフェクトを分けています。当たった場合は渦巻くエフェクトが増えます。
こうすることで、魔法が当たっているか外れているかを暗に示せるため「魔法が当たったっぽいけど効かない」とプレイヤーが誤認識する可能性を下げる狙いがあります。
#3. さいごに
以上、Magic Leap を使って魔法使いになるための術をお話しさせていただきました。
より多くのプレイヤーを理想的な魔法使いにさせるための課題はまだまだ山積みですが、今後も改良を続け、みなさんによりよい魔法使いの体験をお届けできればと思っております 😌
###Magic Leap Meetup vol.2 in Japan に登壇します
Magic Leap Meetup vol.2 in Japan に登壇します!! 👨🏫
ここではお話ししていないプロトタイプ段階での開発の苦悩や、今の妖精魔法に至った経緯などを写真や動画を使ってビジュアルベースにお話しできればと思っております 👐
参加無料のオンライン開催だそうです 😌
###東京タワー🗼で、XR謎解きエンターテインメント 「code name: WIZARD」絶賛開催中
先ほど書かせていただいた魔法が東京タワーで実際に使えます!!🎉
当初は12月末までの開催予定でしたが、ご好評につき3月末までの延長が決定いたしました 😆
Magic Leap によるXR体験だけでなく、紙の謎解きもありますので謎解き好きな方も是非とも。
詳しくは公式ページまで
###新宿小田急百貨店でも、クリスマス明けの12月26日(土)から開催します 🎅
東京タワーにつづき、新宿小田急百貨店でもcode name: WIZARD 開催予定です!
追記(2020年12月18日)
12月17日(木)にMagic Leap Meetup vol.2 in Japan に登壇させていただきました!👐
当日はYoutube Live で配信されたのですが、code name: WIZARD 部分のアーカイブ動画がこちらになります!🎉