【Rhino+Grasshopperを理解して魔改造しよう】③Grasshopperアドオンの開発

【Rhino+Grasshopperを理解して魔改造しよう】③Grasshopperアドオンの開発

はじめに

前章で述べた通り、Grasshopperはその構成要素であるGoo, Param, Componentを自分で追加して利用することが可能です。

簡単な用途ではComponentの追加で事足りますが、自前の形状表現のクラスをGrasshopper上に組み込もうとするとGooとParamの追加が求められてきます。

本章では、チュートリアルや記事が膨大に存在するComponent実装ではなくGooとParamの実装について、コードを交えて紹介していきます。

C#およびVisualStudioなどの開発環境、公式のプロジェクトテンプレートの利用、デバッグの方法などは沢山解説があるのでそちらを見ていただければと思います。そのあたりの知識を前提として書いていきます。

本記事に対応するリポジトリ

https://gitlab.com/natureKotarou/GHAddonDemo

扱うデータ構造

今回は例として有限要素法などで用いられる六面体メッシュを表現するクラスを自前で用意し、それをGrasshopper上で扱えるようにしてみます。

六面体メッシュは、

  • 節点のリスト
  • HexElement(節点を繋いで定義される六面体。含む節点のインデックスの組から定義される)のリスト

から成るHexMeshクラスとします。

実際に有限要素法に使うにはさまざまな抽象化や付加的な情報が必要ですが、ここでは例としてこれに留めます。

GHAddonDemo/Core/HexMesh.cs

public class HexMesh
{
    public List<Point3d> Nodes { get; private set; }
    public List<HexElement> Elements { get; private set; }

    /*
    some constructors, properties and methods
    */

}

GHAddonDemo/Core/HexElement.cs

public class HexElement
{
    public int[] NodeIndices { get; }
    public int this[int index] => NodeIndices[index];

    /*
    some constructors, properties and methods
    */
}

Goo,Param,Componentのミニマム実装

前章の通り、GooはデータをGrasshopper内で扱うためのラッパークラス、ParamはGooの保持と受け渡しをするクラス、Componentは、入力ParamからGooを受け取って処理を実行し出力ParamにGooを受け渡すクラスです。

今回扱いたいHexMeshクラスのGooとParam、および例としてSurfaceからHexMeshを定義するComponentを実装していきます。

GH_Goo<HexMesh>の実装

最も基本的な例としてGH_Gooを継承したGH_HexMeshを実装してみます(リンク先のコードは完成形のものとなるので部分的に異なります)。

GH_Gooを継承したクラスでは、下記の実装が求められます

  • コンストラクタ
  • TypeName
    タイプの名前となる文字列を返すプロパティ
  • TypeDescription
    タイプの概要となる文字列を返すプロパティ
  • IsValid
    Gooに格納された情報がValidであるかを返すプロパティ
  • Duplicate()
    自身のディープコピーを返すメソッド
  • ToString()
    自身の情報を文字列として返すメソッド

GHAddonDemo/GH/GH_HexMesh.cs

public class GH_HexMesh : GH_Goo<HexMesh>
{
    public GH_HexMesh()
    {
    }

    public GH_HexMesh(HexMesh internal_data) : base(internal_data)
    {
    }

    public GH_HexMesh(GH_Goo<HexMesh> other) : base(other)
    {
    }

    public override string TypeName => "GH_HexMesh";

    public override string TypeDescription => "Goo for HexMesh class.";

    public override bool IsValid => Value != null && Value.IsValid;

    public override IGH_Goo Duplicate()
    {
        return new GH_HexMesh(new HexMesh(Value));
    }

    public override string ToString()
    {
        return Value.ToString();
    }
}

GH_Param<GH_HexMesh>の実装

更に、GH_Paramを継承してParamを実装してみます。

ここでは、コンストラクタに加えComponentGuidプロパティの実装が求められます。これはFloatingに設定されコンポーネントのようにUI上で扱われる際に必要となるもので、ほかのParamとComponentと重複しなければ問題ありません。

