プログマのプログラマ日記

技術メモや自社サービスに関する記事を書いていく予定です。

ifcの注記もglTFに変換しようとして、今度こそうまくいった話

ifcOpenShellを使ってifcの注記をglTFに変換してみる

この話は以下の記事の続きです。 前回、FME + 独自Pythonスクリプトでifcの注釈テキストをglTF化することができました。 しかし、FMEで取得したifcAnnotationのテキスト位置がおかしいという課題が残りました。

rhikos-prgm.hatenablog.com

FMEの内部ロジックには手が出せないため、今回はアプローチ方法を変えてFMEの代わりにifcOpenShellを使ってみることにします。

結論

ifcOpenShellを使うことでifcAnnotationをglTFに変換できました。
以下のリポジトリの「annotation_text_extract.py」がifcファイルからテキスト情報(文字列や位置)を抜き出すスクリプトで、「annotation_text_to_gltf.py」が抜き出したテキスト情報をもとにglTFを作成するスクリプトです。

github.com

途中経過

以下は結論に至るまでの話です。興味のある方お読みください。

ifcOpenShellでifcAnnotationの内容をダンプしてみる

まずはifcOpenShellで正しい座標が取得できるのかを確認することにしました。
ifcOpenShellでもFMEと同様に誤った座標取れてきてしまう場合、この方向でアプローチすること自体無意味になってしまうので。
FMEのinspectorで確認した下図①のテキストに注目して調べてみます。
ifcAnnotationのglobalIdは「0OL3LCjon8C8eMGVs718Tk」で、①のテキストのifc_instance_nameは「75793」です。

誤った位置に表示されているテキストの属性(FME)

ちなみに、元ファイルは以下のようなテキスト形式(※)で、

DATA;
#1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);
#3= IFCORGANIZATION($,'Nicht definiert',$,$,$);
#7= IFCPERSONANDORGANIZATION(#1,#3,$);
#10= IFCORGANIZATION('GS','GRAPHISOFT','GRAPHISOFT',$,$);
#11= IFCAPPLICATION(#10,'20.0.0','ARCHICAD-64','IFC2x3 add-on version: 4009 GER FULL');
#12= IFCOWNERHISTORY(#7,#11,$,.ADDED.,$,$,$,1482339244);
#13= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17= IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.0174532925199),#16);
#18= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#19= IFCCONVERSIONBASEDUNIT(#18,.PLANEANGLEUNIT.,'DEGREE',#17);
...

大雑把に、

#[ID] = [IFCタイプ名](カンマ区切りの属性。#[ID]みたいになっているのは他の子要素を参照している)

のような構造になっているのがわかります。
※)ifcと一口に言っても物理フォーマットはいろいろあるみたいですね。今回扱っているのは最も普及しているSPFというフォーマットです。

technical.buildingsmart.org

頑張れば人力でもglobalId, ifc_instance_nameから以下のようにレコードを辿れます。
(このような方向に頑張るのは正しいプログラマの態度とは言えませんが、最初にデータを目視してざっとイメージをつかんでおくのは大事かなと思います。)

#76112= IFCANNOTATION('0OL3LCjon8C8eMGVs718Tk',#12,$,$,$,#75786,#76109);
 -> #75786= IFCLOCALPLACEMENT(#35064,#75785);
  -> ...
 -> #76109= IFCPRODUCTDEFINITIONSHAPE($,$,(#76107));
  -> ...
...

#76107= IFCSHAPEREPRESENTATION(#15265,'Annotation','Annotation2D',(#75793,#75806,#75819,#75832,#75845,#75858,#75871,#75882,#75893,#75904,#75915,#75926,#75937,#75948,#75959,#75970,#75981,#75992,#76003,#76014,#76025,#76036,#76047,#76058,#76069,#76080,#76091,#76102));
 -> #75793= IFCTEXTLITERALWITHEXTENT('50',#75792,.LEFT.,#75787,'bottom-left');
  -> #75792= IFCAXIS2PLACEMENT2D(#75790,#75788);
   -> #75790= IFCCARTESIANPOINT((13.5399999976,10.620000015));
   -> #75788= IFCDIRECTION((0.,1.));
  -> #75787= IFCPLANAREXTENT(0.549829,0.4);
...

元ファイルではid = 75793のIFCTEXTLITERALWITHEXTENTのIFCCARTESIANPOINTは(13.5399999976,10.620000015)となっていました。
これはIFCANNOTATIONの座標からのローカル座標だと思うのでまだ結論は出せないですが、この時点でFMEで取れてきた座標(13.5399999976, 24.1600000126, 0)と食い違って見えます。

