ChatGPTを使ってRhinoでスクリプトを作成してみる

以前のTips記事で、AIによるテキスト生成が可能なChatGPTを使ってGrasshopperのC#コンポーネント内でスクリプトを記述してみるというものがあります。

このページでは、ChatGPT(正確にはBingChat)を使ってGrasshopperではなくRhinoでプラグインを作成するコードを記述してみようという検証記事となります。

Tipsというより体験みたいな内容なので、いつもよりも文体も緩めに書いています。またGrasshopperやC#などを触ったことがある方が理解しやすいかもしれません。

AIの分野は非常に開発が早いため、1,2週間経つと新しい技術が山のように出るので、このページでは現在(2023/05/10時)に、Bingのチャット(GPT4ベースのもの。無課金で利用可能)で行った内容となります。

またより効率的にコーディングを行うのであればVisualStudioなどでCopilotとして使用すると、さらに使いやすいかとも思います。
AIでプログラムをコーディングすると、どういったことができるのか、Rhinoでプラグインを作成する際どういったことを行うかを確認したい時に読んでいただけたら幸いです。

Grasshopperでの作成イメージ

 ターゲットサーフェスと貼り付けるポリサーフェス、UVそれぞれの分割数、高さを指定することでモーフィングを行う下図のようなものをイメージして行ってみます。

アルゴリズム確認のため作成したGrasshopperは下記となります。赤いグループで囲んだ箇所が、それぞれ必要な入力値となります。

(クリックで拡大表示します)

想定している入力値は

  • U分割数(Integer)
  • V分割数(Integer)
  • モーフの高さ(double)
  • モーフとして貼り付けるオブジェクト(Brep)
  • ターゲットとなるサーフェス(Surface)

の5つです。

Grasshopperのアルゴリズムとして気を付ける点は、

・赤い丸のUとVの範囲を組み合わせる際にGraftを付ける必要がある。
https://www.applicraft.com/tips/rhinoceros/data2/

・入力したTargetSurfaceを0から1として扱うためにReparameterizeを付ける。
サーフェスの分割後にも再度SurfaceMorphをUVに入力値を0から1として扱うので、分割後にも再度Reparameterizeを付ける。(共に緑色の丸で図示)
https://www.applicraft.com/tips/rhinoceros/grasshopper_tuv/

くらいだと思います。GHに慣れている方なら、すぐ出来そうなアルゴリズムですが。

また赤いグループの入力値と最後のBakeを差し替えれば、GrasshopperPlayerやスクリプトコンパイラでのRHP化なども可能です。ただしその場合は

  • 初回コマンド起動時にGrasshopperが起動する
  • 実行速度がGrasshopperの速度なので、それほど早くない

などの問題も考えられます。

GrasshopperPlayerコマンドについては下記を、
https://www.applicraft.com/tips/rhinoceros/grasshopperplayer/

スクリプトコンパイラに関しては下記を参照ください。
https://www.applicraft.com/tips/rhinoceros/scriptcompiler/

BingChatのアクセス方法

通常のChatGPTは下記のURLにアクセスし、Loginすれば使用可能です。
https://openai.com/blog/chatgpt

ここではもっと簡単にEdgeブラウザからBingのチャットをクリックして使用してみます。こちらだと無料でGPT4ベースの機能を使用できるようです。

Microsoftアカウントは必要ですが、ChatGPT用のアカウントを作成する必要が無いのでより簡単です。ただしEdge以外のブラウザからは使用できないようなので、Edgeから使用してください。

注意:通常のGPTとは異なり、スレッドを保存できない、1つのスレッドでの会話回数が20と限定されているようです。この辺りが気になる方は、通常のGPTを使用した方が良いかもしれません。

Bing Chatにプロンプト入力

 入力前に会話のスタイルを選択できるようです。違いが正直分からないので、「より厳密に」にしてみます。

  • AIに役割をお願いすると精度が高くなるらしいので、「プロのソフトウェアエンジニア」として振舞うようお願いする
  • 使用する環境を書く(インストール済みと書かないと、インストール方法の説明を受けることが多い)
  • Pythonで使用するScriptSyntaxは使わない。C#で、RhinoCommonのみ使用。

