IFCの形状情報の秘密に迫る
秘密を暴くぞ!!!
そういうわけで細かく調べたいのでひとまずスクラップを作っておく。
IfcOpenShell
とかIFC.js
を使えばいいんだけど、なんか知りたいよね?たぶん車輪の再開発をすることになると思うけど、でもやります。
とりあえずドキュメント。IfcProduct
が形状を持つエンティティ。
自力でIFCファイルから3D形状を取得しようと思ったけど、IFCの形状データは三角形メッシュの形になっているわけではない。たとえば、IfcExtrudedAreaSolid
は平面を押し出す形で定義されているし、IfcFacetedBrep
は平面のみで構成された形状になっている。
つまり、取得した3D形状を表示させるには、それら三角形メッシュ以外の定義をもとにして表示させる必要があるのだけれども、多くの3D形状を表示させるライブラリは三角形メッシュの表示が前提になっている。
Web上で3D表示ができるthree.js
, babylon.js
のどちらも(おそらく)三角形メッシュしか表示の対応していない。
Pythonでメッシュを扱うライブラリに PyMesh
や trimesh
があるが、これらも三角形メッシュを扱うライブラリのようだ。
調べてみた結果、IFCの形状情報をごにょごにょできそうなのが pythonocc-core
ぐらいしかなかった。IfcOpenShell
で使用しているライブラリ。
jsだとOpenCascade.js
があるみたい。
どちらにせよ Open CASCADE Technology
以外でメッシュ以外の3D形状を扱えるライブラリがほぼないっぽい?
ちゃんと調べてないけどCADソフトなんかでも使われていてデファクト?っぽいのでひとまずPythonで使えるようにする。
宗教上の理由?でcondaは使えないのでpythonocc-core
をWSL2でビルドする。
基本的に公式の手順通り進めればいいんだけど一部間違っている?ので実行したコマンドをメモ。
インストール手順
# 作業フォルダ
mkdir ~/build-occ
cd ~/build-occ/
必要なライブラリインストール
sudo apt-get update
sudo apt-get install -y wget libglu1-mesa-dev libgl1-mesa-dev libxmu-dev libxi-dev build-essential cmake libfreetype6-dev tk-dev python3-dev rapidjson-dev python3 git python3-pip libpcre2-dev
swig をビルド
wget http://prdownloads.sourceforge.net/swig/swig-4.1.1.tar.gz
tar -zxvf swig-4.1.1.tar.gz
cd swig-4.1.1
./configure
make -j4
# sudo が必要
sudo make install
OpenCascade のビルド
cd ~/build-occ/
# ファイル名指定は「-O(大文字)」で、URLたぶんクオートする必要あり
wget -O occt-7.7.2.tgz "https://git.dev.opencascade.org/gitweb/?p=occt.git;a=snapshot;h=cec1ecd0c9f3b3d2572c47035d11949e8dfa85e2;sf=tgz"
tar -zxvf occt-7.7.2.tgz
cd occt-cec1ecd
mkdir cmake-build
cd cmake-build
cmake -DINSTALL_DIR=/opt/build/occt772 -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF ..
make -j4
sudo make install
# ルート権限が必要
sudo sh -c 'echo "/opt/build/occt772/lib" >> /etc/ld.so.conf.d/occt.conf'
pythonの仮想環境作成(公式手順書にはない)
mkdir ~/envs
cd ~/envs
# バージョンは環境に合わせて入れる
sudo apt install python3.8-venv
python3 -m venv occ
source occ/bin/activate
pip install svgwrite numpy matplotlib
# PyQt5 は失敗したのでスキップ
# pip install PyQt5
# pythonocc-demos を動かすなら以下も必要
pip install scipy opencv-python pythreejs
pythonocc のビルド
cd ~/build-occ/
git clone https://github.com/tpaviot/pythonocc-core.git
cd pythonocc-core
mkdir cmake-build && cd cmake-build
# INSTALL_DIRECTORY は Python仮想環境を指定する
cmake \
-DOCCT_INCLUDE_DIR=/opt/build/occt772/include/opencascade \
-DOCCT_LIBRARY_DIR=/opt/build/occt772/lib \
-DPYTHONOCC_BUILD_TYPE=Release \
-DPYTHONOCC_INSTALL_DIRECTORY=$HOME/envs/occ/lib/python3.8/site-packages/OCC \
-DPYTHONOCC_MESHDS_NUMPY=ON \
..
make -j4
# sudo が必要
sudo make install
# これは `.bashrc` に追記する
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/build/occt772/lib
ひとまず動いたのでIFCの形状を作って表示させたい
表示はこれで
from OCC.Display.WebGl.jupyter_renderer import JupyterRenderer
shape = BRepPrimAPI_MakeBox(10, 20, 30).Shape()
my_renderer = JupyterRenderer()
my_renderer.DisplayShape(
shape,
render_edges=True,
topo_level="Face",
shape_color="#abdda4",
update=True,
)
形状を取得していく。データはIfcOpenShellのサンプルファイルを使う。
まずは簡単そうな壁を。データを見てみるとこんな感じ。
#15006
の IfcExtrudedAreaSolid
から右が形状の定義。
IfcExtrudedAreaSolid
は平面を押し出すことによって作られる形状で SweptArea
が平面の定義、Position
が位置、ExtrudedDirection
が押し出す方向、Depth
が押し出す長さ。
SweptArea
はこの場合だと、
- 複数の頂点(4点)からなる多角形で定義されていて、
- 押し出し方向は(画像では見えてないけど)Z方向 (0, 0, 1)
- Depth は 2.5 (見えてないけど)
という形状になっている。ひとまず形状だけみたいので位置はおいておく。
そういうわけでこれを pythonocc で表示してみる。
ひとまずゴリ押しで値を取得
import ifcopenshell
ifc = ifcopenshell.open('model/AC20-FZK-Haus.ifc')
element = ifc.by_id(15042)
representations = element.Representation.Representations
for item in representations[0].Items:
if item.is_a() == 'IfcExtrudedAreaSolid':
depth = item.Depth
direction = item.ExtrudedDirection.DirectionRatios
swept_area = item.SweptArea
if swept_area.is_a() == 'IfcArbitraryClosedProfileDef':
outer_curve = swept_area.OuterCurve
if outer_curve.is_a() == 'IfcPolyline':
points = outer_curve.Points
points = [point.Coordinates for point in points]
else:
print('not IfcPolyline !!')
else:
print('not IfcArbitraryClosedProfileDef !!')
else:
print('not IfcExtrudedAreaSolid !!')
データからoccで形状作成
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
from OCC.Core.gp import gp_Pnt, gp_Vec, gp_Dir
from OCC.Extend.ShapeFactory import make_wire, make_face, make_extrusion
def create_extrude_shape(points, direction, length):
gp_points = [gp_Pnt(*p, 0) for p in points]
edges = []
for i in range(len(gp_points) - 1):
edge = BRepBuilderAPI_MakeEdge(gp_points[i], gp_points[i+1]).Edge()
edges.append(edge)
wire = make_wire(edges)
face = make_face(wire)
extrusion = make_extrusion(face, length, gp_Vec(*direction))
return extrusion
shape = create_extrude_shape(points, direction, depth)
結果。できてる。
次は正しい位置に置きたい。位置は IfcLocalPlacement
が連なっているような感じ。
この場合は、#14983
の位置は #477
の相対位置、#477
の位置は #432
の相対位置、 ... というような指定がされている。
これはIfcSite
の位置があり、その位置を基準にしてIfcBuilding
の位置が定義され、さらにIfcBuilding
の位置を基準に IfcBuildingStorey
の位置があり、最後に壁の相対位置が設定されているという状態。
位置を定義しているIfcAxis2Placement3D
の定義はちょっと特殊。
Location
は位置、Axis
はZ軸の方向、RefDirection
はX軸の方向となっている。
物体の位置指定をするために、そこから変換行列を計算して、角度と移動量を取得する。
取得するコード。だいたいChatGPT。
def calculate_transformation_matrix(location, axis, ref_direction):
# Z軸(Axis)の単位ベクトルを計算
z_axis = np.array(axis) / np.linalg.norm(axis)
# X軸(RefDirection)の単位ベクトルを計算
x_axis = np.array(ref_direction) / np.linalg.norm(ref_direction)
# Y軸はZ軸とX軸の外積で計算
y_axis = np.cross(z_axis, x_axis)
# 回転行列を構築
rotation_matrix = np.array([x_axis, y_axis, z_axis]).T
# 変換行列を4x4の形で作成
transformation_matrix = np.eye(4)
transformation_matrix[:3, :3] = rotation_matrix
transformation_matrix[:3, 3] = location # 平行移動ベクトルの追加
return transformation_matrix
def matrix_to_translation_angle(matrix):
# 移動量の抽出
translation = matrix[:3, 3]
# 角度の計算
sy = np.linalg.norm(matrix[:3, 0])
singular = sy < 1e-6
if not singular:
x = np.arctan2(matrix[2, 1], matrix[2, 2])
y = np.arctan2(-matrix[2, 0], sy)
z = np.arctan2(matrix[1, 0], matrix[0, 0])
else:
x = np.arctan2(-matrix[1, 2], matrix[1, 1])
y = np.arctan2(-matrix[2, 0], sy)
z = 0
return translation, np.array([x, y, z])
変換できているか確認のために壁表示
整理したりなんやかんやしたコード
from OCC.Extend.ShapeFactory import translate_shp, rotate_shp_3_axis
from OCC.Extend.ShapeFactory import make_wire, make_face, make_extrusion
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon
def get_polyline(item):
if item.is_a() == 'IfcPolyline':
points = item.Points
points = [point.Coordinates for point in points]
return points
else:
print('not IfcPolyline !!')
def extract_ifc_extruded_shape(item):
if item.is_a() == 'IfcExtrudedAreaSolid':
depth = item.Depth
direction = item.ExtrudedDirection.DirectionRatios
swept_area = item.SweptArea
position = item.Position
if swept_area.is_a() == 'IfcArbitraryClosedProfileDef':
outer_curve = swept_area.OuterCurve
if outer_curve.is_a() == 'IfcPolyline':
points = get_polyline(outer_curve)
return points, direction, depth
else:
print('not IfcPolyline !!')
else:
print('not IfcArbitraryClosedProfileDef !!')
else:
print('not IfcExtrudedAreaSolid !!')
def points2face(points, is_close=True):
gp_points = [(gp_Pnt(*p) if len(p) == 3 else gp_Pnt(*p, 0)) for p in points]
polygon = BRepBuilderAPI_MakePolygon()
for p in gp_points:
polygon.Add(p)
# 必要であれば始点と終点をつなぐ
if is_close:
polygon.Close()
face = BRepBuilderAPI_MakeFace(polygon.Wire()).Face()
return face
def create_extrude_shape(points, direction, length):
face = points2face(points, is_close=False)
extrusion = make_extrusion(face, length, gp_Vec(*direction))
return extrusion
shapes = []
walls = ifc.by_type('IfcWall')
for element in walls:
placement = element.ObjectPlacement
transformation_matrix = placement_to_transformation_matrix(placement)
tr, angle = matrix_to_translation_angle(transformation_matrix)
representations = element.Representation.Representations
for item in representations[0].Items:
ret = extract_ifc_extruded_shape(item)
if ret is not None:
points, direction, depth = ret
shape = create_extrude_shape(*ret)
rotated_shape = rotate_shp_3_axis(shape, *angle, "rad")
trans_shape = translate_shp(rotated_shape, gp_Vec(*tr))
shapes.append(trans_shape)
開口部ないし、2階部分の壁はIfcBooleanClippingResult
だから表示できてないけど、できているということで。
IfcBooleanClippingResult
は2の形状の足したり引いたりした形状。
この場合、IfcExtrudedAreaSolid
から IfcHalfSpaceSolid
を引いている。
(見えてないけど Operator
は DIFFERENCE
)
IfcHalfSpaceSolid
は平面(位置、法線)で2つに区切られた空間。
とりあえず壁に穴を開ける
だいぶIfcOpenShellのC++のコードを参考にした。IfcOpenShell/src/ifcgeom
コード
実行部分
shapes = []
walls = ifc.by_type('IfcWall')
for element in walls:
placement = element.ObjectPlacement
transformation = placement_to_transformation(placement)
representations = element.Representation.Representations
for item in representations[0].Items:
if item.is_a() == 'IfcExtrudedAreaSolid':
shape = ifc_extruded_area_solid_to_shape(item)
trans_shape = BRepBuilderAPI_Transform(shape, transformation).Shape()
opening_shapes = get_opening_shape(element)
for opening_shape in opening_shapes:
trans_shape = BRepAlgoAPI_Cut(trans_shape, opening_shape).Shape()
shapes.append(trans_shape)
else:
print(item.id(), item.is_a())
IFCからOCC
# IfcExtrudedAreaSolid から押し出し形状
def ifc_extruded_area_solid_to_shape(item):
ret = extract_ifc_extruded_shape(item)
if ret is not None:
points, direction, depth, position = ret
transformation = placement3d_to_transformation(position)
shape = create_extrude_shape(points, direction, depth)
trans_shape = BRepBuilderAPI_Transform(shape, transformation).Shape()
return trans_shape
# IfcOpeningElement の形状作成
def get_opening_shape(element):
has_openings = element.HasOpenings
opening_shapes = []
for opening in has_openings:
placement = opening.RelatedOpeningElement.ObjectPlacement
transformation = placement_to_transformation(placement)
items = opening.RelatedOpeningElement.Representation.Representations[0].Items
for item in items:
shape = ifc_extruded_area_solid_to_shape(item)
trans_shape = BRepBuilderAPI_Transform(shape, transformation).Shape()
opening_shapes.append(trans_shape)
return opening_shapes
# IfcLocalPlacement を 変換行列に
def placement_to_transformation(placement):
transform = placement.RelativePlacement
transformation = placement3d_to_transformation(transform)
rel = placement.PlacementRelTo
if rel is not None:
transformation.PreMultiply(placement_to_transformation(rel))
return transformation
# IfcAxis2Placement3D を 変換行列に
def placement3d_to_transformation(item):
location, axis, direction = extract_ifc_placement3d(item)
ax3 = gp_Ax3(gp_Pnt(*location), gp_Dir(*axis), gp_Dir(*direction))
ax2 = gp.gp.XOY()
xoy = gp_Ax3(ax2.Location(), ax2.Direction(), ax2.XDirection())
transformation = gp_Trsf()
transformation.SetTransformation(ax3, xoy)
return transformation
OCC
# 点から面
def points2face(points, is_close=True):
gp_points = [(gp_Pnt(*p) if len(p) == 3 else gp_Pnt(*p, 0)) for p in points]
polygon = BRepBuilderAPI_MakePolygon()
for p in gp_points:
polygon.Add(p)
# 必要であれば始点と終点をつなぐ
if is_close:
polygon.Close()
face = BRepBuilderAPI_MakeFace(polygon.Wire()).Face()
return face
# 押し出し形状
def create_extrude_shape(points, direction, length):
face = points2face(points, is_close=True)
extrusion = make_extrusion(face, length, gp_Vec(*direction))
return extrusion
IFCから値取得
# IfcAxis2Placement3D から値取得
def extract_ifc_placement3d(item):
return (
item.Location.Coordinates,
item.Axis.DirectionRatios,
item.RefDirection.DirectionRatios,
)
# IfcExtrudedAreaSolid から値取得
def extract_ifc_extruded_shape(item):
if item.is_a() == 'IfcExtrudedAreaSolid':
depth = item.Depth
direction = item.ExtrudedDirection.DirectionRatios
position = item.Position
swept_area = item.SweptArea
if swept_area.is_a() == 'IfcArbitraryClosedProfileDef':
points = extract_ifc_arbitrary_closed(swept_area)
if points is None:
return
elif swept_area.is_a() == 'IfcRectangleProfileDef':
points = extract_ifc_rectangle(swept_area)
else:
print_not_defined(item)
return points, direction, depth, position
else:
print_not_defined(item)
# IfcArbitraryClosedProfileDef から値取得
def extract_ifc_arbitrary_closed(item):
outer_curve = item.OuterCurve
if outer_curve.is_a() == 'IfcPolyline':
points = get_polyline(outer_curve)
return points
else:
print_not_defined(item)
# IfcRectangleProfileDef から値取得
def extract_ifc_rectangle(item):
x, y = item.XDim / 2, item.YDim / 2
points = [
[-x, -y],
[x, -y],
[x, y],
[-x, y],
]
return points
# IfcPolyline から値取得
def get_polyline(item):
if item.is_a() == 'IfcPolyline':
points = item.Points
points = [point.Coordinates for point in points]
return points
else:
print_not_defined(item)
# 表示用
def print_not_defined(item):
print(item.id(), item.is_a())
いくつか形状作ってみた感触、ドキュメントとIfcOpenShell
のコード(IfcOpenShell/src /ifcgeom/
のエンティティ名のコード)を見れば、occで一通り形状を作ることは難しくなさそう。ドキュメントだけじゃよくわかんないし、ソースコードも一緒に見て自分で実装し直してみると理解できたりもする。
そんなわけである程度理解できたので、とりあえず一区切りということで一旦Close。
気が向いたらモデル全部表示させるくらいは試すかも?