""" Blender script to remap texture paths after texture organization. Usage: blender --background --factory-startup --python remap_texture_paths.py -- [blend_file_path] [texture_moves_json_path] """ import bpy import json import os import sys def normalize_path(path): """Normalize a path for comparison (handle case, separators, etc.)""" if not path: return "" # Convert to absolute path and normalize abs_path = os.path.abspath(path) # Normalize separators (Windows uses backslash, but we'll compare case-insensitively) normalized = os.path.normpath(abs_path).replace('\\', '/') return normalized.lower() def load_move_log(json_path): """Load the move log JSON file and build lookup dictionaries""" try: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) # Debug: Check what keys are in the data print(f" JSON keys: {list(data.keys())}") # PowerShell ConvertTo-Json uses PascalCase by default, so check both cases moves = data.get('moves', []) or data.get('Moves', []) print(f" Found {len(moves)} move entries in JSON") if len(moves) > 0: # Debug: Show first move entry structure print(f" First move entry keys: {list(moves[0].keys())}") print(f" First move entry: {moves[0]}") # Build lookup dictionaries: normalized original path -> actual new path original_to_new = {} original_to_replacement = {} for move in moves: # Try both camelCase and PascalCase property names move_type = move.get('type', '') or move.get('Type', '') original = move.get('originalPath', '') or move.get('OriginalPath', '') if not original: continue orig_norm = normalize_path(original) if move_type == 'moved' or move_type == 'Moved': new_path = move.get('newPath', '') or move.get('NewPath', '') if new_path: original_to_new[orig_norm] = new_path elif move_type == 'deleted' or move_type == 'Deleted': replacement = move.get('replacedBy', '') or move.get('ReplacedBy', '') if replacement: original_to_replacement[orig_norm] = replacement print(f" Built {len(original_to_new)} move mappings and {len(original_to_replacement)} replacement mappings") return original_to_new, original_to_replacement, data.get('textureFolderPath', '') or data.get('TextureFolderPath', '') except Exception as e: print(f"ERROR: Failed to load move log: {e}") import traceback traceback.print_exc() return {}, {}, "" def remap_texture_paths(blend_file_path, move_log_path): """Remap all texture paths in the blend file""" print(f"\n=== REMAPPING TEXTURE PATHS ===") print(f"Blend file: {blend_file_path}") print(f"Move log: {move_log_path}") # Load move log original_to_new, original_to_replacement, texture_folder_path = load_move_log(move_log_path) if not original_to_new and not original_to_replacement: print("WARNING: No moves found in move log. Nothing to remap.") return 0 print(f"Loaded {len(original_to_new)} move mappings and {len(original_to_replacement)} replacement mappings") # Get blend file directory for relative path conversion blend_file_dir = os.path.dirname(os.path.abspath(blend_file_path)) remapped_count = 0 not_found_count = 0 # Remap paths in image datablocks print("\nRemapping image datablock paths...") for image in bpy.data.images: if not image.filepath: continue # Convert to absolute path for comparison abs_path = bpy.path.abspath(image.filepath) if not abs_path: continue abs_norm = normalize_path(abs_path) was_relative = not os.path.isabs(image.filepath) # Check if this path needs remapping new_path = None # First check moved files if abs_norm in original_to_new: new_path = original_to_new[abs_norm] # Then check deleted files (replaced by) elif abs_norm in original_to_replacement: new_path = original_to_replacement[abs_norm] if new_path: # Convert to relative path if original was relative if was_relative: try: rel_path = bpy.path.relpath(new_path, blend_file_dir) image.filepath = rel_path except: # Fallback to absolute if relative conversion fails image.filepath = new_path else: image.filepath = new_path print(f" Remapped: {os.path.basename(abs_path)} -> {os.path.basename(new_path)}") remapped_count += 1 else: # Check if file exists - if not, it might be a broken reference if not os.path.exists(abs_path): not_found_count += 1 # Also check material node trees for image texture nodes # Note: Most image texture nodes reference the image datablock (which we already remapped above), # but we check node.filepath for completeness in case any nodes have direct file paths print("\nRemapping material node texture paths...") node_remapped_count = 0 for material in bpy.data.materials: if not material.use_nodes or not material.node_tree: continue for node in material.node_tree.nodes: if node.type == 'TEX_IMAGE' and hasattr(node, 'filepath') and node.filepath: abs_path = bpy.path.abspath(node.filepath) if abs_path: abs_norm = normalize_path(abs_path) new_path = None if abs_norm in original_to_new: new_path = original_to_new[abs_norm] elif abs_norm in original_to_replacement: new_path = original_to_replacement[abs_norm] if new_path: node.filepath = new_path print(f" Remapped node texture: {os.path.basename(abs_path)} -> {os.path.basename(new_path)}") node_remapped_count += 1 if node_remapped_count > 0: remapped_count += node_remapped_count print(f"\n=== REMAPPING SUMMARY ===") print(f"Paths remapped: {remapped_count}") print(f"Broken references (not found): {not_found_count}") # Save the blend file try: print(f"\nSaving blend file...") # Ensure we use an absolute path abs_blend_path = os.path.abspath(blend_file_path) print(f" Saving to: {abs_blend_path}") bpy.ops.wm.save_mainfile(filepath=abs_blend_path) # Verify the file was actually saved if os.path.exists(abs_blend_path): file_time = os.path.getmtime(abs_blend_path) print(f"Successfully saved: {abs_blend_path}") print(f" File modified time: {file_time}") return remapped_count else: print(f"ERROR: File was not created at {abs_blend_path}") return -1 except Exception as e: print(f"ERROR: Failed to save blend file: {e}") import traceback traceback.print_exc() return -1 def main(): # Get command line arguments (after --) if '--' in sys.argv: args = sys.argv[sys.argv.index('--') + 1:] else: args = sys.argv[1:] if len(args) < 2: print("ERROR: Usage: blender --background --factory-startup --python remap_texture_paths.py -- [blend_file_path] [texture_moves_json_path]") return 1 blend_file_path = args[0] move_log_path = args[1] # Validate paths if not os.path.exists(blend_file_path): print(f"ERROR: Blend file not found: {blend_file_path}") return 1 if not os.path.exists(move_log_path): print(f"ERROR: Move log file not found: {move_log_path}") return 1 # Load the blend file try: print(f"Loading blend file: {blend_file_path}") bpy.ops.wm.open_mainfile(filepath=blend_file_path) except Exception as e: print(f"ERROR: Failed to load blend file: {e}") return 1 # Remap texture paths result = remap_texture_paths(blend_file_path, move_log_path) if result >= 0: print("\nTexture path remapping completed successfully!") return 0 else: print("\nTexture path remapping failed!") return 1 if __name__ == "__main__": exit_code = main() sys.exit(exit_code)