diff --git a/Texturing/MergeTextures2/__pycache__/merge_textures.cpython-312.pyc b/Texturing/MergeTextures2/__pycache__/merge_textures.cpython-312.pyc new file mode 100644 index 0000000..5b7eb41 Binary files /dev/null and b/Texturing/MergeTextures2/__pycache__/merge_textures.cpython-312.pyc differ diff --git a/Texturing/MergeTextures2/build/merge_textures/Analysis-00.toc b/Texturing/MergeTextures2/build/merge_textures/Analysis-00.toc index 8753716..0e82b49 100644 --- a/Texturing/MergeTextures2/build/merge_textures/Analysis-00.toc +++ b/Texturing/MergeTextures2/build/merge_textures/Analysis-00.toc @@ -2238,6 +2238,10 @@ ('_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', 'PYMODULE'), + ('_py_abc', + 'C:\\Program ' + 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\_py_abc.py', + 'PYMODULE'), ('stringprep', 'C:\\Program ' 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\stringprep.py', @@ -2246,10 +2250,6 @@ 'C:\\Program ' 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\tracemalloc.py', 'PYMODULE'), - ('_py_abc', - 'C:\\Program ' - 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\_py_abc.py', - 'PYMODULE'), ('typing', 'C:\\Program ' 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\typing.py', @@ -3232,35 +3232,35 @@ 'E:\\Arma Reforger ' 'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip', 'DATA'), - ('setuptools-65.5.0.dist-info\\LICENSE', + ('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\\LICENSE', - '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', + '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\\REQUESTED', 'C:\\Program ' 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED', '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', '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', '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', 'C:\\Program ' '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')]) diff --git a/Texturing/MergeTextures2/build/merge_textures/EXE-00.toc b/Texturing/MergeTextures2/build/merge_textures/EXE-00.toc index 2813861..35424ba 100644 --- a/Texturing/MergeTextures2/build/merge_textures/EXE-00.toc +++ b/Texturing/MergeTextures2/build/merge_textures/EXE-00.toc @@ -277,42 +277,42 @@ 'E:\\Arma Reforger ' 'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip', 'DATA'), - ('setuptools-65.5.0.dist-info\\LICENSE', + ('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\\LICENSE', - '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', + '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\\REQUESTED', 'C:\\Program ' 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED', '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', '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', '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', 'C:\\Program ' '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')], [], False, False, - 1743968683, + 1759164736, [('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')], diff --git a/Texturing/MergeTextures2/build/merge_textures/PKG-00.toc b/Texturing/MergeTextures2/build/merge_textures/PKG-00.toc index a0a028e..807dbf3 100644 --- a/Texturing/MergeTextures2/build/merge_textures/PKG-00.toc +++ b/Texturing/MergeTextures2/build/merge_textures/PKG-00.toc @@ -253,37 +253,37 @@ 'E:\\Arma Reforger ' 'Work\\1960-utils\\Texturing\\MergeTextures2\\build\\merge_textures\\base_library.zip', 'DATA'), - ('setuptools-65.5.0.dist-info\\LICENSE', + ('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\\LICENSE', - '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', + '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\\REQUESTED', 'C:\\Program ' 'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\\lib\\site-packages\\setuptools-65.5.0.dist-info\\REQUESTED', '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', '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', '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', 'C:\\Program ' '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')], 'python310.dll', False, diff --git a/Texturing/MergeTextures2/build/merge_textures/base_library.zip b/Texturing/MergeTextures2/build/merge_textures/base_library.zip index 2c5d5de..a6b9d38 100644 Binary files a/Texturing/MergeTextures2/build/merge_textures/base_library.zip and b/Texturing/MergeTextures2/build/merge_textures/base_library.zip differ diff --git a/Texturing/MergeTextures2/build/merge_textures/merge_textures.pkg b/Texturing/MergeTextures2/build/merge_textures/merge_textures.pkg index aafbc47..a188327 100644 Binary files a/Texturing/MergeTextures2/build/merge_textures/merge_textures.pkg and b/Texturing/MergeTextures2/build/merge_textures/merge_textures.pkg differ diff --git a/Texturing/MergeTextures2/dist/merge_textures.exe b/Texturing/MergeTextures2/dist/merge_textures.exe index 4a2e131..72a20cf 100644 Binary files a/Texturing/MergeTextures2/dist/merge_textures.exe and b/Texturing/MergeTextures2/dist/merge_textures.exe differ diff --git a/Texturing/MergeTextures2/merge_textures.py b/Texturing/MergeTextures2/merge_textures.py index 7f4bada..24387a3 100644 --- a/Texturing/MergeTextures2/merge_textures.py +++ b/Texturing/MergeTextures2/merge_textures.py @@ -1,96 +1,121 @@ import os import sys -from PIL import Image # type: ignore +from PIL import Image # type: ignore import time import concurrent.futures 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.'] NORMAL_SUFFIXES = ['_nrm.', '_normal.', '_n.'] RMA_SUFFIXES = ['_rma.'] ORM_SUFFIXES = ['_orm.'] +ROUGHNESS_SUFFIXES = ['_roughness.', '_rough.', '_rgh.'] +METALLIC_SUFFIXES = ['_metallic.', '_metalness.', '_metal.', '_met.'] +AO_SUFFIXES = ['_ao.', '_ambientocclusion.', '_occlusion.'] EMISSIVE_SUFFIXES = ['_emissive.'] OPACITY_SUFFIXES = ['_opacity.'] -MASK_SUFFIXES = ['_mask.','_m.'] +MASK_SUFFIXES = ['_mask.', '_m.'] def detect_texture_type(filename): - """ Detects the type of texture based on its suffix """ - if any(suffix in filename.lower() for suffix in BASECOLOR_SUFFIXES): + """Detect the type of texture based on naming suffixes.""" + lowered = filename.lower() + if any(suffix in lowered for suffix in BASECOLOR_SUFFIXES): 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' - elif any(suffix in filename.lower() for suffix in RMA_SUFFIXES): + if any(suffix in lowered for suffix in RMA_SUFFIXES): 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' - 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' - elif any(suffix in filename.lower() for suffix in OPACITY_SUFFIXES): + if any(suffix in lowered for suffix in OPACITY_SUFFIXES): 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 None def get_material_name(filename): - """ Strips the 'T_' or 'TX_' prefix but keeps the suffix for texture type detection. - Returns the full material name without the suffix for output file naming. """ + """Strip the T_/TX_ prefix while keeping the suffix for detection and return the material name.""" base_name = os.path.basename(filename) - - # Remove the 'T_' or 'TX_' prefix + if base_name.startswith('T_'): base_name = base_name[2:] elif base_name.startswith('TX_'): base_name = base_name[3:] - - # Return the base_name without the suffix for output naming - return base_name.rsplit('_', 1)[0] # Split only at the last underscore + + return base_name.rsplit('_', 1)[0] 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 basecolor_file = files.get('BaseColor') normal_file = files.get('Normal') rma_file = files.get('RMA') 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') opacity_file = files.get('Opacity') mask_file = files.get('Mask') - + 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." - 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: return False, f"Error processing {material}: {str(e)}" def process_textures(input_files): - """ Main function to process all textures in a folder and convert to BCR/NMO """ - textures = {} - - # Group files by material name + """Main function to process all textures in a folder and convert to BCR/NMO.""" + textures: Dict[str, Dict[str, str]] = {} + for filepath in input_files: filename = os.path.basename(filepath) material_name = get_material_name(filename) texture_type = detect_texture_type(filename) - + + if texture_type is None: + continue + if material_name not in textures: textures[material_name] = {} 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]) output_folder = os.path.join(base_path, 'merged') os.makedirs(output_folder, exist_ok=True) - + material_count = len(textures) print(f"Detected {material_count} Materials to process.") - # Check for required textures and filter out incomplete materials - valid_materials = {} + valid_materials: Dict[str, Dict[str, str]] = {} failed_converts = 0 - + for material, files in textures.items(): missing_files = [] if not files.get('BaseColor'): @@ -98,24 +123,24 @@ def process_textures(input_files): if not files.get('Normal'): missing_files.append('Normal') 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: print(f"Skipping {material}: missing {', '.join(missing_files)}") failed_converts += 1 else: valid_materials[material] = files - # Process materials in parallel success_count = 0 with concurrent.futures.ThreadPoolExecutor() as executor: - # Submit all materials for processing 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() } - - # Process results as they complete + for future in concurrent.futures.as_completed(future_to_material): material = future_to_material[future] try: @@ -128,62 +153,106 @@ def process_textures(input_files): except Exception as e: failed_converts += 1 print(f"Error processing {material}: {str(e)}") - + print(f"+++{success_count} of {material_count} materials successfully converted+++") 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): - """ Converts given textures to BCR and NMO formats """ +def _ensure_single_channel(image_path: str): + """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') 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: - rma_img = Image.open(rma_file).convert('RGBA') - if not (basecolor_img.size == normal_img.size == rma_img.size): - return False - # BCR conversion - 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")) + packed_img = Image.open(rma_file).convert('RGBA') + if not (basecolor_img.size == normal_img.size == packed_img.size): + return False, warnings + roughness_channel, metallic_channel, ao_channel, _ = packed_img.split() elif orm_file: - rma_img = Image.open(orm_file).convert('RGBA') - if not (basecolor_img.size == normal_img.size == rma_img.size): - return False - # BCR conversion - 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) - 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.save(os.path.join(output_folder, f"{material}_NMO.tga")) - # Optionally handle emissive and opacity maps + packed_img = Image.open(orm_file).convert('RGBA') + if not (basecolor_img.size == normal_img.size == packed_img.size): + return False, warnings + ao_channel, roughness_channel, metallic_channel, _ = packed_img.split() + 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")) + + 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: emissive_img = Image.open(emissive_file) - # Preserve original color mode instead of forcing RGB if emissive_img.mode != 'RGBA': emissive_img = emissive_img.convert('RGBA') emissive_img.save(os.path.join(output_folder, f"{material}_EM.tga")) - + if opacity_file: opacity_img = Image.open(opacity_file) - # Preserve original color mode instead of forcing grayscale if opacity_img.mode != 'RGBA': opacity_img = opacity_img.convert('RGBA') opacity_img.save(os.path.join(output_folder, f"{material}_OP.tga")) - + if mask_file: mask_img = Image.open(mask_file) - # Preserve original color mode instead of forcing grayscale if mask_img.mode != 'RGBA': mask_img = mask_img.convert('RGBA') mask_img.save(os.path.join(output_folder, f"{material}_MASK.tga")) - return True + return True, warnings + if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: drag and drop texture files onto the script") else: - # Get the file paths from sys.argv (ignoring the first argument which is the script name) input_files = sys.argv[1:] process_textures(input_files)