本記事はAEC and Related Tech Advent Calendar 2020の20日目の記事、及びGELのテックページの記事の一つとなります。
Rhino 7からの新機能であるRhino.InsideはRhino 7がまだベータバージョンのときから試していますが、今回正式にバージョンがメジャーアップデートされたということで、Rhino.Insideを普段お仕事でよく使うUnity上で利用することを考えました。
実はつい最近Rhino.InsideとUnityの組み合わせは試したことがあって、そのときはUnityとGrasshopperをRhino.InsideでつなげるためのUnityプロジェクトテンプレートを作り公開していました。
今回は、このとき作ったテンプレートをベースに、次のようなゴールを定めて、プロジェクトをアップデートしようと考えました。
- Grasshopperファイルを読み込んで表示できるUnityプロジェクト
- Grasshopper上のスライダーやボタンをUnityプロジェクトで表示できるようにする
- Grasshopperファイルを更新したときすぐにUnityに反映できるようにする
完璧ではないですが、比較的自分が目指す動くものができたので、あまりテクニカルによらず、どういった形でテンプレートプロジェクトをアップデートしていったかを書いていきたいと思います。
GHファイルを読み込んで表示できるUnityプロジェクト
まずはなによりGrasshopperファイルから出力されるジオメトリをUnityに出力することが必要なのですが、この機能自体は以前作ったテンプレートに含まれているのでそんなに難しいことはありませんでした。
このプロジェクトではOpen GHボタンを押すことでGrasshopperを起動し、そこからファイルをいつもGHを使うように開いたり作ったりすることができます。また、Open Rhinoボタンを押すとライノの画面も表示され、GHで作ったモデルをそのRhino画面で見ることも可能です。
このとき一点気をつけなければいけないのは、GrasshopperからUnityにモデルを送りたいときは、メッシュ形式に変換する必要があるということです。UnityではBrepやSurfaceのジオメトリの型をサポートしていないためです。
UnityとGrasshopperの相互通信はそれぞれC#スクリプトを使って行います。
GrasshopperからUnityに通信する場合は、先にUnity側でコールバックを登録し、それをGrasshopperから呼び出すという手順になります。
// GHからのデータを受け取るコールバックをUnityで登録する
void Start()
{
if (!Startup.isLoaded)
{
Startup.Init();
}
Rhino.Runtime.HostUtils.RegisterNamedCallback("FromGHMesh", FromGHMesh);
}
// GHからUnityで登録したコールバックを呼び出す
using(var args = new Rhino.Runtime.NamedParametersEventArgs())
{
args.Set("mesh", mesh.ToArray());
Rhino.Runtime.HostUtils.ExecuteNamedCallback("FromGHMesh", args);
}
この方法はメッシュに限らず、Unity上で解釈できるデータであればどんなデータでも送ることができます。実はRhino3dmのライブラリをUnity側にインポートしておけば、SurfaceやBrepなども送ることはできたりします。ただ、Unityのシーン画面で表示するためには結局メッシュに変換しなおさないと描写できないので、GH側でメッシュに変換しておいたほうが調整も効くのでいいと思います。
GHのスライダーやボタンをUnityプロジェクトで表示できるようにする
今回の一番の挑戦(?)はこれかなと思うのですが、以前作ったテンプレートプロジェクトでは、UnityからGHにデータ(例えばスライダーの数値データとか)を送るときは、GHファイルごとにスライダーをUnityでマニュアルで作る必要がありました。そのため、想像がつくと思いますが、GHファイルの更新をするとUnity側のデータを送るためのコードも更新する必要があって、なかなかの手間でした。
ということで考えたのが、Grasshopperファイルにあるスライダーの情報自体がそのままUnityに送れて、その情報を元に自動でUnity上でスライダーが作れたらいいなと思いました。そしてもちろん、Unityで作ったスライダーを動かすとGHファイル上で既存のスライダーの値を変更できたらいいなと。もしそれができたら、GHファイルを作るだけでUnity側ではコードを変更する必要がなくなり、ビルドしたプロジェクトを配布すればそのまま更新せず汎用的なGHプレイヤーとして使ってもらえるというシナリオが実現できるようになります。
今回はそんなゴールを踏まえて、GH上のスライダーとボタンをUnityに送ることを考えました。
ひとつ頭を抱えたのが、キャンバス上にあるスライダーの情報を取得してその最小値最大値などを取得してUnityにMeshを送ったときと同じような手段で送ること自体はできることを確認したのですが、その情報を使ってスライダーをUnityで作ったとき、今度はUnityからGHへそのスライダーの情報を送って既存のスライダーの値の代わりに送られた値を使わなければいけません。
スライダー自体の値をコードからアクセスして変更するみたいなことをすればいいのかと思ったのですが、ちょっと手順が煩雑になりそうな予感がしたので今回はスライダーなりボタンのアウトプットと、その値を使うインプットの間にC#コンポーネントをかませて、Unityからスライダーの情報が来たときはそのC#からの値はUnityからの値を使い、GH上でスライダーを動かしたときはGH上のスライダーの値をC#からアウトプットするようにしました。
この方法のメリットの一つとして考えられるのは、転送したいスライダーやボタンを絞ることができるという点です。キャンバス上にある全てのUIコンポーネントを送ったらいらないものまでUnityに表示してしまうという可能性が大いにあるので。
C#コンポーネントには基本的な情報としてUIコンポーネントの名前と、スライダーの場合は最小値最大値を自分で設定できるようにもしておきました。これらのコンポーネントはUnityからの値を受信するためのコンポーネントなので、MeshをUnityで受け取ったときはUnity上でコールバックを登録していたのに対して、今回はGrasshopper側でコールバックを登録します。
// Grasshopper側のUIデータ受け取りコンポーネントでコールバックを登録する
void Register(IGH_Component component, string name, bool init)
{
if(init == true || registered == true)
{
Rhino.Runtime.HostUtils.RegisterNamedCallback("ToGH_Slider_" + name, ToGrasshopper);
comp = component;
registered = true;
}
}
転送したいUIの分だけC#コンポーネントを作ったら、それらの情報をすべて取得してUnityに送るコンポーネントを別に作ります。いわゆる初期化の機能をもったコンポーネントで、起動するたびにキャンバス上の全UIの情報を取得しUnityに送り、Unity上のUIをアップデートします。
最終的にはプラグイン化できたらいいとは思っていますが、今回はC#スクリプトなので、C#で書いたUIコンポーネントの種類(スライダーなりボタンなり)に応じてNickNameを決めておいて、その値に応じたコンポーネントをキャンバスから探し出し、その情報を利用してUnity上で別途登録しておいたボタンやスライダーを新規に作るためのコールバックを呼び出します。
// Unity上でボタンやスライダーを作るコールバックを登録する
Rhino.Runtime.HostUtils.RegisterNamedCallback("FromGHCreateSlider", FromGHCreateSlider);
Rhino.Runtime.HostUtils.RegisterNamedCallback("FromGHCreateButton", FromGHCreateButton);
// GH上の初期化コンポーネントを使ってキャンバス上にあるUIコンポーネントごとにスライダーやボタンを作る
...
if(component.NickName == "UnitySlider"){
...
Rhino.Runtime.HostUtils.ExecuteNamedCallback("FromGHCreateSlider", args);
...
}
この手法は比較的うまくいきました
GHファイルを更新したときすぐにUnityに反映できるようにする
GHファイルを更新したときに、Unity上のモデルやスライダーも即時反映されるというのが最終的なゴールで、今回は次のような手段を使うことでそれを実現しました。
今回一番悩んだのが、Rhino.Runtime.HostUtils.RegisterNamedCallback関数の挙動です。この関数はUnity側でもGrasshopper側でも使っていて、情報を受取るためのコールバックを登録するための関数ですが、以前のテンプレートだと特にGH側でこの関数を使ってUnity側からの値を取得するためのコールバックを登録すると、それ以降中のスクリプトを変更しても事前に登録したコールバックが残ったままになり、Unityからの情報を正しく受け取れなくなってしまいます。そうなってしまうと、GHファイルを開き直さなければいけなくなります。それはツールとしてあまりにも不便なので、その問題点を解消する方法を考えました。
今回暫定的に考えたのはinitというbool型のインプットを作り、そのインプットにtrueの値を送ったときにコールバックを強制的に登録し直すという手法をとることにしました。単純で汚い方法ですが、取り急ぎこの方法でまずはうまくいったので、今後はこのボタンを押すという手順も自動化できるような方法を考えていきたいと思います。
今回のトライアルは以上です。結果としては満足していますが、まだまだアップデートできる余地がある試みだと思うので、まだ続けていきたいと思います。
これのメリットは、動画で見せているようにビルドしたUnityプロジェクトで特定のフォーマットにのっとった異なるGHファイルを自由に開けることができるようになっているという点で、これはRhino.Insideとの通信ができる限りVR・ARアプリとしても利用することができます。
こういった内容の具体的な実装等に興味がある人はGEL主催のミートアップなどに来てみてください。
今日はここまでです。
堀川淳一郎
ミートアップや情報発信などの活動を維持するためにPatreonでのサポートをお願いします。