投稿日:2024-03-30
#Rhinoceros
#Plugin
#Three.js
#R3F
#C#
Rhinoceros で作成したモデルを Web に持っていくことがよくあるのですが、設定した 属性情報の UserText も一緒に Web へ持っていきたくなることもしばしばあり glb / gltf Exporter を実装しました。以前記事にもしましたが(以下リンク)Python で同じものを実装したことがあるのですが、流石に Python では容量の大きなモデルを Export するとクラッシュしてしまったので今回 C#で実装することにしました。C#はそんなに書き慣れていないので、おかしな書き方等あるかもしれませんがその辺はご了承ください。 以下に、動作状況の Youtube リンクをのせときますのでぜひ確認してみてください。
・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 で表示するまでのざっくりとした流れになってます。
今回は、C#で作成したプラグインの部分のみを書いていこうと思います。Nodejs / Three.js 部分は解説していませんのでご了承ください。 また、ここから先の解説は gltf フォーマットの構造がわかっていないと理解しずらいかと思います。
の GLTF データの開発元である Khronos Group の repository を参照してください。以前私のブログでもちょっとだけ触れたので、若干参考になるかもしれません。
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 .....省略
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}
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 .....省略
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}
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.....省略
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}
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}
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;
以上になります。本来であれば Draco 圧縮もこの処理の中に含めたかったのですが、私の技術力が足りず含めることができませんでした。現状はここで吐き出した gltf / glb を Node.js へ持っていき、gltf-pipeline で Draco 圧縮をかけています。そのうち再チャレンジしようと思います。
【参考】