mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +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):
|
||||
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
|
||||
):
|
||||
@@ -55,7 +55,7 @@ class MaterialCreator:
|
||||
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
|
||||
@@ -80,51 +80,23 @@ class MaterialCreator:
|
||||
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"]):
|
||||
@@ -133,130 +105,6 @@ class MaterialCreator:
|
||||
]
|
||||
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:
|
||||
@@ -266,11 +114,6 @@ class MaterialCreator:
|
||||
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):
|
||||
@@ -336,6 +179,10 @@ class IfcImporter:
|
||||
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()
|
||||
@@ -718,7 +565,9 @@ class IfcImporter:
|
||||
|
||||
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)
|
||||
@@ -837,8 +686,8 @@ class IfcImporter:
|
||||
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?
|
||||
@@ -859,6 +708,7 @@ class IfcImporter:
|
||||
|
||||
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
|
||||
@@ -1145,6 +995,47 @@ class IfcImporter:
|
||||
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)
|
||||
|
||||
@@ -1161,8 +1052,8 @@ class IfcImporter:
|
||||
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"):
|
||||
@@ -1349,9 +1240,7 @@ class IfcImporter:
|
||||
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:
|
||||
@@ -1414,14 +1303,7 @@ class IfcImporter:
|
||||
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:
|
||||
@@ -1473,7 +1355,7 @@ class IfcImporter:
|
||||
break
|
||||
|
||||
def link_element(self, element, obj):
|
||||
self.added_data[element.GlobalId] = obj
|
||||
self.added_data[element.id()] = obj
|
||||
IfcStore.link_element(element, obj)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user