If you're reading this, we've just migrated servers! If anything looks broken please email dion@thinkmoult.com :)

IfcOpenShell : Associate material constituents to specific faces in a Brep representation

I'm trying to export an IfcWindow constituted with several parts, which I've simplified to wooden frames and a glass.

It's a single object in Blender. i've assigned the Wood material to the frame, and the Glass material to the glass.

Up until there, easy peezey. After skimming through the docs and studying the example library that comes with the BlenderBIM addon I've elected using a IfcMaterialConstituentSet, adding two constituents linked to a Wood and a Glass material. I then create a Brep geometry representation for the object, and link everything together.

The Constituent Set seems to be correctly exported, along with my two materials, but I'm missing something somewhere to actually assign the correct faces to their corrsponding material. The Brep creation code in get_brep_representation does seggregate the polygons by material index in items so the information is already here and passed along with file.createIfcShapeRepresentation, I just don't understand how to make the link between the geometry and the material in ifc :)

The docs state "NOTE See the "Material Use Definition" at the individual element to which an IfcMaterialConstituentSet may apply for a required or recommended definition of such keywords." but I've not seen this information explained anywhere...

When I roundtrip the file the occurence does have an IfcMaterialConstituentSet with my two materials

But the object is only assigned the Glass material

I'm using Blender but I guess this could be applied to any software or even plain geometry data.

Here's the code for anyone interested in lending a hand or giving any insight into my endeavour :p

Cheers :)

# This can be substituted for your own filepath
from pathlib import Path
import bpy
blend_path = Path(bpy.data.filepath)
blend_name = blend_path.stem
filepath = str(blend_path.with_name(blend_name + "_test.ifc"))
obj_blend = bpy.context.active_object


# Retrieve the object placement in the world
def edit_object_placement(file, product):
    run(
        "geometry.edit_object_placement",
        file,
        product=product,
        matrix=obj_blend.matrix_world.copy(),
        is_si=False,
    )


# Transform the blender mesh data to Brep
def get_brep_representation(file, body):
    import bpy
    # Note : copy/pasted from https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py#L552-L575
    matrix = obj_blend.matrix_world.copy()
    depsgraph = bpy.context.evaluated_depsgraph_get()
    obj_blender_evaluated = obj_blend.evaluated_get(depsgraph)
    mesh_evaluated = obj_blender_evaluated.data

    ifc_vertices = [file.createIfcCartesianPoint(v.co) for v in mesh_evaluated.vertices]

    ifc_raw_items = [[]] * max(1, len(obj_blender_evaluated.material_slots))
    for polygon in mesh_evaluated.polygons:
        ifc_raw_items[polygon.material_index % max(1, len(obj_blender_evaluated.material_slots))].append(
            file.createIfcFace(
                [
                    file.createIfcFaceOuterBound(
                        file.createIfcPolyLoop([ifc_vertices[vertex] for vertex in polygon.vertices]),
                        True,
                    )
                ]
            )
        )
    items = [file.createIfcFacetedBrep(file.createIfcClosedShell(i)) for i in ifc_raw_items if i]
    # items is a list of list of faces
    # the index of sub-lists corresponds to the index of the material in the material slots
    # How do I link it to the MaterialConstituentSet ??
    return file.createIfcShapeRepresentation(
        body,
        body.ContextIdentifier,
        "Brep",
        items,
    )


from ifcopenshell.api import run
import ifcopenshell


file = run("project.create_file")
# Boilerplate
project = run("root.create_entity", file, ifc_class="IfcProject", name="My Project")
context = run("context.add_context", file, context_type="Model")
body = run(
    "context.add_context",
    file,
    context_type="Model",
    context_identifier="Body",
    target_view="MODEL_VIEW",
    parent=context,
)
run("unit.assign_unit", file, length={"is_metric": True, "raw": "METERS"})
site = run("root.create_entity", file, ifc_class="IfcSite", name="My Site")
building = run("root.create_entity", file, ifc_class="IfcBuilding", name="Building A")
storey = run("root.create_entity", file, ifc_class="IfcBuildingStorey", name="Storey 0")
run("aggregate.assign_object", file, relating_object=project, product=site)
run("aggregate.assign_object", file, relating_object=site, product=building)
run("aggregate.assign_object", file, relating_object=building, product=storey)

# Create material constituent set with 2 materials : Glass and Wood
material_set = run(
            "material.add_material_set", file, **{"name": "WindowSet", "set_type": "IfcMaterialConstituentSet"}
        )
material_glass = run("material.add_material", file, name="Glass")
run("material.add_constituent", file, **{"constituent_set": material_set, "material": material_glass})
material_wood = run("material.add_material", file, name="Wood")
run("material.add_constituent", file, **{"constituent_set": material_set, "material": material_wood})

