2024-10-18 15:50:58 +02:00
import os
import sys
2024-10-19 17:46:28 +02:00
from PIL import Image # type: ignore
2024-10-18 15:50:58 +02:00
import time
# Define suffix lists for BaseColor, Normal, RMA/ORM
2024-10-19 17:46:28 +02:00
BASECOLOR_SUFFIXES = [ ' _alb. ' , ' _albedo. ' , ' _bc. ' , ' _basecolor. ' , ' _b. ' ]
NORMAL_SUFFIXES = [ ' _nrm. ' , ' _normal. ' , ' _n. ' ]
RMA_SUFFIXES = [ ' _rma. ' ]
ORM_SUFFIXES = [ ' _orm. ' ]
EMISSIVE_SUFFIXES = [ ' _emissive. ' ]
OPACITY_SUFFIXES = [ ' _opacity. ' ]
MASK_SUFFIXES = [ ' _mask. ' , ' _m. ' ]
2024-10-18 15:50:58 +02:00
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 ) :
return ' BaseColor '
elif any ( suffix in filename . lower ( ) for suffix in NORMAL_SUFFIXES ) :
return ' Normal '
elif any ( suffix in filename . lower ( ) for suffix in RMA_SUFFIXES ) :
return ' RMA '
elif any ( suffix in filename . lower ( ) for suffix in ORM_SUFFIXES ) :
return ' ORM '
elif any ( suffix in filename . lower ( ) for suffix in EMISSIVE_SUFFIXES ) :
return ' Emissive '
elif any ( suffix in filename . lower ( ) for suffix in OPACITY_SUFFIXES ) :
return ' Opacity '
2024-10-19 17:46:28 +02:00
elif any ( suffix in filename . lower ( ) for suffix in MASK_SUFFIXES ) :
return ' Mask '
2024-10-18 15:50:58 +02:00
return None
def get_material_name ( filename ) :
2024-10-19 17:46:28 +02:00
""" 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 . """
2024-10-18 15:50:58 +02:00
base_name = os . path . basename ( filename )
2024-10-19 17:46:28 +02:00
# Remove the 'T_' or 'TX_' prefix
2024-10-18 15:50:58 +02:00
if base_name . startswith ( ' T_ ' ) :
2024-10-19 17:46:28 +02:00
base_name = base_name [ 2 : ]
2024-10-18 15:50:58 +02:00
elif base_name . startswith ( ' TX_ ' ) :
2024-10-19 17:46:28 +02:00
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
2024-10-18 15:50:58 +02:00
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
for filepath in input_files :
filename = os . path . basename ( filepath )
material_name = get_material_name ( filename )
texture_type = detect_texture_type ( filename )
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 )
2024-10-19 17:46:28 +02:00
material_count = len ( textures )
print ( f " Detected { material_count } Materials to process. " )
2025-01-25 23:16:01 +01:00
failed_converts = 0
2024-10-19 17:46:28 +02:00
2024-10-18 15:50:58 +02:00
# Process each material group
2024-10-19 17:46:28 +02:00
for index , ( material , files ) in enumerate ( textures . items ( ) ) :
2024-10-18 15:50:58 +02:00
basecolor_file = files . get ( ' BaseColor ' )
normal_file = files . get ( ' Normal ' )
rma_file = files . get ( ' RMA ' )
orm_file = files . get ( ' ORM ' )
emissive_file = files . get ( ' Emissive ' )
opacity_file = files . get ( ' Opacity ' )
2024-10-19 17:46:28 +02:00
mask_file = files . get ( ' Mask ' )
missing_files = [ ]
# Check for required textures
if not basecolor_file :
missing_files . append ( ' BaseColor ' )
if not normal_file :
missing_files . append ( ' Normal ' )
if not ( rma_file or orm_file ) :
missing_files . append ( ' RMA or ORM ' )
2025-01-25 23:16:01 +01:00
# Report missing files if any
2024-10-19 17:46:28 +02:00
if missing_files :
2025-01-25 23:16:01 +01:00
print ( f " ( { index + 1 } / { material_count } ) Skipping { material } : missing { ' , ' . join ( missing_files ) } " )
2024-10-19 17:46:28 +02:00
failed_converts + = 1
2024-10-18 15:50:58 +02:00
else :
2025-01-25 23:16:01 +01:00
# Convert to BCR/NMO format and track success or failure
if convert_to_bcr_nmo ( material , basecolor_file , normal_file , rma_file , orm_file , emissive_file , opacity_file , mask_file , output_folder ) :
print ( f " ( { index + 1 } / { material_count } ) { material } : Successfully converted. " )
else :
failed_converts + = 1 # Increment counter here if conversion fails
print ( f " ( { index + 1 } / { material_count } ) Skipping { material } : input file sizes do not match. " )
2024-10-18 15:50:58 +02:00
2024-10-19 17:46:28 +02:00
print ( f " +++ { material_count - failed_converts } of { material_count } materials successfully converted+++ " )
2024-10-18 15:50:58 +02:00
time . sleep ( 3 )
2024-10-19 17:46:28 +02:00
def convert_to_bcr_nmo ( material , basecolor_file , normal_file , rma_file , orm_file , emissive_file , opacity_file , mask_file , output_folder ) :
2024-10-18 15:50:58 +02:00
""" Converts given textures to BCR and NMO formats """
basecolor_img = Image . open ( basecolor_file ) . convert ( ' RGBA ' )
normal_img = Image . open ( normal_file ) . convert ( ' RGBA ' )
if rma_file :
rma_img = Image . open ( rma_file ) . convert ( ' RGBA ' )
2025-01-25 23:16:01 +01:00
if not ( basecolor_img . size == normal_img . size == rma_img . size ) :
return False
2024-10-18 15:50:58 +02:00
# 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.png " ) )
# 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.png " ) )
elif orm_file :
rma_img = Image . open ( orm_file ) . convert ( ' RGBA ' )
2025-01-25 23:16:01 +01:00
if not ( basecolor_img . size == normal_img . size == rma_img . size ) :
return False
2024-10-18 15:50:58 +02:00
# 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.png " ) )
# 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.png " ) )
# Optionally handle emissive and opacity maps
if emissive_file :
emissive_img = Image . open ( emissive_file ) . convert ( ' RGB ' )
emissive_img . save ( os . path . join ( output_folder , f " { material } _EM.png " ) )
if opacity_file :
opacity_img = Image . open ( opacity_file ) . convert ( ' L ' )
opacity_img . save ( os . path . join ( output_folder , f " { material } _OP.png " ) )
2024-10-19 17:46:28 +02:00
if mask_file :
mask_img = Image . open ( mask_file ) . convert ( ' L ' )
mask_img . save ( os . path . join ( output_folder , f " { material } _MASK.png " ) )
2025-01-25 23:16:01 +01:00
return True
2024-10-18 15:50:58 +02:00
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 )