見出し画像

コンテキストメニューでアバターの表情をイイ感じに切り替える方法

※『イイ感じ』は多分に主観を含んだ表現です―――

【はじめに】

どうも、ホーカムです。
今回は、コンポーネント&LogiXを使って、コンテキストメニューからアバターの表情を切り替える方法について紹介します。
アドカレ10日目の記事を公開する数日前に完成し、ふと「これ記事に出来るのでは?」と思い立って急遽書きました。


この記事は、NeosVR(その3) Advent Calendar 2022,16日目の記事です。
前日12/15(木)はtotegammaさんによる『NeosVR最大ケモノコミュニティ「ケモノhub」を支える技術』 でした。
また、1枚目のAdvent Calendarでは同じくtotegammaさんによる『コンポーネントの値を外から操作したい!RefIDオフセットの世界』 、
2枚目のAdvent Calendarではtanossyさんによる『素材サイト紹介』でした。
まだご覧になっていない方はこちらも是非!


【概要】

アバターの表情は、Bodyスロット内のシェイプキーを触ると変化するものが一般的です(多分)。
ただ、表情を変えるために毎回インスペクターを出してスロットを選んでシェイプキーを見つけて…なんてことは当然したくないので自動化したいわけです。
JPチュートリアルワールドに入って右手側には、ハンドサインと表情を紐づけることができるツールが設置されているので、それを使うのが一番簡単で確実な方法です。

多分これが一番楽だと思います

しかし、デスクトップモードではハンドサインは使えませんし、複数シェイプキーを同時に操作するなどの複雑な制御もハンドサイン紐づけでは難しいと感じたため、別の方法で表情を変えることができないか考えた結果、ならばコンテキストメニューで操作できないか、ということになりました。
思い立ったらやってみるの精神で、慣れないながらも最初の実装を作ったのが10月頃の話ですが、この時の実装には色々と問題がありました(後述)。
その改良版として作成したのが今回紹介するものです。改良版といいつつ、元があまりにもアレだったので一から作り直してます…。

なお、ここから先の記事の内容はコンポーネントとLogiXの基礎をそれなりに理解していることを前提としています。

【完成イメージ】

先に完成イメージの動画を貼っておきます。直接動画を貼れないため、Twitterの埋め込みになっている点はご容赦ください。

この動画では、以下の内容を順番に行っています。
① 目・眉・口・その他 の各部位ごとに表情を設定できます。基本的に設定は部位ごとにどれか一つを選ぶ形式ですが、一部の設定は単独でオン・オフすることができます。
② "setup"メニューから表情の一括設定ができます。
③ 表情の一括設定をすると、部位ごとの設定の選択状態も追従します。

【要件】

作り方の前に、今回の実装に至るまでの経緯と、その内容を基にした要件について書きます。「とりあえず実装はよ」という方は【実装】の項まで飛ばしてください。
先述した通り、表情設定の機能は過去に一度作ったことがありましたが、その時の実装にはいくつか問題がありました。

  • 同一のシェイプキーを複数のメニューから設定できない

  • 表情の切り替えがスムーズにできない

  • メンテナンスが面倒

それぞれ詳しく見ていきます。

○ 同一のシェイプキーを複数のメニューから設定できない

以前の実装では、同じシェイプキーを複数のメニュー項目から設定することができませんでした。
これが原因で、特定の表情に対して、部位ごとの個別切り替えと、全ての部位を一括で設定する機能を両方同時には取り入れることができず、どちらかを諦めなければならない状況でした。

○ 表情の切り替えがスムーズにできない

同じ部位で複数のシェイプキーを同時に有効にすると、シェイプキーによっては表情が破綻してしまう恐れがあります。
それを防ぐために、『切り替えようとしているシェイプキーと同じ部位のシェイプキーが有効な場合は切り替えできないようにする』という仕組みを(超強引なLogiXで)実装していました。

ヤバさを感じる超強引なLogiX。
すべてのシェイプキー(を駆動するDriver)のStateを全ORして切り替え可否を判断している。

この実装の問題として、表情を切り替えたい時は毎回、現在の表情設定をオフにしなければならないという面倒くささがありました。

○ メンテナンスが面倒

従来の実装では、表情の追加をする場合に既存設定との競合や編集するLogiXの特定など、考えなければならないことが多くメンテナンスに手間がかかる代物でしたが、それに拍車をかけるようにガバガバな階層設計により、何がどこにあるか分かりにくい状況にありました。