# Create a window occurrence, setup its representation
product = run(
    "root.create_entity",
    file,
    ifc_class="IfcWindow",
    predefined_type="WINDOW",
    name="Window",
)
representation = get_brep_representation(file, body)
run(
    "geometry.assign_representation",
    file,
    product=product,
    representation=representation,
)
edit_object_placement(file, product)
run("spatial.assign_container", file, relating_structure=storey, product=product)

# Add the constituent set to the window
file.createIfcRelAssociatesMaterial(
    ifcopenshell.guid.new(),
    None,
    None,
    None,
    [product],
    material_set,
)

file.write(filepath)

Comments

  • but I'm missing something somewhere to actually assign the correct faces to their corrsponding material.

    As I understand correctly IfcMaterialConstituentSet can be used to assigne multiple IfcMaterials to several parts of the Geometry of just one IfcElement?
    I modelled two cubes and exported them as one IfcWindow:

    I see BlenderBIM has a button to change the order of the IfcMaterialConstituentSet, but I don't see a menu to assign the material to a geometry representation.
    I opened this file in BIMVision too, it shows up as an IfcMaterialLayer, is that correct?

    Looking into the IFC itself I see this

    #62=IFCWINDOW('3SNnRBHFTANhBCGuLjA_1Z',$,'Cube',$,$,#111,#86,$,$,$,.WINDOW.,$,$);
    #86=IFCPRODUCTDEFINITIONSHAPE($,$,(#126,#129));
    #87=IFCRELCONTAINEDINSPATIALSTRUCTURE('3qvsT$7BT2oPK31o0J3uYl',$,$,$,(#62),#38);
    #93=IFCMATERIAL('glass',$,$);
    #94=IFCMATERIAL('wood',$,$);
    #95=IFCSURFACESTYLE('wood',.BOTH.,(#137));
    #99=IFCSTYLEDITEM($,(#95),'wood');
    #100=IFCSTYLEDREPRESENTATION(#15,'Body',$,(#99));
    #101=IFCMATERIALDEFINITIONREPRESENTATION($,$,(#100),#94);
    #102=IFCMATERIALCONSTITUENTSET('glass and wood',$,(#106,#105));
    #103=IFCRELASSOCIATESMATERIAL('1_Ibgohs1DPQwklOnlu9Jw',$,$,$,(#62),#102);
    #105=IFCMATERIALCONSTITUENT('','',#93,0.,'');
    #106=IFCMATERIALCONSTITUENT($,$,#94,$,$);
    

    I don't even know how to hack into the IFC file to assign the correct material to one geometry representation. Wish I could be of some more use.

    GorgioustheoryshawAce
  • Hehe thank you for that, you are helpful ! Simplifying down a problem to find the common denominator is almost always the most sane way to solve it :) I am pretty sure there is no UI in the BlenderBIM Addon to do that, but I know there is one in the IfcOpenShell API, that I have just not found yet.

    I do have a file exported from Revit that seems to implement it correctly and that passes the roundtripping unharmed. I'll have a look inside and share it here for whom it may interest.

    theoryshaw
  • edited December 2022

    Nice. Also pushed a test here, too, from Revit that works when imported into BB.
    Not sure the subutlies, however, of how to recreate this, via the UI, in BB... if even possible.

  • So it seems I have made quite a breakthrough with IfcShapeAspect which looks like it is used to map the correct materials to the correct representations. Since the representation is decomposed with items relating to the material index, it should be straightforward to map it correctly. However I still haven't found a way. I don't fully understand the diagram in the docs

    Here's how I would naively implement it :

    for i, brep in enumerate(representation.Items):
        brep_representation = file.createIfcShapeRepresentation(
            body,
            body.ContextIdentifier,
            "Brep",
            [brep],
        )
        shape_aspect = file.createIfcShapeAspect(
            [brep_representation],
            materials[i]["Name"],
            PartOfProductDefinitionShape=representation,  # This is where I think I'm wrong. In my working file it links to a IFCREPRESENTATIONMAP and not a IFCSHAPEREPRESENTATION but I don't know how to create this. It seems it is related to object placement.
        )
    
    theoryshawCoenAce
  • edited December 2022

    IfcShapeAspect: one entity to rule them all, one entity to find them, one entity to bring them all, and in the darkness bind them.

    CoenGorgiousAce
  • Relative to how it seems most entities are related to one another, that "Correlation by Name" seems tenuous and unusual.
    Perhaps i'm missing something.

  • I see BlenderBIM has a button to change the order of the IfcMaterialConstituentSet, but I don't see a menu to assign the material to a geometry representation.

    @Coen, I might have misunderstood this comment (and you might know how to do this already) but this is how you apply a material to different geometries in one mesh.
    https://www.dropbox.com/s/g2olabxw0cz5n6b/2022-12-01_16-04-09_Blender_blender.mp4?dl=0

    Gorgious
  • edited December 2022

    Awesome ! I actually was wrong in saying BlenderBIM doesn't offer a way to do it, it does it automagically witout any special UI, which is great ! I have to say I'm not a big fan of linking entities by name either with IfcShapeAspect, it seems very error-prone.

    BlenderBIM doesn't use IfcShapeAspect but IfcStyledItem which I still don't understand the difference between the two but it looks like it is more straightforward to use.

    #82=IFCPOLYGONALFACESET(#80,$,(#74,#75,#76,#77,#78,#79),$);
    #92=IFCSURFACESTYLE('Wood',.BOTH.,(#93));
    #97=IFCSTYLEDITEM(#82,(#92),'Wood');
    

    It seems the constituent and the surface style actually don't keep a reference between themselves, so I'm wondering if in theory the assigned materials can be different from the material constituents ?
    Edit : The answer is YES. Hmm.

    FWIW I'll share the equivalent file from your video, which I'll dig into to find a solution.

  • So... Success ?
    It seems "style.add_surface_style" automatically adds an IfcStyledItem in the file in addtion to the surface style parameters. I just had to fetch the correct one in my for loop and assign it to the brep entity.

    for material in materials:
        material["ifc"] = run("material.add_material", file, name=material["Name"])
        run("material.add_constituent", file, **{"constituent_set": material_set, "material": material["ifc"]})
        style = run("style.add_style", file, name=material["ifc"].Name)
        run(
            "style.add_surface_style",
            file,
            style=style,
            attributes={
                "SurfaceColour": {
                    "Name": material["Name"],
                    "Red": material["Red"],
                    "Green": material["Green"],
                    "Blue": material["Blue"],
                },
                "Transparency": material["Transparency"],
                "ReflectanceMethod": "PLASTIC",
            },
        )
    

    Then

    for i, brep in enumerate(representation.Items):
        material = materials[i]
        for styled_item in file.by_type("IfcStyledItem"):
            if styled_item.Name == material["Name"]:
                styled_item.Item = brep
                break
    

    Or so I thought. It only applies the material to one of the faces ??

    Input

    Output

  • edited December 2022

    Okay, Color me ashamed...

    This is once again a lesson to everyone to not try to be smarter than other people when "copying" their code. I have found my error and it is dumb.

    I thought this could be optimised

        ifc_raw_items = [None] * max(1, len(obj_blender_evaluated.material_slots))
        for i, value in enumerate(ifc_raw_items):
            ifc_raw_items[i] = []
    

    into this
    ifc_raw_items = [[]] * max(1, len(obj_blender_evaluated.material_slots))

    But alas these are not equivalent. The first script creates a list of pointers to independent empty lists. The second one creates a list of pointers to a single, shared, empty list. ifc_raw_items[0] and ifc_raw_items [1] actually point to the same list, so appending an item to one appends it to the second, since they're the same object.

    Naturally, it leads to funky behaviour when you're trying to reconstruct geometry :)

    Fixing this, I arrived to my goal. Revit doesn't even complain :) I'll clean a bit my finalised code and share it afterwards.

    AceVDobranov
  • edited December 2022

    Success !

    Here's the full script

    # This can be substituted for your own filepath
    from pathlib import Path
    import bpy
    
    blend_path = Path(bpy.data.filepath)
    blend_name = blend_path.stem
    filepath = str(blend_path.with_name(blend_name + "_test.ifc"))
    obj_blend = bpy.context.active_object
    
    
    # Retrieve the materials
    def get_object_materials():
        for slot in obj_blend.material_slots:
            material = slot.material
            if material is None:
                continue
            yield {
                "Name": material.name,
                "Red": material.diffuse_color[0],
                "Green": material.diffuse_color[1],
                "Blue": material.diffuse_color[2],
                "Transparency": 1 - material.diffuse_color[3],
            }
    
    
    # Transform the blender mesh data to Brep
    def get_brep_representation(file, body):
        import bpy
    
        matrix = obj_blend.matrix_world.copy()
        depsgraph = bpy.context.evaluated_depsgraph_get()
        obj_blender_evaluated = obj_blend.evaluated_get(depsgraph)
        mesh_evaluated = obj_blender_evaluated.data
    
        # Note : copy/pasted from https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py#L552-L575
        ifc_vertices = [file.createIfcCartesianPoint(v.co) for v in mesh_evaluated.vertices]
    
        ifc_raw_items = [None] * max(1, len(obj_blender_evaluated.material_slots))
        for i, _ in enumerate(ifc_raw_items):
            ifc_raw_items[i] = []
        for polygon in mesh_evaluated.polygons:
            ifc_raw_items[polygon.material_index].append(
                file.createIfcFace(
                    [
                        file.createIfcFaceOuterBound(
                            file.createIfcPolyLoop([ifc_vertices[vertex] for vertex in polygon.vertices]),
                            True,
                        )
                    ]
                )
            )
        breps = [file.createIfcFacetedBrep(file.createIfcClosedShell(i)) for i in ifc_raw_items if i]
        return file.createIfcShapeRepresentation(
            body,
            body.ContextIdentifier,
            "Brep",
            breps,
        )
    
    
    from ifcopenshell.api import run
    import ifcopenshell
    
    
    file = run("project.create_file")
    # Boilerplate
    project = run("root.create_entity", file, ifc_class="IfcProject", name="My Project")
    context = run("context.add_context", file, context_type="Model")
    body = run(
        "context.add_context",
        file,
        context_type="Model",
        context_identifier="Body",
        target_view="MODEL_VIEW",
        parent=context,
    )
    run("unit.assign_unit", file, length={"is_metric": True, "raw": "METERS"})
    site = run("root.create_entity", file, ifc_class="IfcSite", name="My Site")
    building = run("root.create_entity", file, ifc_class="IfcBuilding", name="Building A")
    storey = run("root.create_entity", file, ifc_class="IfcBuildingStorey", name="Storey 0")
    run("aggregate.assign_object", file, relating_object=project, product=site)
    run("aggregate.assign_object", file, relating_object=site, product=building)
    run("aggregate.assign_object", file, relating_object=building, product=storey)
    
    
    # Create a window occurrence, place it in storey
    product = run(
        "root.create_entity",
        file,
        ifc_class="IfcWindow",
        predefined_type="WINDOW",
        name="Window",
    )
    run("spatial.assign_container", file, relating_structure=storey, product=product)
    
    # Create material constituent set and link it to the window occurrence
    material_set = run("material.add_material_set", file, **{"name": "WindowSet", "set_type": "IfcMaterialConstituentSet"})
    file.createIfcRelAssociatesMaterial(
        ifcopenshell.guid.new(),
        None,
        None,
        None,
        [product],
        material_set,
    )
    
    # Setup Representation
    representation = get_brep_representation(file, body)
    run(
        "geometry.assign_representation",
        file,
        product=product,
        representation=representation,
    )
    
    # Init material properties
    materials = list(get_object_materials())
    for i, material in enumerate(materials):
        material["ifc"] = run("material.add_material", file, name=material["Name"])
        run("material.add_constituent", file, **{"constituent_set": material_set, "material": material["ifc"]})
        style = run("style.add_style", file, name=material["ifc"].Name)
    
        run(
            "style.add_surface_style",
            file,
            style=style,
            attributes={
                "SurfaceColour": {
                    "Name": material["Name"],
                    "Red": material["Red"],
                    "Green": material["Green"],
                    "Blue": material["Blue"],
                },
                "Transparency": material["Transparency"],
                "ReflectanceMethod": "PLASTIC",
            },
        )
        run(
            "style.assign_material_style",
            file,
            material=material["ifc"],
            style=style,
            context=context,
        )
        for styled_item in file.by_type("IfcStyledItem"):  # This looks inefficient. How to optimise ?
            if styled_item.Name == material["Name"]:
                if styled_item.Item:  # This styled item is already tied to another brep
                    continue
                styled_item.Item = representation.Items[
                    i
                ]  # This will throw an error if the object contains a material that is assigned to 0 polygon
                break
    
    file.write(filepath)
    
    
    theoryshawAceCoenlfertig
  • related conversation around IfcShapeAspect, for better or worse: https://forums.buildingsmart.org/t/ifcshapeaspect/4342

    Gorgious
  • Hehe thanks @theoryshaw, so I guess the answer is to use an aggregate if one wants to define an element composed with different materials... I wonder how this will pass the roundtripping test, especially to other softs like Revit or Archicad...

  • For my understanding, in theory this could be used to assign a different material to each of the six faces of a simple IfcWall?

  • @Coen yes I think so, as long as each face is defined using its own representation (or brep definition).

    However I think it would be illegal regarding the ifc schema because you would have an association of non-manifold (non-watertight) meshes.

    Coen
  • For anyone interested Dion added a very thorough documentation to the ifcopenshell material API

    https://github.com/IfcOpenShell/IfcOpenShell/commit/cf87852d9ce442e59fe3f08dd44553b59afd41a5

    VDobranovCoen
Sign In or Register to comment.