""" Copyright (C) 2023 Adobe. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # file: material/manager.py # brief: Material operations manager # author Adobe - 3D & Immersive # copyright 2023 Adobe Inc. All rights reserved. import os import bpy import time import traceback from copy import deepcopy from ..thread_ops import SUBSTANCE_Threads from ..utils import SUBSTANCE_Utils from ..common import RENDER_IMG_UPDATE_DELAY_S, ADDON_PACKAGE class SUBSTANCE_MaterialManager(): @staticmethod def get_image(name, filepath): if name in bpy.data.images: _image = bpy.data.images[name] _image_filepath = bpy.path.abspath(_image.filepath) _image_filepath = os.path.abspath(_image_filepath) _image_filepath = _image_filepath.replace("\\", "/") _new_filepath = filepath.replace("\\", "/") if _image_filepath != _new_filepath: _image.filepath = _new_filepath _image.reload() return _image _image = bpy.data.images.load(filepath=filepath.replace("\\", "/")) _image.name = name return _image @staticmethod def get_existing_material(material_name): if material_name in bpy.data.materials: return bpy.data.materials[material_name] return None @staticmethod def reset_image(node, image): def _set_image(): node.image = image SUBSTANCE_Threads.main_thread_run(_set_image) @staticmethod def auto_apply_material(context, data, material): _selected_geo = SUBSTANCE_Utils.get_selected_geo(context.selected_objects) if len(_selected_geo) > 0 and data["graph_idx"] == 0: _selected_objects = [bpy.context.active_object] SUBSTANCE_MaterialManager.apply_material(context, _selected_objects, material) @staticmethod def apply_material(context, objects, material): _addon_prefs = context.preferences.addons[ADDON_PACKAGE].preferences for _obj in objects: if not hasattr(_obj.data, "materials"): continue if _addon_prefs.default_apply_type == "INSERT" and _obj.data.materials: # _obj.data.materials.append(material) _obj.active_material = material else: _obj.data.materials.append(material) @staticmethod def empty_material(material, graph): material.use_nodes = True try: material.cycles.displacement_method = 'BOTH' except Exception: pass # Cleanup graph _mat_nodes = material.node_tree.nodes for _node in _mat_nodes: _mat_nodes.remove(_node) # Cleanup Node groups if graph.material.name in bpy.data.node_groups: _subgroup = bpy.data.node_groups[graph.material.name] for _node in _subgroup.nodes: _subgroup.nodes.remove(_node) if bpy.app.version >= (4, 0, 0): for _item in _subgroup.interface.items_tree: if _item.item_type == 'SOCKET': if _item.in_out == 'INPUT' or _item.in_out == 'OUTPUT': _subgroup.interface.remove(_item) else: for _input in _subgroup.inputs: _subgroup.inputs.remove(_input) for _output in _subgroup.outputs: _subgroup.outputs.remove(_output) @staticmethod def init_nodes(material, graph, shader_graph): _items = {} for _node in shader_graph["nodes"]: _name = _node["name"].replace("$matname", graph.material.name) if _node["path"] == "root": _nodes = material.node_tree.nodes else: _nodes = _items[_node["path"].replace("root/", "")].node_tree.nodes if _name in _nodes: _items[_node["id"]] = _nodes[_name] else: _items[_node["id"]] = _nodes.new(_node["type"]) _items[_node["id"]].name = _name if _node["type"] == "NodeFrame": _items[_node["id"]].label = _node["label"] continue _items[_node["id"]].location = (_node["location"][0], _node["location"][1]) if _node["type"] == "ShaderNodeGroup": if graph.material.name in bpy.data.node_groups: _node_tree = bpy.data.node_groups[graph.material.name] else: _node_tree = bpy.data.node_groups.new( type='ShaderNodeTree', name=graph.material.name) _items[_node["id"]].node_tree = _node_tree return _items @staticmethod def init_outputs(items, sbs_group, shader_prefs, shader_graph, addons_prefs, data, graph, new_nodes=None): _location = [-600, 0] for _output in graph.outputs: if _output.name not in data["outputs"]: continue if not data["outputs"][_output.name]["enabled"]: continue if _output.type == "image": if data["outputs"][_output.name]["path"] != "": _filepath = data["outputs"][_output.name]["path"] _filename = bpy.path.basename(data["outputs"][_output.name]["path"]) _output.filepath = _filepath _output.filename = _filename else: _output.filepath = "" _output.filename = "" continue _name = "Tx {}".format(_output.name) _key = "tx_{}".format(_output.name) if _name in sbs_group.node_tree.nodes: items[_key] = sbs_group.node_tree.nodes[_name] else: if new_nodes is not None: new_nodes.append(_key) items[_key] = sbs_group.node_tree.nodes.new("ShaderNodeTexImage") items[_key].name = "Tx {}".format(_output.name) _img_name = "{}_{}".format(graph.material.name, _output.name) _img = SUBSTANCE_MaterialManager.get_image(_img_name, _filepath) items[_key].image = _img if _output.name in shader_prefs["outputs"]: _shader_output = shader_prefs["outputs"][_output.name] _img.colorspace_settings.name = _shader_output["colorspace"] else: _img.colorspace_settings.name = addons_prefs.output_default_colorspace if (_output.name in shader_graph["outputs"] and "normal" in shader_graph["outputs"][_output.name]): _nrm_key = "normal_{}".format(_output.name) if new_nodes is not None: new_nodes.append(_nrm_key) items[_nrm_key] = sbs_group.node_tree.nodes.new("ShaderNodeNormalMap") items[_nrm_key].name = "Normal {}".format(_output.name) items[_nrm_key].location = (_location[0] + 300, _location[1]) # Add common properties to outputs that are not part of the shader if _output.name not in shader_graph["outputs"]: for _property in shader_graph["properties"]: if _property["id"] == "tx_": _new_propery = deepcopy(_property) _new_propery["id"] = "tx_{}".format(_output.name) shader_graph["properties"].append(_new_propery) items[_key].location = (_location[0], _location[1]) _location[1] -= 300 else: _name = "Val {}".format(_output.name) _key = "val_{}".format(_output.name) _value = data["outputs"][_output.name]["value"] if _output.type == "integer2" or _output.type == "float2": _value.append(0) _value.append(0) elif _output.type == "integer3" or _output.type == "float3": _value.append(0) if _name in sbs_group.node_tree.nodes: items[_key] = sbs_group.node_tree.nodes[_name] else: if new_nodes is not None: new_nodes.append(_key) if _output.type == "integer" or _output.type == "float": items[_key] = sbs_group.node_tree.nodes.new("ShaderNodeValue") else: items[_key] = sbs_group.node_tree.nodes.new("ShaderNodeCombineXYZ") if _output.type == "integer" or _output.type == "float": items[_key].outputs[0].default_value = _value else: items[_key].inputs[0].default_value = _value[0] items[_key].inputs[1].default_value = _value[1] items[_key].inputs[2].default_value = _value[2] items[_key].location = (_location[0], _location[1]) _location[1] -= 300 @staticmethod def init_sockets(items, shader_graph): # Initialize shader sockets for _socket in shader_graph["sockets"]: if "dependency" in _socket: if _socket["dependency"] not in items: continue if bpy.app.version >= (4, 0, 0): if _socket["source"] == "input" and _socket["name"] not in items[_socket["id"]].inputs: items[_socket["id"]].node_tree.interface.new_socket( _socket["name"], in_out="INPUT", socket_type=_socket["type"]) elif _socket["source"] == "output" and _socket["name"] not in items[_socket["id"]].outputs: items[_socket["id"]].node_tree.interface.new_socket( _socket["name"], in_out="OUTPUT", socket_type=_socket["type"]) else: if _socket["source"] == "input" and _socket["name"] not in items[_socket["id"]].inputs: items[_socket["id"]].node_tree.inputs.new(_socket["type"], _socket["name"]) elif _socket["source"] == "output" and _socket["name"] not in items[_socket["id"]].outputs: items[_socket["id"]].node_tree.outputs.new(_socket["type"], _socket["name"]) @staticmethod def create_dyna_links(dyna_links, sbs_group, graph, data): for _output in graph.outputs: if _output.name not in data["outputs"]: continue if _output.name not in sbs_group.outputs and data["outputs"][_output.name]["enabled"]: # Create links if _output.type == "image": dyna_links.append({ "from": "NODE_GROUP_IN", "to": "tx_{}".format(_output.name), "output": "UV", "input": "Vector", "path": "root/SBSAR" }) dyna_links.append({ "from": "tx_{}".format(_output.name), "to": "NODE_GROUP_OUT", "output": "Color", "input": _output.name, "path": "root/SBSAR" }) elif _output.type == "integer" or _output.type == "float": dyna_links.append({ "from": "val_{}".format(_output.name), "to": "NODE_GROUP_OUT", "output": "Value", "input": _output.name, "path": "root/SBSAR" }) else: dyna_links.append({ "from": "val_{}".format(_output.name), "to": "NODE_GROUP_OUT", "output": "Vector", "input": _output.name, "path": "root/SBSAR" }) # Create sockets if bpy.app.version >= (4, 0, 0): if _output.type == "image": sbs_group.node_tree.interface.new_socket( name=_output.name, in_out="OUTPUT", socket_type="NodeSocketColor") elif _output.type == "integer": sbs_group.node_tree.interface.new_socket( name=_output.name, in_out="OUTPUT", socket_type="NodeSocketInt") elif _output.type == "float": sbs_group.node_tree.interface.new_socket( name=_output.name, in_out="OUTPUT", socket_type="NodeSocketFloat") else: sbs_group.node_tree.interface.new_socket( name=_output.name, in_out="OUTPUT", socket_type="NodeSocketVectorXYZ") else: if _output.type == "image": sbs_group.node_tree.outputs.new("NodeSocketColor", _output.name) elif _output.type == "integer": sbs_group.node_tree.outputs.new("NodeSocketInt", _output.name) elif _output.type == "float": sbs_group.node_tree.outputs.new("NodeSocketFloat", _output.name) else: sbs_group.node_tree.outputs.new("NodeSocketVectorXYZ", _output.name) @staticmethod def init_links(material, items, shader_graph): for _link in shader_graph["links"]: if bpy.app.version >= (4, 0, 0): pass else: if _link["input"] == "Emission Color": _link["input"] = "Emission" if (_link["from"] in items and _link["to"] in items and _link["output"] in items[_link["from"]].outputs and _link["input"] in items[_link["to"]].inputs): if _link["path"] == "root": material.node_tree.links.new( items[_link["from"]].outputs[_link["output"]], items[_link["to"]].inputs[_link["input"]]) else: items[_link["path"].replace("root/", "")].node_tree.links.new( items[_link["from"]].outputs[_link["output"]], items[_link["to"]].inputs[_link["input"]]) @staticmethod def init_dyna_links(material, items, dyna_links): for _link in dyna_links: if (_link["from"] in items and _link["to"] in items and _link["output"] in items[_link["from"]].outputs and _link["input"] in items[_link["to"]].inputs): if _link["path"] == "root": material.node_tree.links.new( items[_link["from"]].outputs[_link["output"]], items[_link["to"]].inputs[_link["input"]]) else: items[_link["path"].replace("root/", "")].node_tree.links.new( items[_link["from"]].outputs[_link["output"]], items[_link["to"]].inputs[_link["input"]]) @staticmethod def remove_unused_nodes(items, shader_graph, mat_nodes): for _node in shader_graph["nodes"]: if "dependency" in _node: for _value in _node["dependency"]: if _value not in items: if _node["path"] == "root": mat_nodes.remove(items[_node["id"]]) else: _node_path = _node["path"].replace("root/", "") items[_node_path].node_tree.nodes.remove(items[_node["id"]]) del items[_node["id"]] break if _node["type"] == "NodeFrame": for _key in _node["children"]: if _key in items: items[_key].parent = items[_node["id"]] items[_node["id"]].update() @staticmethod def init_properties(items, shader_graph, shader_prefs, graph, data, context, new_nodes=None): if new_nodes is None: new_nodes = [] for _item in items.keys(): new_nodes.append(_item) for _property in shader_graph["properties"]: if _property["type"] == "emissive_intensity": if "input" in _property and "emissive" in data["outputs"]: _value = getattr(shader_prefs["inputs"], _property["type"]) if bpy.app.version >= (4, 0, 0): setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) else: setattr(items[_property["id"]].inputs[20], _property["name"], _value) else: if bpy.app.version >= (4, 0, 0): setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], 0.0) else: setattr(items[_property["id"]].inputs[20], _property["name"], 0.0) else: if _property["id"] not in items: continue if _property["id"] not in new_nodes: continue if "dependency" in _property: for _value in _property["dependency"]: if _value not in items: continue if _property["type"] == "string": setattr(items[_property["id"]], _property["name"], _property["value"]) elif _property["type"] == "float": setattr(items[_property["id"]], _property["name"], _property["value"]) elif _property["type"] == "tiling": if "input" in _property: _value = graph.tiling.get() setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) elif _property["type"] == "physical_size": if "input" in _property: _value = graph.physical_size.get() _value = SUBSTANCE_Utils.get_physical_size(_value, context) _value = [1/_value[0], 1/_value[1], 1/_value[0]] setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) elif _property["type"] == "ao_mix": if "input" in _property: _value = getattr(shader_prefs["inputs"], _property["type"]) setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) elif _property["type"] == "disp_midlevel": if "input" in _property: _value = getattr(shader_prefs["inputs"], _property["type"]) setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) elif _property["type"] == "disp_scale": if "input" in _property: _value = getattr(shader_prefs["inputs"], _property["type"]) setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) elif _property["type"] == "disp_physical_scale": if "input" in _property: _value = graph.physical_size.get()[2] setattr(items[_property["id"]].inputs[_property["input"]], _property["name"], _value) elif _property["type"] == "projection_blend": _value = getattr(shader_prefs["inputs"], _property["type"]) setattr(items[_property["id"]], _property["name"], _value) else: setattr(items[_property["id"]], _property["name"], _property["value"]) @staticmethod def clean_sbsar_group(sbs_group, sbs_group_nodes, graph, data): # Remove unused sockets if bpy.app.version >= (4, 0, 0): for _output in sbs_group.node_tree.interface.items_tree: if _output.name not in data["outputs"]: if _output.item_type == 'SOCKET': if _output.in_out == 'OUTPUT': sbs_group.node_tree.interface.remove(_output) else: for _output in sbs_group.node_tree.outputs: if _output.name not in data["outputs"]: sbs_group.node_tree.outputs.remove(_output) # Remove disabled SBSAR nodes for _node in sbs_group_nodes: if (type(_node) is not bpy.types.ShaderNodeTexImage and type(_node) is not bpy.types.ShaderNodeNormalMap and type(_node) is not bpy.types.ShaderNodeValue and type(_node) is not bpy.types.ShaderNodeCombineXYZ): continue _output = _node.name.replace("Tx ", "").replace("Normal ", "").replace("Val ", "") if not graph.outputs[_output].shader_enabled: sbs_group_nodes.remove(_node) @staticmethod def update_images(graph, data): for _output in graph.outputs: if _output.type == "image": if _output.name in data["outputs"]: _filepath = data["outputs"][_output.name]["path"] _img_name = "{}_{}".format(graph.material.name, _output.name) SUBSTANCE_MaterialManager.get_image(_img_name, _filepath) @staticmethod def get_current_nodes(items, new_nodes, mat_nodes, shader_graph, graph): for _node in shader_graph["nodes"]: _name = _node["name"].replace("$matname", graph.material.name) if _node["path"] == "root": _nodes = mat_nodes else: _nodes = items[_node["path"].replace("root/", "")].node_tree.nodes if _name in _nodes: items[_node["id"]] = _nodes[_name] else: new_nodes.append(_node["id"]) items[_node["id"]] = _nodes.new(_node["type"]) items[_node["id"]].name = _name if _node["type"] == "NodeFrame": items[_node["id"]].label = _node["label"] continue items[_node["id"]].location = (_node["location"][0], _node["location"][1]) if _node["type"] == "ShaderNodeGroup": _node_tree = bpy.data.node_groups.new( type='ShaderNodeTree', name=graph.material.name) items[_node["id"]].node_tree = _node_tree @staticmethod def cycles_update(items): for _key in items: if not hasattr(items[_key], "image"): continue _img = items[_key].image items[_key].image = None SUBSTANCE_Threads.timer_thread_run( RENDER_IMG_UPDATE_DELAY_S, SUBSTANCE_MaterialManager.reset_image, (items[_key], _img)) @staticmethod def create_shader_network(context, sbsar, graph, data): try: # Time SUBSTANCE_Threads.cursor_push('WAIT') _time_start = time.time() _addons_prefs, _shader_idx, _shader = SUBSTANCE_Utils.get_selected_shader(context, int(graph.shaders_list)) _shader_file = SUBSTANCE_Utils.get_shader_file(_shader.filename) _shader_graph = SUBSTANCE_Utils.get_json(_shader_file) _shader_prefs = SUBSTANCE_Utils.get_shader_prefs(context, _shader) _material = SUBSTANCE_MaterialManager.get_existing_material(graph.material.name) if _material is None or _shader.name != graph.material.shader: if _material is None: _material = bpy.data.materials.new(name=graph.material.name) if _addons_prefs.auto_apply_material: SUBSTANCE_MaterialManager.auto_apply_material(context, data, _material) SUBSTANCE_MaterialManager.empty_material(_material, graph) _items = SUBSTANCE_MaterialManager.init_nodes(_material, graph, _shader_graph) # Get the substance Node Group _mat_nodes = _material.node_tree.nodes _sbs_group = _material.node_tree.nodes["{}_sbsar".format(graph.material.name)] _sbs_group_nodes = _sbs_group.node_tree.nodes # Initialize Enabled outputs SUBSTANCE_MaterialManager.init_outputs( _items, _sbs_group, _shader_prefs, _shader_graph, _addons_prefs, data, graph) # Initialize Sbsar NodeGroup socket SUBSTANCE_MaterialManager.init_sockets(_items, _shader_graph) # Create sockets & links for enabled outputs that are not part of the shader _dyna_links = [] SUBSTANCE_MaterialManager.create_dyna_links(_dyna_links, _sbs_group, graph, data) # Remove unused Nodes SUBSTANCE_MaterialManager.remove_unused_nodes(_items, _shader_graph, _mat_nodes) # Initialize shader properties SUBSTANCE_MaterialManager.init_properties( _items, _shader_graph, _shader_prefs, graph, data, context) # Initialize links SUBSTANCE_MaterialManager.init_links(_material, _items, _shader_graph) # Initialize dynamic links SUBSTANCE_MaterialManager.init_dyna_links(_material, _items, _dyna_links) graph.material.shader = _shader.name _material.use_fake_user = _addons_prefs.auto_fake_usr _out_message_type = "INFO" _out_message = "Material [{}] create in [{}]seconds" else: # Reload image files and update file format SUBSTANCE_MaterialManager.update_images(graph, data) # Get the substance Node Group _mat_nodes = _material.node_tree.nodes _sbs_group = _material.node_tree.nodes["{}_sbsar".format(graph.material.name)] _sbs_group_nodes = _sbs_group.node_tree.nodes if graph.update_tx_only: _items = {} # Add all SBSAR texture nodes for _node in _sbs_group_nodes: if (type(_node) is not bpy.types.ShaderNodeTexImage): continue _output = _node.name.replace("Tx ", "") _items[_output] = _node _out_message_type = "INFO" _out_message = "Textures [{}] updated in [{}]seconds" else: # Cleanup SBSAR group SUBSTANCE_MaterialManager.clean_sbsar_group(_sbs_group, _sbs_group_nodes, graph, data) # Get all graph items _new_nodes = [] _items = {} SUBSTANCE_MaterialManager.get_current_nodes( _items, _new_nodes, _mat_nodes, _shader_graph, graph) SUBSTANCE_MaterialManager.init_outputs( _items, _sbs_group, _shader_prefs, _shader_graph, _addons_prefs, data, graph, _new_nodes) # Remove unused Nodes SUBSTANCE_MaterialManager.remove_unused_nodes(_items, _shader_graph, _mat_nodes) # Initialize shader properties SUBSTANCE_MaterialManager.init_properties( _items, _shader_graph, _shader_prefs, graph, data, context, _new_nodes) # Initialize Sbsar NodeGroup socket SUBSTANCE_MaterialManager.init_sockets(_items, _shader_graph) # Create sockets & links for enabled outputs that are not part of the shader _dyna_links = [] SUBSTANCE_MaterialManager.create_dyna_links(_dyna_links, _sbs_group, graph, data) # Sort sockets if bpy.app.version >= (4, 0, 0): pass else: _n = len(_sbs_group.node_tree.outputs) for _idx in range(_n): _sorted = True for _jdx in range(_n - _idx - 1): _a_key = _sbs_group.node_tree.outputs[_jdx].name _b_key = _sbs_group.node_tree.outputs[_jdx + 1].name _a_weight = graph.outputs[_a_key].index _b_weight = graph.outputs[_b_key].index if _a_weight > _b_weight: _sbs_group.node_tree.outputs.move(_jdx, _jdx + 1) _sorted = False if _sorted: break # Initialize links SUBSTANCE_MaterialManager.init_links(_material, _items, _shader_graph) # Initialize dynamic links SUBSTANCE_MaterialManager.init_dyna_links(_material, _items, _dyna_links) _out_message_type = "INFO" _out_message = "Material [{}] updated in [{}]seconds" # Hack to autoupdate images in cycles if _addons_prefs.cycles_autoupdate_enabled: SUBSTANCE_MaterialManager.cycles_update(_items) except Exception: _out_message_type = "ERROR" _out_message = "An error ocurred while setting the material [{}]. [{}]seconds" SUBSTANCE_Utils.log_data("ERROR", "Exception - Susbtance material creation error") SUBSTANCE_Utils.log_traceback(traceback.format_exc()) # Time _time_end = time.time() _load_time = round(_time_end - _time_start, 3) SUBSTANCE_Utils.log_data( _out_message_type, _out_message.format(graph.material.name, _load_time), display=True) # REDRAW # _idx = context.scene.sbsar_index # context.scene.sbsar_index = _idx SUBSTANCE_Utils.toggle_redraw(context) SUBSTANCE_Threads.cursor_pop()