そんなわけで、問題が多かったこれまでの実装を改善すべく、以下の要件で設計を行いました。

  1. 表情の切り替えは出来る限り自由&スムーズに
    『出来る限り自由』というのは、顔パーツの破綻を起こさないことを前提として、あとは自由、すなわち同じシェイプキーを個別設定・一括設定の双方から変更できるようにすることが条件です。
    『スムーズ』は、切り替えるためにいちいち前の設定をオフにする必要がないことを条件とします。

  2. 表情の追加はできるだけ簡単にする
    以前の実装だと、追加する表情によっては複数のLogiXを編集する必要がありました。はっきり言って面倒です。
    スロットのコピーと多少の手直しだけで動作する程度のメンテナンス性を目指します。

  3. できるだけスロットを整理する
    以前の実装では、表情の設定・Driver用のスロット・LogiX用のスロットが全て同じ階層に共存していたため、なかなかに滅茶苦茶な状態でした。
    そんな滅茶苦茶な状態だったのでメンテナンスする気も失せてしまっていた、というのが本心なので今回はちゃんと階層を考えて設計します。

さて、前説が長くなってしまいましたが、次の項から実装を紹介します。

【実装】

☆全体像

インスペクターで全景を映すとこんな感じになります。

最上位のfaceスロットが表情設定用のRootContextMenuItemになります。

☆コンテキストメニューの構成

表情設定の前に、コンテキストメニューについて紹介しておきます。
コンテキストメニューを作るために必要なコンポーネントは、ContextMenuItemSource,RootContextMenuItem,ContextMenuSubmenu
の3つです。すべて Radiant UI/Context Menu の中にあります。
まず、ContextMenuItemSourceはメニューの階層(親・子・孫)を問わず、コンテキストメニューにしたい項目ごとに設定が必要です。このコンポーネントでメニューの色やテキストなどを設定します。
次に、RootContextMenuItemは最上位のメニューにのみ設定します。このコンポーネントの"Item"に登録したContextMenuItemSourceが、コンテキストメニューを出した際、最初に表示される項目になります。
最後にContextMenuSubmenuは、下の階層が存在するメニューに設定が必要です。項目を選んだとき、"ItemsRoot"で指定したスロットの配下に存在するContextMenuItemSourceを表示します。
文字だけだとあまりピンとこないので、実際の設定例も紹介します。

今回の実装では、コンテキストメニューは3階層で構成されます。

こんな感じで、Face→(部位)→(表情)と辿ると表情を変更可能。

便宜上、上の画像で示したコンテキストメニューを左から順に、親・子・孫と呼ぶことにします。
まず、親のスロットには先述した3つのコンポーネント全てが必要です。

親となるスロットの設定例。

RootContextMenuItemの"Item"には同じスロットのContextMenuItemSourceを指定します。
また、ContextMenuSubmenuの"ItemsRoot"には自身のスロットを指定しします。こうすることで、この項目を選んだときに自身の配下のスロットに設定したContextMenuItemSourceを子として表示することができます。

補足:一つのスロットに同種のコンポーネントを複数つけることも可能ですが、コンテキストメニュー関連については分かりやすさの観点から、一つのスロットに一つのメニュー項目を対応させることをお勧めします。

親が設定できれば子と孫の設定は難しくありません。
子はRootContextMenuItem以外の2つを設定し、
孫はContextMenuItemSourceだけを設定すればOKです。
子のContextMenuSubmenuには、"ItemsRoot"に自身のスロットを設定することをお忘れなく。
(孫にはコンテキストメニュー関連で、さらに追加で設定するコンポーネントがありますが、それは後程。)

☆表情の制御①

driversスロット配下には部位ごとに階層分けして、シェイプキーをドライブするためのBooleanValueDriver<float>を設定しています。
このコンポーネントは Transform/Drivers の中にあります。私はよく間違えてRelationsを開いてしまいますが、そちらにはありませんのでご注意を。

本当にDriverしかないので、コンテキストメニュー自体には何の関与もしていません。
(driversスロット自体にもContextMenu関連のコンポーネントはついていません)

設定方法は、設定したいシェイプキーの名前をトリガーしてBooleanValueDriver<float>の"TargetField"の表示部(デフォルトでnullになっている部分)で離せばOKです。
また、"TrueValue"には通常、1を入れておけば問題ないと思います。
この状態で"State"のチェックを切り替えると、設定したシェイプキーの値が連動して切り替わるはずです。

☆eye, eyeblow, mouth, othersスロット

基本的な構造は上記4つとも同じです。それぞれ目・眉・口・その他(頬染めなど)に対応しています。eye, eyeblow, mouthの各スロットには、コンテキストメニュー関連のコンポーネント以外に、ValueRegister<int>を追加しています。
後述しますが、各スロットの配下で『何を選択しているか』を保持するためのコンポーネントです。

