STUDIO TAMA


thumbnail

投稿日:2024-03-30

【Rhinoceros-plugin C#】Exporting Rhino Geometries with Attributes User Text to GLB/GLTF

  • #Rhinoceros

  • #Plugin

  • #Three.js

  • #R3F

  • #C#

Rhinoceros で作成したモデルを Web に持っていくことがよくあるのですが、設定した 属性情報の UserText も一緒に Web へ持っていきたくなることもしばしばあり glb / gltf Exporter を実装しました。以前記事にもしましたが(以下リンク)Python で同じものを実装したことがあるのですが、流石に Python では容量の大きなモデルを Export するとクラッシュしてしまったので今回 C#で実装することにしました。C#はそんなに書き慣れていないので、おかしな書き方等あるかもしれませんがその辺はご了承ください。 以下に、動作状況の Youtube リンクをのせときますのでぜひ確認してみてください。

thumbnail

【Rhinoceros】作成したモデルをUser Textを持たせたままWebで表示する

Rhinocerosで作成したモデルをUserTextを持たせたままWebで表示する

概要

・webGL やゲームエンジンなどよく触る人は glb / gltf 形式はなじみがあるかと思います。Rhino8 がリリースされる前は、Rhino で作ったモデルを FBX などで出力し Blender へ持っていき gltf / glb 更には Draco 圧縮をかけたりしていましたが、Rhinoceros8 では glb / gltf draco 圧縮がサポートされ、ほかのツールを経由することなく glb / gltf 形式での Export・圧縮が可能となり個人的にはとても便利になりました。

ただ、最近 Rhinoceros 上で設定した UserText も付与した状態で Web へ持っていきたい状況に直面し、Rhinoceros8 のデフォルトの glb / gltf Exporter では UserText を維持したままのモデルの Export が現状 ( 2024/03/29 時点) ではサポートされていないため自分で実装することにしました。

以下画像が Web で表示するまでのざっくりとした流れになってます。

thumbnail
  1. Rhinoceros のモデルを作成し、各ジオメトリに任意の UserText を持たせます。
  2. Rhinoceros のプラグインを C#で作成し、作成したモデルを UserText ごと gltf 又は glb で Export します。
  3. Export したモデルを必要であれば Node.js の gltf-pipeline で Draco 圧縮します。(※本来であれば 2 プラグイン内で Draco 圧縮まで実装したかったのですが、私の技術力不足でそこまで実装できませんでした。そのうち再チャレンジしようと思います!)
  4. Three.js (React Three Fiber)で作成したモデルを表示していきます。

今回は、C#で作成したプラグインの部分のみを書いていこうと思います。Nodejs / Three.js 部分は解説していませんのでご了承ください。 また、ここから先の解説は gltf フォーマットの構造がわかっていないと理解しずらいかと思います。

こちら

の GLTF データの開発元である Khronos Group の repository を参照してください。以前私のブログでもちょっとだけ触れたので、若干参考になるかもしれません。

開発概要

  • 今回開発するプラグインのgithub リポジトリ
  • Rhinoceros のプラグイン開発概要は公式ドキュメントを参照ください。Target Version は Rhino7 にしてください。
  • モジュールのバージョン等は以下の画像。今回は SharpGltf を使用して Gltf/Glb を Export していきます。
thumbnail

プログラムの解説

  • こちらが今回のgithub リポジトリ となります。 Class の抽出をしているとこも多々あるのですべてではないですが、以下が今回のコードになります。 Rhino 上で打ち込むコマンドは GlbWithUserAttributes としています。 冒頭でも述べましたが、あまり C#書き慣れていないので変なとこあるかもしれませんがご了承ください。m(_ _)m

ExportGlbCommands.cs

