Rhino C++ DLLをビルドしてGrasshopperから使ってみる
背景
Rhinoには.Net言語(主にC#)からアクセスできるAPI、"RhinoCommon"がありますが、これの基となっているのが"Rhino C++ API"です。C++APIはより深いコア機能にアクセスできるので、RhinoCommonでは出来ないような高度なハックを必要とするプラグインを作成するために使ったりします(例: VisualARQ)。
単純なRhinoのC++プラグインを作るだけであれば、ソフトウェア開発キット(SDK)が用意されているので、これをインストールし、公式マニュアル通りに進めれば容易に作成できます。ただ、GHからアクセスしたい等の場合、C#から使えるようにするため、ライブラリ(DLL)としてビルドする必要があります。ここではそのライブラリの作り方と使い方を紹介します。
先に結論を言ってしまうと、↓に書いてある手順に沿えばC++APIに依存したDLLを作成することが出来ます。
ただ、これに行き着くのに少々手間取ることもあるかと思うので、以下に詳細な手順を載せておきます。
C++SDKのインストール
C++SDKのインストールは簡単なのでこちら↓を参考に進めてください。
プロジェクトのセットアップ(C++)
DLLとなるC++側のプロジェクトのセットアップを行います。
プロジェクトの作成
テンプレートはRhino 3D Plug-in(C++)を選択
名前や保存先を指定します。
DLL用にセットアップ
そのままだとRhinoプラグイン(.rhp)が作られちゃうので、.dllをビルドできるように設定します。
プロジェクトの作成が終わったら、以下の3つのファイルを削除します。
- *Plugin.h
- *Plugin.cpp
- cmd*.cpp
続いて、プロパティマネージャーでReleaseとDebugの両方の構成でRhino.Cpp.Pluginを削除します。
代わりにRhino.Cpp.PluginComponentを追加します。これはインストールしたC++SDKフォルダのPropertySheetsの中にあります。
出力ディレクトリの設定
ここはお好みですが、ビルドしたdllをbin下に置きたいため、プロジェクトのプロパティを開いて、"すべての構成"で出力ディレクトリを以下に変えておきます。
bin\$(Platform)\$(Configuration)\
ビルドしてみる
試しにビルドしてみます。
普通に"ソリューションのビルド"をしてもいいのですが、DebugとReleaseを同時にビルドしてみたいため、"パッチビルド"を使います。
対象の構成にチェックを入れ、ビルドします。
プロジェクトフォルダにbinが配置され、その中にdllが生成されていればokです。
プロジェクトのセットアップ(C#)
ここではビルドしたDLLをGrasshopperから使うため、GHプラグインのプロジェクトのセットアップを行います。
GHプラグインのプロジェクトを追加
ソリューションエクスプローラーからソリューションを右クリック→追加→新しいプロジェクト
テンプレートは"Grasshopper Assembly for Rhino 3D(C#)"を選択し、GHプラグインのプロジェクトを作成します。まだGH用のテンプレートをインストールしていなければこちらを参考にインストールしてください。
プロジェクトファイルの編集
GHプラグインがビルドされたときに必要なものが諸々コピーされるようにプロジェクトファイルを編集します。ここでは以下の2点を行います。
- DLLプロジェクトから.dllを(Debug時は.pdbも)TargetDir(ビルドしたものが生成される場所)にコピー
- TargetDirをまるごとGHのライブラリフォルダにコピー
ソリューションエクスプローラーでGHプラグインのプロジェクトを右クリック→プロジェクトファイルの編集で以下のコードを追加してください。
試しにビルドして、指定したものがコピーされているか確認してください。
<!--中略-->
<PropertyGroup>
<!-- ビルドしたファイルをコピーする先のフォルダ -->
<MyDestinationFolder>$(USERPROFILE)\AppData\Roaming\Grasshopper\Libraries</MyDestinationFolder>
<!-- dllプロジェクトの名前(ここは適宜変更してください) -->
<DllName>MyRhinoPlugin</DllName>
<!-- dllのパス -->
<MyLibPath>$(SolutionDir)$(DllName)\bin\x64\$(Configuration)\$(DllName).dll</MyLibPath>
<!-- pdbのパス -->
<MyPdbPath Condition="'$(Configuration)' == 'Debug'">$(SolutionDir)$(DllName)\bin\x64\$(Configuration)\$(DllName).pdb</MyPdbPath>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<!-- cppのライブラリをTargetDirにコピーする -->
<Copy SourceFiles="$(MyLibPath)" DestinationFolder="$(TargetDir)" />
<!-- Debugの場合、pdbをTargetDirにコピーする -->
<Copy SourceFiles="$(MyPdbPath)" DestinationFolder="$(TargetDir)" Condition="'$(Configuration)' == 'Debug'"/>
<!-- ビルドしたファイルをghライブラリのディレクトリにコピーする -->
<ItemGroup>
<MySourceFiles Include="$(TargetDir)\**" />
</ItemGroup>
<Copy SourceFiles="@(MySourceFiles)" DestinationFolder="$(MyDestinationFolder)\$(ProjectName)" />
</Target>
</Project>
コードを書く
今回は簡単な例として、2点を結ぶLineをC++側で生成し、それをC#側に渡したいと思います。
P/Invokeと呼ばれる手法でC++側で定義したメソッドをC#から呼び出します。P/Invokeの詳細はここでは省きます。
C++
小規模なのでヘッダーファイル(.h)は使わずソースファイル(.cpp)に直接記述します。
二つのON_3dPointを受け取り、ON_Lineのポインタを返す関数を定義します。
#include "stdafx.h"
#include <Windows.h>
extern "C"
{
__declspec(dllexport) ON_Line* CreateLine(ON_3dPoint pt1, ON_3dPoint pt2)
{
ON_Line* line = new ON_Line(*pt1, *pt2);
return line;
}
}
C#
DllImport属性を付けた関数を定義することで、C++でビルドしたDLLに存在する関数をC#からアクセスできるようにします。
C++側の関数に対応するように、二つのPoint3dを引数に持ち、ポインタ型の値を返す関数を宣言します。
using System;
using System.Runtime.InteropServices;
using Rhino.Geometry;
namespace MyGHPlugin
{
internal static class UnsafeNativeMethods
{
// dllの名前は適宜変更してください
[DllImport("MyRhinoPlugin.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateLine(Point3d start, Point3d end);
}
}
Grasshopperのカスタムコンポーネントを作ります。
受け取ったポインタ型の値はLine型に変換します。
using Grasshopper;
using Grasshopper.Kernel;
using Rhino.Geometry;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace MyGHPlugin
{
public class CppCreateLineComponent : GH_Component
{
public CppCreateLineComponent(): base("CppCreateLineComponent", "CppCreateLine", "","MyTools", "Test")
{
}
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddPointParameter("Start", "S", "", GH_ParamAccess.item);
pManager.AddPointParameter("End", "E", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddLineParameter("Line", "L", "", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
//Get input data
Point3d start = new Point3d();
Point3d end = new Point3d();
if (!DA.GetData(0, ref start)) return;
if (!DA.GetData(1, ref end)) return;
IntPtr ptr_line = UnsafeNativeMethods.CreateLine(start, end);
//Convert pointer to Line
Line line = (Line)Marshal.PtrToStructure(ptr_line, typeof(Line));
//Set output data
DA.SetData(0, line);
}
protected override System.Drawing.Bitmap Icon => null;
public override Guid ComponentGuid => new Guid("ab4cccdc-a9a7-430c-b18a-279b45afea3d");
}
}
ビルドする
C++とC#を両方ビルドします。
GHに追加したコンポーネントが正常に機能していればokです。
おわりに
今回はRhino C++ APIを使ったコードをDLLとしてビルドし、それをGHから使ってみました。
一番気になるのはこれをどういった場面で使うかだと思います。
便利な機能は大体RhinoCommonの方でも使えるようになっているので、ほとんどのケースはRhinoCommonで事足りるかなと思います。ただ、VisualARQみたいな変態的なカスタムオブジェクト(おそらくInstanceObjectを拡張している)を作るにはC++ APIを使う必要がありそうです。
あと考えられる用途としては、別のC++ライブラリと直接連携したいときとかでしょうか。
(筆者はRhinoObjectの属性ユーザーテキストをFbxに埋め込みたいと思ってた時期に、FBXSDKをつかったプラグイン(かなり雑)を作ってました。)
補足
C++で作ったDLLをRhinoとGH両方で使う可能性がある場合、同様のDllImportを二か所で記述するのは面倒なので、ラッパーライブラリを作成すると良いかもしれません。
また、今回はLine(構造体)だったのでMarshal.PtrToStructure()
を使いましたが、Rhino.Runtime.Interopを使って変換することもできます。
ここら辺は以下のgithubリポジトリが参考になります。
因みにRhinoCommonの一部の実装でも同じようにP/Invokeを使用していたりします。
Discussion