diff --git a/Blender/L1960_Tools/_Release/L1960_Tools_1_8_1.zip b/Blender/L1960_Tools/_Release/L1960_Tools_1_8_1.zip new file mode 100644 index 0000000..f794bd5 Binary files /dev/null and b/Blender/L1960_Tools/_Release/L1960_Tools_1_8_1.zip differ diff --git a/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/ColorPalette_01.png b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/ColorPalette_01.png new file mode 100644 index 0000000..d2ac185 Binary files /dev/null and b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/ColorPalette_01.png differ diff --git a/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/ColorPalette_02.png b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/ColorPalette_02.png new file mode 100644 index 0000000..68a926b Binary files /dev/null and b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/ColorPalette_02.png differ diff --git a/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/CubeProjection.py b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/CubeProjection.py new file mode 100644 index 0000000..c586c48 --- /dev/null +++ b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/CubeProjection.py @@ -0,0 +1,94 @@ +import bpy +from math import pi + +L1960_Arr_PlainData = [ + ["Proj_Empty_01", (0, 0, 2), (0, 0, 0)], + ["Proj_Empty_02", (2, 0, 0), ((pi * 90 / 180), 0, (pi * 90 / 180))], + ["Proj_Empty_03", (0, 2, 0), ((pi * 90 / 180), 0, (pi * 180 / 180))], + ["Proj_Empty_04", (0, 0, -2), ((pi * 180 / 180), 0, 0)], + ["Proj_Empty_05", (-2, 0, 0), ((pi * 90 / 180), 0, (pi * -90 / 180))], + ["Proj_Empty_06", (0, -2, 0), ((pi * 90 / 180), 0, 0)] +] + +class MESH_OT_add_auto_cube_projection(bpy.types.Operator): + """Check Empty´s for projecting, if not existing create new one´s""" + + bl_idname = "mesh.add_auto_cube_projection" + bl_label = "Add Plain_Axes to scene" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + + scene = bpy.context.scene + oldSelection = bpy.context.selected_objects + oldActive = bpy.context.active_object + + for EmptyData in L1960_Arr_PlainData: + + EmptyName = EmptyData[0] + EmptyLocation = EmptyData[1] + EmptyRotation = EmptyData[2] + + if not scene.objects.get(EmptyName): + bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=EmptyLocation, scale=(1, 1, 1), rotation=EmptyRotation) + empty = bpy.context.active_object + empty.name = EmptyName + empty.hide_select = True + empty.hide_set(True) + + #Change back to old selection and select old active + for obj in oldSelection: + obj.select_set(True) + bpy.context.view_layer.objects.active = oldActive + + self.report({'INFO'}, 'Added/Fixed Emptys for Projection to Scene') + return {"FINISHED"} + +class MESH_OT_add_modifier_to_mesh(bpy.types.Operator): + """Add Modifier to selected Mesh´s and prepare UV-Maps""" + + bl_idname = "mesh.add_modifier_to_mesh" + bl_label = "Add Modifier to selected Mesh" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + + newModifierName = "CubeTexModifier" + Arr_obj = bpy.context.selected_objects + if len(Arr_obj) < 1: + self.report({'WARNING'}, 'Select a mesh to add the UVProject-Modifier') + return {"CANCELLED"} + + empty_objs = [emp_obj for emp_obj in bpy.context.scene.objects if emp_obj.name.startswith("Proj_Empty")] + if len(empty_objs) < 6: + self.report({'WARNING'}, 'Create/Recreate projectors, they need to be set up first!') + return {"CANCELLED"}; + + for obj in Arr_obj: + + if obj.data.uv_layers.get("UVMap"): + obj.data.uv_layers['UVMap'].name = 'UVMap0' + + if not obj.data.uv_layers.get("UVMap0"): + obj.data.uv_layers.new(name = 'UVMap0') + + if not obj.data.uv_layers.get("UVMap1"): + obj.data.uv_layers.new(name = 'UVMap1') + + + if obj.type == "MESH" and newModifierName not in obj.modifiers: + obj.modifiers.new(type='UV_PROJECT', name=newModifierName) + mod = obj.modifiers[newModifierName] + mod.uv_layer = "UVMap1" + mod.projector_count = 6 + + i = 0 + for p in mod.projectors: + p.object = bpy.data.objects[L1960_Arr_PlainData[i][0]] + i = i+1 + else: + self.report({'INFO'}, 'UVProject-Modifier allready set') + return {"FINISHED"} + + self.report({'INFO'}, 'Added UVProject-Modifier to mesh') + return {"FINISHED"} \ No newline at end of file diff --git a/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/Helper.py b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/Helper.py new file mode 100644 index 0000000..bdcfc6c --- /dev/null +++ b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/Helper.py @@ -0,0 +1,174 @@ +import bpy +import re + +### FIX MATERIALS ### + +class MESH_OT_fix_material_names(bpy.types.Operator): + """Fixes the material naming, if duplicated are present e.g. Material.001, Material.002 ...""" + + bl_idname = "mesh.fix_material_names" + bl_label = "Fixes the material naming, if duplicated are present" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + #Remove all duplicated materials + self.merge_duplicated_materials() + + #Merge material slots for every mesh in the scene + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + self.merge_material_slots(obj) + + + self.report({'INFO'}, 'All duplicated Materials fixed') + return {"FINISHED"} + + def merge_duplicated_materials(self): + for material in bpy.data.materials: + #Remove MI for Dekogon material names + if material.name.split('_')[0] == 'MI': + material.name = material.name[3:] + #Check for .001 at end and remove + if material.name[-3:].isnumeric(): + opti_matName = material.name[:-4] + if bpy.data.materials.get(opti_matName): #check if og_mat exists + material.user_remap(bpy.data.materials.get(opti_matName)) + print("Removed Material: " + material.name) + bpy.data.materials.remove(material) + else: + material.name = opti_matName + + def merge_material_slots(self, obj): + duplicated_material_list = [] + + #create list with indexes of material slots with the same name and merge them + for og_slot in obj.material_slots: + for slot in obj.material_slots: + if slot.name == og_slot.name: + if slot.slot_index == og_slot.slot_index: + continue + if og_slot.slot_index in duplicated_material_list: + continue + duplicated_material_list.append(int(slot.slot_index)) + + #delete all material slots within list + for slot_index in sorted(duplicated_material_list, reverse=True): + obj.data.materials.pop(index = slot_index) + + +### GROUP OBJECTS TO COLLECTIONS ### + +class MESH_OT_group_objects_in_collections(bpy.types.Operator): + """Groups objects by name into seperate collections, usefull for Unreal Decogon import""" + + bl_idname = "mesh.group_objects_in_collections" + bl_label = "Groups objects by name into seperate collections, usefull for Unreal Decogon import" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + # Erstellen einer leeren Dictionary zur Speicherung der Collections nach Basisnamen + collection_dict = {} + + # Durchlaufen aller Objekte in der Szene + for obj in bpy.context.scene.objects: + # Überprüfen, ob das Objekt ein leeres Elternobjekt ist + if obj.type == 'EMPTY': + # Anwenden der Regex auf den Objektnamen + match = re.match(r'^(.*?)_(.*?)_(\d+)([a-zA-Z]+)$', obj.name) + if match: + prefix = match.group(1) + name = match.group(2) + number = match.group(3) + letter = match.group(4) + + # Extrahieren des Basisnamens für die Collection + base_name = f"L1960_{name}" + + # Hinzufügen des Objekts zur entsprechenden Liste in der Dictionary + if base_name not in collection_dict: + collection_dict[base_name] = {} + if number not in collection_dict[base_name]: + collection_dict[base_name][number] = {} + if letter not in collection_dict[base_name][number]: + collection_dict[base_name][number][letter] = {'empty': obj, 'children': []} + + # Durchlaufen der Dictionary und Erstellen der Collections + for base_name, number_dict in collection_dict.items(): + # Erstellen einer neuen Collection für den Basisnamen + base_collection = bpy.data.collections.new(base_name) + bpy.context.scene.collection.children.link(base_collection) + + # Durchlaufen der sortierten Liste der Objekte und Verschieben in die Collections + for number, letter_dict in number_dict.items(): + # Erstellen einer Collection für die Nummer und Verschieben des leeren Elternobjekts dorthin + number_collection = bpy.data.collections.new(f"{base_name}_{number}") + base_collection.children.link(number_collection) + + # Durchlaufen der Buchstaben und Erstellen der Buchstaben-Collection unter der Nummer + for letter, obj_data in letter_dict.items(): + empty = obj_data['empty'] + children = empty.children + + letter_collection = bpy.data.collections.new(f"{base_name}_{number}{letter}") + number_collection.children.link(letter_collection) + + # Verschieben des leeren Elternobjekts in die entsprechende Collection + letter_collection.objects.link(empty) + + # Verschieben der Kinder des leeren Elternobjekts in die entsprechende Collection + for child in children: + letter_collection.objects.link(child) + + # Entfernen des leeren Elternobjekts und seiner Kinder aus der Szene + bpy.context.collection.objects.unlink(empty) + for child in children: + bpy.context.collection.objects.unlink(child) + + self.report({'INFO'}, 'All objects sorted') + return {"FINISHED"} + +### FIX NAMING CONVENTIONS ### + +class MESH_OT_fix_naming_conventions(bpy.types.Operator): + """Changes . to _ to solve naming issues with workbench""" + + bl_idname = "mesh.fix_naming_convention" + bl_label = "Changes . to _ to solve naming issues with workbench" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for obj in bpy.data.objects: + obj.name = obj.name.replace(".","_") + + self.report({'INFO'}, 'Fixed Naming') + return {"FINISHED"} + +class MESH_OT_generate_empty_for_mesh(bpy.types.Operator): + """Generates a Empty with the objects name at it´s position""" + + bl_idname = "mesh.generate_empty_for_mesh" + bl_label = "Generates a Empty with the objects name at it´s position" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + scene = bpy.context.scene + Selection = bpy.context.selected_objects + FilteredSelection = [obj for obj in Selection if obj.type != 'EMPTY'] + ActiveObj = bpy.context.active_object + + for obj in FilteredSelection: + oldEmpty = scene.objects.get("Socket_" + obj.name) + if oldEmpty: + oldEmpty.location = obj.location + oldEmpty.rotation_euler = obj.rotation_euler + else: + obj.select_set(True) + bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=obj.location, scale=(1, 1, 1), rotation=obj.rotation_euler) + empty = bpy.context.active_object + empty.name = "Socket_" + obj.name + + #Change back to selection and select old active + bpy.ops.object.select_all(action='DESELECT') + + self.report({'INFO'}, 'Generated Emptys for selectes Meshes in Scene') + return {"FINISHED"} diff --git a/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/PrepareLods.py b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/PrepareLods.py new file mode 100644 index 0000000..911f43d --- /dev/null +++ b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/PrepareLods.py @@ -0,0 +1,128 @@ +import bpy +import os + +### GENRATE MLOD ### + +plugin_dir = bpy.utils.user_resource('SCRIPTS') +plugin_path = "addons\L1960Tools" + +L1960_path = os.path.join(plugin_dir, plugin_path) +colorpalettes = [ + "ColorPalette_01.png", + "ColorPalette_02.png" +] + +enum_palettes = [] +for file in colorpalettes: + enum_palettes.append((file, file[:-4], "Select " + file[:-4] + " for MLOD")) + +class EnumColorPalettes(bpy.types.PropertyGroup): + mlod_enum_selection: bpy.props.EnumProperty( + name="Color Palettes for MLOD", + items=enum_palettes, + description="Choose a palette", + default=0 + ) + +class MESH_OT_set_up_mlod(bpy.types.Operator): + """Set´s up a material to be used for MLOD´s""" + + bl_idname = "mesh.set_up_mlod" + bl_label = "Set´s up a material to be used for MLOD´s" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + + #Load Color Palettes + self.import_palettes_textures() + + #Selected Mesh + obj = bpy.context.active_object + + if obj not in bpy.context.selected_objects or obj.type != "MESH": + self.report({'WARNING'}, 'Select a Mesh to continue') + return {"CANCELLED"} + + arr_layers = obj.data.uv_layers + if not arr_layers.get("MLOD") or len(arr_layers) > 1: + for uv_layer in reversed(arr_layers): + arr_layers.remove(uv_layer) + arr_layers.new(name = 'MLOD') + + + texture_filepath = os.path.join(L1960_path, colorpalettes[1]) + + if not len(obj.data.materials) == 0: + obj.data.materials.clear() + + palette_enum_selection = context.scene.color_palettes.mlod_enum_selection + mlod_material_name = "MLOD_" + palette_enum_selection[:-4] + + if not bpy.data.materials.get(mlod_material_name): + material = bpy.data.materials.new(name=mlod_material_name) + material.use_nodes = True + bsdf_node = material.node_tree.nodes["Principled BSDF"] + texImage = material.node_tree.nodes.new('ShaderNodeTexImage') + texImage.image = bpy.data.images.get(palette_enum_selection) + + material.node_tree.links.new(bsdf_node.inputs['Base Color'], texImage.outputs['Color']) + + obj.data.materials.append(bpy.data.materials.get(mlod_material_name)) + + + self.report({'INFO'}, 'Mesh configured like MLOD') + return {"FINISHED"} + + def import_palettes_textures(self): + for image_name in colorpalettes: + texture_name = image_name.split(".")[0] + if texture_name not in bpy.data.textures: + texture = bpy.data.textures.new(name=texture_name, type='IMAGE') + else: + texture = bpy.data.textures.get(texture_name) + if image_name not in bpy.data.images: + image = bpy.data.images.load(os.path.join(L1960_path, image_name)) + else: + image = bpy.data.images.get(image_name) + texture.image = image + +### PREPARE LODS ### + +class MESH_OT_prepare_lods_decimate(bpy.types.Operator): + """Copy current Mesh and apply decimate Modifier""" + + bl_idname = "mesh.prepare_lods_decimate" + bl_label = "Copy current Mesh and apply decimate Modifier" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + + #Selected Mesh + obj = bpy.context.active_object + + if obj not in bpy.context.selected_objects or obj.type != "MESH": + self.report({'WARNING'}, 'Select a Mesh to continue') + return {"CANCELLED"} + + if not obj.name[:-1].endswith('LOD'): + obj.name = obj.name + '_LOD0' + + LODnumber = context.scene.lod_slider #Get from Slider + startLODcount = int(obj.name[-1]) + endLODcount = startLODcount + LODnumber + + for i in range (startLODcount + 1, endLODcount): + new_obj = obj.copy() + new_obj.data = obj.data.copy() + new_obj.name = obj.name[:-1] + str(i) + bpy.context.collection.objects.link(new_obj) + + for t in range (startLODcount, i): + newModifierName = 'LOD_Decimate_' + str(t) + new_obj.modifiers.new(type='DECIMATE', name=newModifierName) + mod = new_obj.modifiers[newModifierName] + mod.ratio = 0.49 + mod.use_collapse_triangulate = True + + self.report({'INFO'}, 'LOD´s created') + return {"FINISHED"} \ No newline at end of file diff --git a/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/__init__.py b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/__init__.py new file mode 100644 index 0000000..abc6aaf --- /dev/null +++ b/Blender/L1960_Tools/_Source/L1960_Tools_1_8_1/__init__.py @@ -0,0 +1,122 @@ +bl_info = { + "name": "L1960 Tools", + "author": "Prodeath21", + "version": (1, 8, 1), + "blender": (2, 80, 0), + "location": "3D Viewport > Sidebar > 1960Life category", + "description": "Set´s up the Projection-Modifier automatically and add´s in the Emptys if not allready created.", + "category": "Object", +} + +# ---------------------------------------------- +# Import modules +# ---------------------------------------------- +if "bpy" in locals(): + import imp + imp.reload(CubeProjection) + imp.reload(Helper) + imp.reload(PrepareLods) + print("L1960 Tools: Reloaded multifiles") +else: + from . import CubeProjection + from . import Helper + from . import PrepareLods + +import bpy + +from . CubeProjection import MESH_OT_add_auto_cube_projection, MESH_OT_add_modifier_to_mesh +from . Helper import MESH_OT_fix_material_names, MESH_OT_group_objects_in_collections, MESH_OT_fix_naming_conventions, MESH_OT_generate_empty_for_mesh +from . PrepareLods import MESH_OT_prepare_lods_decimate, MESH_OT_set_up_mlod, EnumColorPalettes + +class L1960_PT_tools(bpy.types.Panel): + pass + #where to add the panel + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + + #add labels + bl_label = "1960-Life Tools" + bl_category = "1960-Life" + + def draw(self, context): + """define the layout of the panel""" + box = self.layout.box() + # UV-Project helper + box.label(text="UV-Projection") + row = box.row() + row.operator("mesh.add_auto_cube_projection", text="Set up Projectors") + row = box.row() + row.operator("mesh.add_modifier_to_mesh", text="Add Modifier to Mesh") + self.layout.separator() + # Helpers + box = self.layout.box() + box.label(text="Various Helper") + row = box.row() + row.operator("mesh.fix_material_names", text="Fix Material Name´s") + row = box.row() + row.operator("mesh.group_objects_in_collections", text="Group to Collections") + row = box.row() + row.operator("mesh.fix_naming_convention", text="Fix Naming Convention") + row = box.row() + row.operator("mesh.generate_empty_for_mesh", text="Generate Empty") + self.layout.separator() + # Generate LODs + box = self.layout.box() + box.label(text="LOD´s") + row = box.row() + row.operator("mesh.prepare_lods_decimate", text="Create LOD´s") + box.prop(context.scene, "lod_slider", text="Amount") + box = self.layout.box() + box.prop(context.scene.color_palettes, "mlod_enum_selection", expand=True) + row = box.row() + row.operator("mesh.set_up_mlod", text="Set up MLOD") + self.layout.separator() + ############################### + # Enfusion Blender Tools Linked + box = self.layout.box() + box.label(text="EBT Linked") + row = box.row() + row.operator("view3d.ebt_sort", text="Sort Objects") + # colliders setup is allowed in both OBJECT and EDIT mode + row = box.row() + row.operator("view3d.ebt_colliders_setup", text=" Colliders Setup") + row = box.row() + # Light Setup + row.operator("view3d.ebt_setup_light", text=" Light Setup") + row = box.row() + col = row.column(align=True) + # Update Materials + col.operator("scene.ebt_update_enf_materials",) + row = box.row() + col = row.column(align=True) + + +#register the panel with blender +modules = [ L1960_PT_tools, + MESH_OT_add_auto_cube_projection, + MESH_OT_add_modifier_to_mesh, + MESH_OT_fix_material_names, + MESH_OT_group_objects_in_collections, + MESH_OT_fix_naming_conventions, + MESH_OT_generate_empty_for_mesh, + MESH_OT_prepare_lods_decimate, + MESH_OT_set_up_mlod, + EnumColorPalettes +] + +def register(): + for mod in modules: + bpy.utils.register_class(mod) + + bpy.types.Scene.lod_slider = bpy.props.IntProperty(name="LOD Number", default=3, min=0, max=10) + bpy.types.Scene.color_palettes = bpy.props.PointerProperty(type=EnumColorPalettes) + +def unregister(): + for mod in modules: + bpy.utils.unregister_class(mod) + + del bpy.types.Scene.lod_slider + del bpy.types.Scene.color_palettes + +if __name__== "__main__": + register() \ No newline at end of file