投稿日:2024-01-04
#Grasshopper
#Rhinoceros
#Python
#R3F
今回は、Rhinoceros で作成したモデルに UserText をもたせ、その UserText をもたせたまま Web で表示する方法について書いていきます。今回は Python で実装していますが、容量の大きなモデルだとクラッシュします。C#で書いたものも記事にしているのでそちらも併せて読んでいただけると幸いです。
ざっくりとした手順は以下のようになってます。
といった感じの手順になります。
Rhino8 から gltf , glb 形式でのエクスポートがサポートされましたが、UserText を持たせたままの Export がサポートされていないようなので、Rhino で作成したモデルを独自で glb 形式で Export し userText をデータに入れ込むといった感じの内容となってます。
上記が Grasshopper の概要となっております。Rhinoceros で作成したモデルを取得し、glb 形式に変換するところまで行っております。上画像最後の「json to glb」は python3 の環境が必要なので Rhino7 以前の方は基本的には Grasshopper 外で行う必要があります。
【Mesh の quad face を triangle に変換】
【UserText の取得】
【レイヤのマテリアルを取得】
1# coding=utf-8 2import json 3import ghpythonlib.treehelpers as th 4 5meshes = th.tree_to_list(mesh) 6attr = th.tree_to_list(attributes) 7color = th.tree_to_list(colors) 8export_data = [] 9 10for index,(mesh, attr_data) in enumerate(zip(meshes,attr)): 11 mesh_vertices =[] 12 mesh_faces_indices = [] 13 mesh_normals = [] 14 mesh_uvs = [] 15 mesh_material=[ 16 color[index*3], 17 color[index*3+1], 18 color[index*3+2] 19 ] 20 21 22 for i in range(mesh.Vertices.Count): 23 vertex = mesh.Vertices[i] 24 vert_list = [vertex.X, vertex.Y, vertex.Z] 25 mesh_vertices.append(vert_list) 26 27 # 頂点法線の取得 28 normal = mesh.Normals[i] 29 norm_list = [normal.X, normal.Y, normal.Z] 30 mesh_normals.append(norm_list) 31 32 for face in mesh.Faces: 33 if face.IsTriangle: 34 indices = [face.A, face.B, face.C] 35 else: 36 indices = [face.A, face.B, face.C] 37 mesh_faces_indices.append(indices) 38 indices = [face.C, face.D, face.A] 39 mesh_faces_indices.append(indices) 40 41 42 export_data.append({ 43 "vertices": mesh_vertices, 44 "faces": mesh_faces_indices, 45 "normals": mesh_normals, 46 "materials":mesh_material, 47 "uvs": mesh_uvs, 48 "userData": attr_data 49 }) 50 51 52json_data = json.dumps(export_data) 53output = json_data 54 55print("Export completed.") 56 57##RHINO7以前は一度JSONファイルを外部に保存↓ 58file_path = "C:\\Users\\81803\\Desktop\\export_data3.json" 59with open(file_path, "w") as file: 60 json.dump(export_data, file)
上記のコードをざっくり説明します。
出力されるJSONデータがどのようになるかは
の github リポジトリの sample data に入ってます。ざっくり説明すると以下のように 5 つの Mesh データが配列に格納され、JSON 形式に変換されてます。
1[ 2 { 3 "vertices": [ 4 [-219.17027282714844, 430.4172058105469, 285.9709167480469], 5 [-219.17027282714844, 430.4172058105469, 258.9709167480469], 6 [-219.17027282714844, 400.4172058105469, 285.9709167480469], 7 [-219.17027282714844, 400.4172058105469, 258.9709167480469], 8 [-219.17027282714844, -99.5827865600586, 285.9709167480469], 9 ....たくさん頂点ある 10 [-679.1702880859375, -129.58277893066406, 285.9709167480469], 11 [-679.1702880859375, -129.58277893066406, 258.9709167480469] 12 ], 13 "faces": [ 14 [107, 105, 3], 15 [107, 5, 7], 16 [7, 11, 99], 17 ....たくさん頂点インデックスある 18 [11, 15, 59], 19 [1, 8, 0], 20 [109, 104, 108] 21 ], 22 "normals": [ 23 [0.3712981045246124, 0.0060758935287594795, 0.928493857383728], 24 [0.7069647908210754, 0.011568717658519745, -0.7071540951728821], 25 ....たくさん法線ある 26 [-0.5545745491981506, -0.009075014851987362, 0.832084596157074], 27 [-0.4471058249473572, -0.0073164054192602634, -0.8944511413574219] 28 ], 29 "materials": [0.807843137254902, 0.6549019607843137, 0.4470588235294118], 30 "uvs": [], 31 "userData": "m-1" 32 }, 33 { 34 "vertices": [ 35 ....たくさん頂点ある 36 ], 37 "faces": [ 38 ....たくさん頂点のインデックスある 39 ], 40 "normals": [ 41 ....たくさん法線ある 42 ], 43 "materials": [0.807843137254902, 0.6549019607843137, 0.4470588235294118], 44 "uvs": [], 45 "userData": "m-2" 46 }, 47 ....のこり3つのMeshも同様 48] 49
JSON データを元に glb データを生成していきます。私自身も深く理解ができてないので、説明が不足してしまうこともあるかもしれませんのでご了承ください。詳しく知りたい方は
の GLTF データの開発元である Khronos Group の repository を参照してください。Tutorial などもあるのでぜひご参考ください。
まず初めに glb 形式とは gltf 形式のデータをバイナリ化したものになります。なので、まずは gltf データを生成する必要があります。
1{ 2 "scene": 0, 3 "scenes": [ 4 { 5 "nodes": [0] 6 } 7 ], 8 9 "nodes": [ 10 { 11 "mesh": 0 12 } 13 ], 14 15 "meshes": [ 16 { 17 "primitives": [ 18 { 19 "attributes": { 20 "POSITION": 1 21 }, 22 "indices": 0 23 } 24 ] 25 } 26 ], 27 28 "buffers": [ 29 { 30 "uri": "data:application/octet-stream;base64, 31 AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", 32 "byteLength": 44 33 } 34 ], 35 "bufferViews": [ 36 { 37 "buffer": 0, 38 "byteOffset": 0, 39 "byteLength": 6, 40 "target": 34963 41 }, 42 { 43 "buffer": 0, 44 "byteOffset": 8, 45 "byteLength": 36, 46 "target": 34962 47 } 48 ], 49 "accessors": [ 50 { 51 "bufferView": 0, 52 "byteOffset": 0, 53 "componentType": 5123, 54 "count": 3, 55 "type": "SCALAR", 56 "max": [2], 57 "min": [0] 58 }, 59 { 60 "bufferView": 1, 61 "byteOffset": 0, 62 "componentType": 5126, 63 "count": 3, 64 "type": "VEC3", 65 "max": [1.0, 1.0, 0.0], 66 "min": [0.0, 0.0, 0.0] 67 } 68 ], 69 70 "asset": { 71 "version": "2.0" 72 } 73}
上記のデータはシンプルな三角形を構成する gltf データの中身で、Khronos Group のチュートリアルから
しています。
また上画像は gltf の基本的な構造であり、KhronosGroup の
から参照させてもらっています。
ざっくり構造を説明すると、
めちゃくちゃざっくり説明するとこんな感じだと思いますが、詳しくは Khronos Group のリポジトリを読んでみてください。
1import numpy 2import pygltflib 3import json 4import numpy as np 5import ghpythonlib.treehelpers as th 6 7def convert_to_y_up(points): 8 """Z-UpからY-Upへ座標変換""" 9 return np.array([[x, z, y] for x, y, z in points]) 10 11attr = th.tree_to_list(json_data) 12meshes_data = json.loads(attr[0]) 13 14pointsArray= [] 15normalsArray = [] 16facesArray = [] 17userDataArray = [] 18materialsArray = [] 19 20for mesh_data in meshes_data: 21 # 頂点データを準備 22 vertices = convert_to_y_up(mesh_data["vertices"]) 23 npArray = np.array(vertices, dtype="float32") 24 pointsArray.append(npArray) 25 26 # 法線データを準備 27 normals = convert_to_y_up(mesh_data["normals"]) # 法線も座標変換が必要 28 npNormals = np.array(normals, dtype="float32") 29 normalsArray.append(npNormals) 30 31 # 頂点のインデックスを準備 32 faces = mesh_data["faces"] 33 npFaces = np.array(faces, dtype="uint32") 34 facesArray.append(npFaces) 35 36 # userDataを準備 37 userData = mesh_data["userData"] 38 userDataArray.append(userData) 39 40 # マテリアルを準備 41 normalized_color_data = mesh_data["materials"] 42 normalized_color_data.append(1.0) 43 44 material = pygltflib.Material( 45 pbrMetallicRoughness=pygltflib.PbrMetallicRoughness( 46 baseColorFactor=normalized_color_data, 47 metallicFactor=0.0, 48 roughnessFactor=1.0 49 ), 50 doubleSided=True 51 ) 52 materialsArray.append(material) 53 54 55binary_blob = bytearray() 56for points, normals,faces in zip(pointsArray, normalsArray, facesArray): 57 58 points_binary_blob = points.tobytes() 59 normals_binary_blob = normals.tobytes() 60 faces_binary_blob = faces.flatten().tobytes() 61 62 binary_blob.extend(faces_binary_blob) 63 binary_blob.extend(points_binary_blob) 64 binary_blob.extend(normals_binary_blob) 65 66buffer = pygltflib.Buffer(byteLength=len(binary_blob)) 67 68# バッファビューの作成 69bufferViews = [] 70byte_offset = 0 71accessors = [] 72 73for i, (points, normals, faces) in enumerate(zip(pointsArray, normalsArray, facesArray)): 74 faces_byte_length = len(faces.flatten().tobytes()) 75 bufferViews.append(pygltflib.BufferView( 76 buffer=0, 77 byteOffset=byte_offset, 78 byteLength=faces_byte_length, 79 target=pygltflib.ELEMENT_ARRAY_BUFFER 80 )) 81 byte_offset += faces_byte_length 82 83 # 頂点位置データのバッファビュー 84 points_byte_length = len(points.tobytes()) 85 bufferViews.append(pygltflib.BufferView( 86 buffer=0, 87 byteOffset=byte_offset, 88 byteLength=points_byte_length, 89 target=pygltflib.ARRAY_BUFFER 90 )) 91 byte_offset += points_byte_length 92 93 # 法線データのバッファビュー 94 normals_byte_length = len(normals.tobytes()) 95 bufferViews.append(pygltflib.BufferView( 96 buffer=0, 97 byteOffset=byte_offset, 98 byteLength=normals_byte_length, 99 target=pygltflib.ARRAY_BUFFER 100 )) 101 byte_offset += normals_byte_length 102 103 accessors.extend([ 104 pygltflib.Accessor( 105 bufferView=i * 3, 106 componentType=pygltflib.UNSIGNED_INT, 107 count=len(faces.flatten()), 108 type=pygltflib.SCALAR, 109 max=[int(faces.max())], 110 min=[int(faces.min())], 111 ), 112 pygltflib.Accessor( 113 bufferView=i * 3 + 1, 114 componentType=pygltflib.FLOAT, 115 count=len(points), 116 type=pygltflib.VEC3, 117 max=points.max(axis=0).tolist(), 118 min=points.min(axis=0).tolist(), 119 ), 120 pygltflib.Accessor( 121 bufferView=i * 3 + 2, 122 componentType=pygltflib.FLOAT, 123 count=len(normals), 124 type=pygltflib.VEC3, 125 max=normals.max(axis=0).tolist(), 126 min=normals.min(axis=0).tolist(), 127 ) 128 ]) 129 130 131# メッシュとノードの作成 132meshes = [] 133nodes = [] 134 135for i, userData in enumerate(userDataArray): 136 mesh_index = len(meshes) 137 meshes.append(pygltflib.Mesh( 138 primitives=[ 139 pygltflib.Primitive 140 ( 141 attributes=pygltflib.Attributes( 142 POSITION=i * 3 + 1,NORMAL=i * 3 + 2 143 ), 144 indices=i * 3, 145 material = i) 146 ] 147 )) 148 meshes[mesh_index].extras = {"id": userData} 149 nodes.append(pygltflib.Node(mesh=mesh_index)) 150 151gltf = pygltflib.GLTF2( 152 scene=0, 153 scenes=[pygltflib.Scene(nodes=list(range(len(nodes))))], 154 nodes=nodes, 155 meshes=meshes, 156 materials=materialsArray, 157 accessors=accessors, 158 bufferViews=bufferViews, 159 buffers=[buffer] 160) 161 162 163# バイナリブロブをGLTFに設定 164gltf.set_binary_blob(bytes(binary_blob)) 165 166# GLBファイルの保存 167gltf.save("C:\\Users\\81803\\Desktop\\test_model.glb") 168print("export fin")
上記のコードが先ほど生成した json ファイルを glb に変換するコードとなっております。私は Rhnio8 なので Grasshopper 内で実装してますが、Rhino7 以前のかたは export した json ファイルを外部で環境構築した python 環境で実行する必要があると思います。またその場合、上記のコードの最初の json データの読み込み方が変わってくるかと思いますのでご注意ください。
上記のコードで、Rhno8 環境で numpy や pygltflib などの外部ライブラリを使用してますが、numpy はもとから入ってたような気がしますが、pygltflibf が自分で入れたのか元から入ってたか記憶が定かではなく、もし最初から入ってなければ rhinoceros が参照している python のライブラリーにぶち込む必要があるかもです。また、numpy などを最初実行する際は、
を参照していただき、もしかしたら # r: numpy の記述がないと実行できないかもしれませんのでご注意ください。
では上記のコードを解説していきます。
基本的には、前項で説明した gltf の構造を下から実装していきます。
【json データを整理】
まずは json 生成した json データをばらして整理していきます。python の入力端子に json_data という端子をつくり、Tree Access で入力します。
これらを配列直して、json.load で json データを parse し meshes_data として取得します。
これら meshes_data に格納されているデータを for ループでまわし、頂点・法線・インデックス・マテリアル(diffuse)・userText にそれぞれ配列で格納しますが、threejs でロードしようと思っており、
threejs 上では Y 方向が上なので、z 成分と y 成分を convert_to_y_up で入れ替えてます。
material に関しては、最初に normalized_color_data に以前 json 化した RGB の値を配列で取得し、そこに alpha = 1.0 を配列の最後に append しています。
その後 pygltflib を使って material を作成し、baseColorFactor に配列で格納した RGBA の値をセットし、metaric = 0.0 と roughness=1.0 をセットしております。
また、マテリアルは web 上で両面レンダリングしたいので、doubleSided=True としています。
【頂点・法線・インデックスをバイナリ形化】
頂点や法線・インデックスをバイナリ形式にしていきます。binary_blob = bytearray()で binary オブジェクトを生成します。ここにバイナリデータを格納していきます。
先ほど取得した頂点の座標・法線・インデックスを for ループで回してバイナリ化したものを bytearray()に append してます。
注意する点としては、頂点座標と法線は[x,y,z]成分をもつデータの配列で扱いますが、インデックスに関しては gltf 上では flatten して 1 次元配列で扱うようです。
buffer = pygltflib.Buffer(byteLength=len(binary_blob))でバイナリデータを格納するための buffer を作成してます。buffer のサイズは len(binary_blob)で生成したバイナリの length を指定してます。
【bufferView の作成】
buffer はバイナリデータが格納されており 2 進数、あるいは 16 進数などに変換したも単一の文字列が格納されていると思ってください。
その文字列の中のどこからどこまでが何のデータでどのような値が入っているかを参照しているのが bufferView になります。69 行目から 101 行目で bufferView を生成しています。
face(インデックス)points(頂点座標) normal(法線)の順番で作成しております。まず face を少し詳しく見ていきます。
1bufferViews = [] 2byte_offset = 0 3accessors = [] 4 5for i, (points, normals, faces) in enumerate(zip(pointsArray, normalsArray, facesArray)): 6 faces_byte_length = len(faces.flatten().tobytes()) 7 bufferViews.append(pygltflib.BufferView( 8 buffer=0, 9 byteOffset=byte_offset, 10 byteLength=faces_byte_length, 11 target=pygltflib.ELEMENT_ARRAY_BUFFER 12 )) 13 byte_offset += faces_byte_length
まず、faces_byte_length で for ループで取得した face をバイナリーに変換した際の length を取得します。
pygltflib.BufferView で bufferView を生成しています。buffer=0 は先ほど生成したバイナリデータを参照しており、bytearray()に 1 つのバイナリーデータしか格納されていないため 0 番目が指定されています。
byteOffset=byte_offset は参照している buffer のバイナリーデータのスタート地点です。for ループの一番最初は 0 番目がスタートになります。
byteLength=faces_byte_length は参照しているバイナリーデータの終わり位置です。一番最初の face データは 0 番目がスタートで faces_byte_length までが参照する対象になります。
target=pygltflib.ELEMENT_ARRAY_BUFFER は参照するバイナリにどのようなデータが格納されているかを示しています。今回はインデックスなので pygltflib.ELEMENT_ARRAY_BUFFER となります。
その後、次のデータの参照するバイナリーデータの開始位置を byte_offset += faces_byte_length で更新しています。
次に points と normal を見てみます。
1# バッファビューの作成 2bufferViews = [] 3byte_offset = 0 4accessors = [] 5 6for i, (points, normals, faces) in enumerate(zip(pointsArray, normalsArray, facesArray)): 7 faces_byte_length = len(faces.flatten().tobytes()) 8 bufferViews.append(pygltflib.BufferView( 9 buffer=0, 10 byteOffset=byte_offset, 11 byteLength=faces_byte_length, 12 target=pygltflib.ELEMENT_ARRAY_BUFFER 13 )) 14 byte_offset += faces_byte_length 15 16 # 頂点位置データのバッファビュー 17 points_byte_length = len(points.tobytes()) 18 bufferViews.append(pygltflib.BufferView( 19 buffer=0, 20 byteOffset=byte_offset, 21 byteLength=points_byte_length, 22 target=pygltflib.ARRAY_BUFFER 23 )) 24 byte_offset += points_byte_length 25 26 # 法線データのバッファビュー 27 normals_byte_length = len(normals.tobytes()) 28 bufferViews.append(pygltflib.BufferView( 29 buffer=0, 30 byteOffset=byte_offset, 31 byteLength=normals_byte_length, 32 target=pygltflib.ARRAY_BUFFER 33 )) 34 byte_offset += normals_byte_length
points も normal も基本的には face と同じで、バイナリーの length を求めて、参照するバイナリの位置を指定してます。違うのは target ですが、頂点・法線は xyz の 3 成分を持つ Vec3 型なので target=pygltflib.ARRAY_BUFFER としています。
【accessors の生成】
accessors はどの bufferView を参照しているかまとめているものになります。先ほどの for ループの続きで生成しています。以下で詳しく見ていきましょう。
1bufferViews = [] 2byte_offset = 0 3accessors = [] 4 5for i, (points, normals, faces) in enumerate(zip(pointsArray, normalsArray, facesArray)): 6 faces_byte_length = len(faces.flatten().tobytes()) 7 bufferViews.append(pygltflib.BufferView( 8 buffer=0, 9 byteOffset=byte_offset, 10 byteLength=faces_byte_length, 11 target=pygltflib.ELEMENT_ARRAY_BUFFER 12 )) 13 byte_offset += faces_byte_length 14 15 # 頂点位置データのバッファビュー 16 points_byte_length = len(points.tobytes()) 17 bufferViews.append(pygltflib.BufferView( 18 buffer=0, 19 byteOffset=byte_offset, 20 byteLength=points_byte_length, 21 target=pygltflib.ARRAY_BUFFER 22 )) 23 byte_offset += points_byte_length 24 25 # 法線データのバッファビュー 26 normals_byte_length = len(normals.tobytes()) 27 bufferViews.append(pygltflib.BufferView( 28 buffer=0, 29 byteOffset=byte_offset, 30 byteLength=normals_byte_length, 31 target=pygltflib.ARRAY_BUFFER 32 )) 33 byte_offset += normals_byte_length 34 35 accessors.extend([ 36 pygltflib.Accessor( 37 bufferView=i * 3, 38 componentType=pygltflib.UNSIGNED_INT, 39 count=len(faces.flatten()), 40 type=pygltflib.SCALAR, 41 max=[int(faces.max())], 42 min=[int(faces.min())], 43 ), 44 pygltflib.Accessor( 45 bufferView=i * 3 + 1, 46 componentType=pygltflib.FLOAT, 47 count=len(points), 48 type=pygltflib.VEC3, 49 max=points.max(axis=0).tolist(), 50 min=points.min(axis=0).tolist(), 51 ), 52 pygltflib.Accessor( 53 bufferView=i * 3 + 2, 54 componentType=pygltflib.FLOAT, 55 count=len(normals), 56 type=pygltflib.VEC3, 57 max=normals.max(axis=0).tolist(), 58 min=normals.min(axis=0).tolist(), 59 ) 60 ])
accessors.extend で生成した accessors を追加してます。accessor は mesh に参照され、結果的に mesh が参照する頂点のインデックス・頂点座標・法線を示すことになります。
上記のコードでも pygltflib.Accessor が 3 つ生成され配列で格納されてます。0 番目が Mesh が参照する頂点のインデックス情報の 1 番目が頂点座標の情報 2 番目が法線の情報となってます。
0 番目を詳しく見ていくと、bufferView=i _ 3 で参照する bufferView の要素のインデックスを指定してます。先ほど bufferView の配列を生成する際に face => points => normals の順番で配列に格納したので、face は i _ 3 番目になります。
componentType=pygltflib.UNSIGNED_INT は値の型を指定してます。インデックスは整数なので UNSIGNED_INT になります。count はこの accsessor が含む要素の数を指定してます。type=pygltflib.SCALAR はデータタイプを示しており、SCALAR は単一の要素が値になっていることを示します。min と max は参照しているデータの最小値・最大値を示します。バウンディングボックスなどを計算する際に使うようです。
points、normal も同様ですが、bufferView の位置がとデータの型が Vec3 型で単一要素の型が float 型になってます。
【Mesh と node を作成】
Mesh と Node を作成していきます。
1# メッシュとノードの作成 2meshes = [] 3nodes = [] 4 5for i, userData in enumerate(userDataArray): 6 mesh_index = len(meshes) 7 meshes.append(pygltflib.Mesh( 8 primitives=[ 9 pygltflib.Primitive( 10 attributes=pygltflib.Attributes( 11 POSITION=i * 3 + 1, 12 NORMAL=i * 3 + 2 13 ), 14 indices=i * 3, 15 material = i) 16 ] 17 )) 18 meshes[mesh_index].extras = {"id": userData} 19 nodes.append(pygltflib.Node(mesh=mesh_index))
上記のコードで mesh と node を作ってます。上記のコードは userData の数で for ループ回してますが、すべての Mesh に userData を格納している前提なのでこのようにしてます。本来は Mesh の配列で for ループを回して、userData が存在したら格納するみたいにしたほうが良かったかもしれませんが、とりあえずこれで進めます。
mesh_index = len(meshes)で mesh のインデックス番号を生成します。
7 ~ 9 行までで一気に Mesh を生成しています。Mesh を Primitive(基本形状)などの情報を持たせ、Attributes で先ほど作成した accsessor の位置と以前作成した material 配列の index 番号を指定してます。accsessor はインデックス=>頂点=>法線の順番で配列に格納したので、上記のような記述になってます。
そして、ここがこの記事で一番やりたかったことなんですが、userText を持たせたまま、web にモデルを持っていく必要があります。gltf では mesh に限らず、scene でも node でも extras に User が独自にデータを格納することができます。Rhinoceros が提供している gltf / glb のエクスポートはこの部分をサポートしてくれていないので、やむなく独自で実装したといった感じです。今回は key を id にし、Rhinoceros から取得してきた userData を value として格納します。
最後に pygltflib.Node で Node を生成して、参照する Mesh の index 番号渡してあげます。
ちなみに結構昔ですが
以下は fbx で話は出たみたいです・・・
【glb データの作成】
1gltf = pygltflib.GLTF2( 2 scene=0, 3 scenes=[pygltflib.Scene(nodes=list(range(len(nodes))))], 4 nodes=nodes, 5 meshes=meshes, 6 materials=materialsArray, 7 accessors=accessors, 8 bufferViews=bufferViews, 9 buffers=[buffer] 10) 11 12 13# バイナリブロブをGLTFに設定 14gltf.set_binary_blob(bytes(binary_blob)) 15 16# GLBファイルの保存 17gltf.save("C:\\Users\\81803\\Desktop\\test_model.glb") 18print("export fin")
最後に glb データを生成します。pygltflib.GLTF2 で gltf のインスタンスを生成し、いままで生成してきたものを入れていきます。 gltf.set_binary_blob(bytes(binary_blob))で以前生成した bytearray()を bytes 型に変換して gltf にセットします。 最後に、gltf.save("C:\Users\81803\Desktop\test_model.glb") で任意の場所に任意の名前で glb データを保存して完了です。
作成したデータを Three.js で読み込んでみたのが上の gif になります。それぞれの Mesh をクリックすると Mesh に割り当てた UserText が左上に表示されるように実装しています。
また、上記画像で Mesh の UserData に Rhinoceros で指定した UserText が格納されているのがわかります。
Threejs 自体は詳しく説明しませんが、React Three Fiber を使用して glb モデルを表示する記事を以前書いたので良ければ参考にしてください。また上の github リポジトリは
になるので、ご参照ください。
以上になります。gltf の理解が浅いので、もしかしたら説明が不足しているかもしれません。その際は
のリポジトリをぜひのぞいてみてください。
【参考】