などを意識してプロンプトを書いてみます。また敬語で書くと精度が高くなるようなので、ちゃんと敬語で書きます、、。

Rhinoceros でモーフィングを行うスクリプトを記述したいです。プロのソフトウェアエンジニアとして手伝って頂けたらと思います。
Rhino7、VisualStudio2022、RhinoとGrasshopperのテンプレートはインストール済みです。

またPythonで使用するScriptSyntaxは使わず、RhinoCommonでC#で記述をお願いします。

RhinoCommonは下記を参照ください。
https://developer.rhino3d.com/api/rhinocommon/

モーフィングは、
入力:ターゲットとなるサーフェス(Surface)、張り付けるオブジェクト(Brep)、高さ(double)を入力して実行します。
処理は張り付けるオブジェクトのBoundingBoxを取得し、その底面のサーフェスをSourceSurfaceとしてターゲットサーフェスに張り付けるイメージです。RhinoのFlowAlongSrfコマンドと似た処理です。

のような形で尋ねてみました。下記のように返信があったので、具体的なコードをクリックして書いてもらいます(通常のGPTはクリックして、次の動作を指定することはできないのでここは良い機能ですね!)。

下記のように返答がありました。内容を確認すると、ちゃんとRhinoのコマンドを作成するテンプレートを使ってコードを記述しているようです。

以下、軽い補足です。

・Commandクラスを継承 
Rhinoのコマンドとして登録するにはCommandクラスを継承する必要があります(VisualStudioのテンプレートを使用して新規プロジェクトを作成する場合、標準で設定されているので、特に気にする必要はありません)。

・EnglishNameの箇所
ここで設定された名前が、Rhinoから呼び出す際のコマンド名となります。クラス名やプロジェクト名とは関係なく任意の名前で設定もできます。Commandクラス自体をプロジェクト内で複数扱うことも可能です(その場合、複数のコマンドを持つプラグインとなります)。

・protected override Result RunCommand(RhinoDoc doc, RunMode mode) の箇所
元々あるRunCommandメソッドを上書きしている形。基本的にはこのメソッドがスタートメソッドなので、Rhinoでコマンドを実行すると、このメソッドを最初から最後まで実行します。

基本的にはGrasshopperのC#コンポーネント内で使用するのと同様の記述で行えますが、このメソッドの戻り値はResult型なので、Result.SuccessやResult.Cancelなどを戻り値として返す必要があります(if文で分岐した場合なども、すべてのルートで設定されていないとエラーとなりコンパイルできません)。

またRhinoDocをdocという変数名で引数として入力している(doc.Objects.Add型名(変数) 個所は、Grasshopperで言うBakeのような処理、doc.Views.Redraw()はビューの更新、どちらもよく使う)ので、Grasshopperで使用する際は
var doc = Rhino.RhinoDoc.ActiveDoc;
などの一文を入れておくと、RhinoとGrasshopperでコードが共通化しやすいです。

以下、必要なコードはRunCommand内となりますので、GPTから返答があってもRunCommand内の箇所のみスクリーンショットを上げることとします。

VisualStudioからモーフ部分を実行

ではコードをプラグインとして作成してみたいと思います。Rhino7でのプラグイン作成には下記の環境が必要です。インストールしていない方はご確認ください。

・Visual Studio 2022 (無料の[コミニュティ]でも開発可能。Microsoftアカウントは必須)
https://visualstudio.microsoft.com/ja/downloads/

・Microsoft .NET Framework 4.8 Developer Pack
https://dotnet.microsoft.com/download/dotnet-framework/net48

・Rhino7 用のプラグイン開発テンプレート ( Grasshopper も含む )
https://github.com/mcneel/RhinoVisualStudioExtensions

最後のプラグインのテンプレートはWindows版はvsixをダブルクリックで、Mac版はVisualStudioを起動してmpackを読み込むことで使用可能となります。詳細はリンク内を参照ください。

VisualStudioを起動して、[新しいプロジェクトの作成]から、Rhinoプラグインを作成するテンプレートを選択して、フォルダを指定して新規プロジェクトファイルを作成します。

