diff --git a/src/blenderbim/blenderbim/bim/module/style/__init__.py b/src/blenderbim/blenderbim/bim/module/style/__init__.py index 45a8d3364f..8d37b57ee1 100644 --- a/src/blenderbim/blenderbim/bim/module/style/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/style/__init__.py @@ -37,6 +37,7 @@ classes = ( operator.UnlinkStyle, operator.UpdateStyleColours, operator.UpdateStyleTextures, + operator.ClearTextureMapPath, prop.Style, prop.BIMStylesProperties, prop.BIMStyleProperties, diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py index 97519569dc..f3ee2ace67 100644 --- a/src/blenderbim/blenderbim/bim/module/style/operator.py +++ b/src/blenderbim/blenderbim/bim/module/style/operator.py @@ -392,3 +392,25 @@ class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.select_by_style(tool.Style, tool.Spatial, style=tool.Ifc.get().by_id(self.style)) + + +class ClearTextureMapPath(bpy.types.Operator): + bl_idname = "bim.clear_texture_map_path" + bl_label = "Clear Texture Map Path" + bl_options = {"REGISTER", "UNDO"} + texture_map_prop: bpy.props.StringProperty(default="") + + @classmethod + def poll(cls, context): + poll = getattr(context, "material", None) + if not poll: + cls.poll_message_set("Select a material") + return poll + + def execute(self, context): + if not self.texture_map_prop: + self.report({"ERROR"}, "Provide a texture map") + return {"CANCELLED"} + props = context.material.BIMStyleProperties + setattr(props, self.texture_map_prop, "") + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/style/prop.py b/src/blenderbim/blenderbim/bim/module/style/prop.py index 21d86530f4..f2faba9dc2 100644 --- a/src/blenderbim/blenderbim/bim/module/style/prop.py +++ b/src/blenderbim/blenderbim/bim/module/style/prop.py @@ -100,7 +100,7 @@ def update_shading_style(self, context): tool.Style.record_shading(blender_material) -# TODO: support more more methods and also textures +# TODO: support more more methods REFLECTANCE_METHODS = [ ("PHYSICAL", "PHYSICAL", ""), ("FLAT", "FLAT", ""), @@ -117,7 +117,9 @@ def update_shader_graph(self, context): material = self.id_data style_data = tool.Style.get_surface_style_from_props(material) + textures_data = tool.Style.get_texture_style_from_props(material) tool.Loader.create_surface_style_rendering(material, style_data) + tool.Loader.create_surface_style_with_textures(material, style_data, textures_data) def update_graph_get(self): @@ -184,3 +186,32 @@ class BIMStyleProperties(PropertyGroup): ) roughness: bpy.props.FloatProperty(name="Roughness", default=0.0, min=0.0, max=1.0, update=update_shader_graph) metallic: bpy.props.FloatProperty(name="Metallic", default=0.0, min=0.0, max=1.0, update=update_shader_graph) + normal_path: bpy.props.StringProperty( + name="NormalMap", + maxlen=1024, + default="", + subtype="FILE_PATH", + update=update_shader_graph, + ) + emissive_path: bpy.props.StringProperty( + name="Emissive", + maxlen=1024, + default="", + subtype="FILE_PATH", + update=update_shader_graph, + ) + metallic_roughness_path: bpy.props.StringProperty( + name="Metallic/Roughness", + maxlen=1024, + default="", + subtype="FILE_PATH", + update=update_shader_graph, + description="Green Channel = Roughness,\nBlue Channel = Metallic", + ) + diffuse_path: bpy.props.StringProperty( + name="Diffuse", + maxlen=1024, + default="", + subtype="FILE_PATH", + update=update_shader_graph, + ) diff --git a/src/blenderbim/blenderbim/bim/module/style/ui.py b/src/blenderbim/blenderbim/bim/module/style/ui.py index b66fc8c69e..54a0837921 100644 --- a/src/blenderbim/blenderbim/bim/module/style/ui.py +++ b/src/blenderbim/blenderbim/bim/module/style/ui.py @@ -251,10 +251,34 @@ class BIM_PT_STYLE_GRAPH(Panel): layout.prop(props, "reflectance_method", text="") layout.prop(props, "surface_color") layout.prop(props, "transparency") - layout.prop(props, "diffuse_color") - if props.reflectance_method == "PHYSICAL": + if not (props.reflectance_method == "PHYSICAL" and props.diffuse_path) and not ( + props.reflectance_method == "FLAT" and props.emissive_path + ): + prop_name = "Emissive Color" if props.reflectance_method == "FLAT" else "Diffuse Color" + layout.prop(props, "diffuse_color", text=prop_name) + + if props.reflectance_method == "PHYSICAL" and not props.metallic_roughness_path: layout.prop(props, "metallic") - if props.reflectance_method not in ("FLAT", "NOTDEFINED"): + if props.reflectance_method not in ("PHYSICAL", "FLAT", "NOTDEFINED") or ( + props.reflectance_method == "PHYSICAL" and not props.metallic_roughness_path + ): layout.prop(props, "roughness") + + layout.label(text="Texture Maps:") + + def add_texture_path(path_name): + row = layout.row(align=True) + row.prop(props, path_name) + op = row.operator("bim.clear_texture_map_path", text="", icon="X") + op.texture_map_prop = path_name + + if props.reflectance_method == "PHYSICAL": + add_texture_path("diffuse_path") + add_texture_path("emissive_path") + add_texture_path("normal_path") + add_texture_path("metallic_roughness_path") + + if props.reflectance_method == "FLAT": + add_texture_path("emissive_path") diff --git a/src/blenderbim/blenderbim/core/style.py b/src/blenderbim/blenderbim/core/style.py index c2a720a28d..5978f983eb 100644 --- a/src/blenderbim/blenderbim/core/style.py +++ b/src/blenderbim/blenderbim/core/style.py @@ -69,13 +69,18 @@ def update_style_colours(ifc, style, obj=None, verbose=False): "style.add_surface_style", style=element, ifc_class="IfcSurfaceStyleRendering", attributes=attributes ) - # # TODO: uncomment to support saving textures - # # TODO: uvs? - # textures = ifc.run("style.add_surface_textures", material=obj) - # if not texture_style and textures: - # texture_style = ifc.get().createIfcSurfaceStyleWithTextures() - # if texture_style: - # texture_style.Textures = textures + # TODO: uvs? + textures = ifc.run("style.add_surface_textures", material=obj) + if not texture_style and textures: + ifc.run( + "style.add_surface_style", + style=element, + ifc_class="IfcSurfaceStyleWithTextures", + attributes={"Textures": textures}, + ) + elif texture_style: + # TODO: should we remove blender images and IFCIMAGETEXTURE here if they're not used by other objects? + ifc.run("style.edit_surface_style", style=texture_style, attributes={"Textures": textures}) else: shading_style = style.get_surface_shading_style(obj) attributes = style.get_surface_shading_attributes(obj) diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index eb2683acf0..1648c4bca0 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -110,10 +110,21 @@ class Blender: @classmethod def show_error_message(cls, text): """useful for showing error messages outside blender operators""" + def error(self, context): self.layout.label(text=text) + bpy.context.window_manager.popup_menu(error, title="Error", icon="ERROR") + @classmethod + def get_blender_prop_default_value(cls, props, prop_name): + prop_bl_rna = props.bl_rna.properties[prop_name] + if getattr(prop_bl_rna, "array_length", 0) > 0: + prop_value = prop_bl_rna.default_array + else: + prop_value = prop_bl_rna.default + return prop_value + @classmethod def get_viewport_context(cls): """Get viewport area context for context overriding. diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index 8099a24439..9b3c658314 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -23,6 +23,7 @@ import blenderbim.core.tool import blenderbim.tool as tool import os from mathutils import Vector +from pathlib import Path # Progressively we'll refactor loading elements into Blender objects into this @@ -168,31 +169,43 @@ class Loader(blenderbim.core.tool.Loader): @classmethod def create_surface_style_with_textures(cls, blender_material, rendering_style, texture_style): - for texture in texture_style.Textures: - mode = getattr(texture, "Mode", None) + """supposed to be called after `create_surface_style_rendering`""" + if not isinstance(texture_style, list): + textures = [t.get_info() for t in texture_style.Textures] + else: + textures = texture_style + rendering_style = cls.surface_style_to_dict(rendering_style) + + reflectance_method = rendering_style["ReflectanceMethod"] + if reflectance_method not in ("PHYSICAL", "NOTDEFINED", "FLAT"): + print(f'WARNING. Unsupported reflectance method "{reflectance_method}" on style {rendering_style}') + return + + for texture in textures: + mode = texture.get("Mode", None) node = None - if texture.is_a("IfcImageTexture"): - image_url = texture.URLReference - if not os.path.abspath(texture.URLReference) and tool.Ifc.get_path(): - image_url = os.path.join(os.path.dirname(tool.Ifc.get_path()), texture.URLReference) + if texture["type"] == "IfcImageTexture": + image_url = texture["URLReference"] + ifc_path = tool.Ifc.get_path() + if ifc_path: + image_url = bpy.path.abspath(image_url, start=Path(ifc_path).parent) - reflectance_method = rendering_style.ReflectanceMethod - if reflectance_method not in ("PHYSICAL", "NOTDEFINED", "FLAT"): - print(f'WARNING. Unsupported reflectance method "{reflectance_method}" on style {rendering_style}') - return + if not os.path.exists(image_url): + print(f"WARNING. Couldn't find texture by path {image_url}, it will be skipped.") + continue - if rendering_style.ReflectanceMethod in ["PHYSICAL", "NOTDEFINED"]: + if reflectance_method in ["PHYSICAL", "NOTDEFINED"]: bsdf = tool.Blender.get_material_node(blender_material, "BSDF_PRINCIPLED") if mode == "NORMAL": # add normal map node normalmap = blender_material.node_tree.nodes.new(type="ShaderNodeNormalMap") - normalmap.location = bsdf.location - Vector((200, 0)) + normalmap.location = bsdf.location - Vector((200, 600)) blender_material.node_tree.links.new(normalmap.outputs[0], bsdf.inputs["Normal"]) # add normal map sampler node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = normalmap.location - Vector((200, 0)) + node.location = normalmap.location - Vector((300, 0)) image = bpy.data.images.load(image_url) image.colorspace_settings.name = "Non-Color" node.image = image @@ -203,7 +216,7 @@ class Loader(blenderbim.core.tool.Loader): # add "Add Shader" node add = blender_material.node_tree.nodes.new(type="ShaderNodeAddShader") - add.location = bsdf.location + Vector((200, 0)) + add.location = bsdf.location + Vector((200, 350)) blender_material.node_tree.links.new(bsdf.outputs[0], add.inputs[1]) blender_material.node_tree.links.new(add.outputs[0], output.inputs[0]) @@ -214,19 +227,19 @@ class Loader(blenderbim.core.tool.Loader): # add emission texture sampler node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = emission.location - Vector((200, 0)) + node.location = emission.location - Vector((350, 0)) image = bpy.data.images.load(image_url) node.image = image blender_material.node_tree.links.new(node.outputs[0], emission.inputs[0]) elif mode == "METALLICROUGHNESS": separate = blender_material.node_tree.nodes.new(type="ShaderNodeSeparateRGB") - separate.location = bsdf.location - Vector((200, 0)) + separate.location = bsdf.location - Vector((200, 300)) blender_material.node_tree.links.new(separate.outputs[1], bsdf.inputs["Roughness"]) blender_material.node_tree.links.new(separate.outputs[2], bsdf.inputs["Metallic"]) node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = separate.location - Vector((200, 0)) + node.location = separate.location - Vector((300, 0)) image = bpy.data.images.load(image_url) image.colorspace_settings.name = "Non-Color" node.image = image @@ -246,18 +259,23 @@ class Loader(blenderbim.core.tool.Loader): blender_material.node_tree.links.new(node.outputs[1], bsdf.inputs["Alpha"]) blender_material.blend_method = "BLEND" - elif rendering_style.ReflectanceMethod == "FLAT": + elif reflectance_method == "FLAT": bsdf = tool.Blender.get_material_node(blender_material, "MIX_SHADER") if mode != "EMISSIVE": continue + # remove RGB node from `create_surface_style_rendering` + prev_node = bsdf.inputs[2].links[0].from_node + blender_material.node_tree.nodes.remove(prev_node) + node = blender_material.node_tree.nodes.new(type="ShaderNodeTexImage") - node.location = bsdf.location - Vector((200, 0)) + node.location = bsdf.location - Vector((200, 250)) image = bpy.data.images.load(image_url) # TODO: orphaned textures after shader recreated? node.image = image blender_material.node_tree.links.new(node.outputs[0], bsdf.inputs[2]) + # TODO: add support for texture data not ifc elements if node and getattr(texture, "IsMappedBy", None): coordinates = texture.IsMappedBy[0] coord = blender_material.node_tree.nodes.new(type="ShaderNodeTexCoord") diff --git a/src/blenderbim/blenderbim/tool/style.py b/src/blenderbim/blenderbim/tool/style.py index f360be55fe..647519205b 100644 --- a/src/blenderbim/blenderbim/tool/style.py +++ b/src/blenderbim/blenderbim/tool/style.py @@ -22,7 +22,14 @@ import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool import blenderbim.bim.helper +import os.path +# fmt: off +TEXTURE_MAPS_BY_METHODS = { + "PHYSICAL": ("NORMAL", "EMISSIVE", "METALLICROUGHNESS", "DIFFUSE"), + "FLAT": ("EMISSIVE",) +} +# fmt: on STYLE_PROPS_MAP = { "reflectance_method": "ReflectanceMethod", @@ -33,6 +40,13 @@ STYLE_PROPS_MAP = { "metallic": "SpecularColour", } +STYLE_TEXTURE_PROPS_MAP = { + "EMISSIVE": "emissive_path", + "NORMAL": "normal_path", + "METALLICROUGHNESS": "metallic_roughness_path", + "DIFFUSE": "diffuse_path", +} + class Style(blenderbim.core.tool.Style): @classmethod @@ -117,6 +131,29 @@ class Style(blenderbim.core.tool.Style): surface_style_data["DiffuseColour"] = ("IfcColourRgb", surface_style_data["DiffuseColour"]) return surface_style_data + @classmethod + def get_texture_style_from_props(cls, blender_material): + props = blender_material.BIMStyleProperties + + textures = [] + texture_maps = TEXTURE_MAPS_BY_METHODS[props.reflectance_method] + for prop_mode in texture_maps: + prop_name = STYLE_TEXTURE_PROPS_MAP[prop_mode] + path = getattr(props, prop_name) + if not path: + continue + if not os.path.abspath(path) and tool.Ifc.get_path(): + path = os.path.join(os.path.dirname(tool.Ifc.get_path()), path) + + texture_data = { + "Mode": prop_mode, + "type": "IfcImageTexture", + "URLReference": path, + } + textures.append(texture_data) + + return textures + @classmethod def set_surface_style_props(cls, blender_material): """set blender style props based on current surface material""" @@ -127,6 +164,8 @@ class Style(blenderbim.core.tool.Style): style_elements = tool.Style.get_style_elements(blender_material) surface_style = style_elements["IfcSurfaceStyleRendering"] + texture_style = style_elements.get("IfcSurfaceStyleWithTextures", None) + style_data = tool.Loader.surface_style_to_dict(surface_style) if style_data["ReflectanceMethod"] == "NOTDEFINED": style_data["ReflectanceMethod"] = "PHYSICAL" @@ -134,26 +173,49 @@ class Style(blenderbim.core.tool.Style): for prop_blender, prop_ifc in STYLE_PROPS_MAP.items(): prop_value = style_data[prop_ifc] - # TODO: set to default? if prop_value is None: - continue + prop_value = tool.Blender.get_blender_prop_default_value(props, prop_blender) setattr(props, prop_blender, prop_value) + texture_maps = TEXTURE_MAPS_BY_METHODS[style_data["ReflectanceMethod"]] + unused_texture_maps = list(STYLE_TEXTURE_PROPS_MAP.keys()) + for texture in texture_style.Textures: + if texture.Mode not in texture_maps: + print(f"WARNING. Unsupported texture mode: {texture.Mode}. Supported maps: {texture_maps}") + continue + prop_blender = STYLE_TEXTURE_PROPS_MAP.get(texture.Mode, None) + setattr(props, prop_blender, texture.URLReference) + unused_texture_maps.remove(texture.Mode) + + # clear empty texture fields + for texture_mode in unused_texture_maps: + prop_blender = STYLE_TEXTURE_PROPS_MAP[texture_mode] + setattr(props, prop_blender, "") + props["update_graph"] = prev_update_graph_value @classmethod def get_surface_rendering_attributes(cls, obj, verbose=True): - transparency = 1 - obj.diffuse_color[3] - diffuse_color = obj.diffuse_color - report = (lambda *x: print(*x)) if verbose else (lambda *x: None) - viewport_color = { - "Name": None, - "Red": obj.diffuse_color[0], - "Green": obj.diffuse_color[1], - "Blue": obj.diffuse_color[2], - } + def color_to_ifc_format(color): + return { + "Name": None, + "Red": color[0], + "Green": color[1], + "Blue": color[2], + } + + def get_input_node(node, input_name=None, of_type=None, input_index=None): + input_pin = node.inputs[input_name] if input_index is None else node.inputs[input_index] + if of_type: + return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None) + return next((l.from_node for l in input_pin.links), None) + + props = obj.BIMStyleProperties + transparency = 1 - obj.diffuse_color[3] + diffuse_color = obj.diffuse_color + viewport_color = color_to_ifc_format(obj.diffuse_color) attributes = { "SurfaceColour": viewport_color, "Transparency": transparency, @@ -162,12 +224,6 @@ class Style(blenderbim.core.tool.Style): BLUE = "\033[1;34m" R = "\033[0m" # RESET symbol - def get_input_node(node, input_name=None, of_type=None, input_index=None): - input_pin = node.inputs[input_name] if input_index is None else node.inputs[input_index] - if of_type: - return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None) - return next((l.from_node for l in input_pin.links), None) - report("--------------------") report("Verbose method of getting surface rendering attributes enabled.") report("If some attribute is not mentioned below, then it won't be saved to IFC.") @@ -188,20 +244,31 @@ class Style(blenderbim.core.tool.Style): if ( get_input_node(mix_shader, "Fac", "LIGHT_PATH") and get_input_node(mix_shader, input_index=1, of_type="BSDF_TRANSPARENT") - and (rgb := get_input_node(mix_shader, input_index=2, of_type="RGB")) + and (second_input_node := get_input_node(mix_shader, input_index=2)) + and second_input_node.type in ("RGB", "TEX_IMAGE") ): report( - f"Because of {BLUE}MIX_SHADER + LIGHT_PATH + BSDF_TRANSPARENT + RGB{R} node setup reflectance method identified as {BLUE}FLAT{R}" + f"Because of {BLUE}MIX_SHADER + LIGHT_PATH + BSDF_TRANSPARENT + RGB/TEX{R} node setup reflectance method identified as {BLUE}FLAT{R}" ) attributes["ReflectanceMethod"] = "FLAT" attributes["SpecularHighlight"] = None - report(f"RGB {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}") - diffuse_color = rgb.outputs[0].default_value - elif surface_output and surface_output.type == "BSDF_PRINCIPLED": + if second_input_node.type == "RGB": + report(f"RGB {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}") + diffuse_color = second_input_node.outputs[0].default_value + elif second_input_node.type == "TEX_IMAGE": + report(f"{GREEN}BBIM Panel Diffuse Color{R} saved as {GREEN}DiffuseColour{R}") + diffuse_color = props.diffuse_color + + elif surface_output and ( + (surface_output.type == "BSDF_PRINCIPLED" and (bsdf := surface_output)) + or ( + surface_output.type == "ADD_SHADER" + and (bsdf := get_input_node(surface_output, input_index=1, of_type="BSDF_PRINCIPLED")) + ) + ): report(f"Because of {BLUE}BSDF_PRINCIPLED{R} node reflectance method identified as {BLUE}PHYSICAL{R}") attributes["ReflectanceMethod"] = "NOTDEFINED" if tool.Ifc.get_schema() != "IFC4X3" else "PHYSICAL" - bsdf = surface_output report(f"BSDF {GREEN}Base Color{R} saved as {GREEN}DiffuseColour{R}") diffuse_color = bsdf.inputs["Base Color"].default_value @@ -286,12 +353,7 @@ class Style(blenderbim.core.tool.Style): attributes["DiffuseColour"] = viewport_color return attributes - attributes["DiffuseColour"] = { - "Name": None, - "Red": diffuse_color[0], - "Green": diffuse_color[1], - "Blue": diffuse_color[2], - } + attributes["DiffuseColour"] = color_to_ifc_format(diffuse_color) return attributes @classmethod