OSArch Community

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

  1. G

    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)
    
  2. C

    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.

  3. G

    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.

  4. T

    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.

  5. G

    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.
    
        )
    
  6. T

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

  7. T

    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.

  8. T

    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.

  9. G

    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.

  10. G

    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

  11. G

    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.

  12. G

    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)
    
    
  13. T
  14. G

    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...

  15. C

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

  16. G

    @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.

  17. G

Login or Register to reply.