1using Rhino;
2using Rhino.Commands;
3using Rhino.Geometry;
4using Rhino.Input.Custom;
5using Rhino.DocObjects;
6using System.Numerics;
7using System.Collections.Generic;
8using SharpGLTF.Geometry;
9using SharpGLTF.Geometry.VertexTypes;
10using SharpGLTF.Materials;
11using SharpGLTF.Scenes;
12using RHINOMESH = Rhino.Geometry.Mesh;
13using SharpGLTF.IO;
14using System.IO;
15using ExportGlb.Models;
16using ExportGlb.Utilities;
17using ExportGlb.Helpers;
18
19
20namespace ExportGlb
21{
22    public class ExportGlbCommand : Command
23    {
24        public ExportGlbCommand()
25        {
26            Instance = this;
27        }
28
29        public static ExportGlbCommand Instance { get; private set; }
30
31        public override string EnglishName => "GlbWithUserAttributes";
32
33
34        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
35        {
36            var geometry = new GetObject();
37            geometry.SetCommandPrompt("Select objects to mesh");
38            geometry.GeometryFilter = ObjectType.Mesh | ObjectType.Brep;
39            geometry.SubObjectSelect = false;
40            geometry.GroupSelect = true;
41            geometry.GetMultiple(1, 0);
42            if (geometry.CommandResult() != Result.Success)
43            {
44                Rhino.RhinoApp.WriteLine("An error occurred: " + geometry.CommandResult().ToString());
45                return geometry.CommandResult();
46            }
47
48            Rhino.RhinoApp.WriteLine("Please waiting...");
49
50            var settings = new MeshingParameters(0);
51            List<MeshWithUserData> meshWithUserDataList = new List<MeshWithUserData>();
52
53            Rhino.DocObjects.Material defaultMaterial = new Rhino.DocObjects.Material();
54            defaultMaterial.Name = "Default";
55            defaultMaterial.DiffuseColor = System.Drawing.Color.White;
56
57            foreach (var objRef in geometry.Objects())
58            {
59                List<UserAttribute> userAttributes = new List<UserAttribute>();
60                var attributes = objRef.Object().Attributes;
61                var keys = attributes.GetUserStrings();
62                foreach (string key in keys)
63                {
64                    var value = attributes.GetUserString(key);
65                    userAttributes.Add(new UserAttribute { key = key, value = value });
66                }
67
68                Rhino.DocObjects.Material rhinoMaterial = defaultMaterial;
69                if (objRef.Object().Attributes.MaterialSource == ObjectMaterialSource.MaterialFromObject)
70                {
71                    rhinoMaterial = doc.Materials[objRef.Object().Attributes.MaterialIndex];
72                }
73                else if (objRef.Object().Attributes.MaterialSource == ObjectMaterialSource.MaterialFromLayer)
74                {
75                    var layerIndex = objRef.Object().Attributes.LayerIndex;
76                    var layer = doc.Layers[layerIndex];
77                    if (layer.RenderMaterialIndex >= 0)
78                    {
79                        rhinoMaterial = doc.Materials[layer.RenderMaterialIndex];
80                    }
81                }
82
83                RHINOMESH mesh = null;
84                if (objRef.Mesh() != null)
85                {
86                    mesh = objRef.Mesh();
87                }
88                else if (objRef.Brep() != null)
89                {
90                    var brepMeshes = RHINOMESH.CreateFromBrep(objRef.Brep(), settings);
91                    if (brepMeshes.Length > 0)
92                    {
93                        mesh = new RHINOMESH();
94                        foreach (var m in brepMeshes)
95                        {
96                            mesh.Append(m);
97                        }
98                    }
99                }
100
101                if (mesh != null && rhinoMaterial != null)
102                {
103                    meshWithUserDataList.Add(new MeshWithUserData(mesh, userAttributes, rhinoMaterial));
104                }
105            }
106
107            float scaleFactor = Utility.GetModelScaleFactor(RhinoDoc.ActiveDoc);
108            Dictionary<string, MaterialBuilder> materialBuilders = new Dictionary<string, MaterialBuilder>();
109            var sceneBuilder = new SceneBuilder();
110
111
112            foreach (var item in meshWithUserDataList)
113            {
114                RHINOMESH rhinoMesh = item.Mesh;
115                var scaleTransform = Rhino.Geometry.Transform.Scale(Point3d.Origin, scaleFactor);
116                rhinoMesh.Transform(scaleTransform);
117
118                List<UserAttribute> attributes = item.UserAttributes;
119                Rhino.DocObjects.Material rhinoMaterial = item.RhinoMaterial;
120
121                string materialName =
122                  !string.IsNullOrEmpty(rhinoMaterial.Name)
123                  ? rhinoMaterial.Name
124                  : "Material_" + materialBuilders.Count.ToString();
125                if (!materialBuilders.TryGetValue(materialName, out MaterialBuilder materialBuilder))
126                {
127                    materialBuilder = new MaterialBuilder(materialName)
128                        .WithDoubleSide(true)
129                        .WithMetallicRoughnessShader()
130                        .WithChannelParam(KnownChannel.BaseColor, new Vector4(
131                            (float)rhinoMaterial.DiffuseColor.R / 255,
132                            (float)rhinoMaterial.DiffuseColor.G / 255,
133                            (float)rhinoMaterial.DiffuseColor.B / 255,
134                            1.0f - (float)rhinoMaterial.Transparency
135                        ));
136
137                    var texture = rhinoMaterial.GetBitmapTexture();
138                    if (texture != null)
139                    {
140                        var texturePath = texture.FileReference?.FullPath;
141                        if (!string.IsNullOrEmpty(texturePath) && File.Exists(texturePath))
142                        {
143
144                            materialBuilder.WithChannelImage(KnownChannel.BaseColor, texturePath);
145                        }
146                    }
147                    materialBuilders.Add(materialName, materialBuilder);
148                }
149
150                var meshBuilder = new MeshBuilder<VertexPositionNormal, VertexTexture1>("mesh");
151                var prim = meshBuilder.UsePrimitive(materialBuilder);
152
153                rhinoMesh.Faces.ConvertQuadsToTriangles();
154                rhinoMesh.Normals.ComputeNormals();
155                rhinoMesh.Compact();
156
157                foreach (var face in rhinoMesh.Faces)
158                {
159                    var vertexA = VertexUtility.CreateVertexBuilderWithUV(rhinoMesh, face.A);
160                    var vertexB = VertexUtility.CreateVertexBuilderWithUV(rhinoMesh, face.B);
161                    var vertexC = VertexUtility.CreateVertexBuilderWithUV(rhinoMesh, face.C);
162
163                    prim.AddTriangle(vertexA, vertexB, vertexC);
164                }
165
166                if (attributes.Count > 0)
167                {
168                    Dictionary<string, string> attributesDict = new Dictionary<string, string>();
169                    foreach (var attribute in item.UserAttributes)
170                    {
171                        var key = attribute.key;
172                        var value = attribute.value;
173                        attributesDict.Add(key, value);
174                    }
175
176                    var extras = JsonContent.CreateFrom(attributesDict);
177                    meshBuilder.Extras = extras;
178                }
179
180                sceneBuilder.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
181            }
182
183
184            //Export glb or gltf file
185            bool isSaved = FileSaver.ShowSaveFileDialogAndSave(sceneBuilder, doc);
186            if (!isSaved)
187            {
188                Rhino.RhinoApp.WriteLine("File saving cancelled or failed.");
189                return Result.Cancel;
190            }
191
192            return Result.Success;
193        }
194
195    }
196 }