また、余談ですがここで引数なしのコンストラクタを定義しておくとFloatingのパラメータが利用可能となります(GH立ち上げ時に走査フォルダ内にあるアセンブリからReflectionを用いてコンポーネントが読み込まれるようで、その際にここが分岐となっているようです)

GHAddonDemo/GH/Param_HexMesh.cs

public class Param_HexMesh : GH_Param<GH_HexMesh>
{
    public Param_HexMesh() : base("HexMesh", "HM", "", "Demo", "0_Params", GH_ParamAccess.tree)
    {
    }
    public Param_HexMesh(IGH_InstanceDescription tag) : base(tag)
    {
    }

    public Param_HexMesh(IGH_InstanceDescription tag, GH_ParamAccess access) : base(tag, access)
    {
    }

    public Param_HexMesh(string name, string nickname, string description, GH_ParamAccess access) : base(name, nickname, description, "Demo", "Mesh", access)
    {
    }

    public override Guid ComponentGuid => new Guid("10f558b7-e786-482b-b0c2-de589b284bfd");
}

Paramを入出力として利用したGH_Componentの実装

コンポーネントの実装方法については他コンテンツに譲りますが、GH_Componentクラスを継承してクラスを定義します。

ここでは試しに、Surface(NURBSで定義された面)、オフセット量、UVW方向それぞれの分割数、オフセット方向の反転の有無を入力にSurfaceの法線方向を利用してUVWグリッド状にHexMeshを定義するコンポーネントを書いてみます。詳細はリポジトリをご覧ください。

GHAddonDemo/GH/HexMeshFromSurfaceOffset.cs

public class HexMeshFromSurfaceOffset : GH_Component
{
    public HexMeshFromSurfaceOffset() : base("HexMeshFromSurfaceOffset", "HexMeshFromSrfOffset", "Define HexMesh from offset of Surface.", "Demo", "1_Mesh")
    {
    }

    public override Guid ComponentGuid => new Guid("14b945cc-c4bd-4c88-854b-d5b4308616a6");

    protected override void RegisterInputParams(GH_InputParamManager pManager)
    {
        pManager.AddSurfaceParameter("Surface", "S", "Untrimmed Surface to generate HexMesh.", GH_ParamAccess.item);
        pManager.AddNumberParameter("Offset", "O", "Offset distance", GH_ParamAccess.item, 1);
        pManager.AddIntegerParameter("UDivision", "U", "Division along U-direction", GH_ParamAccess.item, 10);
        pManager.AddIntegerParameter("VDivision", "V", "Division along V-direction", GH_ParamAccess.item, 10);
        pManager.AddIntegerParameter("WDivision", "W", "Division along W-direction", GH_ParamAccess.item, 1);
    }

    protected override void RegisterOutputParams(GH_OutputParamManager pManager)
    {
        pManager.AddParameter(new Param_HexMesh_Geometric("HesMesh", "HM", "Generated HexMesh", GH_ParamAccess.item));
    }

    protected override void SolveInstance(IGH_DataAccess DA)
    {
        /*
        入力を取得しHexMeshを定義し出力に渡す処理
        */
    }
}

自前のParamを入出力に登録する際には上記コードのRegisterOutputParamsメソッド内の記述のようにします。

ここは、GH_Componentクラスの拡張メソッドを作ったり自前Param追加用のメソッドを持つGH_Componentを継承した抽象クラスを定義するなどすると多少扱いやすいかと思います。

ここまででミニマムは出来ました。

Rhinoのビューポートにはまだ何も表示されないですが、生成されたHexMeshの節点数および要素数を見るにどうやら処理は動いているようです。

描画機能の実装

Grasshopperがデフォルトで提供しているジオメトリはRhinoビューポート上に描画されますが、それは対応するGooがIGH_PreviewDataインターフェースを、対応するParamがIGH_PreviewObjectインターフェースを実装しているからです。

