235 lines
9.2 KiB
Python
235 lines
9.2 KiB
Python
bl_info = {
|
|
"name": "Material Image Remapper",
|
|
"author": "Chipp Walters, Anthony Aragues",
|
|
"version": (1, 0, 1),
|
|
"blender": (2, 82, 0),
|
|
"location": "View3D > Tool Shelf > ♥U",
|
|
"description": "Finds duplicate images in all scene materials and remaps them to one",
|
|
"warning": "",
|
|
"wiki_url": "https://www.chippwalters.com",
|
|
"category": "Edit",
|
|
}
|
|
|
|
import bpy
|
|
import os
|
|
import hashlib
|
|
import tempfile
|
|
|
|
# Ensure the temp directory exists
|
|
temp_dir = tempfile.gettempdir()
|
|
os.makedirs(temp_dir, exist_ok=True)
|
|
|
|
def save_and_hash_image(image, temp_dir, scene):
|
|
"""Save the image to a temporary file and calculate its hash."""
|
|
temp_filename = os.path.join(temp_dir, image.name)
|
|
temp_filepath = bpy.path.abspath(temp_filename)
|
|
|
|
# Attempt to save the image to the temp directory
|
|
try:
|
|
# Ensure the scene parameter is correctly passed as a keyword argument
|
|
image.save_render(filepath=temp_filepath, scene=scene)
|
|
print(f"*** Image saved to: {temp_filepath}")
|
|
except Exception as e:
|
|
print(f"*** Error saving image {image.name}: {e}")
|
|
return None
|
|
|
|
# Calculate the hash of the saved image
|
|
try:
|
|
with open(temp_filepath, 'rb') as f:
|
|
img_data = f.read()
|
|
img_hash = hashlib.md5(img_data).hexdigest()
|
|
print(f"*** Hash for {image.name}: {img_hash}")
|
|
except Exception as e:
|
|
print(f"*** Error hashing image {image.name}: {e}")
|
|
return None
|
|
|
|
# Optionally, clean up the temporary file
|
|
os.remove(temp_filepath)
|
|
print(f"*** Temporary file removed: {temp_filepath}")
|
|
|
|
return img_hash
|
|
|
|
|
|
def image_hash(image, temp_dir):
|
|
"""Generate a hash for an image, handling both packed and unpacked images."""
|
|
if image.packed_file:
|
|
# Pass the current scene from the context to save_and_hash_image
|
|
return save_and_hash_image(image, temp_dir, bpy.context.scene)
|
|
else:
|
|
# For unpacked images, directly hash the file data
|
|
try:
|
|
abs_path = bpy.path.abspath(image.filepath)
|
|
with open(abs_path, 'rb') as f:
|
|
img_data = f.read()
|
|
img_hash = hashlib.md5(img_data).hexdigest()
|
|
return img_hash
|
|
except Exception as e:
|
|
print(f"*** Error processing image {image.name}: {e}")
|
|
return None
|
|
|
|
def check_image_nodes(node, images_info, seen_hashes, temp_dir):
|
|
"""Recursive function to check nodes for image textures, including in node groups, avoiding duplicates based on hash."""
|
|
if node.type == 'TEX_IMAGE':
|
|
image = node.image
|
|
if image is not None:
|
|
img_hash = image_hash(image, temp_dir)
|
|
if img_hash and img_hash not in seen_hashes:
|
|
seen_hashes.add(img_hash) # Mark this hash as seen
|
|
image_info = {
|
|
'name': image.name,
|
|
'filepath': image.filepath if not image.packed_file else "Packed Image",
|
|
'exists': os.path.exists(image.filepath) if not image.packed_file else "N/A (packed image)",
|
|
'packed': image.packed_file is not None,
|
|
'hash': img_hash
|
|
}
|
|
images_info.append(image_info)
|
|
elif node.type == 'GROUP':
|
|
# Dive into node groups
|
|
for group_node in node.node_tree.nodes:
|
|
check_image_nodes(group_node, images_info, seen_hashes, temp_dir)
|
|
|
|
def process_materials(temp_dir):
|
|
"""Process all materials in the scene."""
|
|
all_images = []
|
|
hash_to_image_info = {} # Maps hash values to image info for detecting duplicates
|
|
duplicates = {} # Dictionary to hold duplicate image mappings (old image -> unique image)
|
|
|
|
for mat in bpy.data.materials:
|
|
if mat.use_nodes:
|
|
images_info = []
|
|
seen_hashes = set() # Keep track of image hashes
|
|
for node in mat.node_tree.nodes:
|
|
check_image_nodes(node, images_info, seen_hashes, temp_dir)
|
|
for image_info in images_info:
|
|
img_hash = image_info['hash']
|
|
if img_hash in hash_to_image_info:
|
|
# If the hash is already present, this is a duplicate
|
|
old_image_info = image_info
|
|
unique_image_info = hash_to_image_info[img_hash]
|
|
duplicates[old_image_info['name']] = unique_image_info['name']
|
|
else:
|
|
# New unique image, track it
|
|
hash_to_image_info[img_hash] = image_info
|
|
all_images.append(image_info) # Track all processed images
|
|
|
|
# Print out all unique images found in the scene
|
|
print("\n*** All images processed in the scene: " + str(len(duplicates)))
|
|
for image in all_images:
|
|
print(f"- {image['name']} (Filepath: {image['filepath']}, Packed: {image['packed']}, Hash: {image['hash']})")
|
|
|
|
# Print duplicates
|
|
num_replaced = 0
|
|
if duplicates:
|
|
|
|
for old_image, unique_image in duplicates.items():
|
|
if old_image != unique_image:
|
|
print(old_image,unique_image)
|
|
print("\n*** Duplicate images found:")
|
|
print(f"- {old_image} -> {unique_image}")
|
|
# Replace all occurrences of old_image with unique_image in materials
|
|
replace_duplicate_image(old_image, unique_image)
|
|
num_replaced += 1
|
|
else:
|
|
print("*** No duplicate images found.")
|
|
#num_replaced = 1
|
|
|
|
#num_replaced -= 1
|
|
return num_replaced
|
|
|
|
def replace_duplicate_image(old_image_name, unique_image_name):
|
|
"""Replace all occurrences of old_image_name with unique_image_name in materials."""
|
|
print(f"*** Replacing {old_image_name} with {unique_image_name} in materials...")
|
|
for mat in bpy.data.materials:
|
|
if mat.use_nodes:
|
|
for node in mat.node_tree.nodes:
|
|
replace_image_in_node(node, old_image_name, unique_image_name)
|
|
|
|
def replace_image_in_node(node, old_image_name, unique_image_name):
|
|
"""Replace old_image_name with unique_image_name in a single node."""
|
|
if node.type == 'TEX_IMAGE' and node.image.name == old_image_name:
|
|
print(f"*** Found {old_image_name} in node {node.name}")
|
|
node.image = bpy.data.images[unique_image_name]
|
|
print(f"*** Replaced {old_image_name} with {unique_image_name} in node {node.name}")
|
|
elif node.type == 'GROUP':
|
|
# Recursively replace images within node groups
|
|
for group_node in node.node_tree.nodes:
|
|
replace_image_in_node(group_node, old_image_name, unique_image_name)
|
|
|
|
class SimpleConfirmOperator(bpy.types.Operator):
|
|
"""Simple Operator to confirm remapping."""
|
|
bl_idname = "object.simple_confirm_operator"
|
|
bl_label = "Remap Material Images"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
num_replaced = process_materials(temp_dir)
|
|
self.report({'INFO'}, f"Number of images replaced: " + str(num_replaced))
|
|
print("Images replaced " + str(num_replaced))
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
return wm.invoke_confirm(self, event)
|
|
|
|
|
|
class CleanUpUnusedMatImagesOperator(bpy.types.Operator):
|
|
bl_idname = "wm.clean_up_unused_mat_images"
|
|
bl_label = "Clean Up Unused Mat Images"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
# Remove unused images
|
|
removed_images = self.remove_unused_images()
|
|
|
|
if removed_images:
|
|
print("Removed unused material images:")
|
|
for image_name in removed_images:
|
|
print(f"- {image_name}")
|
|
else:
|
|
print("No unused material images found.")
|
|
|
|
self.report({'INFO'}, f"Number of images replaced: " + str(len(removed_images)))
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
def draw(self, context):
|
|
self.layout.label(text="Are you sure you want to clean up unused material images?")
|
|
# Additional UI components can be added here if necessary.
|
|
|
|
def remove_unused_images(self):
|
|
removed_image_names = []
|
|
images_to_remove = [image for image in bpy.data.images if not image.users]
|
|
for image in images_to_remove:
|
|
removed_image_names.append(image.name)
|
|
bpy.data.images.remove(image)
|
|
return removed_image_names
|
|
|
|
|
|
class SimplePanel(bpy.types.Panel):
|
|
"""Panel to house the Remap Material Images button."""
|
|
bl_label = "Remap Material Images Panel"
|
|
bl_idname = "OBJECT_PT_simple_panel"
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Edit'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.operator("object.simple_confirm_operator", text="Remap Material Images")
|
|
layout.operator("wm.clean_up_unused_mat_images", text="Delete Unused Images")
|
|
|
|
def register():
|
|
bpy.utils.register_class(SimpleConfirmOperator)
|
|
bpy.utils.register_class(SimplePanel)
|
|
bpy.utils.register_class(CleanUpUnusedMatImagesOperator)
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(SimpleConfirmOperator)
|
|
bpy.utils.unregister_class(SimplePanel)
|
|
bpy.utils.unregister_class(CleanUpUnusedMatImagesOperator)
|
|
|
|
if __name__ == "__main__":
|
|
register()
|