-
Notifications
You must be signed in to change notification settings - Fork 9
5. Getting the geometric representations (minimal)
This is a minimal example of parsing an IFC file and getting all geometric representations as polygonal geometry. There is no user interface and also no 3D display of the geometry. We do this to first get a grip on accessing the representations as list of vertices, edges, faces and normals. When we want to display the geometry, there is a lot of additional work to do and this typically requires additional libraries.
Just like the basic console application, we start with an almost empty main function which opens the file and which preloads the parser settings from the ifcopenshell.geom library. We simply start from the default settings and adjust later, if needed.
import sys
import ifcopenshell
import ifcopenshell.geom
# Our Main function
def main():
ifc_file = ifcopenshell.open(sys.argv[1])
settings = ifcopenshell.geom.settings()
geometry_iterator(ifc_file, settings)
if __name__ == "__main__":
main()The geometry_iterator() function will take the file and the settings and use them to initialise an iterator. This will do all the heavy lifting for us.
We create an iterator object and call .initialize(). After that it is ready to be used. This is easy to do in a while loop. This appears to loop forever, but whenever the iterator can not find a .next() item, the loop will be stopped.
Inside the loop, we get to the actual shape using iterator.get().
# iterating all geometric representations
def geometry_iterator(ifc_file, settings):
iterator = ifcopenshell.geom.iterator(settings, ifc_file)
iterator.initialize()
while True:
shape = iterator.get()
if not iterator.next():
breakYou could run the code now, but nothing will be seen or fed back to the user.
Each shape we get is an actual representation of an IfcProduct, which we can retrieve by calling shape.product. From there on, we could start requesting attributes or other information.
while True:
shape = iterator.get()
product = shape.product
print("Product #" + str(product.id()))If you run the code now, we get a listing of all products (from their shapes).
Product #34
Product #77
Product #121
Product #142
Product #188
Product #189
Product #219
Product #292
Product #299
...
To get to the actual geometric components, we create a new function, called parse_shape().
while True:
shape = iterator.get()
product = shape.product
print("Product #" + str(product.id()))
parse_shape(shape)This function will grab all information it can and print it to the console.
- id = the STEP ID of the Shape (actually the
IfcProduct) - geometry = the geometric representation, as a polygonal mesh
- id = the STEP ID of the geometry itself and the name of the shape
- guid = the GlobalId of the product which is represented (very important!)
- verts = a list of vertices (the XYZ-coordinates of each vertex)
- edges = a list of edges (as indices)
- faces = a list of faces (as indices)
- normals = a list of normals (the XYZ-coordinates of each normal vector)
- material_ids = a list of STEP IDs for the different materials which are referenced
def parse_shape(shape):
geometry = shape.geometry
print("id(shape):", str(shape.id))
print("guid: ", str(shape.guid))
print("id(geom): ", str(geometry.id))
print("verts: ", geometry.verts)
print("edges: ", geometry.edges)
print("faces: ", geometry.faces)
print("normals: ", geometry.normals)
print("mat_ids: ", geometry.material_ids)When we run the code now, we get a very extensive list of all these coordinates and indices. We show only the first two shapes for the IfcOpenHouse.ifc sample file.
Product #34
id(shape): 34
guid: 2eZ4QSizXEuRNpHMBut_dk
id(geom): 49-openings-122-143
verts: (-5.0, -0.18, 0.0, 5.0, -0.18, 0.0, -5.0, -0.18, 0.4, 0.5, -0.17999999999999994, 0.4, 0.5, -0.17999999999999994, 2.0, -5.0, -0.18, 2.0, -5.0, -0.18, 3.0, 5.0, -0.18, 3.0, 2.07, -0.17999999999999994, 0.4, 3.93, -0.17999999999999994, 0.4, 3.93, -0.17999999999999994, 2.0, 2.07, -0.17999999999999994, 2.0, 5.0, 0.18, 0.0, -5.0, 0.18, 0.0, 5.0, 0.18, 3.0, -5.0, 0.18, 3.0, -5.0, 0.18, 2.0, 0.5, 0.17999999999999994, 2.0, 0.5, 0.17999999999999994, 0.4, -5.0, 0.18, 0.4, 2.07, 0.17999999999999994, 0.4, 3.93, 0.17999999999999994, 0.4, 2.07, 0.17999999999999994, 2.0, 3.93, 0.17999999999999994, 2.0)
edges: (5, 6, 4, 5, 0, 2, 2, 3, 3, 4, 8, 11, 10, 11, 6, 7, 8, 9, 9, 10, 1, 7, 0, 1, 0, 1, 1, 12, 0, 13, 12, 13, 1, 7, 1, 12, 7, 14, 12, 14, 6, 7, 7, 14, 14, 15, 6, 15, 15, 16, 5, 16, 6, 15, 5, 6, 4, 5, 4, 17, 5, 16, 16, 17, 3, 18, 3, 4, 4, 17, 17, 18, 2, 3, 3, 18, 18, 19, 2, 19, 13, 19, 0, 13, 2, 19, 0, 2, 20, 21, 8, 20, 8, 9, 9, 21, 8, 20, 20, 22, 11, 22, 8, 11, 11, 22, 22, 23, 10, 11, 10, 23, 9, 21, 9, 10, 10, 23, 21, 23, 12, 14, 21, 23, 22, 23, 20, 21, 20, 22, 17, 18, 14, 15, 16, 17, 15, 16, 18, 19, 13, 19, 12, 13)
faces: (6, 5, 4, 2, 0, 3, 11, 4, 3, 11, 3, 8, 11, 10, 7, 6, 4, 7, 4, 11, 7, 9, 8, 1, 10, 9, 1, 7, 10, 1, 3, 0, 1, 8, 3, 1, 1, 0, 12, 0, 13, 12, 7, 1, 12, 14, 7, 12, 14, 6, 7, 14, 15, 6, 15, 16, 5, 6, 15, 5, 4, 5, 17, 5, 16, 17, 18, 3, 4, 18, 4, 17, 18, 2, 3, 18, 19, 2, 19, 13, 0, 2, 19, 0, 21, 20, 8, 21, 8, 9, 8, 20, 22, 8, 22, 11, 11, 22, 23, 10, 11, 23, 21, 9, 10, 21, 10, 23, 23, 14, 12, 23, 12, 21, 14, 23, 22, 14, 22, 17, 21, 12, 20, 22, 20, 18, 17, 22, 18, 14, 17, 15, 15, 17, 16, 19, 18, 13, 20, 12, 13, 18, 20, 13)
normals: ()
mat_ids: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
Product #77
id(shape): 77
guid: 2YmZTDa0T3h97PCthS2e$n
id(geom): 78
verts: (-5.05, -2.73, 0.0, -5.05, -2.73, 2.0, 5.05, -2.73, 2.0, 5.05, -2.73, 0.0, 5.05, 2.73, 2.0, 5.05, 2.73, 0.0, -5.05, 2.73, 2.0, -5.05, 2.73, 0.0)
edges: (0, 1, 0, 3, 1, 2, 2, 3, 2, 3, 3, 5, 2, 4, 4, 5, 4, 5, 5, 7, 4, 6, 6, 7, 6, 7, 0, 7, 1, 6, 0, 1, 0, 3, 3, 5, 0, 7, 5, 7, 1, 2, 2, 4, 4, 6, 1, 6)
faces: (1, 0, 3, 2, 1, 3, 2, 3, 5, 4, 2, 5, 4, 5, 7, 6, 4, 7, 6, 7, 0, 1, 6, 0, 3, 0, 5, 0, 7, 5, 4, 1, 2, 4, 6, 1)
normals: ()
mat_ids: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
With these lists, we will have to do some work to decipher everything. But if you have any understanding of how a polygonal mesh is represented in code, this is not too complicated.
The list of vertices contains a sequence of x, y and z values for each vertex, so something like x1, y1, z1, x2, y2, z2.... It is possible to reorganise them per three numbers, so each will represent a single vertex.
This is a nice one-liner in Python:
vertices = [geometry.verts[i: i + 3] for i in range(0, len(geometry.verts), 3)]What this does is it runs through the list of vertices, each time picking up three numbers and putting them into a new list. So instead of the following flat list:
(-5.0, -0.18, 0.0, 5.0, -0.18, 0.0, -5.0, -0.18, 0.4, 0.5, -0.17999999999999994, 0.4, 0.5, -0.17999999999999994, 2.0, -5.0, -0.18, 2.0, -5.0, -0.18, 3.0, 5.0, -0.18, 3.0, 2.07, -0.17999999999999994, 0.4, 3.93, -0.17999999999999994, 0.4, 3.93, -0.17999999999999994, 2.0, 2.07, -0.17999999999999994, 2.0, 5.0, 0.18, 0.0, -5.0, 0.18, 0.0, 5.0, 0.18, 3.0, -5.0, 0.18, 3.0, -5.0, 0.18, 2.0, 0.5, 0.17999999999999994, 2.0, 0.5, 0.17999999999999994, 0.4, -5.0, 0.18, 0.4, 2.07, 0.17999999999999994, 0.4, 3.93, 0.17999999999999994, 0.4, 2.07, 0.17999999999999994, 2.0, 3.93, 0.17999999999999994, 2.0)
we get a list of vertices, with each vertex containing a triple of coordinates.
[(-5.0, -0.18, 0.0), (5.0, -0.18, 0.0), (-5.0, -0.18, 0.4), (0.5, -0.17999999999999994, 0.4), (0.5, -0.17999999999999994, 2.0), (-5.0, -0.18, 2.0), (-5.0, -0.18, 3.0), (5.0, -0.18, 3.0), (2.07, -0.17999999999999994, 0.4), (3.93, -0.17999999999999994, 0.4), (3.93, -0.17999999999999994, 2.0), (2.07, -0.17999999999999994, 2.0), (5.0, 0.18, 0.0), (-5.0, 0.18, 0.0), (5.0, 0.18, 3.0), (-5.0, 0.18, 3.0), (-5.0, 0.18, 2.0), (0.5, 0.17999999999999994, 2.0), (0.5, 0.17999999999999994, 0.4), (-5.0, 0.18, 0.4), (2.07, 0.17999999999999994, 0.4), (3.93, 0.17999999999999994, 0.4), (2.07, 0.17999999999999994, 2.0), (3.93, 0.17999999999999994, 2.0)]
So geometry.verts[0] will only give us the first X-coordinate of the first vertex, but vertices[0] will now contain the full vertex as three numbers.
A similar one-liner can also reorganise the list of edges.
edges = [geometry.edges[i: i + 2] for i in range(0, len(geometry.edges), 2)]Here we do this per two numbers. Each edge contains the index of one of the vertices from the previous list: an edge from vertex 5 to vertex 6, from vertex 4 to 5 etcetera.
[(5, 6), (4, 5), (0, 2), (2, 3), (3, 4), (8, 11), (10, 11), (6, 7), (8, 9), (9, 10), (1, 7), (0, 1), (0, 1), (1, 12), (0, 13), (12, 13), (1, 7), (1, 12), (7, 14), (12, 14), (6, 7), (7, 14), (14, 15), (6, 15), (15, 16), (5, 16), (6, 15), (5, 6), (4, 5), (4, 17), (5, 16), (16, 17), (3, 18), (3, 4), (4, 17), (17, 18), (2, 3), (3, 18), (18, 19), (2, 19), (13, 19), (0, 13), (2, 19), (0, 2), (20, 21), (8, 20), (8, 9), (9, 21), (8, 20), (20, 22), (11, 22), (8, 11), (11, 22), (22, 23), (10, 11), (10, 23), (9, 21), (9, 10), (10, 23), (21, 23), (12, 14), (21, 23), (22, 23), (20, 21), (20, 22), (17, 18), (14, 15), (16, 17), (15, 16), (18, 19), (13, 19), (12, 13)]
The faces we receive contain also indices rather than coordinates and they are grouped per three: each three indices define a single triangle. This is the easiest geometry, as we don't need to worry about triangulation or complex geometrical operations. That has been prepared by IfcOpenShell for us.
Again, a similar one-liner: each three indices define a single triangle in the faces list.
faces = [geometry.faces[i: i + 3] for i in range(0, len(geometry.faces), 3)]The result are indices of triangles, with the first one defined by referring to vertices 6, 5 and 4.
[(6, 5, 4), (2, 0, 3), (11, 4, 3), (11, 3, 8), (11, 10, 7), (6, 4, 7), (4, 11, 7), (9, 8, 1), (10, 9, 1), (7, 10, 1), (3, 0, 1), (8, 3, 1), (1, 0, 12), (0, 13, 12), (7, 1, 12), (14, 7, 12), (14, 6, 7), (14, 15, 6), (15, 16, 5), (6, 15, 5), (4, 5, 17), (5, 16, 17), (18, 3, 4), (18, 4, 17), (18, 2, 3), (18, 19, 2), (19, 13, 0), (2, 19, 0), (21, 20, 8), (21, 8, 9), (8, 20, 22), (8, 22, 11), (11, 22, 23), (10, 11, 23), (21, 9, 10), (21, 10, 23), (23, 14, 12), (23, 12, 21), (14, 23, 22), (14, 22, 17), (21, 12, 20), (22, 20, 18), (17, 22, 18), (14, 17, 15), (15, 17, 16), (19, 18, 13), (20, 12, 13), (18, 20, 13)]
This is very, very similar:
normals = [geometry.normals[i: i + 3] for i in range(0, len(geometry.normals), 3)]Leading to the following list, which is empty:
[]
We don't have our normals! But don't worry: IfcOpenShell is able to generate them for us, when we modify the settings of the geometry iterator.
def main():
ifc_file = ifcopenshell.open(sys.argv[1])
settings = ifcopenshell.geom.settings()
settings.set(settings.WELD_VERTICES, False) # false = generate normalsWelding our vertices is on by default, which makes for a more compact list of vertices as they get reused. However, this will also disable normals generation. We need to normals to show the rendering system what the outside of each face is (actually it is set per vertex).
With this adjusted setting, the lists of vertices becomes longer and all indices are updated. It will also take a little longer. In the documentation, it is stated that you can skip this, when you will regenerate your normals in the 3D system you are using.
Here is the updated list of normals (using the adjusted list to make them easier to decipher).
[(0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (-1.0, -0.0, -0.0), (-1.0, -0.0, -0.0), (-1.0, -0.0, -0.0), (-1.0, -0.0, -0.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (-1.0, -0.0, -0.0), (-1.0, -0.0, -0.0), (-1.0, -0.0, -0.0), (-1.0, -0.0, -0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-0.0, -0.0, -1.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0)]
The first normal (normals[Ø]) contains the vector (0.0, -1.0, 0.0) which is the unit vector pointing straight to the negative Y-direction. This particular example has only 1.0 and 0.0 values, indicating that it is an axis-oriented shape only containing orthogonal faces.
Each vertex has a single normal vector, in exactly the same order, so vertices[12] has normals[12].
In IfcOpenShell we receive material information as indices: IDs which we can use to retrieve a material from a list. These IDs are attached to each vertex, so we may get a different color per vertex. This is a structure that is widely used in 3D visualisation and which is easy to send to the 3D rendering system.
We know the approach by now:
mat_ids = [geometry.material_ids[i: i + 3] for i in range(0, len(geometry.material_ids), 3)]This leads to the following list of material indices:
[(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)]
What does that mean? The first face (faces[0]) will have the color indices mat_ids[0] which is (0,0,0). This implies that the first triangle has a single material for all its vertices.
Beware that sometimes the index can be negative (-1) indicating that the representation lacks material or style information.
But wait... what is the actual material color? We only have an index?
IfcOpenShell also delivers a list of materials with the geometry it returns: shape.geometry.materials[] so that is why we need the index from the mat_ids.
From that material, we can retrieve the main color in the material.diffuse attribute, which is, again, a triplet of values, each being a number between 0.0 and 1.0 and representing the RGB value. So material.diffuse[0] is the red component of the diffuse channel of that material.
That is about as far as we can meaningfully get in a console application: we have retrieved all vertices, edges, triangles, normals and vertex colors. But to actually display anything on screen, we need to develop a GUI application.
You can download the file here.
A big thanks to the IfcOpenShell library and the many people contributing with code, but also examples.