テンプレートで設定してあるRunCommand内の記述をGPTが書いた内容でコピーアンドペーストして差し替えます。差し替え後に確認すると、VisualStudioが下図のようにエラーを表示しているようです。

エラー内容を見ると、

・ref height の箇所。
→ ref は初期化していないとプログラムのC#の文法上ダメなのでエラー。double height = 1; に修正

・BrepのメソッドとしてMorphを使用しているが、存在しない(GPTが独自のメソッドを作成、、)。
→ Morphメソッドは、Rhino.Geometry.SpaceMorphクラスにあるのでこちらを使用するようお願いします。
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.spacemorph/morph

のような対応が必要そうです。

補足:

out、refは参照渡しと言うC#の機能になります。C#の標準のデータの渡し方は値渡しとなります。またC#で扱うデータ自体にも値型、参照型がありそれぞれの組み合わせ(値-値、値-参照、参照-値、参照-参照)で動作が異なります。興味がある方はプログラムの仕様をご確認ください。

ここでは戻り値はResult型(成功したか、キャンセルかなど)の列挙体を返しています。ユーザが実際に入力した値は refをつけたdouble型 の height変数で受け取る形です。
https://developer.rhino3d.com/api/rhinocommon/rhino.input.rhinoget/getnumber

ただしメソッドなどを定義するときに、outやrefを自分で設定することは少ないと思うので(自作のクラスを戻り値に使えば良いので)API等での使用法を知っていれば良いかなと思います。

再度、チャットで問い合わせてみます。

ありがとうございます。BrepにはMorphメソッドは存在しないようです。SpaceMorphクラスにあるのでこちらを使用して頂けますか?
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.spacemorph/morph

返答があり直ってはいるのですが、ReturnがBrepに変わってしまいました。。引数の数もheightを入れる3つのメソッドはRhinoCommonには存在しないようです。一応下記のように修正。また元のBrepを変更しないよう複製後にモーフするように手動で修正しました。このあたりは上手くいきませんでしたね、

var surfaceMorph = new SporphSpaceMorph(sourceSurface, targetSurface);
var duplicateBrep = brep.DuplicateBrep();
var isMorph = surfaceMorph.Morph(duplicateBrep);

doc.Objects.AddBrep(duplicateBrep);

では、実際に動くかVisualStudioでデバッグ(F5)します(三角形▶のアイコン)。

コンパイルが終わると、RHPが作成されRhinoが起動します(プロジェクトファイル>右クリック プロパティ 出力の箇所から、RHPを作成するフォルダの変更も可能)。

RHPを使用するにはWindowsでは初回デバッグ時はRHPをインストールする必要があります。Macでは特定のフォルダに入れて置くことでRhinoの起動時に自動的に読み込みます。詳細は下記リンクを参照。
https://www.applicraft.com/qanda/rhinoceros/rhp_install/

インストール後に設定したコマンド名で実行すると動作します(ここではコマンド名をBingGPTTestCommandと設定)が、サーフェスと高さの入力はできるが、モーフオブジェクトの入力ができません。おそらく

  1. サーフェスを選択実行
  2. サーフェスが選択状態になる
  3. モーフオブジェクトを選択実行
  4. 既に選択されているサーフェスがあるので、そのサーフェスを使用
  5. 高さを設定

と、実行されモーフオブジェクトが選択できないようです。

ですので、2.サーフェスの選択と3.モーフオブジェクトの選択の間に、[選択を解除する]動作が必要です。チャットで聞いてみます。

選択しているオブジェクトをすべて解除するメソッドはありますでしょうか?

doc.Objects.UnselectAll();

選択をすべて解除するというメソッド(これも良く使います)を提案してくれたので、2と3の間に入れて再度実行してみると、3.モーフするオブジェクトの選択ができるようになりました。
下図のようにサーフェスを選択と、モーフするBrepを共に選択ができるようになったのが確認できます。

補足:

VisualStudioで再度デバッグするときは、Windows版のVisualStudio2022にはホットリロード機能があるので、ここをクリックすることで変更内容を反映できます。Mac版は現時点ではホットリロード機能は無いです。停止して、再度デバッグを実行ください。