Grasshopperでは、特殊なコンポーネント以外ではParamおよびその内部のGooが描画の単位となっており、定義されたParamのうち描画可能なものがビューポートに描画されていきます。

HexMeshがRhinoの世界に描画されるよう、GH_HexMeshとParam_HexMeshにそれぞれインターフェースを実装してみます。

GooにIGH_PreviewDataの実装

ここではHexMeshから抽出されたエッジが描画されるようにしてみました。

DrawViewportWiresDrawViewportMeshesメソッドはRhinoビューポート操作(パンやズームなど)の各フレームに呼ばれるので、ここに重い処理を含むことは推奨されません。

ここでは、エッジ抽出はHexMeshの定義時および変更時にのみ実行、描画用のジオメトリを_previewLinesフィールドで保持、描画メソッド内では描画処理のみを実行という形にしています。

また、ビューポート上での表示判定に用いられるClippingBoxプロパティの実装が求められます。これは、ジオメトリ全体を包含するBoundingBoxを返すことが求められます。

GHAddonDemo/GH/GH_HexMesh.cs

public class GH_HexMesh : GH_Goo<HexMesh>, IGH_PreviewData
{
    /*
    省略
    */

    private Line[] _previewLines;

    public BoundingBox ClippingBox => new BoundingBox(Value.Nodes);

    private void UpdatePreviewGeometries()
    {
        if (Value == null) return;
        var nodes = Value.Nodes;

        var edges = Value.GetIndexSetOfEdges();
        _previewLines = edges.Select(edge => new Line(nodes[edge.OrderedIndices[0]], nodes[edge.OrderedIndices[1]])).ToArray();
    }

    public void DrawViewportWires(GH_PreviewWireArgs args)
    {
        args.Pipeline.DrawLines(_previewLines, args.Color);
    }

    public void DrawViewportMeshes(GH_PreviewMeshArgs args)
    {
    }
}

ParamにIGH_PreviewObjectの実装

ParamにおけるIGH_PreviewObjectの実装においては下のようなプロパティとメソッドの実装が求められますがこの実装方法以外を使うことは少ないです。

GH_Paramクラスに実装されているPreview_*メソッドが、Param内のGooを用いて適切な処理を行ってくれます。

GHAddonDemo/GH/Param_HexMesh.cs

public class Param_HexMesh : GH_Param<GH_HexMesh>, IGH_PreviewObject
{
    /*
    省略
    */

    public bool Hidden { get; set; }

    public bool IsPreviewCapable => true;

    public BoundingBox ClippingBox => Preview_ComputeClippingBox();

    public void DrawViewportMeshes(IGH_PreviewArgs args)
    {
        Preview_DrawMeshes(args);
    }

    public void DrawViewportWires(IGH_PreviewArgs args)
    {
        Preview_DrawWires(args);
    }
}

これでHexMeshが描画されるようになりました!

もちろん入力のSurfaceの形状パラメータや分割数などをいじってもリアルタイムに反映されます。楽しい。

他クラスとの変換機能の実装

Grasshopperがデフォルトで提供しているデータは、可能なものの間での相互変換が自動で行われますが、これはGooのCastTo/CastFromメソッドが実装されているからです。

ここを実装してあげることで、他クラスとの変換に対応することが可能となります。

GooのCastメソッドのオーバーライド

挙動としては、下記のようなタイミング呼ばれるメソッドです。

  • 異なる型のParamからParamにデータが受け渡されて変換の必要があるとき
  • GH_ComponentのSolveInstanceメソッド内で、DA.GetData系メソッドやDA.SetData系メソッドによって自身と異なる型のインスタンスとの間での受け渡しを求められて変換の必要があるとき

詳細な話ですが、ジェネリクスで指定された型T(GH_HexMeshにおいてはHexMesh型)との変換はここで書いてあげる必要があり、基本的に自前のGooでは書いてあげると便利です。それにより、コンポーネント内でのDA.GetData/SetData系メソッドからの利用時に明示的にGooを定義してから渡す必要がなくなります。

