mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 02:07:36 +00:00
Fix #1529. Properly represent Blender materials as 1:1 mappings to surface styles, with support for material definition representation.
This commit is contained in:
@@ -39,14 +39,14 @@ class MaterialCreator:
|
|||||||
def __init__(self, ifc_import_settings, ifc_importer):
|
def __init__(self, ifc_import_settings, ifc_importer):
|
||||||
self.mesh = None
|
self.mesh = None
|
||||||
self.materials = {}
|
self.materials = {}
|
||||||
|
self.styles = {}
|
||||||
self.parsed_meshes = set()
|
self.parsed_meshes = set()
|
||||||
self.ifc_import_settings = ifc_import_settings
|
self.ifc_import_settings = ifc_import_settings
|
||||||
self.ifc_importer = ifc_importer
|
self.ifc_importer = ifc_importer
|
||||||
|
|
||||||
def create(self, element, obj, mesh):
|
def create(self, element, obj, mesh):
|
||||||
self.obj = obj
|
|
||||||
self.mesh = mesh
|
self.mesh = mesh
|
||||||
self.parse_material(element)
|
self.obj = obj
|
||||||
if (hasattr(element, "Representation") and not element.Representation) or (
|
if (hasattr(element, "Representation") and not element.Representation) or (
|
||||||
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
|
hasattr(element, "RepresentationMaps") and not element.RepresentationMaps
|
||||||
):
|
):
|
||||||
@@ -55,7 +55,7 @@ class MaterialCreator:
|
|||||||
return
|
return
|
||||||
self.parsed_meshes.add(self.mesh.name)
|
self.parsed_meshes.add(self.mesh.name)
|
||||||
if self.parse_representations(element):
|
if self.parse_representations(element):
|
||||||
self.assign_material_slots_to_faces(obj)
|
self.assign_material_slots_to_faces()
|
||||||
|
|
||||||
def parse_representations(self, element):
|
def parse_representations(self, element):
|
||||||
has_parsed = False
|
has_parsed = False
|
||||||
@@ -80,51 +80,23 @@ class MaterialCreator:
|
|||||||
def parse_representation_item(self, item):
|
def parse_representation_item(self, item):
|
||||||
if not item.StyledByItem:
|
if not item.StyledByItem:
|
||||||
return
|
return
|
||||||
|
style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
|
||||||
item_id = self.mesh.BIMMeshProperties.ifc_item_ids.add()
|
if not style_ids:
|
||||||
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:
|
|
||||||
return
|
return
|
||||||
|
for style_id in style_ids:
|
||||||
if self.mesh.materials.get(style_name):
|
material = self.styles[style_id]
|
||||||
item_id.slot_index = self.mesh.materials.find(style_name)
|
if self.mesh.materials.find(material.name) == -1:
|
||||||
return True
|
self.mesh.materials.append(material)
|
||||||
|
|
||||||
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
|
|
||||||
return True
|
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"]:
|
if "ios_materials" not in self.mesh or not self.mesh["ios_materials"]:
|
||||||
return
|
return
|
||||||
if len(obj.material_slots) == 1:
|
if len(self.obj.material_slots) == 1:
|
||||||
return
|
return
|
||||||
material_to_slot = {}
|
material_to_slot = {}
|
||||||
for i, material in enumerate(self.mesh["ios_materials"]):
|
for i, material in enumerate(self.mesh["ios_materials"]):
|
||||||
if material == "NULLMAT":
|
slot_index = self.obj.material_slots.find(self.styles[material].name)
|
||||||
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)
|
|
||||||
material_to_slot[i] = slot_index
|
material_to_slot[i] = slot_index
|
||||||
|
|
||||||
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
|
if len(self.mesh.polygons) == len(self.mesh["ios_material_ids"]):
|
||||||
@@ -133,130 +105,6 @@ class MaterialCreator:
|
|||||||
]
|
]
|
||||||
self.mesh.polygons.foreach_set("material_index", material_index)
|
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):
|
def resolve_mapped_representation_items(self, representation):
|
||||||
items = []
|
items = []
|
||||||
for item in representation.Items:
|
for item in representation.Items:
|
||||||
@@ -266,11 +114,6 @@ class MaterialCreator:
|
|||||||
items.append(item)
|
items.append(item)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
def assign_style_to_mesh(self, material):
|
|
||||||
if not self.mesh:
|
|
||||||
return
|
|
||||||
self.mesh.materials.append(material)
|
|
||||||
|
|
||||||
|
|
||||||
class IfcImporter:
|
class IfcImporter:
|
||||||
def __init__(self, ifc_import_settings):
|
def __init__(self, ifc_import_settings):
|
||||||
@@ -336,6 +179,10 @@ class IfcImporter:
|
|||||||
self.profile_code("Create aggregate tree")
|
self.profile_code("Create aggregate tree")
|
||||||
self.create_openings_collection()
|
self.create_openings_collection()
|
||||||
self.profile_code("Create opening 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.process_element_filter()
|
||||||
self.profile_code("Process element filter")
|
self.profile_code("Process element filter")
|
||||||
self.parse_native_elements()
|
self.parse_native_elements()
|
||||||
@@ -718,7 +565,9 @@ class IfcImporter:
|
|||||||
|
|
||||||
def create_empty_products(self):
|
def create_empty_products(self):
|
||||||
for element in self.file.by_type("IfcProduct"):
|
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
|
continue
|
||||||
if not element.Representation:
|
if not element.Representation:
|
||||||
self.create_product(element)
|
self.create_product(element)
|
||||||
@@ -837,8 +686,8 @@ class IfcImporter:
|
|||||||
def get_representation_item_material_name(self, item):
|
def get_representation_item_material_name(self, item):
|
||||||
if not item.StyledByItem:
|
if not item.StyledByItem:
|
||||||
return
|
return
|
||||||
styled_item = item.StyledByItem[0]
|
style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")]
|
||||||
return self.material_creator.get_surface_style_name(styled_item)
|
return style_ids[0] if style_ids else None
|
||||||
|
|
||||||
def create_native_faceted_brep(self, element, mesh_name):
|
def create_native_faceted_brep(self, element, mesh_name):
|
||||||
# TODO: georeferencing?
|
# TODO: georeferencing?
|
||||||
@@ -859,6 +708,7 @@ class IfcImporter:
|
|||||||
|
|
||||||
for representation in self.native_data[element.GlobalId]["representations"]:
|
for representation in self.native_data[element.GlobalId]["representations"]:
|
||||||
for item in representation["raw"].Items:
|
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")
|
materials.append(self.get_representation_item_material_name(item) or "NULLMAT")
|
||||||
mesh = item.get_info_2(recursive=True) # See bug #841
|
mesh = item.get_info_2(recursive=True) # See bug #841
|
||||||
total_item_polygons = 0
|
total_item_polygons = 0
|
||||||
@@ -1145,6 +995,47 @@ class IfcImporter:
|
|||||||
self.opening_collection = bpy.data.collections.new("IfcOpeningElements")
|
self.opening_collection = bpy.data.collections.new("IfcOpeningElements")
|
||||||
self.project["blender"].children.link(self.opening_collection)
|
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):
|
def get_name(self, element):
|
||||||
return "{}/{}".format(element.is_a(), element.Name)
|
return "{}/{}".format(element.is_a(), element.Name)
|
||||||
|
|
||||||
@@ -1161,8 +1052,8 @@ class IfcImporter:
|
|||||||
bpy.ops.object.delete({"selected_objects": objects_to_purge})
|
bpy.ops.object.delete({"selected_objects": objects_to_purge})
|
||||||
|
|
||||||
def place_objects_in_spatial_tree(self):
|
def place_objects_in_spatial_tree(self):
|
||||||
for global_id, obj in self.added_data.items():
|
for ifc_definition_id, obj in self.added_data.items():
|
||||||
self.place_object_in_spatial_tree(self.file.by_guid(global_id), obj)
|
self.place_object_in_spatial_tree(self.file.by_id(ifc_definition_id), obj)
|
||||||
|
|
||||||
def place_object_in_spatial_tree(self, element, obj):
|
def place_object_in_spatial_tree(self, element, obj):
|
||||||
if element.is_a("IfcProject"):
|
if element.is_a("IfcProject"):
|
||||||
@@ -1349,9 +1240,7 @@ class IfcImporter:
|
|||||||
if "TargetView" in pset:
|
if "TargetView" in pset:
|
||||||
camera.BIMCameraProperties.target_view = pset["TargetView"]
|
camera.BIMCameraProperties.target_view = pset["TargetView"]
|
||||||
if "Scale" in pset:
|
if "Scale" in pset:
|
||||||
valid_scales = [
|
valid_scales = [i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]]
|
||||||
i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]
|
|
||||||
]
|
|
||||||
if valid_scales:
|
if valid_scales:
|
||||||
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
|
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
|
||||||
else:
|
else:
|
||||||
@@ -1414,14 +1303,7 @@ class IfcImporter:
|
|||||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||||
mesh.from_pydata(vertices, edges, [])
|
mesh.from_pydata(vertices, edges, [])
|
||||||
|
|
||||||
ios_materials = []
|
mesh["ios_materials"] = [int(m.name.split("-")[2]) for m in geometry.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_material_ids"] = geometry.material_ids
|
mesh["ios_material_ids"] = geometry.material_ids
|
||||||
return mesh
|
return mesh
|
||||||
except:
|
except:
|
||||||
@@ -1473,7 +1355,7 @@ class IfcImporter:
|
|||||||
break
|
break
|
||||||
|
|
||||||
def link_element(self, element, obj):
|
def link_element(self, element, obj):
|
||||||
self.added_data[element.GlobalId] = obj
|
self.added_data[element.id()] = obj
|
||||||
IfcStore.link_element(element, obj)
|
IfcStore.link_element(element, obj)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user