またこのモーフィングするメソッドには高さを設定できる引数がないので、あらかじめ指定した長さになるようにモーフ前にXY方向はスケーリングせずにZ方向にだけスケーリングを行う記述を加えたいと思います。

ありがとうございます。高さを指定した長さにしたいので、モーフィングの前にBoundingBoxの底面を基準としてXY方向は拡大縮小せずにZ方向にだけ指定した実際の長さ(height)となるようにスケーリングを行いたいです。

下記のような解答が得られました。既存のモーフ前のコードにコピーします。

再度実行した図。高さが変更できるようになりました。下図は高さ違いで2回作成したものです。

モーフのコードはここまでにして、次はサーフェスの分割のコードを確認してみます。

サーフェスをUVで指定数分割

同様の手順で再度、新規チャットからプロンプトを実行してみます。一度ホウキのアイコン(新しいトピック)をクリックして、今までの内容をクリアします。

サーフェスをUVでそれぞれ指定した数に分割する内容を聞いてみます。先ほど入れたテンプレートっぽい箇所は長くなるのでここでは省略しています。

入力:ターゲットとなるサーフェス(Surface)、U方向の分割数(Integer)、V方向の分割数(Integer)を入力して実行します。
処理:横方向縦方向にそれぞれ分割したサーフェスを作成する。

返答は下記。おおよそ良さそうですね。ただし先ほどの内容を消しリセットして新規にチャットを行ったため、使用している変数名が違いますね(surface とtargetSurface)。

変数名を指定してプロンプトを入力することもできますが、VisualStudioの機能で名前の変更(F2キー、スコープされている変数名を一括して変更)することもできるので、このまま進めます。

モーフの処理箇所を一度コメント化(Ctrl + k→Ctrl + cで複数行まとめてコメント化、Ctrl + k → Ctrl + uで複数行まとめてコメント解除できます。 /*  と  */ で囲っても可)して、サーフェスを分割する箇所だけVisualStudioにコピーしてデバッグして確認します。下図がUを5、Vを5で実行した図です。特に直す箇所もなく問題なく実行できていますね。

自分で書くときはこういう二重のループとかは苦手なので、U方向に分割するメソッド、V方向に分割するメソッド、みたいに細かく分けて書きがちなのですがサラッと上手く行っています。

ただし分割したサーフェスのドメイン値が、RhinoのWhatコマンドで確認したところ、0から1になっていません(GrasshopperでいうReparametriseが設定できていない状態)。

再度GPTに聞いてみます。プロンプトのテンプレート部分は、長くなるので省略しています。

サーフェスのドメイン値を、0から1にするRhinoCommonのコードの記述を教えてください。

サーフェス型の変数に対して、SetDomainメソッドを使用すれば良いのが分かります。引数の1つ目は、UVの方向を指定する値です。0がU方向で、1がV方向。U方向とV方向の両方を設定するので、SetDomainメソッドを2回使っているということですね。
上記の内容を、doc.Objects.AddSurface(~)の前に入れます。再度デバッグを実行後、Rhinoの[What]コマンドで情報を確認してみると、ドメイン値がUV共に0から1に正しく変更されているのが分かります。

複数のBrepの結合

最後の処理、Brepの結合を書いてみます。プロンプトのテンプレート部分は省略しています。

複数のBrepを結合するには、どう書けばよいでしょうか?

しっかりした返答ですね(笑)。上記のように、Brep型のstaticなJoinBrepsメソッドを使えば良いと教えてくれたので、それで実装します。
正確にはJoinBrepsメソッドの引数は、IEnummerable<Brep> Brep複数、double 許容差の順に入力します。

補足:

IEnummerableは、GetEnumeratorメソッドだけを持つインターフェースです。またGetEnumeratorメソッドは、foreach文で次のインデックスのデータを取得するときに内部的に使用しているMoveNextメソッドなどを持つIEnumerator型を戻り値として返すようです。

なのでややこしいのですが、RhinoCommonで引数にIEnummerable<Brep> と書かれている箇所は、

  • リスト List<Brep>
  • 配列 Brep[]

などの形でIEnummerableを実装した複数のBrepの型なら、入力が可能ということになります。逆に単数のBrepはIEnummerable型を継承していないので入力はできません。