ValueRegister<int>がスムーズな切り替えを実現するためのカギ

☆表情の制御②

各部位を示すスロットの配下には表情の制御を行うスロットがあり、1つのスロットに1つの表情設定が対応している形になっています。
コンテキストメニューの孫にあたる部分なので、コンテキストメニュー関連ではContextMenuItemSourceだけを設定すればよいことになりますが、ボタンとして機能させたいので、さらにButtonValueSet<int>も設定します。このコンポーネントは Common UI/Button Interactions にあります。
ButtonValueSet<int>の"TargetValue"には自身の親スロットに設定したValueRegister<int>の"Value"を、"SetValue"にはスロットごとに固有の値を振っています。
"SetValue"に設定する値はどのようなルールでも構いませんが、ここでは0を『表情設定なし(normal)』として、各表情には1から始まる連番を設定しています。

ButtonValueSet<int>の"TargetValue"は親(ここではeye)のValueRegister<int>

そして、この階層の各スロットにはLogiXがパックされています。
中身の構造はほとんど同じなので、まずは標準形を出します。

☆LogiX(標準形)

これが一番標準的なタイプ。

やっていることは大きく2つ。
1つは、現在のValueRegisterの値が自身のButtonValueSetに設定されているSetValueと等しいか判定し、コンテキストメニューの色を変更する……要するに、設定状態を可視化しています。オン(true)の時は緑(0, 1, 0, 0)、オフ(false)の時は黒(0, 0, 0, 0)を表示します。
そしてもう1つは、ValueRegisterの値が変化してButtonValueSetに設定したSetValueとの比較結果が変化したとき……すなわち、自身のスロットが担当する表情の設定がオンまたはオフになったタイミングで、担当する表情のBooleanValueDriverの"State"に値をWriteする。これが表情の変更動作です。

別の表情を追加するときは設定済みのスロットごと複製し、インスペクターからButtonValueSetの"SetValue"を変えることと、LogiX中のWriteノードの書き込み先を変更すればOKです。

補足:FireOnChangeの入力に繋がっているUserですが、ここには何も繋がなくて良いようです(未指定の場合、ノードの親であるユーザー=アバターを着ている人 となるため)。他人が着ることができないアバターに限って言えば、画像のようにユーザーIDベタ書きでも動作は特に変わらないはずです。

次に、各部位ごとの子スロットの先頭にある、normal(表情設定なし)のスロットについてですが、こちらは標準形よりシンプルです。

☆LogiX(表情設定なし, mouth以外)

BooleanValueDriverにWriteする処理がないので、標準形よりもシンプル。

各表情設定のスロットがオン・オフ両方の責務を担っているので、表情設定をしないスロットは単にコンテキストメニューの見た目だけ切り替えている形です。

☆LogiX(表情設定なし, mouth)

表情設定を行わないnormalスロットでも、口を担当するnormalスロットだけはとある事情でLogiXが異なります。

ほとんど標準形と同一で、Write先だけが異なります。

このLogiXのWrite先は、Head ProxyスロットのVisime Analyzerコンポーネント(のEnable)です。このコンポーネントは入力音声に合わせてアバターを口パクさせる役目を担っています。
口に係るシェイプキーを変更した状態で口を動かしてしまうと、同一部位のシェイプキーを複数動かした時と同じ状況になる(場合によっては表情が破綻する)ので、口のシェイプキーを変更している(="normal以外"が設定されている)ときはコンポーネントを一時的に無効化しています。

☆LogiX(オン・オフ切替タイプ)

上記以外に、眉の上下やキラ目など、他の設定とは個別でオン・オフできるようにしたい項目がある場合は少しアレンジを加えます。
設定するコンポーネントは、ButtonValueSet<int>の代わりにButtonToggleとValueRegister<bool>の二つを設定します。
このとき、ButtonToggleの"TargetValue"はValueRegister<bool>の"Value"に設定します。

オン・オフ切替タイプのスロットは区別できるようにした方が幸せかも。
(画像の例ではスロット名の末尾に"_t"をつけています)

また、LogiXはこんな感じになります。

入力が2値の比較からRegister単体になっただけ。

ここまでが各部位の表情を個別で切り替える機能についてです。
次に、複数の部位を一括で設定する機能の仕組みを紹介します。

☆複数部位の一括設定

