Compare commits
3 Commits
1.1
...
78c1b78488
| Author | SHA1 | Date | |
|---|---|---|---|
| 78c1b78488 | |||
| c68d02dc50 | |||
|
|
bd885f1b99 |
Binary file not shown.
BIN
Blender/L1960_Tools/_Release/L1960_Tools_1_8_3.zip
Normal file
BIN
Blender/L1960_Tools/_Release/L1960_Tools_1_8_3.zip
Normal file
Binary file not shown.
@@ -0,0 +1,333 @@
|
|||||||
|
import bpy
|
||||||
|
import math
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
### GENRATE MLOD ###
|
||||||
|
|
||||||
|
plugin_dir = bpy.utils.user_resource('SCRIPTS')
|
||||||
|
plugin_path = "addons\L1960Tools"
|
||||||
|
L1960_path = os.path.join(plugin_dir, plugin_path)
|
||||||
|
|
||||||
|
colorpalettes = [
|
||||||
|
json.dumps(["ColorPalette_01.png", "BF1BC94A0DE4398F"]),
|
||||||
|
json.dumps(["ColorPalette_02.png", "A95B2794A0EE8340"])
|
||||||
|
]
|
||||||
|
|
||||||
|
enum_palettes = []
|
||||||
|
for palettes in colorpalettes:
|
||||||
|
file = json.loads(palettes)[0]
|
||||||
|
enum_palettes.append((palettes, 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')
|
||||||
|
|
||||||
|
if not len(obj.data.materials) == 0:
|
||||||
|
obj.data.materials.clear()
|
||||||
|
|
||||||
|
palette_enum_selection = json.loads(context.scene.color_palettes.mlod_enum_selection)
|
||||||
|
mlod_material_name = "MLOD_" + palette_enum_selection[0][:-4] + "_" + palette_enum_selection[1]
|
||||||
|
|
||||||
|
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[0])
|
||||||
|
|
||||||
|
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:
|
||||||
|
image_name = json.loads(image_name)[0]
|
||||||
|
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
|
||||||
|
|
||||||
|
# texture_filepath = os.path.join(L1960_path, colorpalettes[1])
|
||||||
|
|
||||||
|
### Palette MLOD ###
|
||||||
|
# class MESH_OT_bake_basic_mlod(bpy.types.Operator):
|
||||||
|
# """Set´s up a basic MLOD via Bake"""
|
||||||
|
|
||||||
|
# bl_idname = "mesh.bake_basic_mlod"
|
||||||
|
# bl_label = "Set´s up a basic MLOD via Bake"
|
||||||
|
# bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
# def execute(self, context):
|
||||||
|
# obj = context.active_object
|
||||||
|
|
||||||
|
# #Palette selection
|
||||||
|
# palette_enum_selection = json.loads(context.scene.color_palettes.mlod_enum_selection)
|
||||||
|
|
||||||
|
# if obj not in bpy.context.selected_objects or obj.type != "MESH":
|
||||||
|
# self.report({'WARNING'}, 'Select a Mesh to continue')
|
||||||
|
# return {"CANCELLED"}
|
||||||
|
|
||||||
|
# faces_with_color = self.get_faces_with_color(obj)
|
||||||
|
|
||||||
|
# grouped_colors = self.groupe_colors(faces_with_color, threshold=50)
|
||||||
|
|
||||||
|
# matched_colors = self.find_matching_color(grouped_colors, palette_enum_selection[0][:-4])
|
||||||
|
|
||||||
|
# self.adjust_UV(obj, matched_colors)
|
||||||
|
|
||||||
|
# #Entfernen des referenz Bake Image
|
||||||
|
# #bpy.data.images.remove(bpy.data.images["MLOD_Temp"])
|
||||||
|
|
||||||
|
# # Entfernen aller Meterials
|
||||||
|
# if not len(obj.data.materials) == 0:
|
||||||
|
# obj.data.materials.clear()
|
||||||
|
|
||||||
|
# # Create new Material with selected Palette
|
||||||
|
# mlod_material_name = "MLOD_" + palette_enum_selection[0][:-4] + palette_enum_selection[1]
|
||||||
|
|
||||||
|
# texture_filepath = os.path.join(L1960_path, palette_enum_selection[0])
|
||||||
|
# image = bpy.data.images.load(texture_filepath)
|
||||||
|
|
||||||
|
# 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 = image
|
||||||
|
# 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'}, f'MLOD sucessfully created with {palette_enum_selection[0][:-4]}.')
|
||||||
|
# return {"FINISHED"}
|
||||||
|
|
||||||
|
# def get_faces_with_color(self, obj):
|
||||||
|
|
||||||
|
# #Bake Image
|
||||||
|
# image = self.bake_diffuse(obj)
|
||||||
|
|
||||||
|
# mesh = obj.data
|
||||||
|
# uv_layer = mesh.uv_layers.active.data # direct UV access
|
||||||
|
|
||||||
|
# colors = []
|
||||||
|
|
||||||
|
# for poly in mesh.polygons: # same as "face" in BMesh
|
||||||
|
# # Durchschnittliche UV-Koordinaten berechnen
|
||||||
|
# uv = Vector((0.0, 0.0))
|
||||||
|
# for loop_index in poly.loop_indices:
|
||||||
|
# uv += uv_layer[loop_index].uv
|
||||||
|
# uv /= len(poly.loop_indices)
|
||||||
|
|
||||||
|
# # UV → Pixel-Koordinaten (Bildgröße)
|
||||||
|
# x = int(uv.x * (image.size[0] - 1))
|
||||||
|
# y = int(uv.y * (image.size[1] - 1))
|
||||||
|
|
||||||
|
# # RGBA Pixel holen
|
||||||
|
# index = (y * image.size[0] + x) * 4
|
||||||
|
# pixel = image.pixels[index:index+4]
|
||||||
|
|
||||||
|
# # In 0–255 Werte umwandeln
|
||||||
|
# rgb_255 = [round(c * 255) for c in pixel][:3]
|
||||||
|
|
||||||
|
# colors.append({
|
||||||
|
# "face": poly.index,
|
||||||
|
# "rgb": rgb_255
|
||||||
|
# })
|
||||||
|
|
||||||
|
# # print(colors[-1]) # -> [{'face': <BMFace(0x000002B417086A30), index=0, totverts=4>, 'rgb': [0, 0, 0]}]
|
||||||
|
|
||||||
|
# return colors
|
||||||
|
|
||||||
|
# def groupe_colors(self, color_list, threshold = 10):
|
||||||
|
# groups = []
|
||||||
|
|
||||||
|
# for color in color_list:
|
||||||
|
# found_group = False
|
||||||
|
# for group in groups:
|
||||||
|
# # Compute average color of the group
|
||||||
|
# avg_color = [sum(c["rgb"][i] for c in group)/len(group) for i in range(3)]
|
||||||
|
# if self.color_distance(color["rgb"], avg_color) <= threshold:
|
||||||
|
# group.append(color)
|
||||||
|
# found_group = True
|
||||||
|
# break
|
||||||
|
# if not found_group:
|
||||||
|
# groups.append([color])
|
||||||
|
|
||||||
|
# # Compute the average color for each group
|
||||||
|
# grouped_colors = [{
|
||||||
|
# "face": [c["face"] for c in group],
|
||||||
|
# "rgb": [round(sum(c["rgb"][i] for c in group) / len(group)) for i in range(3)]
|
||||||
|
# }
|
||||||
|
# for group in groups
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# return grouped_colors
|
||||||
|
|
||||||
|
# def color_distance(self, c1, c2):
|
||||||
|
# return math.sqrt(
|
||||||
|
# (c1[0]-c2[0])**2 +
|
||||||
|
# (c1[1]-c2[1])**2 +
|
||||||
|
# (c1[2]-c2[2])**2
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def read_color_csv(self, palette):
|
||||||
|
# colors = []
|
||||||
|
# try:
|
||||||
|
# with open(f"{palette}.csv", newline="") as datei:
|
||||||
|
# reader = csv.DictReader(datei)
|
||||||
|
# for row in reader:
|
||||||
|
# colors.append({
|
||||||
|
# "rgb": [int(row["r"]), int(row["g"]), int(row["b"])],
|
||||||
|
# "norm_x": float(row["norm_x"]),
|
||||||
|
# "norm_y": float(row["norm_y"])
|
||||||
|
# })
|
||||||
|
# return colors
|
||||||
|
# except:
|
||||||
|
# return []
|
||||||
|
|
||||||
|
# def find_matching_color(self, colors, palette):
|
||||||
|
# match = []
|
||||||
|
# for color in colors:
|
||||||
|
# best = None
|
||||||
|
# min_dist = float("inf")
|
||||||
|
|
||||||
|
# for target in self.read_color_csv(palette):
|
||||||
|
# dist = self.color_distance(target["rgb"], color["rgb"])
|
||||||
|
# if dist < min_dist:
|
||||||
|
# min_dist = dist
|
||||||
|
# best = {
|
||||||
|
# "face": color["face"],
|
||||||
|
# "rgb": color["rgb"],
|
||||||
|
# "norm_x": target["norm_x"],
|
||||||
|
# "norm_y": target["norm_y"]
|
||||||
|
# }
|
||||||
|
# match.append(best)
|
||||||
|
# return match
|
||||||
|
|
||||||
|
# def adjust_UV(self, obj, new_face_coords):
|
||||||
|
# scale = 0.001 # Skalierung
|
||||||
|
|
||||||
|
# mesh = obj.data
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
# uv_layer = arr_layers.get("MLOD")
|
||||||
|
# mesh.uv_layers.active = uv_layer
|
||||||
|
|
||||||
|
# for poly in mesh.polygons:
|
||||||
|
# for coords in new_face_coords:
|
||||||
|
# if poly.index in coords["face"]:
|
||||||
|
|
||||||
|
# # Mittelpunkt der UVs dieses Polygons berechnen
|
||||||
|
# uvs = [uv_layer.data[i].uv.copy() for i in poly.loop_indices]
|
||||||
|
# center = sum(uvs, Vector((0.0, 0.0))) / len(uvs)
|
||||||
|
|
||||||
|
# # Skalieren + Verschieben
|
||||||
|
# for i in poly.loop_indices:
|
||||||
|
# uv = uv_layer.data[i].uv
|
||||||
|
# uv -= center
|
||||||
|
# uv *= scale
|
||||||
|
# uv += Vector((float(coords["norm_x"]), float(coords["norm_y"])))
|
||||||
|
|
||||||
|
# # Mesh updaten
|
||||||
|
# mesh.update()
|
||||||
|
|
||||||
|
# def bake_diffuse(self, selected_obj, res = 1024):
|
||||||
|
|
||||||
|
# source_obj = None
|
||||||
|
# for obj in bpy.data.objects:
|
||||||
|
# if obj.type == 'MESH' and obj.name.endswith("_LOD0"):
|
||||||
|
# print(obj.name)
|
||||||
|
# source_obj = obj.copy()
|
||||||
|
# source_obj.data = obj.data.copy()
|
||||||
|
# bpy.context.collection.objects.link(source_obj)
|
||||||
|
# break
|
||||||
|
|
||||||
|
# if source_obj is None:
|
||||||
|
# source_obj = selected_obj.copy()
|
||||||
|
# source_obj.data = selected_obj.data.copy()
|
||||||
|
# bpy.context.collection.objects.link(source_obj)
|
||||||
|
|
||||||
|
# # Material erstellen
|
||||||
|
# if "MLOD_Temp" in bpy.data.materials:
|
||||||
|
# mat = bpy.data.materials.remove["MLOD_Temp"]
|
||||||
|
# mat = bpy.data.materials.new(name="MLOD_Temp")
|
||||||
|
# source_obj.data.materials.clear()
|
||||||
|
# source_obj.data.materials.append(mat)
|
||||||
|
|
||||||
|
# # WICHTIG: Material als aktives Material setzen
|
||||||
|
# mat.use_nodes = True
|
||||||
|
# source_obj.active_material = mat
|
||||||
|
|
||||||
|
# # Ziel Image Node konfigurieren
|
||||||
|
# nodes = mat.node_tree.nodes
|
||||||
|
# links = mat.node_tree.links
|
||||||
|
# nodes.clear()
|
||||||
|
# tex_node = nodes.new(type="ShaderNodeTexImage")
|
||||||
|
# bsdf = nodes.new(type="ShaderNodeBsdfPrincipled")
|
||||||
|
# output = nodes.new(type="ShaderNodeOutputMaterial")
|
||||||
|
# links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
||||||
|
# links.new(tex_node.outputs["Color"], bsdf.inputs["Base Color"])
|
||||||
|
# nodes.active = tex_node
|
||||||
|
|
||||||
|
# # Erstellen des Images
|
||||||
|
# if "MLOD_Temp" in bpy.data.images:
|
||||||
|
# bpy.data.images.remove(bpy.data.images["MLOD_Temp"])
|
||||||
|
# image = bpy.data.images.new("MLOD_Temp", res, res)
|
||||||
|
# tex_node.image = image
|
||||||
|
|
||||||
|
# # Selected Object = Quelle
|
||||||
|
# bpy.context.view_layer.objects.active = source_obj
|
||||||
|
# # Active Object = Ziel
|
||||||
|
# selected_obj.select_set(True)
|
||||||
|
|
||||||
|
# # Bake
|
||||||
|
# bpy.context.scene.render.engine = 'CYCLES'
|
||||||
|
# bpy.ops.object.bake(type='COMBINED', width=512, height=512, cage_extrusion=0.5, pass_filter={'COLOR'}, margin=16, use_selected_to_active=True)
|
||||||
|
|
||||||
|
# # Temporäre Helfer entfernen
|
||||||
|
# bpy.data.objects.remove(source_obj, do_unlink=True)
|
||||||
|
# if "MLOD_Temp" in bpy.data.materials:
|
||||||
|
# bpy.data.materials.remove(mat)
|
||||||
|
|
||||||
|
# bpy.context.view_layer.objects.active = selected_obj
|
||||||
|
|
||||||
|
# return image
|
||||||
1025
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/ColorPalette_01.csv
Normal file
1025
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/ColorPalette_01.csv
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
1025
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/ColorPalette_02.csv
Normal file
1025
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/ColorPalette_02.csv
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
@@ -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 = context.scene
|
||||||
|
oldSelection = context.selected_objects
|
||||||
|
oldActive = 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 = 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)
|
||||||
|
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 = context.selected_objects
|
||||||
|
if not arr_obj:
|
||||||
|
self.report({'WARNING'}, 'Select a mesh to add the UVProject-Modifier')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
empty_objs = [emp_obj for emp_obj in 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"}
|
||||||
187
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/Dekogon.py
Normal file
187
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/Dekogon.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import bpy
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
|
||||||
|
### 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 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)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------
|
||||||
|
# Regular expression pattern to match any _LOD1, _LOD2, etc., but not _LOD0
|
||||||
|
pattern = re.compile(r"_LOD[1-9]\d*$")
|
||||||
|
|
||||||
|
# Loop through all objects in the scene
|
||||||
|
for obj in bpy.context.scene.objects:
|
||||||
|
# If the object name matches the pattern, hide it
|
||||||
|
if pattern.search(obj.name):
|
||||||
|
obj.hide_set(True) # This hides the object in the viewport
|
||||||
|
else:
|
||||||
|
obj.hide_set(False) # Ensure _LOD0 objects remain visible
|
||||||
|
|
||||||
|
# Update the view layer to reflect changes
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
self.report({'INFO'}, 'All objects sorted')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
### ASSIGN TEXTURES ###
|
||||||
|
|
||||||
|
class MESH_OT_assign_textures(bpy.types.Operator):
|
||||||
|
"""Assign Textures to Mesh´s, usefull for Unreal Decogon import"""
|
||||||
|
|
||||||
|
bl_idname = "mesh.assign_textures"
|
||||||
|
bl_label = "Assign Textures to Mesh´s, usefull for Unreal Decogon import"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
|
||||||
|
# Define the path to the folder containing your textures
|
||||||
|
texture_folder_path = context.scene.dekogon_file_path
|
||||||
|
|
||||||
|
texture_prefix = context.scene.dekogon_settings_prefix
|
||||||
|
if texture_prefix != "":
|
||||||
|
if texture_prefix[-1] != "_":
|
||||||
|
texture_prefix = texture_prefix + "_"
|
||||||
|
|
||||||
|
texture_suffix = context.scene.dekogon_settings_suffix
|
||||||
|
if texture_suffix != "":
|
||||||
|
if texture_suffix[0] != "_":
|
||||||
|
texture_suffix = "_" + texture_suffix
|
||||||
|
|
||||||
|
texture_filetype = context.scene.dekogon_settings_filetype
|
||||||
|
|
||||||
|
if not os.path.exists(texture_folder_path):
|
||||||
|
self.report({'ERROR'}, f"The texture folder path '{texture_folder_path}' does not exist.")
|
||||||
|
return {"FINISHED"}
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, f"Texture folder path: {texture_folder_path}")
|
||||||
|
# List all files in the texture folder for debugging purposes
|
||||||
|
#texture_files = os.listdir(texture_folder_path)
|
||||||
|
#self.report({'INFO'}, "Files in texture folder:")
|
||||||
|
#for f in texture_files:
|
||||||
|
# print(f" - {f}")
|
||||||
|
|
||||||
|
# Iterate over all materials in the Blender file
|
||||||
|
for material in bpy.data.materials:
|
||||||
|
self.report({'INFO'}, f"Processing material: {material.name}")
|
||||||
|
# Ensure the material uses nodes
|
||||||
|
if not material.use_nodes:
|
||||||
|
material.use_nodes = True
|
||||||
|
self.report({'INFO'}, f"Enabled nodes for material: {material.name}")
|
||||||
|
|
||||||
|
# Get the node tree of the material
|
||||||
|
if material.node_tree is None:
|
||||||
|
self.report({'WARNING'}, f"Material {material.name} has no node tree.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
nodes = material.node_tree.nodes
|
||||||
|
|
||||||
|
# Find or create an Image Texture node
|
||||||
|
image_texture_node = None
|
||||||
|
for node in nodes:
|
||||||
|
if node.type == 'TEX_IMAGE':
|
||||||
|
image_texture_node = node
|
||||||
|
break
|
||||||
|
if image_texture_node is None:
|
||||||
|
image_texture_node = nodes.new(type='ShaderNodeTexImage')
|
||||||
|
self.report({'INFO'}, f"Created new Image Texture node for material: {material.name}")
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, f"Found existing Image Texture node for material: {material.name}")
|
||||||
|
|
||||||
|
# Construct the expected texture filename based on the material name
|
||||||
|
texture_filename = f"{texture_prefix}{material.name}{texture_suffix}.{texture_filetype}"
|
||||||
|
texture_filepath = os.path.join(texture_folder_path, texture_filename)
|
||||||
|
self.report({'INFO'}, f"Looking for texture: {texture_filepath}")
|
||||||
|
|
||||||
|
# Check if the texture file exists
|
||||||
|
if os.path.exists(texture_filepath):
|
||||||
|
try:
|
||||||
|
# Load the image and assign it to the Image Texture node
|
||||||
|
image = bpy.data.images.load(texture_filepath)
|
||||||
|
image_texture_node.image = image
|
||||||
|
self.report({'INFO'}, f"Loaded texture: {texture_filename} for material: {material.name}")
|
||||||
|
|
||||||
|
# Link the Image Texture node to the Base Color of the Principled BSDF shader
|
||||||
|
principled_node = None
|
||||||
|
for node in nodes:
|
||||||
|
if node.type == 'BSDF_PRINCIPLED':
|
||||||
|
principled_node = node
|
||||||
|
break
|
||||||
|
if principled_node is not None:
|
||||||
|
links = material.node_tree.links
|
||||||
|
links.new(image_texture_node.outputs['Color'], principled_node.inputs['Base Color'])
|
||||||
|
self.report({'INFO'}, f"Linked texture to Principled BSDF for material: {material.name}")
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, f"No Principled BSDF node found for material: {material.name}")
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.report({'ERROR'}, f"Failed to load image {texture_filename}: {e}")
|
||||||
|
else:
|
||||||
|
self.report({'WARNING'}, "Texture file not found for material {material.name}: {texture_filename}")
|
||||||
|
|
||||||
|
self.report({'INFO'}, 'Texture assignment complete.')
|
||||||
|
return {"FINISHED"}
|
||||||
139
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/Helper.py
Normal file
139
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/Helper.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
|
||||||
|
### 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"}
|
||||||
|
|
||||||
|
class MESH_OT_select_face_id(bpy.types.Operator):
|
||||||
|
"""Highlights Face with given ID"""
|
||||||
|
|
||||||
|
bl_idname = "mesh.select_face_id"
|
||||||
|
bl_label = "Highlight/Select Face with given Index"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
EditObj = bpy.context.active_object
|
||||||
|
|
||||||
|
face_Index = int(context.scene.face_id_field)
|
||||||
|
|
||||||
|
if EditObj != None:
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='DESELECT')
|
||||||
|
bm = bmesh.from_edit_mesh(EditObj.data)
|
||||||
|
bm.faces.ensure_lookup_table()
|
||||||
|
|
||||||
|
for area in bpy.context.screen.areas:
|
||||||
|
if area.type == 'VIEW_3D':
|
||||||
|
for space in area.spaces:
|
||||||
|
if space.type == 'VIEW_3D':
|
||||||
|
space.shading.type = 'WIREFRAME'
|
||||||
|
try:
|
||||||
|
bm.faces[face_Index].select = True
|
||||||
|
bmesh.update_edit_mesh(EditObj.data)
|
||||||
|
|
||||||
|
self.report({'INFO'}, f'Highlighted Face with Index: {face_Index}')
|
||||||
|
return {"FINISHED"}
|
||||||
|
except IndexError:
|
||||||
|
self.report({'INFO'}, f'Index: {face_Index} out of range!')
|
||||||
|
return {"FINISHED"}
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, 'Please select a Mesh first.')
|
||||||
|
return {"FINISHED"}
|
||||||
215
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/MaterialToMask.py
Normal file
215
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/MaterialToMask.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
# Callback for Enum Items
|
||||||
|
def get_uv_layers(self, context):
|
||||||
|
items = []
|
||||||
|
obj = context.object
|
||||||
|
|
||||||
|
if obj and obj.type == 'MESH' and obj.data.uv_layers:
|
||||||
|
for i, uv in enumerate(obj.data.uv_layers):
|
||||||
|
items.append((str(i), uv.name, f"UV Layer {i}"))
|
||||||
|
else:
|
||||||
|
items.append(("NONE", "No UVs", "No UV available within Mesh"))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
class UVEnumProperties(bpy.types.PropertyGroup):
|
||||||
|
uv_enum: bpy.props.EnumProperty(name="UV Map", description="Select UV-Layer", items=get_uv_layers, default=0)
|
||||||
|
|
||||||
|
class MESH_OT_merge_materials_to_mask(bpy.types.Operator):
|
||||||
|
"""Merges 4 Materials into one Material (Mask)"""
|
||||||
|
|
||||||
|
bl_idname = "mesh.merge_materials_to_mask"
|
||||||
|
bl_label = "Merges 4 Materials into one Material (Mask)"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
scene = context.scene
|
||||||
|
arr_obj = context.selected_objects
|
||||||
|
activeObj = context.active_object
|
||||||
|
|
||||||
|
ignore_custom = context.scene.settings_multi_ignor
|
||||||
|
island_margin_custom = context.scene.uv_island_margin_slider
|
||||||
|
|
||||||
|
# Scale and offset for UV allignment
|
||||||
|
SCALE = 0.5
|
||||||
|
OFFSETS = [
|
||||||
|
(0.0, 0.5),
|
||||||
|
(0.5, 0.5),
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.5, 0.0)
|
||||||
|
]
|
||||||
|
IGNORE = ["glass", "glas", "grass", "gras"] + ignore_custom.split(",")
|
||||||
|
|
||||||
|
# No Mesh selected
|
||||||
|
if not arr_obj:
|
||||||
|
self.report({'WARNING'}, 'Select one mesh')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# Only one Mesh selected?
|
||||||
|
if len(arr_obj) > 1:
|
||||||
|
self.report({'WARNING'}, 'Only one Mesh can be selected')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
obj = arr_obj[0]
|
||||||
|
selected_uv = context.scene.uv_enum_props.uv_enum
|
||||||
|
|
||||||
|
# UV Maps present?
|
||||||
|
if not obj.data.uv_layers or not selected_uv:
|
||||||
|
self.report({'WARNING'}, 'No UV-Map selected')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
obj.data.uv_layers.active_index = int(selected_uv)
|
||||||
|
|
||||||
|
skip = 0
|
||||||
|
idx_multi = None
|
||||||
|
delete = []
|
||||||
|
|
||||||
|
# Loop through all Materials
|
||||||
|
for i, slot in enumerate(obj.material_slots):
|
||||||
|
|
||||||
|
# Skip Materials in static ignore list or user ignore list
|
||||||
|
if any(name in slot.material.name.lower() for name in IGNORE):
|
||||||
|
self.report({'INFO'}, f'Material "{slot.material.name}" skipped.')
|
||||||
|
skip += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Switch to edit mode
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
# Create bmesh to work with
|
||||||
|
bm = bmesh.from_edit_mesh(obj.data)
|
||||||
|
uv_layer = bm.loops.layers.uv.active
|
||||||
|
|
||||||
|
# Deselect all
|
||||||
|
bpy.ops.mesh.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
if slot.material:
|
||||||
|
# Select Mat slot
|
||||||
|
obj.active_material_index = i
|
||||||
|
|
||||||
|
# Select all Faces of Mat
|
||||||
|
bpy.ops.object.material_slot_select()
|
||||||
|
|
||||||
|
# If UV has assigned faces
|
||||||
|
if any(f.select for f in bm.faces):
|
||||||
|
# Repack UV Islands
|
||||||
|
bpy.ops.uv.select_all(action='SELECT')
|
||||||
|
bpy.ops.uv.pack_islands(margin=island_margin_custom)
|
||||||
|
|
||||||
|
# Select Offset
|
||||||
|
group = (i - skip) % len(OFFSETS)
|
||||||
|
offset = OFFSETS[group]
|
||||||
|
|
||||||
|
# Scale and offset UV
|
||||||
|
for f in bm.faces:
|
||||||
|
if f.select:
|
||||||
|
for loop in f.loops:
|
||||||
|
uv = loop[uv_layer].uv
|
||||||
|
uv[0] = offset[0] + uv[0] * SCALE
|
||||||
|
uv[1] = offset[1] + uv[1] * SCALE
|
||||||
|
|
||||||
|
# Apply Changes
|
||||||
|
bmesh.update_edit_mesh(obj.data)
|
||||||
|
|
||||||
|
# Create Texture Attributes with previous material names
|
||||||
|
match group:
|
||||||
|
case 0:
|
||||||
|
mat = bpy.data.materials.get(obj.material_slots[i].name)
|
||||||
|
mat["Black "] = mat.name
|
||||||
|
|
||||||
|
# Change name and use as base to merge
|
||||||
|
mat.name = "Multi_" + mat.name
|
||||||
|
idx_multi = i
|
||||||
|
self.report({'INFO'}, f'Material "{mat.name}" created.')
|
||||||
|
continue
|
||||||
|
case 1:
|
||||||
|
mat = bpy.data.materials.get(obj.material_slots[idx_multi].name)
|
||||||
|
mat["Red "] = slot.material.name
|
||||||
|
case 2:
|
||||||
|
mat = bpy.data.materials.get(obj.material_slots[idx_multi].name)
|
||||||
|
mat["Green "] = slot.material.name
|
||||||
|
case 3:
|
||||||
|
mat = bpy.data.materials.get(obj.material_slots[idx_multi].name)
|
||||||
|
mat["Blue "] = slot.material.name
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Merge Material A -> B
|
||||||
|
self.merge_mat_A_to_B(obj, i, idx_multi)
|
||||||
|
|
||||||
|
# Add Material B to delete list
|
||||||
|
delete.insert(0, i)
|
||||||
|
|
||||||
|
# Switch to object mode
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
# Delete not used materials from list
|
||||||
|
for mat in delete:
|
||||||
|
obj.active_material_index = mat
|
||||||
|
bpy.ops.object.material_slot_remove()
|
||||||
|
|
||||||
|
# Put UV first
|
||||||
|
self.put_UV_first(obj, selected_uv)
|
||||||
|
|
||||||
|
# Change UV names like in Reforger Tools
|
||||||
|
mesh = obj.data
|
||||||
|
uv_layers = mesh.uv_layers
|
||||||
|
for i, layer in enumerate(uv_layers):
|
||||||
|
layer.name = f"UVMap{i + 1}"
|
||||||
|
|
||||||
|
self.report({'INFO'}, 'Merged Materials to Multitextures.')
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
def merge_mat_A_to_B(self, obj, mat_A, mat_B):
|
||||||
|
# check if Mat Name is index or string and convert
|
||||||
|
if type(mat_A) is str:
|
||||||
|
mat_A = bpy.data.materials.get(mat_A)
|
||||||
|
mat_A = obj.material_slots.find(mat_A)
|
||||||
|
if type(mat_B) is str:
|
||||||
|
mat_B = bpy.data.materials.get(mat_B)
|
||||||
|
mat_B = obj.material_slots.find(mat_B)
|
||||||
|
|
||||||
|
# Change to Edit Mode
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
# Select all Faces from Mat B
|
||||||
|
obj.active_material_index = mat_A
|
||||||
|
bpy.ops.object.material_slot_select()
|
||||||
|
|
||||||
|
# Select Mat A and assign Faces from Mat B
|
||||||
|
obj.active_material_index = mat_B
|
||||||
|
bpy.ops.object.material_slot_assign() # Faces from B → A
|
||||||
|
|
||||||
|
# Switch to object mode
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
def put_UV_first(self, obj, target_index):
|
||||||
|
mesh = obj.data
|
||||||
|
uv_layers = mesh.uv_layers
|
||||||
|
|
||||||
|
if len(uv_layers) > 1:
|
||||||
|
# Transform UV-Layers to dict
|
||||||
|
uv_data_dict = {i: [loop_uv.uv.copy() for loop_uv in layer.data] for i, layer in enumerate(uv_layers)}
|
||||||
|
|
||||||
|
# Order: target_index -> first
|
||||||
|
indices = list(range(len(uv_layers)))
|
||||||
|
new_order = [target_index] + [i for i in indices if i != target_index]
|
||||||
|
|
||||||
|
# Delete all UV-layers
|
||||||
|
while len(uv_layers) > 0:
|
||||||
|
uv_layers.remove(uv_layers[0])
|
||||||
|
|
||||||
|
# write new order of UV´s
|
||||||
|
new_layers = []
|
||||||
|
for old_idx in new_order:
|
||||||
|
new_layer = uv_layers.new(name=f"Temp_{old_idx}") # temp Name
|
||||||
|
old_data = uv_data_dict[old_idx]
|
||||||
|
for j, loop_uv in enumerate(old_data):
|
||||||
|
new_layer.data[j].uv = loop_uv
|
||||||
|
new_layers.append(new_layer)
|
||||||
|
|
||||||
|
# Set active UV
|
||||||
|
uv_layers.active_index = 0
|
||||||
42
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/PrepareLods.py
Normal file
42
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/PrepareLods.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
### 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"}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import csv
|
||||||
|
|
||||||
|
palettes = ["ColorPalette_01", "ColorPalette_02"]
|
||||||
|
|
||||||
|
for palette in palettes:
|
||||||
|
# Bild öffnen (Pfad anpassen)
|
||||||
|
bild = Image.open(f"{palette}.png").convert("RGB")
|
||||||
|
|
||||||
|
breite, hoehe = bild.size
|
||||||
|
tiles_x, tiles_y = 32, 32
|
||||||
|
|
||||||
|
# Auf 32x32 verkleinern (nimmt repräsentative Pixel pro Kachel)
|
||||||
|
klein = bild.resize((32, 32), Image.NEAREST)
|
||||||
|
|
||||||
|
# CSV-Datei schreiben
|
||||||
|
with open(f"{palette}.csv", mode="w", newline="") as datei:
|
||||||
|
writer = csv.writer(datei)
|
||||||
|
writer.writerow(["norm_x", "norm_y", "r", "g", "b"])
|
||||||
|
|
||||||
|
for py in range(tiles_y):
|
||||||
|
for px in range(tiles_x):
|
||||||
|
# Farbe an diesem Pixel holen
|
||||||
|
r, g, b = bild.getpixel((int(px), int(py)))
|
||||||
|
|
||||||
|
# Normalisierung [0,1]
|
||||||
|
norm_x = (px / (breite)) + 1/64
|
||||||
|
norm_y = 1.0 - ((py / (hoehe))) - 1/64
|
||||||
|
|
||||||
|
writer.writerow([f"{norm_x:.6f}", f"{norm_y:.6f}", r, g, b])
|
||||||
207
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/__init__.py
Normal file
207
Blender/L1960_Tools/_Source/L1960_Tools_1_8_3/__init__.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
bl_info = {
|
||||||
|
"name": "L1960 Tools",
|
||||||
|
"author": "Prodeath21",
|
||||||
|
"version": (1, 8, 3),
|
||||||
|
"blender": (4, 2, 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(Dekogon)
|
||||||
|
imp.reload(CubeProjection)
|
||||||
|
imp.reload(Helper)
|
||||||
|
imp.reload(PrepareLods)
|
||||||
|
imp.reload(AutoColorPalette)
|
||||||
|
imp.reload(MaterialToMask)
|
||||||
|
print("L1960 Tools: Reloaded multifiles")
|
||||||
|
else:
|
||||||
|
from . import Dekogon
|
||||||
|
from . import CubeProjection
|
||||||
|
from . import Helper
|
||||||
|
from . import PrepareLods
|
||||||
|
from . import AutoColorPalette
|
||||||
|
from . import MaterialToMask
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from . Dekogon import MESH_OT_group_objects_in_collections, MESH_OT_assign_textures
|
||||||
|
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_fix_naming_conventions, MESH_OT_generate_empty_for_mesh, MESH_OT_select_face_id
|
||||||
|
from . PrepareLods import MESH_OT_prepare_lods_decimate
|
||||||
|
from . AutoColorPalette import MESH_OT_set_up_mlod, EnumColorPalettes # MESH_OT_bake_basic_mlod,
|
||||||
|
from . MaterialToMask import UVEnumProperties, MESH_OT_merge_materials_to_mask
|
||||||
|
|
||||||
|
class L1960_PT_dekogon(bpy.types.Panel):
|
||||||
|
#where to add the panel
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
|
||||||
|
#add labels
|
||||||
|
bl_label = "1960-Life Import"
|
||||||
|
bl_category = "1960-Life"
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
box = self.layout.box()
|
||||||
|
# Dekogon-Import
|
||||||
|
box.label(text="Dekogon-Import")
|
||||||
|
row = box.row()
|
||||||
|
row.operator("mesh.group_objects_in_collections", text="Group to Collections")
|
||||||
|
row = box.row()
|
||||||
|
split = row.split(factor=0.5)
|
||||||
|
split.operator("mesh.assign_textures", text="Assign Textures")
|
||||||
|
split.prop(context.scene, "dekogon_file_path", text="")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(context.scene, "section1",
|
||||||
|
text="Settings",
|
||||||
|
icon="TRIA_DOWN" if context.scene.section1 else "TRIA_RIGHT",
|
||||||
|
emboss=False)
|
||||||
|
if context.scene.section1:
|
||||||
|
box.prop(context.scene, "dekogon_settings_prefix", text="Prefix")
|
||||||
|
box.prop(context.scene, "dekogon_settings_suffix", text="Suffix")
|
||||||
|
box.prop(context.scene, "dekogon_settings_filetype", text="File Type")
|
||||||
|
self.layout.separator()
|
||||||
|
|
||||||
|
box = self.layout.box()
|
||||||
|
# Americano-Import
|
||||||
|
box.label(text="Materials-to-Multi")
|
||||||
|
row = box.row()
|
||||||
|
row.operator("mesh.merge_materials_to_mask", text="Merge Materials to Mask")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(context.scene, "section2",
|
||||||
|
text="Settings",
|
||||||
|
icon="TRIA_DOWN" if context.scene.section2 else "TRIA_RIGHT",
|
||||||
|
emboss=False)
|
||||||
|
if context.scene.section2:
|
||||||
|
row = box.row()
|
||||||
|
props = context.scene.uv_enum_props
|
||||||
|
row.prop(props, "uv_enum", text="Use UV")
|
||||||
|
box.prop(context.scene, "settings_multi_ignor", text="Ignore")
|
||||||
|
box.prop(context.scene, "uv_island_margin_slider", text="Island Margin")
|
||||||
|
|
||||||
|
class L1960_PT_tools(bpy.types.Panel):
|
||||||
|
#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.fix_naming_convention", text="Fix Naming Convention")
|
||||||
|
row = box.row()
|
||||||
|
row.operator("mesh.generate_empty_for_mesh", text="Generate Empty")
|
||||||
|
row = box.row()
|
||||||
|
split = row.split(factor=0.7)
|
||||||
|
split.operator("mesh.select_face_id", text="Show Face Index")
|
||||||
|
split.prop(context.scene, "face_id_field", text="")
|
||||||
|
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")
|
||||||
|
row = box.row()
|
||||||
|
row.operator("mesh.set_up_mlod", text="Generate MLOD (Palette)")
|
||||||
|
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_dekogon,
|
||||||
|
L1960_PT_tools,
|
||||||
|
MESH_OT_add_auto_cube_projection,
|
||||||
|
MESH_OT_add_modifier_to_mesh,
|
||||||
|
MESH_OT_merge_materials_to_mask,
|
||||||
|
MESH_OT_fix_material_names,
|
||||||
|
MESH_OT_group_objects_in_collections,
|
||||||
|
MESH_OT_assign_textures,
|
||||||
|
MESH_OT_fix_naming_conventions,
|
||||||
|
MESH_OT_generate_empty_for_mesh,
|
||||||
|
MESH_OT_prepare_lods_decimate,
|
||||||
|
# MESH_OT_bake_basic_mlod,
|
||||||
|
MESH_OT_set_up_mlod,
|
||||||
|
EnumColorPalettes,
|
||||||
|
MESH_OT_select_face_id,
|
||||||
|
UVEnumProperties
|
||||||
|
]
|
||||||
|
|
||||||
|
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)
|
||||||
|
bpy.types.Scene.face_id_field = bpy.props.IntProperty(name="Face ID", default=0, min=0)
|
||||||
|
bpy.types.Scene.section1 = bpy.props.BoolProperty(name="Expand Section", default=False)
|
||||||
|
bpy.types.Scene.section2 = bpy.props.BoolProperty(name="Expand Section", default=False)
|
||||||
|
bpy.types.Scene.dekogon_file_path = bpy.props.StringProperty(name="Textures", description="Path to the texture folder, textures to be applied from", subtype="DIR_PATH", default="")
|
||||||
|
bpy.types.Scene.dekogon_settings_prefix = bpy.props.StringProperty(name="Prefix", default="TX")
|
||||||
|
bpy.types.Scene.dekogon_settings_suffix = bpy.props.StringProperty(name="Suffix", default="ALB")
|
||||||
|
bpy.types.Scene.dekogon_settings_filetype = bpy.props.StringProperty(name="File Type", default="tga")
|
||||||
|
bpy.types.Scene.settings_multi_ignor = bpy.props.StringProperty(name="Ignore", default="", description="Ignore material when string is in name (split by comma)")
|
||||||
|
bpy.types.Scene.uv_enum_props = bpy.props.PointerProperty(type=UVEnumProperties)
|
||||||
|
bpy.types.Scene.uv_island_margin_slider = bpy.props.FloatProperty(name="Island Margin", default=0.02, min=0.01, max=0.1)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for mod in modules:
|
||||||
|
bpy.utils.unregister_class(mod)
|
||||||
|
|
||||||
|
del bpy.types.Scene.lod_slider
|
||||||
|
del bpy.types.Scene.color_palettes
|
||||||
|
del bpy.types.Scene.face_id_field
|
||||||
|
del bpy.types.Scene.section1
|
||||||
|
del bpy.types.Scene.section2
|
||||||
|
del bpy.types.Scene.dekogon_file_path
|
||||||
|
del bpy.types.Scene.dekogon_settings_prefix
|
||||||
|
del bpy.types.Scene.dekogon_settings_suffix
|
||||||
|
del bpy.types.Scene.dekogon_settings_filetype
|
||||||
|
del bpy.types.Scene.settings_multi_ignor
|
||||||
|
del bpy.types.Scene.uv_enum_props
|
||||||
|
del bpy.types.Scene.uv_island_margin_slider
|
||||||
|
|
||||||
|
if __name__== "__main__":
|
||||||
|
register()
|
||||||
Binary file not shown.
@@ -2238,6 +2238,10 @@
|
|||||||
('_pyi_rth_utils',
|
('_pyi_rth_utils',
|
||||||
'C:\\Users\\Niklas\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\PyInstaller\\fake-modules\\_pyi_rth_utils\\__init__.py',
|
'C:\\Users\\Niklas\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\PyInstaller\\fake-modules\\_pyi_rth_utils\\__init__.py',
|
||||||
'PYMODULE'),
|
'PYMODULE'),
|
||||||
|
('_py_abc',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\_py_abc.py',
|
||||||
|
'PYMODULE'),
|
||||||
('stringprep',
|
('stringprep',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\stringprep.py',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\stringprep.py',
|
||||||
@@ -2246,10 +2250,6 @@
|
|||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\tracemalloc.py',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\tracemalloc.py',
|
||||||
'PYMODULE'),
|
'PYMODULE'),
|
||||||
('_py_abc',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\_py_abc.py',
|
|
||||||
'PYMODULE'),
|
|
||||||
('typing',
|
('typing',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\typing.py',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\typing.py',
|
||||||
@@ -3232,35 +3232,35 @@
|
|||||||
'E:\\Arma Reforger '
|
'E:\\Arma Reforger '
|
||||||
'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip',
|
'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\LICENSE',
|
('setuptools-65.5.0.dist-info\\INSTALLER',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\LICENSE',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\INSTALLER',
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\WHEEL',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\WHEEL',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\top_level.txt',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\top_level.txt',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\METADATA',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\METADATA',
|
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\REQUESTED',
|
('setuptools-65.5.0.dist-info\\REQUESTED',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\METADATA',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\METADATA',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\LICENSE',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\LICENSE',
|
||||||
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\entry_points.txt',
|
('setuptools-65.5.0.dist-info\\entry_points.txt',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\entry_points.txt',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\entry_points.txt',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\INSTALLER',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\INSTALLER',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\RECORD',
|
('setuptools-65.5.0.dist-info\\RECORD',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\RECORD',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\RECORD',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\top_level.txt',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\top_level.txt',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\WHEEL',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\WHEEL',
|
||||||
'DATA')])
|
'DATA')])
|
||||||
|
|||||||
@@ -277,42 +277,42 @@
|
|||||||
'E:\\Arma Reforger '
|
'E:\\Arma Reforger '
|
||||||
'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip',
|
'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\LICENSE',
|
('setuptools-65.5.0.dist-info\\INSTALLER',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\LICENSE',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\INSTALLER',
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\WHEEL',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\WHEEL',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\top_level.txt',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\top_level.txt',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\METADATA',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\METADATA',
|
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\REQUESTED',
|
('setuptools-65.5.0.dist-info\\REQUESTED',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\METADATA',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\METADATA',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\LICENSE',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\LICENSE',
|
||||||
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\entry_points.txt',
|
('setuptools-65.5.0.dist-info\\entry_points.txt',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\entry_points.txt',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\entry_points.txt',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\INSTALLER',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\INSTALLER',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\RECORD',
|
('setuptools-65.5.0.dist-info\\RECORD',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\RECORD',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\RECORD',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\top_level.txt',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\top_level.txt',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\WHEEL',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\WHEEL',
|
||||||
'DATA')],
|
'DATA')],
|
||||||
[],
|
[],
|
||||||
False,
|
False,
|
||||||
False,
|
False,
|
||||||
1743968683,
|
1759164736,
|
||||||
[('run.exe',
|
[('run.exe',
|
||||||
'C:\\Users\\Niklas\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\run.exe',
|
'C:\\Users\\Niklas\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\run.exe',
|
||||||
'EXECUTABLE')],
|
'EXECUTABLE')],
|
||||||
|
|||||||
@@ -253,37 +253,37 @@
|
|||||||
'E:\\Arma Reforger '
|
'E:\\Arma Reforger '
|
||||||
'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip',
|
'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\LICENSE',
|
('setuptools-65.5.0.dist-info\\INSTALLER',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\LICENSE',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\INSTALLER',
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\WHEEL',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\WHEEL',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\top_level.txt',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\top_level.txt',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\METADATA',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\METADATA',
|
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\REQUESTED',
|
('setuptools-65.5.0.dist-info\\REQUESTED',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\METADATA',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\METADATA',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\LICENSE',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\LICENSE',
|
||||||
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\entry_points.txt',
|
('setuptools-65.5.0.dist-info\\entry_points.txt',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\entry_points.txt',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\entry_points.txt',
|
||||||
'DATA'),
|
'DATA'),
|
||||||
('setuptools-65.5.0.dist-info\\INSTALLER',
|
|
||||||
'C:\\Program '
|
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\INSTALLER',
|
|
||||||
'DATA'),
|
|
||||||
('setuptools-65.5.0.dist-info\\RECORD',
|
('setuptools-65.5.0.dist-info\\RECORD',
|
||||||
'C:\\Program '
|
'C:\\Program '
|
||||||
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\RECORD',
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\RECORD',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\top_level.txt',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\top_level.txt',
|
||||||
|
'DATA'),
|
||||||
|
('setuptools-65.5.0.dist-info\\WHEEL',
|
||||||
|
'C:\\Program '
|
||||||
|
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\WHEEL',
|
||||||
'DATA')],
|
'DATA')],
|
||||||
'python310.dll',
|
'python310.dll',
|
||||||
False,
|
False,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
Texturing/MergeTextures2/dist/merge_textures.exe
vendored
BIN
Texturing/MergeTextures2/dist/merge_textures.exe
vendored
Binary file not shown.
@@ -5,81 +5,107 @@ import time
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
# Define suffix lists for BaseColor, Normal, RMA/ORM
|
# Define suffix lists for BaseColor, Normal, packed maps, and single-channel maps
|
||||||
BASECOLOR_SUFFIXES = ['_alb.', '_albedo.', '_bc.', '_basecolor.', '_b.']
|
BASECOLOR_SUFFIXES = ['_alb.', '_albedo.', '_bc.', '_basecolor.', '_b.']
|
||||||
NORMAL_SUFFIXES = ['_nrm.', '_normal.', '_n.']
|
NORMAL_SUFFIXES = ['_nrm.', '_normal.', '_n.']
|
||||||
RMA_SUFFIXES = ['_rma.']
|
RMA_SUFFIXES = ['_rma.']
|
||||||
ORM_SUFFIXES = ['_orm.']
|
ORM_SUFFIXES = ['_orm.']
|
||||||
|
ROUGHNESS_SUFFIXES = ['_roughness.', '_rough.', '_rgh.']
|
||||||
|
METALLIC_SUFFIXES = ['_metallic.', '_metalness.', '_metal.', '_met.']
|
||||||
|
AO_SUFFIXES = ['_ao.', '_ambientocclusion.', '_occlusion.']
|
||||||
EMISSIVE_SUFFIXES = ['_emissive.']
|
EMISSIVE_SUFFIXES = ['_emissive.']
|
||||||
OPACITY_SUFFIXES = ['_opacity.']
|
OPACITY_SUFFIXES = ['_opacity.']
|
||||||
MASK_SUFFIXES = ['_mask.','_m.']
|
MASK_SUFFIXES = ['_mask.', '_m.']
|
||||||
|
|
||||||
def detect_texture_type(filename):
|
def detect_texture_type(filename):
|
||||||
""" Detects the type of texture based on its suffix """
|
"""Detect the type of texture based on naming suffixes."""
|
||||||
if any(suffix in filename.lower() for suffix in BASECOLOR_SUFFIXES):
|
lowered = filename.lower()
|
||||||
|
if any(suffix in lowered for suffix in BASECOLOR_SUFFIXES):
|
||||||
return 'BaseColor'
|
return 'BaseColor'
|
||||||
elif any(suffix in filename.lower() for suffix in NORMAL_SUFFIXES):
|
if any(suffix in lowered for suffix in NORMAL_SUFFIXES):
|
||||||
return 'Normal'
|
return 'Normal'
|
||||||
elif any(suffix in filename.lower() for suffix in RMA_SUFFIXES):
|
if any(suffix in lowered for suffix in RMA_SUFFIXES):
|
||||||
return 'RMA'
|
return 'RMA'
|
||||||
elif any(suffix in filename.lower() for suffix in ORM_SUFFIXES):
|
if any(suffix in lowered for suffix in ORM_SUFFIXES):
|
||||||
return 'ORM'
|
return 'ORM'
|
||||||
elif any(suffix in filename.lower() for suffix in EMISSIVE_SUFFIXES):
|
if any(suffix in lowered for suffix in ROUGHNESS_SUFFIXES):
|
||||||
|
return 'Roughness'
|
||||||
|
if any(suffix in lowered for suffix in METALLIC_SUFFIXES):
|
||||||
|
return 'Metallic'
|
||||||
|
if any(suffix in lowered for suffix in AO_SUFFIXES):
|
||||||
|
return 'AO'
|
||||||
|
if any(suffix in lowered for suffix in EMISSIVE_SUFFIXES):
|
||||||
return 'Emissive'
|
return 'Emissive'
|
||||||
elif any(suffix in filename.lower() for suffix in OPACITY_SUFFIXES):
|
if any(suffix in lowered for suffix in OPACITY_SUFFIXES):
|
||||||
return 'Opacity'
|
return 'Opacity'
|
||||||
elif any(suffix in filename.lower() for suffix in MASK_SUFFIXES):
|
if any(suffix in lowered for suffix in MASK_SUFFIXES):
|
||||||
return 'Mask'
|
return 'Mask'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_material_name(filename):
|
def get_material_name(filename):
|
||||||
""" Strips the 'T_' or 'TX_' prefix but keeps the suffix for texture type detection.
|
"""Strip the T_/TX_ prefix while keeping the suffix for detection and return the material name."""
|
||||||
Returns the full material name without the suffix for output file naming. """
|
|
||||||
base_name = os.path.basename(filename)
|
base_name = os.path.basename(filename)
|
||||||
|
|
||||||
# Remove the 'T_' or 'TX_' prefix
|
|
||||||
if base_name.startswith('T_'):
|
if base_name.startswith('T_'):
|
||||||
base_name = base_name[2:]
|
base_name = base_name[2:]
|
||||||
elif base_name.startswith('TX_'):
|
elif base_name.startswith('TX_'):
|
||||||
base_name = base_name[3:]
|
base_name = base_name[3:]
|
||||||
|
|
||||||
# Return the base_name without the suffix for output naming
|
return base_name.rsplit('_', 1)[0]
|
||||||
return base_name.rsplit('_', 1)[0] # Split only at the last underscore
|
|
||||||
|
|
||||||
def convert_single_material(material_data: Tuple[str, Dict[str, str]], output_folder: str) -> Tuple[bool, str]:
|
def convert_single_material(material_data: Tuple[str, Dict[str, str]], output_folder: str) -> Tuple[bool, str]:
|
||||||
"""Convert a single material to BCR/NMO format"""
|
"""Convert a single material to BCR/NMO format."""
|
||||||
material, files = material_data
|
material, files = material_data
|
||||||
basecolor_file = files.get('BaseColor')
|
basecolor_file = files.get('BaseColor')
|
||||||
normal_file = files.get('Normal')
|
normal_file = files.get('Normal')
|
||||||
rma_file = files.get('RMA')
|
rma_file = files.get('RMA')
|
||||||
orm_file = files.get('ORM')
|
orm_file = files.get('ORM')
|
||||||
|
roughness_file = files.get('Roughness')
|
||||||
|
metallic_file = files.get('Metallic')
|
||||||
|
ao_file = files.get('AO')
|
||||||
emissive_file = files.get('Emissive')
|
emissive_file = files.get('Emissive')
|
||||||
opacity_file = files.get('Opacity')
|
opacity_file = files.get('Opacity')
|
||||||
mask_file = files.get('Mask')
|
mask_file = files.get('Mask')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if convert_to_bcr_nmo(material, basecolor_file, normal_file, rma_file, orm_file, emissive_file, opacity_file, mask_file, output_folder):
|
success, warnings = convert_to_bcr_nmo(
|
||||||
|
material,
|
||||||
|
basecolor_file,
|
||||||
|
normal_file,
|
||||||
|
rma_file,
|
||||||
|
orm_file,
|
||||||
|
roughness_file,
|
||||||
|
metallic_file,
|
||||||
|
ao_file,
|
||||||
|
emissive_file,
|
||||||
|
opacity_file,
|
||||||
|
mask_file,
|
||||||
|
output_folder,
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
if warnings:
|
||||||
|
return True, f"{material}: Successfully converted. Warning(s): {' '.join(warnings)}"
|
||||||
return True, f"{material}: Successfully converted."
|
return True, f"{material}: Successfully converted."
|
||||||
else:
|
|
||||||
return False, f"Skipping {material}: input file sizes do not match."
|
return False, f"Skipping {material}: input file sizes do not match."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"Error processing {material}: {str(e)}"
|
return False, f"Error processing {material}: {str(e)}"
|
||||||
|
|
||||||
def process_textures(input_files):
|
def process_textures(input_files):
|
||||||
""" Main function to process all textures in a folder and convert to BCR/NMO """
|
"""Main function to process all textures in a folder and convert to BCR/NMO."""
|
||||||
textures = {}
|
textures: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
# Group files by material name
|
|
||||||
for filepath in input_files:
|
for filepath in input_files:
|
||||||
filename = os.path.basename(filepath)
|
filename = os.path.basename(filepath)
|
||||||
material_name = get_material_name(filename)
|
material_name = get_material_name(filename)
|
||||||
texture_type = detect_texture_type(filename)
|
texture_type = detect_texture_type(filename)
|
||||||
|
|
||||||
|
if texture_type is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if material_name not in textures:
|
if material_name not in textures:
|
||||||
textures[material_name] = {}
|
textures[material_name] = {}
|
||||||
textures[material_name][texture_type] = filepath
|
textures[material_name][texture_type] = filepath
|
||||||
|
|
||||||
# Create a merged folder in the same directory as the input
|
|
||||||
base_path = os.path.dirname(input_files[0])
|
base_path = os.path.dirname(input_files[0])
|
||||||
output_folder = os.path.join(base_path, 'merged')
|
output_folder = os.path.join(base_path, 'merged')
|
||||||
os.makedirs(output_folder, exist_ok=True)
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
@@ -87,8 +113,7 @@ def process_textures(input_files):
|
|||||||
material_count = len(textures)
|
material_count = len(textures)
|
||||||
print(f"Detected {material_count} Materials to process.")
|
print(f"Detected {material_count} Materials to process.")
|
||||||
|
|
||||||
# Check for required textures and filter out incomplete materials
|
valid_materials: Dict[str, Dict[str, str]] = {}
|
||||||
valid_materials = {}
|
|
||||||
failed_converts = 0
|
failed_converts = 0
|
||||||
|
|
||||||
for material, files in textures.items():
|
for material, files in textures.items():
|
||||||
@@ -98,7 +123,10 @@ def process_textures(input_files):
|
|||||||
if not files.get('Normal'):
|
if not files.get('Normal'):
|
||||||
missing_files.append('Normal')
|
missing_files.append('Normal')
|
||||||
if not (files.get('RMA') or files.get('ORM')):
|
if not (files.get('RMA') or files.get('ORM')):
|
||||||
missing_files.append('RMA or ORM')
|
if not files.get('Roughness'):
|
||||||
|
missing_files.append('Roughness')
|
||||||
|
if not files.get('AO'):
|
||||||
|
missing_files.append('AO')
|
||||||
|
|
||||||
if missing_files:
|
if missing_files:
|
||||||
print(f"Skipping {material}: missing {', '.join(missing_files)}")
|
print(f"Skipping {material}: missing {', '.join(missing_files)}")
|
||||||
@@ -106,16 +134,13 @@ def process_textures(input_files):
|
|||||||
else:
|
else:
|
||||||
valid_materials[material] = files
|
valid_materials[material] = files
|
||||||
|
|
||||||
# Process materials in parallel
|
|
||||||
success_count = 0
|
success_count = 0
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
# Submit all materials for processing
|
|
||||||
future_to_material = {
|
future_to_material = {
|
||||||
executor.submit(convert_single_material, (material, files), output_folder): material
|
executor.submit(convert_single_material, (material, files), output_folder): material
|
||||||
for material, files in valid_materials.items()
|
for material, files in valid_materials.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Process results as they complete
|
|
||||||
for future in concurrent.futures.as_completed(future_to_material):
|
for future in concurrent.futures.as_completed(future_to_material):
|
||||||
material = future_to_material[future]
|
material = future_to_material[future]
|
||||||
try:
|
try:
|
||||||
@@ -132,58 +157,102 @@ def process_textures(input_files):
|
|||||||
print(f"+++{success_count} of {material_count} materials successfully converted+++")
|
print(f"+++{success_count} of {material_count} materials successfully converted+++")
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
def convert_to_bcr_nmo(material, basecolor_file, normal_file, rma_file, orm_file, emissive_file, opacity_file, mask_file, output_folder):
|
def _ensure_single_channel(image_path: str):
|
||||||
""" Converts given textures to BCR and NMO formats """
|
"""Load an arbitrary image and return a single-channel version for merging."""
|
||||||
|
channel_image = Image.open(image_path)
|
||||||
|
if channel_image.mode != 'L':
|
||||||
|
channel_image = channel_image.convert('L')
|
||||||
|
return channel_image
|
||||||
|
|
||||||
|
def convert_to_bcr_nmo(
|
||||||
|
material: str,
|
||||||
|
basecolor_file: Optional[str],
|
||||||
|
normal_file: Optional[str],
|
||||||
|
rma_file: Optional[str],
|
||||||
|
orm_file: Optional[str],
|
||||||
|
roughness_file: Optional[str],
|
||||||
|
metallic_file: Optional[str],
|
||||||
|
ao_file: Optional[str],
|
||||||
|
emissive_file: Optional[str],
|
||||||
|
opacity_file: Optional[str],
|
||||||
|
mask_file: Optional[str],
|
||||||
|
output_folder: str,
|
||||||
|
) -> Tuple[bool, List[str]]:
|
||||||
|
"""Convert the provided textures to BCR and NMO targets."""
|
||||||
|
if basecolor_file is None or normal_file is None:
|
||||||
|
raise ValueError('BaseColor and Normal textures are required.')
|
||||||
|
|
||||||
|
warnings: List[str] = []
|
||||||
basecolor_img = Image.open(basecolor_file).convert('RGBA')
|
basecolor_img = Image.open(basecolor_file).convert('RGBA')
|
||||||
normal_img = Image.open(normal_file).convert('RGBA')
|
normal_img = Image.open(normal_file).convert('RGBA')
|
||||||
|
base_r, base_g, base_b, _ = basecolor_img.split()
|
||||||
|
normal_r, normal_g, _, _ = normal_img.split()
|
||||||
|
|
||||||
|
roughness_channel = metallic_channel = ao_channel = None
|
||||||
|
|
||||||
if rma_file:
|
if rma_file:
|
||||||
rma_img = Image.open(rma_file).convert('RGBA')
|
packed_img = Image.open(rma_file).convert('RGBA')
|
||||||
if not (basecolor_img.size == normal_img.size == rma_img.size):
|
if not (basecolor_img.size == normal_img.size == packed_img.size):
|
||||||
return False
|
return False, warnings
|
||||||
# BCR conversion
|
roughness_channel, metallic_channel, ao_channel, _ = packed_img.split()
|
||||||
bcr_img = Image.merge('RGBA', (basecolor_img.split()[0], basecolor_img.split()[1], basecolor_img.split()[2], rma_img.split()[0])) # Use Roughness (Alpha from RMA/ORM)
|
|
||||||
bcr_img.save(os.path.join(output_folder, f"{material}_BCR.tga"))
|
|
||||||
# NMO conversion
|
|
||||||
nmo_img = Image.merge('RGBA', (normal_img.split()[0], normal_img.split()[1], rma_img.split()[1], rma_img.split()[2])) # Use Metallic, AO from RMA/ORM
|
|
||||||
nmo_img.save(os.path.join(output_folder, f"{material}_NMO.tga"))
|
|
||||||
elif orm_file:
|
elif orm_file:
|
||||||
rma_img = Image.open(orm_file).convert('RGBA')
|
packed_img = Image.open(orm_file).convert('RGBA')
|
||||||
if not (basecolor_img.size == normal_img.size == rma_img.size):
|
if not (basecolor_img.size == normal_img.size == packed_img.size):
|
||||||
return False
|
return False, warnings
|
||||||
# BCR conversion
|
ao_channel, roughness_channel, metallic_channel, _ = packed_img.split()
|
||||||
bcr_img = Image.merge('RGBA', (basecolor_img.split()[0], basecolor_img.split()[1], basecolor_img.split()[2], rma_img.split()[1])) # Use Roughness (Alpha from RMA/ORM)
|
else:
|
||||||
|
if roughness_file is None or ao_file is None:
|
||||||
|
raise ValueError('Roughness and AO textures are required when RMA/ORM is absent.')
|
||||||
|
roughness_channel = _ensure_single_channel(roughness_file)
|
||||||
|
ao_channel = _ensure_single_channel(ao_file)
|
||||||
|
if metallic_file is not None:
|
||||||
|
metallic_channel = _ensure_single_channel(metallic_file)
|
||||||
|
else:
|
||||||
|
metallic_channel = Image.new('L', basecolor_img.size, 0)
|
||||||
|
warnings.append(f"{material}: Missing Metallic texture. Using a black channel.")
|
||||||
|
|
||||||
|
if not (
|
||||||
|
basecolor_img.size
|
||||||
|
== normal_img.size
|
||||||
|
== roughness_channel.size
|
||||||
|
== ao_channel.size
|
||||||
|
):
|
||||||
|
return False, warnings
|
||||||
|
if metallic_channel.size != basecolor_img.size:
|
||||||
|
return False, warnings
|
||||||
|
|
||||||
|
if roughness_channel is None or metallic_channel is None or ao_channel is None:
|
||||||
|
raise RuntimeError('Failed to resolve packed or single-channel textures.')
|
||||||
|
|
||||||
|
bcr_img = Image.merge('RGBA', (base_r, base_g, base_b, roughness_channel))
|
||||||
bcr_img.save(os.path.join(output_folder, f"{material}_BCR.tga"))
|
bcr_img.save(os.path.join(output_folder, f"{material}_BCR.tga"))
|
||||||
# NMO conversion
|
|
||||||
nmo_img = Image.merge('RGBA', (normal_img.split()[0], normal_img.split()[1], rma_img.split()[2], rma_img.split()[0])) # Use Metallic, AO from RMA/ORM
|
nmo_img = Image.merge('RGBA', (normal_r, normal_g, metallic_channel, ao_channel))
|
||||||
nmo_img.save(os.path.join(output_folder, f"{material}_NMO.tga"))
|
nmo_img.save(os.path.join(output_folder, f"{material}_NMO.tga"))
|
||||||
# Optionally handle emissive and opacity maps
|
|
||||||
if emissive_file:
|
if emissive_file:
|
||||||
emissive_img = Image.open(emissive_file)
|
emissive_img = Image.open(emissive_file)
|
||||||
# Preserve original color mode instead of forcing RGB
|
|
||||||
if emissive_img.mode != 'RGBA':
|
if emissive_img.mode != 'RGBA':
|
||||||
emissive_img = emissive_img.convert('RGBA')
|
emissive_img = emissive_img.convert('RGBA')
|
||||||
emissive_img.save(os.path.join(output_folder, f"{material}_EM.tga"))
|
emissive_img.save(os.path.join(output_folder, f"{material}_EM.tga"))
|
||||||
|
|
||||||
if opacity_file:
|
if opacity_file:
|
||||||
opacity_img = Image.open(opacity_file)
|
opacity_img = Image.open(opacity_file)
|
||||||
# Preserve original color mode instead of forcing grayscale
|
|
||||||
if opacity_img.mode != 'RGBA':
|
if opacity_img.mode != 'RGBA':
|
||||||
opacity_img = opacity_img.convert('RGBA')
|
opacity_img = opacity_img.convert('RGBA')
|
||||||
opacity_img.save(os.path.join(output_folder, f"{material}_OP.tga"))
|
opacity_img.save(os.path.join(output_folder, f"{material}_OP.tga"))
|
||||||
|
|
||||||
if mask_file:
|
if mask_file:
|
||||||
mask_img = Image.open(mask_file)
|
mask_img = Image.open(mask_file)
|
||||||
# Preserve original color mode instead of forcing grayscale
|
|
||||||
if mask_img.mode != 'RGBA':
|
if mask_img.mode != 'RGBA':
|
||||||
mask_img = mask_img.convert('RGBA')
|
mask_img = mask_img.convert('RGBA')
|
||||||
mask_img.save(os.path.join(output_folder, f"{material}_MASK.tga"))
|
mask_img.save(os.path.join(output_folder, f"{material}_MASK.tga"))
|
||||||
|
|
||||||
return True
|
return True, warnings
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: drag and drop texture files onto the script")
|
print("Usage: drag and drop texture files onto the script")
|
||||||
else:
|
else:
|
||||||
# Get the file paths from sys.argv (ignoring the first argument which is the script name)
|
|
||||||
input_files = sys.argv[1:]
|
input_files = sys.argv[1:]
|
||||||
process_textures(input_files)
|
process_textures(input_files)
|
||||||
|
|||||||
Reference in New Issue
Block a user