e.blog

主にUnity/UE周りのことについてまとめていきます

VR内でつかんだオブジェクトをイメージ通りに投げる

概要

今回の記事は、この記事を熟読して実装したものになります。

www.gamasutra.com

今作っているVRコンテンツは、「VRコンテンツ内で誰しもが共通してやることは、掴んだものは必ず投げる」というところに着目して、「VRで投げる」をコンセプトに開発を進めています。
色々なコンテンツを作ったりやったり見てみたりしていますが、ほぼ確実に、VR空間内の物を掴むとそれを投げます。
いい大人が、まるで子どもに戻ったかのようにひたすら物を投げる。

そしてコンテンツ体験が終わったあとは子どもがやったのかと思うほどに色々な物を投げ散らかした状態になる、という始末。

でもこれって、きっと子どもも大人も「人間」という部分で見たら違いがないことの証明なのかもしれません。大人は分別があるからやらないだけで、深層心理では子どもと変わらない、という。

ということで、ひたすら投げる部分を追求したコンテンツなわけです。
ちなみに別の視点では、物を投げる際、「思ったほど飛ばない」とびっくりするほどストレスが溜まります。
一方、思った以上に飛ぶのはかなり気持ちいいです。どちらも「思ったより」という状況なのにこの違い。

ということで、今作っているコンテンツは投げるだけでなく、投げる力を増幅して誰でもイチローの遠投のように、あるいはベジータドラゴンボールを投げたときのように、ひたすら物を速く、遠くに投げれる、というところを意識して開発しています。

そんなコンテンツなので当然、物を投げる際の挙動はしっかりと作りこまないとなりません。
そして参考にしたのが冒頭の記事、というわけです。

ざっくりフロー

簡単に今回実装したことを列挙すると以下のような感じになります。

  • 物を持っている間、常に最新の10フレーム分の速度ベクトルをサンプルし続ける(つまりデータとしては10個の配列)
  • 投げる動作を検知した際に、最後のフレームから見て90度以上開いているベクトル(つまり後ろ向きのベクトル)は除外する
  • さらに、残ったベクトルから偏差値を求め、一定の偏差値を持つものを信頼するベクトルとして採用する
  • そしてさらに「ローパスフィルタ」を用いて、ベクトルを滑らかにしたグラフを求める
  • 最後に、フィルタリングして残ったベクトルデータを使って「最小二乗平面」を求め、最後のベクトルをその平面に射影したベクトルを、最終的な投げるベクトルとする

という感じで実装しました。
まぁやっていること自体は数学の基本的なところを押さえつつ、みたいな感じで実装しています。
ただおかげでだいぶ数学的なロジックをプログラムに落とす、というところがよりイメージしやすくなりました。

(記事を元に)他に意識した点

参考にした記事には、オブジェクトの重心の話と、投げるときのトリガーの強さの話などが載っていました。
具体的にどういうことかというと。

重心を意識する

プログラムで書いているとついつい、「視覚的に持っているオブジェクト」の位置を元に速度を求めてしまいます。
しかし、プレイヤー(ユーザ)はコントローラを握っているのであって、VR空間内のオブジェクトを実際に持っているわけではありません。

つまりそこに、視覚と、筋肉が認識している重心にずれが生じている、ということです。

冒頭の記事から図を引用させてもらうと以下の場所にコントローラの重心があります。 f:id:edo_m18:20170217111540p:plain

なので、記事ではオブジェクトではなく、あくまでコントローラの重心を採用しろ、と書いてありました。
ただ幸いにして(?)、UnityのViveプラグインが提供してくれているコントローラのUnity上の位置はちょうどその重心が中心になるようになっていました。
なので、今回の実装ではその箇所の移動差分を取って速度としてサンプリングしています。

ユーザが握っているトリガーの強さは一定ではない

Viveのコントローラの場合は「カチッ」となるまで握ると、数値的には1になるので基本は1のままだと思いますが、ユーザが「ここからは物を投げている」という認識になるには多少のゆらぎがあるようです。
これもまた冒頭の記事から図を引用させてもらうと以下のようになるようです。

f:id:edo_m18:20170217111817p:plain

※ 今回の実装では色々試したところ、普通にトリガーを握っているか、のフラグを見るだけでイメージ通りになったのでここについては保留にしてあります。

使った数式や理論など

今回の実装は、かなりの部分で数学的な要素が多いものとなりました。
実装で使った数学的な内容は以下の通りです。

  • 偏差値
  • 標準偏差
  • ローパスフィルタ
  • 最小二乗平面

偏差値

偏差値。もっともよく聞くのは学力での偏差値だと思います。
Wikipediaから引用すると以下の意味になります。

