239 lines
8.8 KiB
Python
239 lines
8.8 KiB
Python
|
|
"""
|
||
|
|
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)
|