では RunCommand 部分を切り分けて解説していこうと思います。

ExportGlbCommands.cs

1        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
2        {
3            var geometry = new GetObject();
4            geometry.SetCommandPrompt("Select objects to mesh");
5            geometry.GeometryFilter = ObjectType.Mesh | ObjectType.Brep;
6            geometry.SubObjectSelect = false;
7            geometry.GroupSelect = true;
8            geometry.GetMultiple(1, 0);
9            if (geometry.CommandResult() != Result.Success)
10            {
11                Rhino.RhinoApp.WriteLine("An error occurred: " + geometry.CommandResult().ToString());
12                return geometry.CommandResult();
13            }
14
15            Rhino.RhinoApp.WriteLine("Please waiting...");
16
17      .....省略
  • まず初めにユーザーに Rhinoceros 上のオブジェクトを選択するように促します。
  • var geometry = new GetObject();で GetObject クラスのインスタンスを作成し画面上のオブジェクトを選択できるようにします。
  • その後、GeometryFilter で選択できるオブジェクトを Mesh か Brep に限定します。こうすることで Curve や Point などが選択に入っていても無視されます。
  • geometry.SubObjectSelect = false;でサブオブジェクトの選択を無効にします。(Ctrl + Shift+左クリックでオブジェクトの一部が選択できるかと思いますが、それを無効にする)
  • geometry.GroupSelect = true;でグループ化されたオブジェクトは選択可能とします。
  • geometry.GetMultiple(1, 0);で選択できるオブジェクトの数を最低 1 つ最大で無限(0 は無限、エンターキーを押さないとオブジェクトの選択を終了しない。)に設定します。
  • if 文でコマンドの実行結果をチェックします。もし結果が成功でなければ、エラーメッセージを表示し、その結果を返します。
  • 問題なければコマンドプロンプトに"Please waiting…"を表示します。

UserAttribute.cs

1namespace ExportGlb.Models
2{
3    public class UserAttribute
4    {
5        public string key { get; set; }
6        public string value { get; set; }
7    }
8}

MeshWithUserData.cs

1using System.Collections.Generic;
2using Rhino.DocObjects;
3using RHINOMESH = Rhino.Geometry.Mesh;
4
5namespace ExportGlb.Models
6{
7    public class MeshWithUserData
8    {
9        public RHINOMESH Mesh { get; set; }
10        public List<UserAttribute> UserAttributes { get; set; }
11        public Material RhinoMaterial { get; set; }
12
13        public MeshWithUserData(RHINOMESH mesh, List<UserAttribute> userAttributes, Material rhinoMaterial)
14        {
15            Mesh = mesh;
16            UserAttributes = userAttributes;
17            RhinoMaterial = rhinoMaterial;
18        }
19    }
20}
  • 次に、プロジェクト直下に Models フォルダを作成し、そこに UserAttribute.cs と MeshWithUserData.cs を作成します。
  • UserAttribute.cs は RhinoGeometry から取得した UserText を key, value 形式で格納するための Class になります。
  • MeshWithUserData.cs は、選択した RhinoGeometry を後々 Mesh 化するのですが、Geometry を Mesh 化した Mesh・Geometry に割り当てられていた Material・UserText をセットにしておくためのものになります。1 Geometry を1 Mesh に変換し、その 1 つの Mesh に対して Material は 1 つですが、UserText は複数割り当てられていることが多いと思いますので先ほど作成した UserAttributes 型の List<UserAttributes>形式になってます。

ExportGlbCommands.cs

1          .....先ほどからの続き
2
3       var settings = new MeshingParameters(0);
4            List<MeshWithUserData> meshWithUserDataList = new List<MeshWithUserData>();
5
6            Rhino.DocObjects.Material defaultMaterial = new Rhino.DocObjects.Material();
7            defaultMaterial.Name = "Default";
8            defaultMaterial.DiffuseColor = System.Drawing.Color.White;
9
10            foreach (var objRef in geometry.Objects())
11            {
12                List<UserAttribute> userAttributes = new List<UserAttribute>();
13                var attributes = objRef.Object().Attributes;
14                var keys = attributes.GetUserStrings();
15                foreach (string key in keys)
16                {
17                    var value = attributes.GetUserString(key);
18                    userAttributes.Add(new UserAttribute { key = key, value = value });
19                }
20
21                Rhino.DocObjects.Material rhinoMaterial = defaultMaterial;
22                if (objRef.Object().Attributes.MaterialSource == ObjectMaterialSource.MaterialFromObject)
23                {
24                    rhinoMaterial = doc.Materials[objRef.Object().Attributes.MaterialIndex];
25                }
26                else if (objRef.Object().Attributes.MaterialSource == ObjectMaterialSource.MaterialFromLayer)
27                {
28                    var layerIndex = objRef.Object().Attributes.LayerIndex;
29                    var layer = doc.Layers[layerIndex];
30                    if (layer.RenderMaterialIndex >= 0)
31                    {
32                        rhinoMaterial = doc.Materials[layer.RenderMaterialIndex];
33                    }
34                }
35
36                RHINOMESH mesh = null;
37                if (objRef.Mesh() != null)
38                {
39                    mesh = objRef.Mesh();
40                }
41                else if (objRef.Brep() != null)
42                {
43                    var brepMeshes = RHINOMESH.CreateFromBrep(objRef.Brep(), settings);
44                    if (brepMeshes.Length > 0)
45                    {
46                        mesh = new RHINOMESH();
47                        foreach (var m in brepMeshes)
48                        {
49                            mesh.Append(m);
50                        }
51                    }
52                }
53
54                if (mesh != null && rhinoMaterial != null)
55                {
56                    meshWithUserDataList.Add(new MeshWithUserData(mesh, userAttributes, rhinoMaterial));
57                }
58            }
59
60          .....省略
  • ExportGlbCommands.cs に戻ります。先ほど User に Rhinoceros 上の Geometry を選択させ、コマンドラインに Please waiting と表示するとこまで行いましたがその後の処理になります。
  • ここでの処理は User が選択した Rhinoceros の Geometry から Material・UserText の取得、Brep だった場合は Mesh 化して、先ほど作成した MeshWithUserData にこれらをまとめるといった作業になります。
  • var settings = new MeshingParameters(0);最初に Brep を Mesh 化する際の設定を記述しておきます。引数は Brep を Mesh 化する際の粗さを表しており double 型で 0 ~ 1 の値を入力します。0 ほど粗い Mesh になります。今回は 0 として進めますが、User に選ばせるような設定にするのもありかもしれません。
  • List<MeshWithUserData> meshWithUserDataList = new List<MeshWithUserData>(); MeshWithUserData の List 型のインスタンスを作成します。
  • Rhino.DocObjects.Material defaultMaterial = new Rhino.DocObjects.Material();から 3 行は万が一選択したオブジェクトに Material が割り当てられていなかった場合にデフォルトのマテリアルを作成しておきます。マテリアルの名前を"Default"とし、Diffuse に白を割り当ててます。
  • User が選択した複数の Geometry に対して for ループで 1 つ 1 つ処理していきます。
  • 最初に UserText を取得します。List<UserAttribute> userAttributes = new List<UserAttributes>();から 8 行分で処理を行っています。先ほど作成した UserAttribute を List 型でインスタンスを作成し、各 Geometry に割り当てられている UserText を key value の形で userAttributes に格納していきます。
  • 次に Material を取得していきます。Rhino.DocObjects.Material rhinoMaterial = defaultMaterial;から 14 行分で処理を行っています。最初に rhinoMaterial に先ほど作成した defaultMaterial を割り当てておき、もし Geometry に個別の Materiarl が割り当てていたら rhinoMaterial をその Material に切替て、そうでなかった場合はその Geometry が属する Layer の Material に切り替えます。どちらも割り当てられてなかったら defaultMaterial のままといった処理になります。
  • 次に Geometry を Mesh に変換していきます。RHINOMESH mesh = null;から 16 行分で処理を行っています。とりあえず RHINOMESH mesh = null;にしておきます。
  • もし選択した Geometry が Mesh だったら変数 mesh にその Mesh をそのまま割り当てます。
  • もし Brep だったら先ほどの settings に基づいて Mesh 化する処理を行いますが、Brep から複数の Mesh が生成される場合があるので brepMeshes に foreach をかけて単一 Mesh に結合しています。
  • 最後に、if (mesh != null && rhinoMaterial != null)以降の部分で最初に作成した meshWithUserDataList に MeshWithUserData 型で mesh ,userAttributes, rhinoMaterial をセットにして格納します。

Utility.cs

1using Rhino;
2
3namespace ExportGlb.Utilities
4{
5    public static class Utility
6    {
7        public static float GetModelScaleFactor(RhinoDoc doc)
8        {
9            var modelUnit = doc.ModelUnitSystem;
10            var scale = 1.0f;
11
12            switch (modelUnit)
13            {
14                case UnitSystem.None:
15                case UnitSystem.Meters:
16                    scale = 1.0f;
17                    break;
18                case UnitSystem.Millimeters:
19                    scale = 0.001f;
20                    break;
21                case UnitSystem.Centimeters:
22                    scale = 0.01f;
23                    break;
24                case UnitSystem.Inches:
25                    scale = 0.0254f;
26                    break;
27                case UnitSystem.Feet:
28                    scale = 0.3048f;
29                    break;
30            }
31
32            return scale;
33        }
34    }
35}
  • 先ほど作成した meshWithUserDataList(Rhinoceros 上で選択した Geometry を Mesh 化して Material と UserText を取得してセットにしたもの)に基づいて SharpGltf のライブラリを使用して SharpGltf 上の Mesh・Material に変換していきます。そのまえの下準備が上記になります。
  • Rhinoceros 上ではあらゆる単位でモデリングを行っているかと思いますが、それらをすべて WebGL 上の 1 単位に合わせるように Scale します。上記は Rhinoceros 上での単位を取得してそれに基づいて ScaleFactor を取得しています。

ExportGlbCommands.cs

1           .....先ほどの続き
2
3           float scaleFactor = Utility.GetModelScaleFactor(RhinoDoc.ActiveDoc);
4           Dictionary<string, MaterialBuilder> materialBuilders = new Dictionary<string, MaterialBuilder>();
5           var sceneBuilder = new SceneBuilder();
6
7           foreach (var item in meshWithUserDataList)
8           {
9               RHINOMESH rhinoMesh = item.Mesh;
10               var scaleTransform = Rhino.Geometry.Transform.Scale(Point3d.Origin, scaleFactor);
11               rhinoMesh.Transform(scaleTransform);
12
13               List<UserAttribute> attributes = item.UserAttributes;
14               Rhino.DocObjects.Material rhinoMaterial = item.RhinoMaterial;
15
16               string materialName =
17                !string.IsNullOrEmpty(rhinoMaterial.Name)
18                ? rhinoMaterial.Name
19                : "Material_" + materialBuilders.Count.ToString();
20               if (!materialBuilders.TryGetValue(materialName, out MaterialBuilder materialBuilder))
21               {
22                   materialBuilder = new MaterialBuilder(materialName)
23                       .WithDoubleSide(true)
24                       .WithMetallicRoughnessShader()
25                       .WithChannelParam(KnownChannel.BaseColor, new Vector4(
26                           (float)rhinoMaterial.DiffuseColor.R / 255,
27                           (float)rhinoMaterial.DiffuseColor.G / 255,
28                           (float)rhinoMaterial.DiffuseColor.B / 255,
29                           1.0f - (float)rhinoMaterial.Transparency
30                       ));
31
32                   var texture = rhinoMaterial.GetBitmapTexture();
33                   if (texture != null)
34                   {
35                       var texturePath = texture.FileReference?.FullPath;
36                       if (!string.IsNullOrEmpty(texturePath) && File.Exists(texturePath))
37                       {
38
39                           materialBuilder.WithChannelImage(KnownChannel.BaseColor, texturePath);
40                       }
41                   }
42                   materialBuilders.Add(materialName, materialBuilder);
43               }
44
45               var meshBuilder = new MeshBuilder<VertexPositionNormal, VertexTexture1>("mesh");
46               var prim = meshBuilder.UsePrimitive(materialBuilder);
47
48               rhinoMesh.Faces.ConvertQuadsToTriangles();
49               rhinoMesh.Normals.ComputeNormals();
50               rhinoMesh.Compact();
51
52               foreach (var face in rhinoMesh.Faces)
53               {
54                   var vertexA = VertexUtility.CreateVertexBuilderWithUV(rhinoMesh, face.A);
55                   var vertexB = VertexUtility.CreateVertexBuilderWithUV(rhinoMesh, face.B);
56                   var vertexC = VertexUtility.CreateVertexBuilderWithUV(rhinoMesh, face.C);
57
58                   prim.AddTriangle(vertexA, vertexB, vertexC);
59               }
60
61               if (attributes.Count > 0)
62               {
63                   Dictionary<string, string> attributesDict = new Dictionary<string, string>();
64                   foreach (var attribute in item.UserAttributes)
65                   {
66                       var key = attribute.key;
67                       var value = attribute.value;
68                       attributesDict.Add(key, value);
69                   }
70
71                   var extras = JsonContent.CreateFrom(attributesDict);
72                   meshBuilder.Extras = extras;
73               }
74
75               sceneBuilder.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
76           }
77
78.....省略
  • では ExportGlbCommand.cs に戻ります。
  • float scaleFactor = Utility.GetModelScaleFactor(RhinoDoc.ActiveDoc);から 3 行分で先ほど作成した Utility.cs の GetModelScaleFactor から scaleFactor を取得します。
  • Dictionary<string, MaterialBuilder> materialBuilders = new Dictionary<string, MaterialBuilder>(); で SharpGltf での MaterialBuilder のインスタンスの一覧を Dictionsry 型で登録するためのインスタンスを生成しています
  • var sceneBuilder = new SceneBuilder();で SharpGltf 上の sceneBuilder を作成しています。ここに SharpGltf の Mesh を格納していきます。(※この辺は gltf の構造の理解が必要です。こちらの GLTF データの開発元である Khronos Group の repository を参照してください。)
  • meshWithUserDataList を foreach で展開していきます。RHINOMESH rhinoMesh = item.Mesh;から 8 行分で先ほどセットにした情報を展開していますが Mesh に関しては先ほど取得した scaleFactor に基づいて Scale しています。
  • また string materialName = !string.IsNullOrEmpty(rhinoMaterial.Name) ? rhinoMaterial.Name : "Material*" + materialBuilders.Count.ToString();の部分で gltf 上での MaterialName を設定しています。基本的には Rhinoceros での Material 名をそのまま使用していますが、万が一名前がなかったら Material*数字としています。
  • 次に if (!materialBuilders.TryGetValue(materialName, out MaterialBuilder materialBuilder))の if 文で Rhino 上での Material を SharpGltf 上での Material に変換します。
  • if 文の中身から解説します。materialBuilder に materialName の名前で MaterialBuilder のインスタンスを生成し Material を生成していきます。.WithDoubleSide(true)は Webgl 上で Material を片面のみレンダリングさせるのではなく両面レンダリングさせてます。.WithMetallicRoughnessShader()でマテリアルにメタリック/ラフネス シェーダーを使用するように設定.WithChannelParam でマテリアルの基本色を RhinoMaterial からし diffuse と alpha(透明度)を取得して設定しています。
  • texture が割り当てられている場合も想定して、テクスチャが割り当てられている場合はテクスチャのパスを取得してテクスチャを適用しています。
  • 最後に materialBuilders に作成した materialBuilder を登録します。
  • if 文に戻りますが、if (!materialBuilders.TryGetValue(materialName, out MaterialBuilder materialBuilder))は既に materialBuilder が materialBuilders に登録されている場合は if 文内は実行せず登録済みの materialBuilder を返します。こうすることで、Rhinoceros 上で同じ Material を共有している場合、無駄に同じ material を生成せずに gltf 内でも共有させています
  • 次に Rhinoceros でも Mesh から SharpGltf での Mesh を生成していきます。var meshBuilder = new MeshBuilder<VertexPositionNormal, Vertextexture1>("mesh");で meshBuilder のインスタンスを生成しています。<VertexPositionNormal, VertexTexture1>は Mesh の頂点がもつ情報を定期鄭織、位置・法線・uv 座標を持つようにしています。
  • var prim = meshBuilder.UsePrimitive(materialBuilder); ここで mesh に新しい primitive を追加します。primitive は Mesh を構成する上での基本的な情報 Vertex / normal / uv などが設定されます。
  • rhinoMesh を少し加工します。rhinoMesh.Faces.ConvertQuadsToTriangles();でポリゴンが Quads だった場合に Triangle に変換します。gltf / glb では Quads ポリゴンが対応していないためです。
  • rhinoMesh.Normals.ComputeNormals();で Normal を計算しています。外部からインポートしてきた Mesh などには頂点に Normal が含まれていないこともあるかと思うのでこの処理を加えています。Normal が適切に計算されていないと Webgl 上の Material の表現に立体感が出てこないので重要です。
  • rhinoMesh.Compact();は Mesh を最適化してくれるらしいです。不要な頂点情報や法線情報などを取り除いてくれるようなので入れてます。
  • 次に rhinoMesh から Vertex・Normal・UV を取得していきますが、その前に以下を準備します。

VertexUtility.cs

1using Rhino.Geometry;
2using RHINOMESH = Rhino.Geometry.Mesh;
3using SharpGLTF.Geometry;
4using SharpGLTF.Geometry.VertexTypes;
5using System.Numerics;
6
7namespace ExportGlb.Models
8{
9    public static class VertexUtility
10    {
11        public static VertexBuilder<VertexPositionNormal, VertexTexture1, VertexEmpty> CreateVertexBuilderWithUV(RHINOMESH mesh, int vertexIndex)
12        {
13            var position = mesh.Vertices[vertexIndex];
14            var normal = mesh.Normals[vertexIndex];
15            var uv = mesh.TextureCoordinates.Count > vertexIndex ? mesh.TextureCoordinates[vertexIndex] : Point2f.Unset;
16            if (uv == Point2f.Unset) uv = new Point2f(0, 0);
17
18            return new VertexBuilder<VertexPositionNormal, VertexTexture1, VertexEmpty>(
19                new VertexPositionNormal(position.X, position.Z, -position.Y, normal.X, normal.Z, -normal.Y),
20                new VertexTexture1(new Vector2(uv.X, uv.Y))
21            );
22        }
23    }
24}
  • 上記は Rhinomesh から頂点・normal・uv を取得して SharpGltf の VertexBuilder を生成しています。WebGL 上ではY-up になることが多いので、VertexPositionNormal の z と y の関係を変えてます。この辺も本来であれば User に選択させてもよいかもしれませんね。
  • ExportGlbCommand.cs に戻って、foreach (var face in rhinoMesh.Faces)部分で、Mesh の三角形 Face1 つ 1 つに対して処理を行っていき各頂点 3 つに対して vertex / normal / uv を先ほどの VertexUtility を使って取得し、prim に格納追加しています。
  • その次に UserText を追加します。if (attributes.Count > 0) もし RhinoMesh が UserText を持っていた場合、その UserText を key value 形式で JSON 化し SharpGltf の Mesh(MeshBuilder)の extras に UserText を付与します。ここが今回一番やりたかったところです!!!
  • 最後に SharpGLTF の Scene に MeshBuilder を追加します。sceneBuilder.AddRigidMesh(meshBuilder, Matrix4x4.Identity);  Matrix4x4.Identity は Mesh に対して追加のメッシュに対して追加の位置変更や回転、スケーリングなどを含めずに元の形状で Scene に追加することを意味します。

FileSaver.cs

1using System.IO;
2using SYSENV = System.Environment;
3using Rhino;
4using SharpGLTF.Scenes;
5using SharpGLTF.Schema2;
6
7namespace ExportGlb.Helpers
8{
9    public static class FileSaver
10    {
11        public static bool ShowSaveFileDialogAndSave(SceneBuilder sceneBuilder, RhinoDoc doc)
12        {
13            var docPath = doc.Path;
14            var docName = string.IsNullOrEmpty(docPath) ? "untitled" : Path.GetFileNameWithoutExtension(docPath);
15            var defaultFileName = $"{docName}.glb";
16            var saveFileDialog = new Rhino.UI.SaveFileDialog
17            {
18                DefaultExt = "glb",
19                FileName = defaultFileName,
20                Filter = "GLB files (*.glb)|*.glb|GLTF files (*.gltf)|*.gltf|All files (*.*)|*.*",
21                InitialDirectory = SYSENV.GetFolderPath(SYSENV.SpecialFolder.Desktop),
22                Title = "Save GLB File"
23            };
24
25            if (!saveFileDialog.ShowSaveDialog())
26            {
27                return false;
28            }
29
30
31            var filePath = saveFileDialog.FileName;
32
33            SaveFile(sceneBuilder, filePath);
34
35            return true;
36        }
37
38        private static void SaveFile(SceneBuilder sceneBuilder, string filePath)
39        {
40            var fileFormat = Path.GetExtension(filePath).ToLower();
41            var model = sceneBuilder.ToGltf2();
42            if (fileFormat == ".glb")
43            {
44                 model.SaveGLB(filePath);
45                 Rhino.RhinoApp.WriteLine("GLB Exported to " + filePath);
46            }
47            else if (fileFormat == ".gltf")
48            {
49                var writeSettings = new WriteSettings
50                {
51                    JsonIndented = true,
52                    MergeBuffers = true
53                };
54                model.SaveGLTF(filePath, writeSettings);
55                Rhino.RhinoApp.WriteLine("GLTF Exported to " + filePath);
56            }
57        }
58
59    }
60}
  • 最後に SharpGLTF の Scene を glb 又は gltf 形式で Export していきます。上記が Export の処理になります。これを実行し、ファイルを保存するためのUIが表示され保存までを行ってます。File の Save が問題なく行われれば true を返します。

ExportGlbCommand.cs

1
2//...先ほどからの続き
3
4bool isSaved = FileSaver.ShowSaveFileDialogAndSave(sceneBuilder, doc);
5if (!isSaved)
6{
7    Rhino.RhinoApp.WriteLine("File saving cancelled or failed.");
8    return Result.Cancel;
9}
10
11return Result.Success;
  • ExportGlbCommand.cs で先ほどの FileSaver.ShowSaveFileDialogAndSave を実行して完了です。

おわり

以上になります。本来であれば Draco 圧縮もこの処理の中に含めたかったのですが、私の技術力が足りず含めることができませんでした。現状はここで吐き出した gltf / glb を Node.js へ持っていき、gltf-pipeline で Draco 圧縮をかけています。そのうち再チャレンジしようと思います。

【参考】

目 次