今回は表面メッシュの抽出を行うことでRhinoでデフォルトでサポートされているMeshへの変換を可能にしてみます。

GHAddonDemo/GH/GH_HexMesh.cs

public class GH_HexMesh : GH_Goo<HexMesh>, IGH_PreviewData
{
    /*
    省略
    */

    public override bool CastFrom(object source)
    {
        if (source is HexMesh hexMesh)
        {
            Value = hexMesh.DeepCopy();
            UpdatePreviewGeometries();
            return true;
        }
        else
        {
            return false;
        }
    }

    public override bool CastTo<Q>(ref Q target)
    {
        target = default;
        if (typeof(Q).IsAssignableFrom(typeof(HexMesh)))
        {
            target = (Q)(object)Value;
            return true;
        }
        else if (typeof(Q).IsAssignableFrom(typeof(GH_Mesh)))
        {
            target = (Q)(object)new GH_Mesh(Value.ToSurfaceMesh());
            return true;
        }
        else
        {
            return false;
        }
    }
}

これで、Meshとの変換が可能になりました。例としてMeshパラメータにHexMeshパラメータをつなぐと、自動で変換が行われます。

もちろん、ケースによってSurfaceやCurveなど他のジオメトリとの変換を実装したり双方向での変換を実装することも可能です。

幾何学的変換処理への対応の実装

Grasshopperがデフォルトで提供しているジオメトリは、並進や回転、拡大縮小といった幾何学的変換処理や複雑な変形処理を行うことができますが、これはGooがそれら処理に対応するIGH_GeometricGooを実装しているからです。

今回は幾何学的なHexMeshクラスを扱っているので、継承するクラスをGooからGeometricGooに変更することでGHが持つ形状処理機能への対応が可能となります。

継承元をGooからGeometricGooに

GHAddonDemo/GH/GH_HexMesh.cs

public class GH_HexMesh : GH_GeometricGoo<HexMesh>, IGH_PreviewData
{
    /*
    省略
    */

    public override BoundingBox Boundingbox => new BoundingBox(Value.Nodes);
    public BoundingBox ClippingBox => Boundingbox;

    public override IGH_GeometricGoo DuplicateGeometry()
    {
        return new GH_HexMesh(new HexMesh(Value));
    }

    public override BoundingBox GetBoundingBox(Transform xform)
    {
        var bbox = Boundingbox;
        bbox.Transform(xform);
        return bbox;
    }

    public override IGH_GeometricGoo Transform(Transform xform)
    {
        var mesh = Value.DeepCopy();

        for (int i = 0; i < mesh.NodeCount; i++)
        {
            var node = mesh.Nodes[i];
            node.Transform(xform);
            mesh.Nodes[i] = node;
        }
        return new GH_HexMesh(mesh);
    }
    public override IGH_GeometricGoo Morph(SpaceMorph xmorph)
    {
        var mesh = Value.DeepCopy();

        var morphedPts = mesh.Nodes.Select(node => xmorph.MorphPoint(node)).ToArray();
        mesh.Nodes.Clear();
        mesh.Nodes.AddRange(morphedPts);

        return new GH_HexMesh(mesh);
    }
}

ここでは追加でいくつかのプロパティとメソッドの実装が求められていますが、幾何学的変換にかかわるのはTransformとMorphメソッドです。

HexMeshでは節点の位置のみが変形に影響するので、与えられた幾何学的変換を節点に対して行うよう処理を書いています。

するとこんな感じで並進や回転、拡大縮小が可能となったり



こんな変形も可能になったりします。楽しい。

上のものに限らず曲面に沿った変形など複雑な処理が多く提供されており、用途によっては非常に多くの資産の恩恵を自前ジオメトリ表現に受けさせることができます。

おわりに

このように、少しマニアックですがGooやParamの実装を含めてGrasshopperのアドオンを実装すると様々な嬉しさがあります。

前章からの繰り返しになりますが様々な用途に便利なツールですので、是非触ってみてください。

Author