次にifcOpenShellを使って階層構造をダンプするツールを作成してみました。
githubの「ifc_dump.py」)
結果は以下の通り。

重要な箇所を抜粋すると以下の通りになります:

(IfcAnnotation#76112): obj_place=(0.0, 0.0, 2.7), GlobalId=0OL3LCjon8C8eMGVs718Tk, Name=None, Description=None, ObjectType=None
 (中略)
 ObjectPlacement(IfcLocalPlacement#75786): 
  PlacementRelTo(IfcLocalPlacement#35064): 
   PlacementRelTo(IfcLocalPlacement#432): 
    PlacementRelTo(IfcLocalPlacement#115): PlacementRelTo=None
     RelativePlacement(IfcAxis2Placement3D#114): 
     (中略: こんな感じで入れ子状態のIfcLocalPlacementの定義が続く。)
     ※ifcopenshellの「ifcopenshell.util.placement.get_local_placement」メソッドを使えばObjectPlacement配下の要素をもとにIfcAnnotationの変換行列を取得できる。
     ※上記「obj_place=(0.0, 0.0, 2.7)」の部分はget_local_placementで取得した行列の4列目の平行移動成分(tx, ty, tz)を出力している。
 Representation(IfcProductDefinitionShape#76109): Name=None, Description=None
  Representation[0](IfcShapeRepresentation#76107): RepresentationIdentifier=Annotation, RepresentationType=Annotation2D
   ContextOfItems(IfcGeometricRepresentationSubContext#15265): ContextIdentifier=Annotation, ContextType=Plan, CoordinateSpaceDimension=None, Precision=None, WorldCoordinateSystem=None, TrueNorth=None, TargetScale=0.01, TargetView=PLAN_VIEW, UserDefinedTargetView=None
    ParentContext(IfcGeometricRepresentationContext#374): ContextIdentifier=None, ContextType=Plan, CoordinateSpaceDimension=3, Precision=1e-05
     WorldCoordinateSystem(IfcAxis2Placement3D#371): 
      Location(IfcCartesianPoint#369): Coordinates=(0.0, 0.0, 0.0)
      Axis(IfcDirection#367): DirectionRatios=(0.0, 0.0, 1.0)
      RefDirection(IfcDirection#365): DirectionRatios=(1.0, 0.0, 0.0)
     TrueNorth(IfcDirection#372): DirectionRatios=(0.766044443119, 0.642787609687)
   Item[0](IfcTextLiteralWithExtent#75793): Literal=50, Path=LEFT, BoxAlignment=bottom-left
    Placement(IfcAxis2Placement2D#75792): 
     Location(IfcCartesianPoint#75790): Coordinates=(13.5399999976, 10.620000015)
     RefDirection(IfcDirection#75788): DirectionRatios=(0.0, 1.0)
    Extent(IfcPlanarExtent#75787): SizeInX=0.549829, SizeInY=0.4
   Item[1](IfcTextLiteralWithExtent#75806): Literal=30, Path=LEFT, BoxAlignment=bottom-left
    Placement(IfcAxis2Placement2D#75805): 
     Location(IfcCartesianPoint#75803): Coordinates=(13.5399999976, 9.13017100882)
     RefDirection(IfcDirection#75801): DirectionRatios=(0.0, 1.0)
    Extent(IfcPlanarExtent#75800): SizeInX=0.549829, SizeInY=0.4
   (中略: Items[n]にテキストや引き出し線の定義が続く。テキストはIfcTextLiteralWithExtent, 引き出し線はIfcGeometricCurveSet)
   Item[6](IfcGeometricCurveSet#75871): 
    Element[0](IfcPolyline#75869): Points=(#75865=IfcCartesianPoint((13.5,-0.68)), #75867=IfcCartesianPoint((13.5,-0.5)))
   Item[7](IfcGeometricCurveSet#75882): 
    Element[0](IfcPolyline#75880): Points=(#75876=IfcCartesianPoint((13.5,-0.5)), #75878=IfcCartesianPoint((13.5,0.)))
   (以下略)

①のIFMETextの部分だけ抜き出すとこのようになります:

IfcAnnotation(globalId = 0OL3LCjon8C8eMGVs718Tk) ... 座標(0.0, 0.0, 2.7)
  +-- fcTextLiteralWithExtent(#75793) ... 座標(13.5399999976, 10.620000015)

#75793のテキスト位置は親IfcAnnotation要素(0.0, 0.0, 2.7)からの相対位置(13.5399999976, 10.620000015)で、 (13.5399999976, 10.620000015, 2.7)ということになりそうです。
ifcOpenShellの座標はなんとなく正しそう!

ちなみに正しい座標x(13.5399999976)とy(10.620000015)の値を足すとFMEで取れる誤った座標y(24.1600000126)になりますね。
FME側では、何らかの条件でIFMETextのy座標にx座標が加算されてしまうバグでもあるのでしょうか?。。

実装

早速実装してみます。
実装コードはgithubのannotation_text_to_gltf.pyです。
glTFファイル生成部分は前回同様trimeshを利用しました。

結果

(こんどこそ)やったぜ!

ifcの注記もglTFに変換しようとして、うまくいったけどうまくいかなかった話

2023/3/8追記

本記事の「FMEで読み込んだifcAnnotationの位置がずれる問題」ですが、SafeSoftwareの人が「修正したよ~」と連絡をくれました。
動作確認してみたところ、ばっちり正しい位置にannotationを表示してくれました。
FME 2022.2のbuild 22790以降であれば、annotationずれ問題は起きないと思います。
修正感謝!

ifcの注記もglTFに変換したかった

この話は、以下の記事の続きです。
FMEでifcからglTFに変換することはできましたが、注記(ifcAnnotation)のテキストまでは変換してくれないという課題が残りました。
(引き出し線の形状は変換されるが、テキストは変換されない状態)

rhikos-prgm.hatenablog.com

引き出し線とテキスト(ifcAnnotation)

ifcAnnotationの注記テキストは文字列で格納されているのですが、glTFではラベルやビルボードのような表現ができないので素直に変換できないのは無理もない話なのかなと思います。
なんとか簡単に変換する方法は無いでしょうか?

結論

注記テキストの描画範囲で板ポリゴンを作って、注記テキストをテクスチャとして張り付ける方針で頑張ってみました。
結果、うまくいったけどうまくいきませんでした。

  1. うまくいった部分。
    FMEで注記テキストの位置および文字列を抽出して、それをもとに板ポリゴンとテクスチャ画像を生成できた。
  2. うまくいかなかった部分。
    1のテキスト位置が一部誤った座標で取れてくる問題に遭遇した。

結局2の問題が致命的で、FMEを使って注記をglTF化する方針はあきらめました。
今後はifcOpenShellを使う方針で検討してみようと思います。待て次号。

途中経過

以下は結論に至るまでの話です。興味のある方お読みください。

※今回使用したワークフローやPythonスクリプトgithubに共有しています。

github.com

ifcAnnotationのFME内部表現

まずはifcAnnotationをFMEに読み込ませるとどのようなfeatureに変換されるのかを確認してみました。

ifcAnnotationのみを抽出してinspectorで属性を確認

どうやら引き出し線(IFMELine)とテキスト(IFMEText)が集まったIFMEAggregate型として変換されるようです。
さらにバラバラにしてIFMETextの属性を確認します。

IFMETextの属性を確認

テキストのバウンディングボックスも入っていますね。
このバウンディングボックスで板ポリゴンを作ってテキストをテクスチャ化した画像を張り付けてあげればglTFを作れそうです!

変換処理

試行錯誤の末、以下の流れでglTFを出力しました。

FMEでの前処理

FMEでifcのIfcAnnotationを読み込んで、以下の2ファイルを出力します。

  1. 引き出し線形状(glTF)
  2. 注釈テキストの文字列・バウンディングボックス・文字列表示位置・文字列の回転(json形式)

ifcを読み込んで引き出し線形状(glTF)と注釈テキスト情報(json)を出力するワークフロー

ワークフローはgithubに共有してあります(ifc_to_gltf_annotation_json.fmw

出力されたjsonファイル(の一部)

[
    {
        "json_featuretype" : "annotation_json",
        "GlobalId" : "2TSghi3E94BuNE_F6jBcWe",
        "_text" : "3,50",
        "_rotation_ccw_degree" : 0,
        "_minx" : 1.65091298808,
        "_miny" : 2.97000000238,
        "_minz" : 0,
        "_maxx" : 2.45091298808,
        "_maxy" : 3.37000000238,
        "_maxz" : 0,
        "ifc_parent_id" : "2eyxpyOx95m90jmsXLOuR0",
        "ifc_parent_unique_id" : "2eyxpyOx95m90jmsXLOuR0_479",
        "ifc_unique_id" : "2TSghi3E94BuNE_F6jBcWe_15372",
        "json_ogc_wkt_crs" : "PROJCS[\"IFC_COORDSYS_0\",GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]],PROJECTION[\"Azimuthal_Equidistant\"],PARAMETER[\"latitude_of_center\",49.100435],PARAMETER[\"longitude_of_center\",8.436539],PARAMETER[\"false_easting\",0],PARAMETER[\"false_northing\",0],UNIT[\"METER\",1]]",
        "json_geometry" : {
            "type" : "Point",
            "coordinates" : [ 1.6509129881, 2.9700000024, 0 ]
        }
    },
...
テキストをtrimeshでglTF化

上記2のjsonファイルをもとにglTFを作成します。
ライブラリはtrimeshを利用しました。
ソースコードはgithuの「annotation_json_to_gltf.py」を参照してください。

変換結果

注記テキスト変換結果(glb)

やったぜ!

…ぜ?

文字の位置が(一部)ずれている問題

よく確認してみると、以下のように一部の注記がずれて表示されてしまっています。。
(赤丸の個所が最も顕著だけど、他にもおかしい場所に出ているテキストはありそう。)

FKZViewerでIfcAnnotationのみを表示した状態

変換後の状態(glb)

そして、あらためて確認してみると、FMEのIfcReaderで読み込んだ時点でテキストがすでにずれていることがわかりました。

なるほど。FMEで読み込んだ時点でずれちゃうのか~。
あれ、これ、詰んだのでは…?

対応案

とりあえずFMEのCommunityに質問を投げつつ、IfcOpenShellでなんとかできないか模索してみようかと思います。

community.safe.com

今日は疲れたのでここまで。

open3dでテクスチャ付きのglTFを出力できなかったときの対応

open3dはテクスチャ付きglTFの出力に未対応?

とある案件で、open3dでglTF(glb)を出力したくなったときがあったのですが、

# テクスチャ付きメッシュ生成
mesh_textured = ...

# glb形式で書き出し
o3d.io.write_triangle_mesh("./output/hogehoge.glb", mesh_textured)

のようなコードでglTFを出力しようとしたときに、

[Open3D WARNING] This file format does not support writing textures and uv coordinates. Consider using .obj

のような警告が出力されてテクスチャを出力できませんでした。そんなぁ~…
(試した時のバージョンはopen3d 0.16.0)
言われた通り.obj形式で試したところ、無事テクスチャ付きのデータを出力できるようです。
でも今回出力したいのはglTFのデータ。さてどうしたものか。

trimeshを使ってみる

PythonでglTFを扱えそうなライブラリを探してみたところ、trimeshというライブラリが見つかりました。これは便利そうです!

github.com

出来上がったソースコードは以下の通り。
いったんobjファイルを出力して、それを読み込んで...ってやっているのでパフォーマンス的にはあまりよろしくありません。
(今回の私のケースではそれほど問題にならなかったのでこのままとしました)

import tempfile
import os
import open3d as o3d
import trimesh

def export_via_trimesh(
    o3d_mesh: o3d.geometry.TriangleMesh,
    file_type: str,
    file_path: str) -> trimesh.Trimesh:
    """
    o3d.geometry.TriangleMeshをtrimesh経由でエクスポート
    """
    with tempfile.TemporaryDirectory() as dname:
        # 1. TriangleMeshをobj形式で出力
        obj_path = os.path.join(dname, "temp.obj")
        o3d.io.write_triangle_mesh(obj_path, o3d_mesh)

        # 2. 1をtrimeshで読み込み
        tri_mesh = trimesh.load(obj_path)

        # 3. 2を指定の形式で出力
        tri_mesh.export(file_type=file_type, file_obj=file_path)

このように使います。

# テクスチャ付きメッシュ生成(open3d)
mesh_textured = ...

# glb形式で書き出し
export_via_trimesh(mesh_textured, "glb", "./output/test.glb")

参考にした記事

この記事の投稿者さんのコードを参考にさせてもらいました。
(元記事の方はなぜかこのコードではうまくテクスチャ出力できなかったようですが、私のケースでは正常に動作しました)

stackoverflow.com

github.com

※the incredibly ugly solutionって書きたくなるときの気持ち、すごくわかるなあ。。