Adjusted input for merge_textures

Updated merge_textures to also accept roughness, ao and metallic maps directly.
Metallic is optional. If no metallic map is supplied, blue channel in the NMO will be black.
This commit is contained in:
Killergnom
2025-09-29 18:57:57 +02:00
parent a60ae1c1ea
commit bd885f1b99
8 changed files with 199 additions and 130 deletions

View File

@@ -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')])

View File

@@ -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')],

View File

@@ -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.

View File

@@ -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)