Skip to content

Commit

Permalink
Fix #1529. Properly represent Blender materials as 1:1 mappings to su…
Browse files Browse the repository at this point in the history
…rface styles, with support for material definition representation.
  • Loading branch information
Moult committed Jul 4, 2021
1 parent 68b6389 commit 8a917dc
Showing 1 changed file with 68 additions and 186 deletions.
254 changes: 68 additions & 186 deletions src/blenderbim/blenderbim/bim/import_ifc.py
Expand Up @@ -39,14 +39,14 @@ class MaterialCreator:
def __init__(self, ifc_import_settings, ifc_importer):
self.mesh = None
self.materials = {}
self.styles = {}
self.parsed_meshes = set()
self.ifc_import_settings = ifc_import_settings
self.ifc_importer = ifc_importer

def create(self, element, obj, mesh):
self.obj = obj
self.mesh = mesh
self.parse_material(element)
self.obj = obj
if (hasattr(element, "Representation") and not element.Representation) or (
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
):
Expand All @@ -55,7 +55,7 @@ def create(self, element, obj, mesh):
return
self.parsed_meshes.add(self.mesh.name)
if self.parse_representations(element):
self.assign_material_slots_to_faces(obj)
self.assign_material_slots_to_faces()

def parse_representations(self, element):
has_parsed = False
Expand All @@ -80,51 +80,23 @@ def parse_representation(self, representation):
def parse_representation_item(self, item):
if not item.StyledByItem:
return

item_id = self.mesh.BIMMeshProperties.ifc_item_ids.add()
item_id.name = str(item.id())

styled_item = item.StyledByItem[0] # Cardinality is S[0:1]
style_name = self.get_surface_style_name(styled_item)

if not style_name:
style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
if not style_ids:
return

if self.mesh.materials.get(style_name):
item_id.slot_index = self.mesh.materials.find(style_name)
return True

style = bpy.data.materials.get(style_name)
if not style:
style = bpy.data.materials.new(style_name)
self.parse_styled_item(styled_item, style)

self.assign_style_to_mesh(style)
item_id.slot_index = len(self.mesh.materials) - 1
for style_id in style_ids:
material = self.styles[style_id]
if self.mesh.materials.find(material.name) == -1:
self.mesh.materials.append(material)
return True

def assign_material_slots_to_faces(self, obj):
def assign_material_slots_to_faces(self):
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
return
if len(obj.material_slots) == 1:
if len(self.obj.material_slots) == 1:
return
material_to_slot = {}
for i, material in enumerate(self.mesh["ios_materials"]):
if material == "NULLMAT":
continue
elif "surface-style-" in material:
material = material.split("-")[2]
if len(bytes(material, "utf-8")) > 63: # Blender material names are up to 63 UTF-8 bytes
material = bytes(material, "utf-8")[0:63].decode("utf-8")

slot_index = obj.material_slots.find(material)
if slot_index == -1:
# If we can't find the material, it is possible that the
# material name is duplicated, and so a '.001' is added.
# The maximum characters for the material name is 59 in this
# scenario.
material = bytes(material, "utf-8")[0:59].decode("utf-8")
slot_index = [self.canonicalise_material_name(s.name) for s in obj.material_slots].index(material)
slot_index = self.obj.material_slots.find(self.styles[material].name)
material_to_slot[i] = slot_index

if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
Expand All @@ -133,130 +105,6 @@ def assign_material_slots_to_faces(self, obj):
]
self.mesh.polygons.foreach_set("material_index", material_index)

def canonicalise_material_name(self, name):
return re.sub(r"\.[0-9]{3}$", "", name)

def parse_material(self, element):
for association in element.HasAssociations:
if association.is_a("IfcRelAssociatesMaterial"):
material_select = association.RelatingMaterial
if material_select.is_a("IfcMaterialDefinition"):
self.create_definition(material_select)
elif material_select.is_a("IfcMaterialUsageDefinition"):
self.create_usage_definition(material_select)
elif material_select.is_a("IfcMaterialList"):
# Note that lists are deprecated
self.create_material_list(material_select)
# To support IFC2X3 equivalent of IfcMaterialDefinition
elif material_select.is_a("IfcMaterial") or material_select.is_a("IfcMaterialLayerSet"):
self.create_definition(material_select)
# To support IFC2X3 equivalent of IfcMaterialUsageDefinition
elif material_select.is_a("IfcMaterialLayerSetUsage"):
self.create_usage_definition(material_select)
# IFC2X3 supports assigning a material layer directly. This is silly.
elif material_select.is_a("IfcMaterialLayer"):
pass