偏差値(へんさち、英: standard score)とは、ある数値がサンプルの中でどれくらいの位置にいるかを表した無次元数。平均値が50、標準偏差が10となるように標本変数を規格化したものである。

要は、点数という絶対値や相対値(平均値)だけでは、その人の学力が全体的に見てどれくらいか、が判断しづらいから偏差(ばらつき)を取って確かな指標としましょう、というようなことですね。

ちなみに今回の実装では、サンプリングした速度データ全体から「より優秀な」値を示しているものを採用する目的で「偏差値」を利用しました。
ここでの「優秀な値」というのは、投げるときは速度が速い、ということを利用して「より速いと思われるデータ」をフィルタリングする目的です。

単純に「一定値以上の」としてしまうと、投げるスピードがまちまちなので「投げたことにならない」場合があったり、あるいはすべてのデータがあまりにも速すぎるとそもそもすべてのデータが採用条件を満たしてしまう、というのを避けるために「全体のデータの中で特に優秀なもの」というのを抽出するために採用しました。

偏差値の求め方は以下のようになります。
偏差値の求め方を参考にしました)

  1. 標準偏差を求める
  2. 平均値との差の絶対値に10をかけ、標準偏差で割る
  3. サンプリングした値が平均値より高ければ(2)で求めた値を50に足す、低い場合は50からその値を引く。それを「偏差値」とする

という具合です。 そして今回は偏差値60以上の値のみを利用することにしました。

標準偏差

偏差値を求める際に必要となる「標準偏差」。

標準偏差とは「データのばらつきの大きさ」を表す指標です。
記号は\(σ\)(シグマ)または\(s\)で表される数値です。 定義としては以下になります。

標準偏差は「各データの値と平均の差の二乗の合計を、データの個数で割った値の正の平方根」となります。 つまり、数式にすると以下。

\begin{align*} s = \sqrt{\frac{1}{n}\sum_{i=1}^{n} (x_i - \vec{x})^{2}} \end{align*}

  • s: 標準偏差
  • n: データの数
  • \(x_i\): 各データの値
  • \(\vec{x}\): データの平均

偏差値を求める際に必要なるため、標準偏差の計算を利用しています。

ローパスフィルタ

ローパスフィルタをWikipediaで調べると以下のように記載されています。

ローパスフィルタ(英語: Low-pass filter: LPF)とは、フィルタの一種で、なんらかの信号のうち、遮断周波数より低い周波数の成分はほとんど減衰させず、遮断周波数より高い周波数の成分を逓減させるフィルタである。ハイカットフィルタ等と呼ぶ場合もある。電気回路・電子回路では、フィルタ回路の一種である。

今回利用したのはノイズを軽減する目的で採用しました。
参考にした以下の記事から画像を引用させてもらうと

ehbtj.com

f:id:edo_m18:20170219095650p:plain

こんな感じで、ぎざぎざしているノイズ部分をうまく滑らかにしてくれます。
今回の実装では、手の動きによる入力のためこうしたノイズが発生してやたらと大きな数値が取られる、ということが何度かありました。
それを軽減する目的で利用しています。

最小二乗平面

最後に「最小二乗平面」。
最小二乗平面とは、標準偏差とも若干似た概念になりますが、全データから求める「とある平面」です。 その平面は、すべての点から、その平面に対しての距離の二乗が最小になる平面です。

最小二乗平面の求め方はQiitaのブログで書いたので、詳細はそちらをご覧ください。

qiita.com

今回の利用点としては、10フレーム分の速度データをサンプリングし、それを元に投げるときの速度を決定しています。
なので、この「最小二乗平面」を求め、「理想的な平面に対する速度データ」を算出することで、「人が思っている方向」になるべく近くなるように速度を計算しています。

実際のところこれがいい、というのは冒頭の記事に書かれていたのをそのまま利用しました。
が、実際に採用してみるとだいぶ思った方向に投げることができたので今回の記事を書くに至ったわけです。

まとめ

実際のところ、ここまでやってもまだ多少の違和感があったり、思ったように投げれない部分もあります。
が、最初に実装したものに比べたら格段によくなったのも事実です。

そしてなにより、思ったところに投げられるというのはそれだけで気持ちよさにつながるなーというのをより強く実感しました。

また今回の「投げる」部分以外でも、物をつかむ・離す、という部分もだいぶこだわって作ったので、だいぶ汎用的にVRで利用できるライブラリが完成したのも大きかったです。

ちなみに、今回のこの実装を利用したコンテンツを「JapanVR Fest.(旧オキュフェス)」で出展予定なので、興味がある方はぜひ遊びに来てください!

http://jvr-fest.com/2017/01/2894/

参考にした記事