def post_load(scene): # get Blender Tools version from last blend file load last_load_bt_ver = _get_scs_globals().last_load_bt_version scs_roots = None """ Applies fixes for v0.6 or less: 1. fixes reflection textures tga's for tobjs as TOBJ load is now supported and unlock that textures 2. calls update on all set textures to correct paths for them 3. tries to fix active shader preset name for materials, because of new flavor system """ if _info_utils.cmp_ver_str(last_load_bt_ver, "0.6") <= 0: print("INFO\t- Applying fixes for version <= 0.6") for material in bpy.data.materials: # ignore materials not related to blender tools if material.scs_props.mat_effect_name == "": continue for tex_type in material.scs_props.get_texture_types().keys(): texture_attr_str = "shader_texture_" + tex_type if texture_attr_str in material.scs_props.keys(): # 1. fix reflection textures if tex_type == "reflection": is_building_ref = material.scs_props[ texture_attr_str].endswith("/bulding_ref.tga") is_generic_s = material.scs_props[ texture_attr_str].endswith( "material/environment/generic_s.tga") is_glass_interior = material.scs_props.active_shader_preset_name == "glass - interior" is_dif_spec_weight_add_env = material.scs_props.active_shader_preset_name == "dif.spec.weight.add.env" is_truckpaint = material.scs_props.active_shader_preset_name.startswith( "truckpaint") # fix paths if is_building_ref: material.scs_props[ texture_attr_str] = material.scs_props[ texture_attr_str][:-4] material.scs_props[texture_attr_str + "_locked"] = False elif is_generic_s: if is_glass_interior: material.scs_props[ texture_attr_str] = "//material/environment/interior_reflection" elif is_dif_spec_weight_add_env: material.scs_props[ texture_attr_str] = "//material/environment/generic_reflection" else: material.scs_props[ texture_attr_str] = "//material/environment/vehicle_reflection" # unlock reflection textures everywhere except on truckpaint shader if not is_truckpaint: material.scs_props[texture_attr_str + "_locked"] = False # acquire roots on demand only once scs_roots = _object_utils.gather_scs_roots( bpy.data.objects) if not scs_roots else scs_roots # propagate reflection texture change on all of the looks. # NOTE: We can afford write through because old BT had all reflection textures locked # meaning user had to use same texture on all looks # NOTE#2: Printouts like: # "Look with ID: X doesn't have entry for material 'X' in SCS Root 'X', # property 'shader_texture_reflection' won't be updated!" # are expected here, because we don't use any safety check, # if material is used on the mesh objects inside scs root for scs_root in scs_roots: _looks.write_through(scs_root, material, texture_attr_str) # 2. trigger update function for path reload and reload of possible missing textures update_func = getattr(material.scs_props, "update_" + texture_attr_str, None) if update_func: update_func(material) # ignore already properly set materials if material.scs_props.active_shader_preset_name in _get_shader_presets_inventory( ): continue # 3. try to recover "active_shader_preset_name" from none flavor times Blender Tools material_textures = {} if "scs_shader_attributes" in material and "textures" in material[ "scs_shader_attributes"]: for texture in material["scs_shader_attributes"][ "textures"].values(): tex_id = texture["Tag"].split(":")[1] tex_value = texture["Value"] material_textures[tex_id] = tex_value (preset_name, preset_section) = _material_utils.find_preset( material.scs_props.mat_effect_name, material_textures) if preset_name: material.scs_props.active_shader_preset_name = preset_name # acquire roots on demand only once scs_roots = _object_utils.gather_scs_roots( bpy.data.objects) if not scs_roots else scs_roots # make sure to fix active preset shader name in all looks # NOTE: Printouts like: # "Look with ID: X doesn't have entry for material 'X' in SCS Root 'X', # property 'active_shader_preset_name' won't be updated!" # are expected here, because we don't use any safety check, # if material is used on the mesh objects inside scs root for scs_root in scs_roots: _looks.write_through(scs_root, material, "active_shader_preset_name") # as last update "last load" Blender Tools version to current _get_scs_globals().last_load_bt_version = get_tools_version()
def _create_scs_root_object(name, loaded_variants, loaded_looks, mats_info, objects, locators, armature): """Creates an 'SCS Root Object' (Empty Object) for currently imported 'SCS Game Object' and parent all import content to it. :param name: :type name: str :param loaded_variants: X :type loaded_variants: list :param loaded_looks: X :type loaded_looks: list :param mats_info: list of material info, one material info consists of list: [ blend_mat_name, mat_effect, original_mat_alias ] :type mats_info: list of list :param objects: X :type objects: list :param locators: X :type locators: list :param armature: Armature Object :type armature: bpy.types.Object :return: SCS Root Object :rtype: bpy.types.Object """ context = bpy.context # MAKE THE 'SCS ROOT OBJECT' NAME UNIQUE name = _name_utils.get_unique(name, bpy.data.objects) # CREATE EMPTY OBJECT & MAKE A PROPER SETTINGS TO THE 'SCS Game Object' OBJECT scs_root_object = bpy.data.objects.new(name, None) bpy.context.view_layer.active_layer_collection.collection.objects.link( scs_root_object) bpy.context.view_layer.objects.active = scs_root_object scs_root_object.scs_props.scs_root_object_export_enabled = True scs_root_object.scs_props.empty_object_type = 'SCS_Root' # print('LOD.pos: %s' % str(scs_root_object.location)) # print('CUR.pos: %s' % str(context.space_data.cursor_location)) # PARENTING bpy.ops.object.select_all(action='DESELECT') if armature: # if armature is present we can specify our game object as animated scs_root_object.scs_props.scs_root_animated = "anim" # print('ARM.pos: %s' % str(armature.location)) armature.select_set(True) armature.scs_props.parent_identity = scs_root_object.name for obj in objects: # print('OBJ.pos: %s' % str(object.location)) obj.select_set(True) obj.scs_props.parent_identity = scs_root_object.name for obj in locators: obj.select_set(True) obj.scs_props.parent_identity = scs_root_object.name bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) bpy.ops.object.parent_clear(type='CLEAR_INVERSE') # LOCATION scs_root_object.location = context.scene.cursor.location # MAKE ONLY 'SCS GAME OBJECT' SELECTED bpy.ops.object.select_all(action='DESELECT') scs_root_object.select_set(True) context.view_layer.objects.active = scs_root_object # MAKE PART RECORD part_inventory = scs_root_object.scs_object_part_inventory parts_dict = _object_utils.collect_parts_on_root(scs_root_object) for part_name in parts_dict: _inventory.add_item(part_inventory, part_name) # MAKE VARIANT RECORD variant_inventory = scs_root_object.scs_object_variant_inventory for variant_i, variant_record in enumerate(loaded_variants): variant_name = variant_record[0] variantparts = variant_record[1] variant = _inventory.add_item(variant_inventory, variant_name) # for every variant create all of the part entries and mark them included properly for part in part_inventory: part = _inventory.add_item(variant.parts, part.name) if part.name in variantparts: part.include = True # cleanup generated terrain points vertex layers by variant for obj in parts_dict[part.name]: if obj.type != "MESH": continue vg_to_delete = [] accepted_vg_nodes = {} for vertex_group in obj.vertex_groups: # ignore any vertex group which isn't from terrain points # (others might come from skinning) if _OP_consts.TerrainPoints.vg_name_prefix not in vertex_group.name: continue # ignore already fixed vertex group if vertex_group.name.startswith( _OP_consts.TerrainPoints.vg_name_prefix): continue # get variant index from first 6 chars -> check PIM importer for more info vg_var_index = int(vertex_group.name[:6]) # ignore other variant vertex groups and add them to delete list if vg_var_index >= 0 and vg_var_index != variant_i: vg_to_delete.append(vertex_group) continue # finally remove variant prefixing name if variant is not defined (-1) # or index matches current one vertex_group.name = vertex_group.name[6:] # log accepted node index for identifying which vertex groups # really have to be deleted accepted_vg_nodes[vertex_group.name[-1]] = 1 # cleanup possible duplicates for the same node # because one object anyway can not have terrain points vertex groups for multiple variants while len(vg_to_delete) > 0 and len(accepted_vg_nodes) > 0: curr_vg = vg_to_delete.pop() # extra caution step where group can be deleted # only if one of vertex groups for this node was accepted if curr_vg.name[-1] in accepted_vg_nodes: obj.vertex_groups.remove(curr_vg) else: part.include = False # MAKE LOOK RECORDS for look_i, look in enumerate(loaded_looks): look_name = look[0] look_mat_settings = look[1] # setup all the materials. NOTE: They should be already created by PIM import. for mat_info in mats_info: mat = bpy.data.materials[mat_info[0]] if mat_info[2] in look_mat_settings: # extract imported shader data material_effect, material_attributes, material_textures, material_section = _get_shader_data( look_mat_settings[mat_info[2]]) # try to find suitable preset (preset_name, preset_section) = _material_utils.find_preset( material_effect, material_textures) # we don't support effect mismatch between looks, even if game works with it, somehow. # most probably imported model is result of third party software, thus let user know and skip look for this material if look_i != 0 and material_effect != mat.scs_props.mat_effect_name: lprint( "E Look %r skipped on material %r, mismatched material effect: %r but should be %r.", (look_name, mat.name, material_effect, mat.scs_props.mat_effect_name)) continue # preset name is found & shader data are compatible with found preset if preset_name and _are_shader_data_compatible( preset_section, material_attributes, material_textures, mat_info[0]): mat.scs_props.active_shader_preset_name = preset_name if preset_section: preset_effect = preset_section.get_prop_value("Effect") mat.scs_props.mat_effect_name = preset_effect if look_i == 0: # apply default shader settings _material_utils.set_shader_data_to_material( mat, preset_section, is_import=True) # reapply settings from material _material_utils.set_shader_data_to_material( mat, material_section, is_import=True, override_back_data=False) lprint("D Using shader preset on material %r.", (mat.name, )) else: print('''NO "preset_section"! (Shouldn't happen!)''') else: # import shader directly from material and mark it as imported mat.scs_props.active_shader_preset_name = "<imported>" _material_utils.set_shader_data_to_material( mat, material_section, is_import=True) lprint( "W Using imported shader on material %r, effect: %r.", (mat.name, mat.scs_props.mat_effect_name)) if look_i == len(loaded_looks) - 1: # delete not needed data on material if "scs_tex_aliases" in mat: del mat["scs_tex_aliases"] # create new look entry on root bpy.ops.object.scs_tools_add_look(look_name=look_name, instant_apply=False) # apply first look after everything is done scs_root_object.scs_props.active_scs_look = 0 # fix scs root children objects count so it won't trigger persistent cycle scs_root_object.scs_cached_num_children = len(scs_root_object.children) return scs_root_object
def execute(self, context): # find group names created by Blender Tools with # dynamic importing of all modules from "internals/shaders" folder # and checking if module has "get_node_group" functions which indicates that # module creates node group groups_to_remove = [ "AddEnvGroup", # from v0.6 "FresnelGroup", # from v0.6 "LampmaskMixerGroup", # from v0.6 "ReflectionNormalGroup", # from v0.6 ] for root, dirs, files in os.walk( _path_utils.get_addon_installation_paths()[0] + os.sep + "internals/shaders"): for file in files: if not file.endswith(".py"): continue module = SourceFileLoader(root + os.sep + file, root + os.sep + file).load_module() if "get_node_group" in dir(module): ng = module.get_node_group() groups_to_remove.append(ng.name) # 1. clear nodes on materials for mat in bpy.data.materials: if not mat.node_tree: continue # also check none blender tools materials just to remove possible nodes usage of our groups if mat.scs_props.active_shader_preset_name == "<none>": nodes_to_remove = [] # check for possible leftover usage of our node groups for node in mat.node_tree.nodes: # filter out nodes which are not node group and node groups without node tree if node.type != "GROUP" or not node.node_tree: continue if node.node_tree.name in groups_to_remove: nodes_to_remove.append(node.node_tree.name) # remove possible leftover used node group nodes for node_name in nodes_to_remove: mat.node_tree.nodes.remove( mat.node_tree.nodes[node_name]) continue mat.node_tree.nodes.clear() # 2. clear nodes on node groups for ng_name in groups_to_remove: if ng_name not in bpy.data.node_groups: continue ng = bpy.data.node_groups[ng_name] ng.nodes.clear() # 3. remove node groups from blender data blocks for ng_name in groups_to_remove: if ng_name not in bpy.data.node_groups: continue bpy.data.node_groups.remove(bpy.data.node_groups[ng_name], do_unlink=True) # 4. finally set preset to material again, which will update nodes and possible input interface changes scs_roots = _object_utils.gather_scs_roots(bpy.data.objects) for mat in bpy.data.materials: # ignore none blender tools materials if mat.scs_props.active_shader_preset_name == "<none>": continue material_textures = {} if "scs_shader_attributes" in mat and "textures" in mat[ "scs_shader_attributes"]: for texture in mat["scs_shader_attributes"][ "textures"].values(): tex_id = texture["Tag"].split(":")[1] tex_value = texture["Value"] material_textures[tex_id] = tex_value (preset_name, preset_section) = _material_utils.find_preset( mat.scs_props.mat_effect_name, material_textures) if preset_section: _material_utils.set_shader_data_to_material( mat, preset_section) # sync shader types on all scs roots by updating looks on them # without this call we might end up with outdated looks raising errors once user will switch to them for scs_root in scs_roots: _looks.update_look_from_material(scs_root, mat, True) return {'FINISHED'}
def _create_scs_root_object(name, loaded_variants, loaded_looks, mats_info, objects, locators, armature): """Creates an 'SCS Root Object' (Empty Object) for currently imported 'SCS Game Object' and parent all import content to it. :param name: :type name: str :param loaded_variants: X :type loaded_variants: list :param loaded_looks: X :type loaded_looks: list :param mats_info: list of material info, one material info consists of list: [ blend_mat_name, mat_effect, original_mat_alias ] :type mats_info: list of list :param objects: X :type objects: list :param locators: X :type locators: list :param armature: Armature Object :type armature: bpy.types.Object :return: SCS Root Object :rtype: bpy.types.Object """ context = bpy.context # MAKE THE 'SCS ROOT OBJECT' NAME UNIQUE name = _name_utils.get_unique(name, bpy.data.objects) # CREATE EMPTY OBJECT bpy.ops.object.empty_add( view_align=False, location=(0.0, 0.0, 0.0), # rotation=rot, ) # , layers=(False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, # False, False)) # MAKE A PROPER SETTINGS TO THE 'SCS Game Object' OBJECT scs_root_object = context.active_object scs_root_object.name = name scs_root_object.scs_props.scs_root_object_export_enabled = True scs_root_object.scs_props.empty_object_type = 'SCS_Root' # print('LOD.pos: %s' % str(scs_root_object.location)) # print('CUR.pos: %s' % str(context.space_data.cursor_location)) # PARENTING if armature: # if armature is present we can specify our game object as animated scs_root_object.scs_props.scs_root_animated = "anim" # print('ARM.pos: %s' % str(armature.location)) bpy.ops.object.select_all(action='DESELECT') armature.select = True bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) armature.scs_props.parent_identity = scs_root_object.name for obj in objects: # print('OBJ.pos: %s' % str(object.location)) bpy.ops.object.select_all(action='DESELECT') obj.select = True bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) obj.scs_props.parent_identity = scs_root_object.name for obj in locators: bpy.ops.object.select_all(action='DESELECT') obj.select = True bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) obj.scs_props.parent_identity = scs_root_object.name # LOCATION scs_root_object.location = context.scene.cursor_location # MAKE ONLY 'SCS GAME OBJECT' SELECTED bpy.ops.object.select_all(action='DESELECT') for obj in bpy.data.objects: obj.select = False scs_root_object.select = True context.scene.objects.active = scs_root_object # MAKE PART RECORD part_inventory = scs_root_object.scs_object_part_inventory parts_dict = _object_utils.collect_parts_on_root(scs_root_object) for part_name in parts_dict: _inventory.add_item(part_inventory, part_name) # MAKE VARIANT RECORD variant_inventory = scs_root_object.scs_object_variant_inventory for variant_i, variant_record in enumerate(loaded_variants): variant_name = variant_record[0] variantparts = variant_record[1] variant = _inventory.add_item(variant_inventory, variant_name) # for every variant create all of the part entries and mark them included properly for part in part_inventory: part = _inventory.add_item(variant.parts, part.name) if part.name in variantparts: part.include = True # cleanup generated terrain points vertex layers by variant for obj in parts_dict[part.name]: if obj.type != "MESH": continue vg_to_delete = [] accepted_vg_nodes = {} for vertex_group in obj.vertex_groups: # ignore any vertex group which isn't from terrain points # (others might come from skinning) if _OP_consts.TerrainPoints.vg_name_prefix not in vertex_group.name: continue # ignore already fixed vertex group if vertex_group.name.startswith(_OP_consts.TerrainPoints.vg_name_prefix): continue # get variant index from first 6 chars -> check PIM importer for more info vg_var_index = int(vertex_group.name[:6]) # ignore other variant vertex groups and add them to delete list if vg_var_index >= 0 and vg_var_index != variant_i: vg_to_delete.append(vertex_group) continue # finally remove variant prefixing name if variant is not defined (-1) # or index matches current one vertex_group.name = vertex_group.name[6:] # log accepted node index for identifying which vertex groups # really have to be deleted accepted_vg_nodes[vertex_group.name[-1]] = 1 # cleanup possible duplicates for the same node # because one object anyway can not have terrain points vertex groups for multiple variants while len(vg_to_delete) > 0 and len(accepted_vg_nodes) > 0: curr_vg = vg_to_delete.pop() # extra caution step where group can be deleted # only if one of vertex groups for this node was accepted if curr_vg.name[-1] in accepted_vg_nodes: obj.vertex_groups.remove(curr_vg) else: part.include = False # MAKE LOOK RECORDS for look_i, look in enumerate(loaded_looks): look_name = look[0] look_mat_settings = look[1] # SETUP ALL THE MATERIALS, NOTE: They should be already created by PIM import. for mat_info in mats_info: mat = bpy.data.materials[mat_info[0]] if mat_info[2] in look_mat_settings: # ASSIGN IMPORTED SHADER DATA material_effect, material_attributes, material_textures, material_section = _get_shader_data(look_mat_settings[mat_info[2]]) # TRY TO FIND SUITABLE PRESET (preset_name, preset_section) = _material_utils.find_preset(material_effect, material_textures) if preset_name: # preset name is found within presets shaders mat.scs_props.active_shader_preset_name = preset_name if preset_section: preset_effect = preset_section.get_prop_value("Effect") mat.scs_props.mat_effect_name = preset_effect if look_i == 0: # apply default shader settings _material_utils.set_shader_data_to_material(mat, preset_section, is_import=True) # reapply settings from material _material_utils.set_shader_data_to_material(mat, material_section, is_import=True, override_back_data=False) lprint("D Using shader preset on material %r.", (mat.name,)) else: print('''NO "preset_section"! (Shouldn't happen!)''') else: # import shader directly from material and mark it as imported mat.scs_props.active_shader_preset_name = "<imported>" _material_utils.set_shader_data_to_material(mat, material_section, is_import=True) lprint("W Using imported shader on material %r, effect: %r.", (mat.name, mat.scs_props.mat_effect_name)) if look_i == len(loaded_looks) - 1: # delete not needed data on material if "scs_tex_aliases" in mat: del mat["scs_tex_aliases"] # CREATE NEW LOOK ENTRY ON ROOT bpy.ops.object.add_scs_look(look_name=look_name, instant_apply=False) # apply first look after everything is done scs_root_object.scs_props.active_scs_look = 0 # fix scs root children objects count so it won't trigger persistent cycle scs_root_object.scs_cached_num_children = len(scs_root_object.children) return scs_root_object
def post_load(scene): # get Blender Tools version from last blend file load last_load_bt_ver = _get_scs_globals().last_load_bt_version scs_roots = None """ Applies fixes for v0.6 or less: 1. fixes reflection textures tga's for tobjs as TOBJ load is now supported and unlock that textures 2. calls update on all set textures to correct paths for them 3. tries to fix active shader preset name for materials, because of new flavor system """ if _info_utils.cmp_ver_str(last_load_bt_ver, "0.6") <= 0: print("INFO\t- Applying fixes for version <= 0.6") for material in bpy.data.materials: # ignore materials not related to blender tools if material.scs_props.mat_effect_name == "": continue for tex_type in material.scs_props.get_texture_types().keys(): texture_attr_str = "shader_texture_" + tex_type if texture_attr_str in material.scs_props.keys(): # 1. fix reflection textures if tex_type == "reflection": is_building_ref = material.scs_props[texture_attr_str].endswith("/bulding_ref.tga") is_generic_s = material.scs_props[texture_attr_str].endswith("material/environment/generic_s.tga") is_glass_interior = material.scs_props.active_shader_preset_name == "glass - interior" is_dif_spec_weight_add_env = material.scs_props.active_shader_preset_name == "dif.spec.weight.add.env" is_truckpaint = material.scs_props.active_shader_preset_name.startswith("truckpaint") # fix paths if is_building_ref: material.scs_props[texture_attr_str] = material.scs_props[texture_attr_str][:-4] material.scs_props[texture_attr_str + "_locked"] = False elif is_generic_s: if is_glass_interior: material.scs_props[texture_attr_str] = "//material/environment/interior_reflection" elif is_dif_spec_weight_add_env: material.scs_props[texture_attr_str] = "//material/environment/generic_reflection" else: material.scs_props[texture_attr_str] = "//material/environment/vehicle_reflection" # unlock reflection textures everywhere except on truckpaint shader if not is_truckpaint: material.scs_props[texture_attr_str + "_locked"] = False # acquire roots on demand only once scs_roots = _object_utils.gather_scs_roots(bpy.data.objects) if not scs_roots else scs_roots # propagate reflection texture change on all of the looks. # NOTE: We can afford write through because old BT had all reflection textures locked # meaning user had to use same texture on all looks # NOTE#2: Printouts like: # "Look with ID: X doesn't have entry for material 'X' in SCS Root 'X', # property 'shader_texture_reflection' won't be updated!" # are expected here, because we don't use any safety check, # if material is used on the mesh objects inside scs root for scs_root in scs_roots: _looks.write_through(scs_root, material, texture_attr_str) # 2. trigger update function for path reload and reload of possible missing textures update_func = getattr(material.scs_props, "update_" + texture_attr_str, None) if update_func: update_func(material) # ignore already properly set materials if material.scs_props.active_shader_preset_name in _get_shader_presets_inventory(): continue # 3. try to recover "active_shader_preset_name" from none flavor times Blender Tools material_textures = {} if "scs_shader_attributes" in material and "textures" in material["scs_shader_attributes"]: for texture in material["scs_shader_attributes"]["textures"].values(): tex_id = texture["Tag"].split(":")[1] tex_value = texture["Value"] material_textures[tex_id] = tex_value (preset_name, preset_section) = _material_utils.find_preset(material.scs_props.mat_effect_name, material_textures) if preset_name: material.scs_props.active_shader_preset_name = preset_name # acquire roots on demand only once scs_roots = _object_utils.gather_scs_roots(bpy.data.objects) if not scs_roots else scs_roots # make sure to fix active preset shader name in all looks # NOTE: Printouts like: # "Look with ID: X doesn't have entry for material 'X' in SCS Root 'X', # property 'active_shader_preset_name' won't be updated!" # are expected here, because we don't use any safety check, # if material is used on the mesh objects inside scs root for scs_root in scs_roots: _looks.write_through(scs_root, material, "active_shader_preset_name") # as last update "last load" Blender Tools version to current _get_scs_globals().last_load_bt_version = get_tools_version()
def execute(self, context): # find group names created by Blender Tools with # dynamic importing of all modules from "internals/shaders" folder # and checking if module has "get_node_group" functions which indicates that # module creates node group groups_to_remove = [ "AddEnvGroup", # from v0.6 "FresnelGroup", # from v0.6 "LampmaskMixerGroup", # from v0.6 "ReflectionNormalGroup", # from v0.6 ] for root, dirs, files in os.walk(_path_utils.get_addon_installation_paths()[0] + os.sep + "internals/shaders"): for file in files: if not file.endswith(".py"): continue module = SourceFileLoader(root + os.sep + file, root + os.sep + file).load_module() if "get_node_group" in dir(module): ng = module.get_node_group() groups_to_remove.append(ng.name) # 1. clear nodes on materials for mat in bpy.data.materials: if not mat.node_tree: continue if mat.scs_props.active_shader_preset_name == "<none>": nodes_to_remove = [] # check for possible leftover usage of our node groups for node in mat.node_tree.nodes: # filter out nodes which are not node group and node groups without node tree if node.type != "GROUP" or not node.node_tree: continue if node.node_tree.name in groups_to_remove: nodes_to_remove.append(node.node_tree.name) # remove possible leftover used node group nodes for node_name in nodes_to_remove: mat.node_tree.nodes.remove(mat.node_tree.nodes[node_name]) continue mat.node_tree.nodes.clear() # 2. clear nodes on node groups for ng_name in groups_to_remove: if ng_name not in bpy.data.node_groups: continue ng = bpy.data.node_groups[ng_name] ng.nodes.clear() # 3. remove node groups from blender data blocks for ng_name in groups_to_remove: if ng_name not in bpy.data.node_groups: continue bpy.data.node_groups.remove(bpy.data.node_groups[ng_name]) # 4. finally set preset to material again, which will update nodes and possible input interface changes for mat in bpy.data.materials: if mat.scs_props.active_shader_preset_name == "<none>": continue material_textures = {} if "scs_shader_attributes" in mat and "textures" in mat["scs_shader_attributes"]: for texture in mat["scs_shader_attributes"]["textures"].values(): tex_id = texture["Tag"].split(":")[1] tex_value = texture["Value"] material_textures[tex_id] = tex_value (preset_name, preset_section) = _material_utils.find_preset(mat.scs_props.mat_effect_name, material_textures) if preset_section: _material_utils.set_shader_data_to_material(mat, preset_section) return {'FINISHED'}