STUDIO TAMA

thumbnail

投稿日:2024-01-04

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

  • #Grasshopper

  • #Rhinoceros

  • #Python

  • #R3F

今回は、Rhinoceros で作成したモデルに UserText をもたせ、その UserText をもたせたまま Web で表示する方法について書いていきます。今回は Python で実装していますが、容量の大きなモデルだとクラッシュします。C#で書いたものも記事にしているのでそちらも併せて読んでいただけると幸いです。

thumbnail

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

Rhinocerosで作成したモデルを属性情報を持たせたままWebで表示する

概要

ざっくりとした手順は以下のようになってます。

  1. Rhinoceros でモデルを作成し UserText を持たせる
  2. Grasshopper へ移行し Mesh 化、また UserText と割り当てられている Material を取得
  3. Grasshopper 内で Python を使用し Mesh の頂点・法線・インデックス、また Materiarl と UserText を JSON 化
  4. JSON 化したものを Python のライブラリ pygltflib で glb 化する。【※ Rhino8 は Pyrhon3 が使用できるので Grasshopper 内でそのまま実行可能ですが、Rhino7 以前は JSON 化したファイルを保存し、Grasshopper 外で pygltflib を実行する必要あり】
  5. Threejs などで表示

といった感じの手順になります。

Rhino8 から gltf , glb 形式でのエクスポートがサポートされましたが、UserText を持たせたままの Export がサポートされていないようなので、Rhino で作成したモデルを独自で glb 形式で Export し userText をデータに入れ込むといった感じの内容となってます。

Rhnioceros でモデルに UserText を割り当てる

thumbnail
thumbnail
  • 今回、私は適当な椅子の Mesh モデルを使って実装していこうと思います。5 つの Mesh で構成されているモデルとなっており、座面(1 Mesh)と、木フレーム部分(4 Mesh)でレイヤ分けをしそれぞれ Material を割り当てていきます。
  • それぞれの Mesh に対して UserText を割り当てていきます。今回は id を各 Mesh に対して[ m-1, m-2, m-3, m-4, m-5] といった感じで割り当てました。
  • 今回は適当に拾ってきた Mesh モデルを使用しますが Brep でも問題ありません。

Grasshopper 全体概要

thumbnail

上記が Grasshopper の概要となっております。Rhinoceros で作成したモデルを取得し、glb 形式に変換するところまで行っております。上画像最後の「json to glb」は python3 の環境が必要なので Rhino7 以前の方は基本的には Grasshopper 外で行う必要があります。

Grrasshopper 必要なデータの取得

thumbnail

【Mesh の quad face を triangle に変換】

  • まず Rhinoceros で作成したモデルを Mesh コンポーネントに set します。もし Rhinoceros で作成したモデルが Brep だったら、Brep コンポーネントに set して、Brep コンポーネントを Mesh コンポーネントつないで Mesh 化してください。
  • 今回私が使用している Mesh データが、なぜか Invalid Mesh になってしまうので、Deconstruct Mesh コンポーネントで一度ばらして、Construct Mesh コンポーネントで再度 Mesh 化しました。この手順は通常は必要ないので飛ばして結構です。
  • その後 Combine&Clean コンポーネントで Mesh 数を減らします。この時入力端子を Graft し、5 つの Mesh が結合されないように注意してください。出力端子は Flatten しときます。
  • glb 形式に変換する際に四角形の Face は対応していないので、Mesh Triangulate コンポーネントで三角形 Face に変換します。

【UserText の取得】

  • それぞれの Mesh に割り当てた UserText を取得します。Elefront の GetUserValue コンポーネントで Key(今回は id)を指定し Value を取得します。出力端子は Flatten しときます。(Elefront はこちらからインストールするか PackageManager からインストールしてください)