doc.ModelAbsoluteToleranceは、Rhinoのドキュメントの絶対許容差を取得するのによく使います。
var tolerance = doc.ModelAbsoluteTolerance;
などの一文を記述の最初に入れておくと、後から許容差の設定が楽にできます。

またGrasshopperのC#コンポーネント内など、RunCommandメソッド以外から行う場合は、
var doc = Rhino.RhinoDoc.ActiveDoc; (現在のドキュメントをdocという名前の変数で定義するということ)
を入れておくと、他の場面でもコードを流用しやすいかと思います。

順番の入れ替え

これで必要なコードはそろったので、モーフ処理個所のコメントを解除(Ctrl + K → Ctrl + U)してVisualStudioでコードのコピー&ペーストを行い順番を入れ替えます。

コードの順番は、

  1. ターゲットサーフェスの選択
  2. モーフオブジェクトの選択
  3. Uの分割数設定
  4. Vの分割数設定
  5. 高さの設定
  6. サーフェスの分割処理
  7. 分割したサーフェス1枚1枚にモーフ処理
  8. モーフしたBrepの結合

の順とします。

入力を求める文言も折角なので、日本語に変更します(” ” の中の文字を変更するだけです)。基本的には上記のコードの順で入れ替えますが、

サーフェスの分割の、下記の箇所はドキュメントに追加する(実際にジオメトリを作成する処理。GrasshopperでいうBake)ような動作です。

doc.Objects.AddSurface(trimmedSurface);

となっているので、var splitedSurfaces = new List <Surface>(); を事前に書いておき、
splitedSurfaces.Add(trimmedSurface);
に書き換えてリストに追加することで、以降の処理でも使用できるようにします。

またモーフ部分もターゲットとなるサーフェスが一枚ではなく複数枚なので、foreachで分割したサーフェス(splitedSurfaces)を一枚一枚順に取り出します。
取り出した後の処理も同様です。こちらもdoc.Objects.AddBrep~~だと実際にRhinoにジオメトリを作成してしまうので、事前に作ったmorphedBrepsリストに追加する形に変更しました。

最後は複数あるリストを結合後に、Rhinoに作成します。

結果確認

再度デバッグして、動作を確認してみます。最初に作成したGrasshoperのアルゴリズムの意図通りに作成できているのが分かります。

いかがでしたでしょうか?とりあえずBingチャットに適切にお願いすれば、コードの7割ほどは書いてくれるのではと思います。

まとめ・振り返り

 このプログラムの後にやりたいこととして、

  • (今は値を順に入力しているので)Rhinoのコマンドオプションのように任意のタイミングで好きな値を変更できるようにしたい
  • 速度の比較がしたいのでストップウォッチで処理時間を計測・表示したい
  • 時間がかかるのなら、プログレスバーを出して進捗が分かるようにしたい
  • マウスカーソルもウェイト状態にして、計算中なのが分かるようにしたい
  • 入力した形状を作成前にプレビューで確認したい
  • UIを作って、プラグインとして見栄えが良くしたい、、、

やった方が良いこととしては、

  • ターゲットサーフェスが、サブオブジェクト選択もしてしまうので、GetObjectクラスなどで書きなおす
  • 記述がべた書きなので、適切にクラスやメソッドにまとめ直してコードをリファクタリングする
  • エラーが起こったときに問題が分かるように、例外処理を入れたい
  • モーフィングは処理が重いので、マルチスレッドで早く動くようにしたい

などなどが考えられます。

次回の記事?があれば、上記の内容を踏まえてAIに作成して貰おうと思います。

また、今回はRunCommand内で使用することを意識してBingChatに書いてもらっています。Grasshopper等と処理を共通化するために単体のクラスで書くには、

RunCommandメソッドではなく、単体のカスタムクラスを定義する形で記述してください。

のようなプロンプト文をつけると、実行する処理をクラス単位で書いてくれることが多いです。是非、色々試して頂けたらと思います。

RunCommand内の記述は下記となります。リファクタリングも何もしていないので見づらい状態ですが、興味があればVisualStudioでコンパイルして確認頂けたらと思います。

protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            //  サーフェス選択
            ObjRef targetSurfaceRef;
            var rc = RhinoGet.GetOneObject("ターゲットとなるサーフェスを選択してください", false, ObjectType.Surface, out targetSurfaceRef);
            if (rc != Result.Success)
                return rc;
            var targetSurface = targetSurfaceRef.Surface();

            doc.Objects.UnselectAll();

            //  張り付けるポリサーフェスを選択
            ObjRef brepRef;
            rc = RhinoGet.GetOneObject("モーフするポリサーフェスを選択してください", false, ObjectType.Brep, out brepRef);
            if (rc != Result.Success)
                return rc;
            var brep = brepRef.Brep();

            //  U数
            int uCount = 2;
            rc = Rhino.Input.RhinoGet.GetInteger("U方向の分割数を設定してください", false, ref uCount);
            if (rc != Result.Success)
                return rc;

            //  V数
            int vCount = 2;
            rc = Rhino.Input.RhinoGet.GetInteger("V方向の分割数を設定してください", false, ref vCount);
            if (rc != Result.Success)
                return rc;

            double height = 1;
            rc = RhinoGet.GetNumber("高さを設定してください", false, ref height);
            if (rc != Result.Success)
                return rc;

            // ドメイン値を求めて、一回の移動ドメイン値を求める
            var uDomain = targetSurface.Domain(0);
            var vDomain = targetSurface.Domain(1);

            var uStep = uDomain.Length / uCount;
            var vStep = vDomain.Length / vCount;

            //  サーフェスをUVで指定した数に分割
            var splitedSurfaces = new List<Surface>();

            for (int i = 0; i < uCount; i++)
            {
                for (int j = 0; j < vCount; j++)
                {
                    var u0 = uDomain.Min + i * uStep;
                    var v0 = vDomain.Min + j * vStep;
                    var u1 = u0 + uStep;
                    var v1 = v0 + vStep;

                    var trimIntervalU = new Interval(u0, u1);
                    var trimIntervalV = new Interval(v0, v1);

                    Surface trimmedSurface = targetSurface.Trim(trimIntervalU, trimIntervalV);
                    if (trimmedSurface != null)
                    {
                        trimmedSurface.SetDomain(0, new Interval(0, 1));
                        trimmedSurface.SetDomain(1, new Interval(0, 1));
                    }
                    // doc.Objects.AddSurface(trimmedSurface);
                    splitedSurfaces.Add(trimmedSurface);
                }
            }

            //バウンディングボックス求める

            var bbox = brep.GetBoundingBox(false);
            var sourceSurface = new PlaneSurface(Plane.WorldXY, new Interval(bbox.Min.X, bbox.Max.X), new Interval(bbox.Min.Y, bbox.Max.Y));

            // 指定した長さになるようにZのスケーリング値設定
            double zScaleFactor = height / (bbox.Max.Z - bbox.Min.Z);

            // XYはスケールせずZ方向にのみスケールする。
            Plane plane = new Plane(bbox.Min, Vector3d.ZAxis);
            Transform scaleTransform = Transform.Scale(plane, 1.0, 1.0, zScaleFactor);

            brep.Transform(scaleTransform);

            //  モーフ処理
            var morphedBreps = new List<Brep>();

            foreach (var splitSurface in splitedSurfaces)
            {
                var surfaceMorph = new SporphSpaceMorph(sourceSurface, splitSurface);

                var duplicateBrep = brep.DuplicateBrep();
                var isMorph = surfaceMorph.Morph(duplicateBrep);

                //doc.Objects.AddBrep(duplicateBrep);
                morphedBreps.Add(duplicateBrep);
            }

            //  結合処理
            var tolerance = doc.ModelAbsoluteTolerance;
            var joinBreps = Brep.JoinBreps(morphedBreps, tolerance);

            //  joinBreps後も複数個ある(結合できなかった)想定で、foreachで書いています
            foreach (var joinBrep in joinBreps)
            {
                //  ドキュメントに追加
                doc.Objects.AddBrep(joinBrep);
            }
            
            doc.Views.Redraw();
            return Result.Success;

        }