Compare commits

...

1 Commits

Author SHA1 Message Date
Killergnom
bd885f1b99 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.
2025-09-29 18:57:57 +02:00
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

@@ -1,96 +1,121 @@
import os import os
import sys import sys
from PIL import Image # type: ignore from PIL import Image # type: ignore
import time 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)
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():
missing_files = [] missing_files = []
if not files.get('BaseColor'): if not files.get('BaseColor'):
@@ -98,24 +123,24 @@ 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)}")
failed_converts += 1 failed_converts += 1
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:
@@ -128,62 +153,106 @@ def process_textures(input_files):
except Exception as e: except Exception as e:
failed_converts += 1 failed_converts += 1
print(f"Error processing {material}: {str(e)}") print(f"Error processing {material}: {str(e)}")
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:
bcr_img.save(os.path.join(output_folder, f"{material}_BCR.tga")) if roughness_file is None or ao_file is None:
# NMO conversion raise ValueError('Roughness and AO textures are required when RMA/ORM is absent.')
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 roughness_channel = _ensure_single_channel(roughness_file)
nmo_img.save(os.path.join(output_folder, f"{material}_NMO.tga")) ao_channel = _ensure_single_channel(ao_file)
# Optionally handle emissive and opacity maps 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"))
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"))
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)