この機能では、現在設定中の表情をすべてリセットしたり、個別に設定するのが面倒な表情をメニューから選ぶだけで設定することができます。
ただし前提として、設定したい表情は部位ごとに個別制御できるようにしておく(前節までの設定を済ませておく)必要があります。
まず、setupスロットの下にスロットを作成します。いつも通りスロットにContextMenuItemSourceを設定した後、これまで設定してきたRegisterの数だけButtonValueSetを出します。

ButtonValueSetの型も各々のRegisterの型と合わせます。
出すのが面倒なので必要な型を1つずつ出した後、複製(緑色のDボタン)した方が楽。

この画像の例では、eye, eyeblow, mouthそれぞれにRegister<int>が設定されているのでintを3つ、オンオフ切替タイプのeye_kira(キラ目),eyeblow_down(眉位置下げ),facial_cheek(頬染め)にそれぞれRegister<bool>を設定しているので、boolも3つ出しています。

あとは、"TargetValue"に各Registerの"Value"をセットし、設定したい表情に合わせて"SetValue"の値を合わせていきます。
表情リセットの場合はすべて0(normal)とチェックなし(オフ)にすればOKです。

表情リセット時の設定例。

一方、以下の画像では『てへぺろ顔』を設定した場合の例です。

『てへぺろ顔』の設定例。右側のインスペクターに赤で追記している数字は、
対応するスロットのButtonValueSet<int>に設定した"SetValue"の値。

eye → 3 (eye_winkR)
eye_kira_t → false (なし)
eyeblow → 2 (eyeblow_sad)
eyeblow_down_t → true (あり)
mouth → 2 (mouth_tehepero)
facial_cheek_t → false (なし)
と設定しています。

設定するとこんな感じ。
目=右ウィンク,眉=下げ+悲しみ,口=てへぺろの全てを1回の操作で設定できる。

こちらも、1つ作った後は既存のスロットを複製して、名前と値(SetValue)を書き換えるだけで項目を追加できます。

【振り返り】

ここまで、今回作った機能について、その構成とともにに紹介してみました。ここで、記事冒頭で示した要件について振り返ってみましょう。

  1. 表情の切り替えは出来る限り自由&スムーズに
    ButtonValueSet+ValueRegisterの組み合わせで、スムーズな切り替えを実現しながらも表情の破綻を発生させない仕組みができました。
    一点だけ、喋っている時に表情を変えると、戻すまで口が閉じない問題があることが残念ポイント(原因は分かっているが解決策どうしよう、な状態)。

  2. 表情の追加はできるだけ簡単にする
    個別設定の場合は、①Driver追加,②スロット複製,③名前・SetValue・LogiXのWrite先をそれぞれ変更 の3ステップで追加完了です。
    一括設定の場合は、①スロット複製,②名前・SetValueをそれぞれ変更 の2ステップです。
    どちらも、スロットの複製でかなり楽ができることがポイントです。少なくとも、以前の手法と比べて大幅に楽になりました。
    ただし、個別設定の方にRegisterを追加しようとしたときだけが問題で、既存の一括設定の全スロットに対して当該Register用のButtonValueSetを追加しなければなりません。Registerのご利用は計画的に。

  3. できるだけスロットを整理する
    これに関してはビフォーアフターで見てもらった方が早いと思います。

まあ、どちらが分かりやすいかは一目瞭然ですね…。

【おわりに】

いかがだったでしょうか。
今回紹介した内容は、先日の100日間振り返り記事で紹介していた、”LogiXのお勉強”の内容とも深く関係していて、この時に教えていただいた内容を基に自分なりのアレンジを加えつつ、実装しています。

当初は「こんなの作ってみました!」という感じで軽く紹介しようと思っていたのですが、自分の考えを整理するついでに色々と書いていたらほぼ全ての実装について書いていました…。
想定していたよりもかなり長い記事になってしまいましたが、最後までご覧いただきありがとうございました!!


NeosVR Advent Calendar 2022 (1枚目)ではkazuさんによる
FBXのバージョン落とすツールをシングルアプリ化した』が、
NeosVR Advent Calendar 2022 (2枚目)では、ハルマキ左衛門さんによる
VR中にロボットをけとばした話』が同日公開です!
こちらも是非ご覧ください!

また、明日12/17(土)のNeosVR Advent Calendar 2022は、以下の三本立てでお送りします。

  • 1枚目:『NeosVRにログインしたきっかけの話』,担当はくつひもごちょうさんです!

  • 2枚目:『kangaetyu』,担当はRheniumさんです!

  • 3枚目:『ねおすでたのしかったこと』,担当はぞぞかすさんです!

お三方ともよろしくお願いします!!

この記事が気に入ったらサポートをしてみませんか?