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:
Binary file not shown.
@@ -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')])
|
||||
|
||||
@@ -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')],
|
||||
|
||||
@@ -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,
|
||||
|
||||
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.
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user