Support for import IfcSurfaceStyleWithTextures

Added UI for adding textures in BBIM shader graph generator

https://imgur.com/a/BI5pOZn
This commit is contained in:
Andrej730
2023-06-27 14:30:30 +05:00
parent 2da86c170f
commit 3aff29dec1
8 changed files with 233 additions and 59 deletions
@@ -37,6 +37,7 @@ classes = (
operator.UnlinkStyle,
operator.UpdateStyleColours,
operator.UpdateStyleTextures,
operator.ClearTextureMapPath,
prop.Style,
prop.BIMStylesProperties,
prop.BIMStyleProperties,
@@ -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"}
@@ -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,
)
@@ -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")
+12 -7
View File
@@ -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)
+11
View File
@@ -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.
+37 -19
View File
@@ -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")
+91 -29
View File
@@ -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