def create_layer_set_usage(self, usage):
# TODO import rest of the layer set usage data
self.create_definition(usage.ForLayerSet)

def create_definition(self, material):
if material.is_a("IfcMaterial"):
self.create_single(material)
elif material.is_a("IfcMaterialConstituentSet"):
self.create_constituent_set(material)
elif material.is_a("IfcMaterialLayerSet"):
self.create_layer_set(material)
elif material.is_a("IfcMaterialProfileSet"):
self.create_profile_set(material)

def create_usage_definition(self, material):
if material.is_a("IfcMaterialLayerSetUsage"):
self.create_layer_set_usage(material)
elif material.is_a("IfcMaterialProfileSetUsage"):
pass # TODO

def create_single(self, material):
if material.Name not in self.materials:
self.create_new_single(material)

def create_layer_set(self, layer_set):
for layer in layer_set.MaterialLayers:
if layer.Material:
if layer.Material.Name not in self.materials:
self.create_new_single(layer.Material)

def create_constituent_set(self, constituent_set):
for constituent in constituent_set.MaterialConstituents:
if constituent.Material.Name not in self.materials:
self.create_new_single(constituent.Material)

def create_profile_set(self, profile_set):
for profile in profile_set.MaterialProfiles:
if profile.Material.Name not in self.materials:
self.create_new_single(profile.Material)

def create_material_list(self, material_list):
for material in material_list.Materials:
if material.Name not in self.materials:
self.create_new_single(material)

def create_new_single(self, material):
self.materials[material.Name] = obj = bpy.data.materials.new(material.Name)
obj.BIMObjectProperties.ifc_definition_id = int(material.id())
if not material.HasRepresentation or not material.HasRepresentation[0].Representations:
return
for representation in material.HasRepresentation[0].Representations:
if not representation.Items:
continue
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
self.parse_styled_item(item, obj)

def get_surface_style_name(self, styled_item):
if styled_item.Name:
return styled_item.Name
styles = self.get_styled_item_styles(styled_item)
for style in styles:
if not style.is_a("IfcSurfaceStyle"):
continue
if style.Name:
return style.Name
return str(style.id())
return None # We only support surface styles right now

def parse_styled_item(self, styled_item, material):
styles = self.get_styled_item_styles(styled_item)
for style in styles:
if not style.is_a("IfcSurfaceStyle"):
continue
material.BIMMaterialProperties.ifc_style_id = int(style.id())
for surface_style in style.Styles:
if surface_style.is_a("IfcSurfaceStyleShading"):
alpha = 1.0
# Transparency was added in IFC4
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
alpha = 1 - surface_style.Transparency
material.diffuse_color = (
surface_style.SurfaceColour.Red,
surface_style.SurfaceColour.Green,
surface_style.SurfaceColour.Blue,
alpha,
)

# IfcPresentationStyleAssignment is deprecated as of IFC4
# However it is still widely used thanks to Revit :(
def get_styled_item_styles(self, styled_item):
styles = []
for style in styled_item.Styles:
if style.is_a("IfcPresentationStyleAssignment"):
styles.extend(self.get_styled_item_styles(style))
else:
styles.append(style)
return styles

def resolve_mapped_representation_items(self, representation):
items = []
for item in representation.Items:
Expand All @@ -266,11 +114,6 @@ def resolve_mapped_representation_items(self, representation):
items.append(item)
return items

def assign_style_to_mesh(self, material):
if not self.mesh:
return
self.mesh.materials.append(material)


class IfcImporter:
def __init__(self, ifc_import_settings):
Expand Down Expand Up @@ -336,6 +179,10 @@ def execute(self):
self.profile_code("Create aggregate tree")
self.create_openings_collection()
self.profile_code("Create opening collection")
self.create_materials()
self.profile_code("Create materials")
self.create_styles()
self.profile_code("Create styles")
self.process_element_filter()
self.profile_code("Process element filter")
self.parse_native_elements()
Expand Down Expand Up @@ -718,7 +565,9 @@ def create_products(self):

def create_empty_products(self):
for element in self.file.by_type("IfcProduct"):
if element.GlobalId in self.added_data:
if element.id() in self.added_data:
continue
if element.is_a("IfcPort"):
continue
if not element.Representation:
self.create_product(element)
Expand Down Expand Up @@ -837,8 +686,8 @@ def create_product(self, element, shape=None, mesh=None):
def get_representation_item_material_name(self, item):
if not item.StyledByItem:
return
styled_item = item.StyledByItem[0]
return self.material_creator.get_surface_style_name(styled_item)
style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
return style_ids[0] if style_ids else None