【レイヤのマテリアルを取得】

  • Mesh に割り当てた Material を取得します。ここに関しては色々なやり方があるかと思います。script で処理してもいいと思います。上画像のやり方はやや回りくどいような気もしますので、各々実装してみてください。
  • Human の Object Attributes コンポーネントで Mesh に割り当てられているレイヤ名を取得します。(※Human はこちらからインストールするか、PackageManager からインストールしてください、)
  • Human の LayerTables コンポーネントで Rhino 上にある Layer 名をすべて取得し、各 Mesh が LayerTables コンポーネントの Layer リストのどっちが割り当てられているのかインデックスで取得します。
  • Human の LayerTables コンポーネントの出力端子 M からは MaterialName が取得出るので、Layer 名のインデックスからどの Material が割り当てられているのかを取得します。
  • Human の MaterialTable コンポーネントで Rhino 上の Material の名前と DiffuseValue を取得し、割り当てられているマテリアル名から検索して Diffuse の値を取得します。
  • SplitARGB コンポーネントで、取得した Diffuse の RGB(0 ~ 1)を Merge してから Flatten します。(※今回 alpha はすべて 1.0 を前提として実装します。ガラスなど透過する要素がある場合は必要かと思います) 最終的に上記画像の Panel で出力されたように値が取得されていれば Ok です。

Grasshopper - Python 取得したデータを JSON 化

thumbnail
  • Python コンポーネントで先ほど取得した Mesh・UserText・material を入力します。入力端子はすべて Tree Access にし、それぞれ Mesh・attributes・colors という名前で取得しています。
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)

上記のコードをざっくり説明します。

  • 入力した mesh / attributes / colors をそれぞれ Tree から List に変換します。
  • for ループで Mesh の数だけループさせます。mesh の頂点・法線・Face のインデックス番号を取得 2 次元配列で格納します。また、Material に関しては、diffuse を RGB に分解し、Flatten した List に格納したので、List の 0, 1, 2 番目が最初の Mesh の RGB, 3, 4, 5 が 2 番目の Mesh の RGB といった感じになるので、上記のようなコードになります。(※uv については、今回は使用しないので空の配列になってますがテクスチャなども Export する際は必要になってくるかと思います。)
  • 1つの Mesh データを辞書型でまとめで、export_data の配列に append します。
  • その後、json.dumps で JSON 化します。
  • Rhino 8の型は output という出力端子を作成してそのまま JSON データを出力し、このデータをもとに pygltlib を使用して Grasshopper 内で glb 形式でモデルをエクスポートしていきます。Rhino7 以前の場合、Python3 が基本的には使えないので、JSON 化したデータを任意の場所に保存して、Grasshopper 外で Python の環境を構築して、glb 変換を行います。

出力される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

gltf データの概要

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}
thumbnail

上記のデータはシンプルな三角形を構成する gltf データの中身で、Khronos Group のチュートリアルから

しています。

また上画像は gltf の基本的な構造であり、KhronosGroup の

から参照させてもらっています。

ざっくり構造を説明すると、

  • Scene が gltf の記述のエントリーポイントとなっており、scene は node を参照しています。
  • node は Mesh やカメラ、skin(※モデル自体にアニメーションが含まれる際にどのように動くか)などを参照し、それらの事態の位置、回転、スケールなどを定義してます。
  • Mesh はシーンに表示されるオブジェクトで、Material や accessor を参照してます。
  • accessor はバッファ内の特定のデータセットにアクセスするための情報をまとめており、bufferView を参照しています。
  • bufferView は buffer 内のどの部分を参照しているかを記述しており buffer を参照しています。
  • buffer は頂点データや・法線・インデックスなどをバイナリ化したものが格納されています。

めちゃくちゃざっくり説明するとこんな感じだと思いますが、詳しくは Khronos Group のリポジトリを読んでみてください。

json を glb に変換

thumbnail
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 で表示してみる

thumbnail
thumbnail

作成したデータを Three.js で読み込んでみたのが上の gif になります。それぞれの Mesh をクリックすると Mesh に割り当てた UserText が左上に表示されるように実装しています。

また、上記画像で Mesh の UserData に Rhinoceros で指定した UserText が格納されているのがわかります。

Threejs 自体は詳しく説明しませんが、React Three Fiber を使用して glb モデルを表示する記事を以前書いたので良ければ参考にしてください。また上の github リポジトリは

になるので、ご参照ください。

以上になります。gltf の理解が浅いので、もしかしたら説明が不足しているかもしれません。その際は

のリポジトリをぜひのぞいてみてください。

【参考】

目 次