def create_native_faceted_brep(self, element, mesh_name):
# TODO: georeferencing?
Expand All @@ -859,6 +708,7 @@ def create_native_faceted_brep(self, element, mesh_name):

for representation in self.native_data[element.GlobalId]["representations"]:
for item in representation["raw"].Items:
# TODO: if I reimplement native faceted breps, recheck this material implementation
materials.append(self.get_representation_item_material_name(item) or "NULLMAT")
mesh = item.get_info_2(recursive=True) # See bug #841
total_item_polygons = 0
Expand Down Expand Up @@ -1145,6 +995,47 @@ def create_openings_collection(self):
self.opening_collection = bpy.data.collections.new("IfcOpeningElements")
self.project["blender"].children.link(self.opening_collection)

def create_materials(self):
for material in self.file.by_type("IfcMaterial"):
blender_material = bpy.data.materials.new(material.Name)
blender_material.BIMObjectProperties.ifc_definition_id = material.id()
self.material_creator.materials[material.id()] = blender_material
blender_material.use_fake_user = True

def create_styles(self):
parsed_styles = set()

for material_definition_representation in self.file.by_type("IfcMaterialDefinitionRepresentation"):
material = material_definition_representation.RepresentedMaterial
for representation in material_definition_representation.Representations:
for style in [e for e in self.file.traverse(representation) if e.is_a("IfcSurfaceStyle")]:
blender_material = self.material_creator.materials[material.id()]
self.create_style(style, blender_material)
parsed_styles.add(style.id())

for style in self.file.by_type("IfcSurfaceStyle"):
if style.id() in parsed_styles:
continue
name = style.Name or str(style.id())
blender_material = bpy.data.materials.new(name)
self.create_style(style, blender_material)

def create_style(self, style, blender_material):
blender_material.BIMMaterialProperties.ifc_style_id = style.id()
self.material_creator.styles[style.id()] = blender_material
for surface_style in style.Styles:
if surface_style.is_a("IfcSurfaceStyleShading"):
alpha = 1.0
# Transparency was added in IFC4
if hasattr(surface_style, "Transparency") and surface_style.Transparency:
alpha = 1 - surface_style.Transparency
blender_material.diffuse_color = (
surface_style.SurfaceColour.Red,
surface_style.SurfaceColour.Green,
surface_style.SurfaceColour.Blue,
alpha,
)

def get_name(self, element):
return "{}/{}".format(element.is_a(), element.Name)

Expand All @@ -1161,8 +1052,8 @@ def purge_diff(self):
bpy.ops.object.delete({"selected_objects": objects_to_purge})

def place_objects_in_spatial_tree(self):
for global_id, obj in self.added_data.items():
self.place_object_in_spatial_tree(self.file.by_guid(global_id), obj)
for ifc_definition_id, obj in self.added_data.items():
self.place_object_in_spatial_tree(self.file.by_id(ifc_definition_id), obj)

def place_object_in_spatial_tree(self, element, obj):
if element.is_a("IfcProject"):
Expand Down Expand Up @@ -1349,9 +1240,7 @@ def create_camera(self, element, shape):
if "TargetView" in pset:
camera.BIMCameraProperties.target_view = pset["TargetView"]
if "Scale" in pset:
valid_scales = [
i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]
]
valid_scales = [i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]]
if valid_scales:
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
else:
Expand Down Expand Up @@ -1414,14 +1303,7 @@ def create_mesh(self, element, shape):
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
mesh.from_pydata(vertices, edges, [])

ios_materials = []
for mat in geometry.materials:
# See bug #866
if mat.original_name():
ios_materials.append(mat.original_name())
else:
ios_materials.append(mat.name)
mesh["ios_materials"] = ios_materials
mesh["ios_materials"] = [int(m.name.split("-")[2]) for m in geometry.materials]
mesh["ios_material_ids"] = geometry.material_ids
return mesh
except:
Expand Down Expand Up @@ -1473,7 +1355,7 @@ def set_default_context(self):
break

def link_element(self, element, obj):
self.added_data[element.GlobalId] = obj
self.added_data[element.id()] = obj
IfcStore.link_element(element, obj)


Expand Down

0 comments on commit 8a917dc

Please sign in to comment.