Asset-Frameworker/Deprecated/POC/Blender-MaterialsFromNodegroups.py
2025-04-29 18:26:13 +02:00

291 lines
19 KiB
Python

import bpy
from pathlib import Path
import time
import os
import math
# Try importing NumPy
try:
import numpy as np
numpy_available = True
# print("NumPy module found.") # Less verbose
except ImportError:
print("Warning: NumPy module not found. Median calc disabled, mean uses loop.")
numpy_available = False
# --- Configuration ---
ASSET_LIBRARY_NAME = "Nodes-Linked" # <<< Name of Asset Library in Prefs
TEMPLATE_MATERIAL_NAME = "Template_PBRMaterial" # <<< Name of template Material in current file
PLACEHOLDER_NODE_LABEL = "PBRSET_PLACEHOLDER" # <<< Label of placeholder node in template mat
ASSET_NAME_PREFIX = "PBRSET_" # <<< Prefix of Node Group assets to process
MATERIAL_NAME_PREFIX = "Mat_" # <<< Prefix for created Materials
THUMBNAIL_PROPERTY_NAME = "thumbnail_filepath" # <<< Custom property name on Node Groups
VALID_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
DERIVED_MAP_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.tif', '.tiff']
VIEWPORT_GAMMA = 0.4
SCALED_SIZE = (32, 32) # Downscale target size for calculations
# --- >>> SET MATERIAL CREATION LIMIT HERE <<< ---
# Max number of *new* materials created per run (0 = no limit)
MATERIAL_CREATION_LIMIT = 900
# ------------------------------------------------
# --- Helper Functions ---
def find_node_by_label(node_tree, label, node_type=None):
# Finds first node by label and optional type (using node.type)
if not node_tree: return None
for node in node_tree.nodes:
if node.label and node.label == label:
if node_type is None or node.type == node_type: return node
return None
def calculate_value_from_image(image, target_size=(64, 64), mode='color', method='median'):
# Calculates median/mean from downscaled image copy, cleans up temp image
temp_img = None; #... (Full implementation from previous step) ...
if not image: return None
try:
if not image.has_data:
try: _ = len(image.pixels); image.update()
except Exception: pass
if not image.has_data: return None # Cannot proceed
temp_img = image.copy()
if not temp_img: return None
temp_img.scale(target_size[0], target_size[1])
try: _ = len(temp_img.pixels); temp_img.update()
except Exception: pass # Ignore access error, check has_data
if not temp_img.has_data: return None
width=temp_img.size[0]; height=temp_img.size[1]; channels=temp_img.channels
if width == 0 or height == 0 or channels == 0: return None
pixels = temp_img.pixels[:]; result_value = None;
if numpy_available: # Use NumPy
np_pixels = np.array(pixels); num_elements = len(np_pixels); num_pixels_actual = num_elements // channels;
if num_pixels_actual == 0: return None
np_pixels = np_pixels[:num_pixels_actual * channels]; pixels_reshaped = np_pixels.reshape((num_pixels_actual, channels))
if mode == 'color': # Color Median/Mean (NumPy)
if channels < 3: return None
calc_linear = np.median(pixels_reshaped[:, :3], axis=0) if method == 'median' else np.mean(pixels_reshaped[:, :3], axis=0)
inv_gamma = 1.0 / VIEWPORT_GAMMA; calc_linear_clamped = np.clip(calc_linear, 0.0, None)
calc_srgb_np = np.power(calc_linear_clamped, inv_gamma); calc_srgb_clamped = np.clip(calc_srgb_np, 0.0, 1.0)
result_value = (calc_srgb_clamped[0], calc_srgb_clamped[1], calc_srgb_clamped[2], 1.0)
elif mode == 'grayscale': # Grayscale Median/Mean (NumPy)
calc_val = np.median(pixels_reshaped[:, 0]) if method == 'median' else np.mean(pixels_reshaped[:, 0])
result_value = min(max(0.0, calc_val), 1.0)
elif method == 'mean': # Fallback Mean Loop
# print(" Calculating mean using standard loop...") # Verbose
actual_len = len(pixels); #... (Mean loop logic) ...
if actual_len == 0: return None; num_pixels_in_buffer=actual_len//channels; max_elements=num_pixels_in_buffer*channels
if num_pixels_in_buffer == 0: return None
if mode == 'color':
sum_r,sum_g,sum_b = 0.0,0.0,0.0; step=channels
for i in range(0, max_elements, step):
if i+2 >= actual_len: break; sum_r+=pixels[i]; sum_g+=pixels[i+1]; sum_b+=pixels[i+2]
avg_r_lin,avg_g_lin,avg_b_lin = sum_r/num_pixels_in_buffer, sum_g/num_pixels_in_buffer, sum_b/num_pixels_in_buffer
inv_gamma = 1.0/VIEWPORT_GAMMA
avg_r_srgb,avg_g_srgb,avg_b_srgb = min(max(0.0,pow(max(0.0,avg_r_lin),inv_gamma)),1.0), min(max(0.0,pow(max(0.0,avg_g_lin),inv_gamma)),1.0), min(max(0.0,pow(max(0.0,avg_b_lin),inv_gamma)),1.0)
result_value = (avg_r_srgb, avg_g_srgb, avg_b_srgb, 1.0)
elif mode == 'grayscale':
sum_val=0.0; step=channels
for i in range(0, max_elements, step): sum_val+=pixels[i]
result_value = min(max(0.0, sum_val/num_pixels_in_buffer), 1.0)
else: print(" Error: NumPy required for median calculation."); return None
return result_value
except Exception as e: print(f" Error during value calculation for '{image.name}': {e}"); return None
finally: # Cleanup
if temp_img:
try: bpy.data.images.remove(temp_img, do_unlink=True)
except Exception: pass # Ignore cleanup errors
# --- Main Function ---
def create_materials_for_library_assets(library_name):
start_time = time.time(); print(f"--- Starting Material Creation for Library '{library_name}' ---")
print(f"Material Creation Limit per run: {'Unlimited' if MATERIAL_CREATION_LIMIT <= 0 else MATERIAL_CREATION_LIMIT}")
# (Prerequisite checks...)
template_mat=bpy.data.materials.get(TEMPLATE_MATERIAL_NAME); #... etc ...
if not template_mat or not template_mat.use_nodes or not find_node_by_label(template_mat.node_tree, PLACEHOLDER_NODE_LABEL, 'GROUP'): print("Template Prereq Failed."); return
library=bpy.context.preferences.filepaths.asset_libraries.get(library_name); #... etc ...
if not library or not Path(bpy.path.abspath(library.path)).exists(): print("Library Prereq Failed."); return
print(f"Found template material and library path...")
# (File scanning...)
materials_created=0; materials_skipped=0; nodegroups_processed=0; link_errors=0; files_to_process=[]; library_path_obj=Path(bpy.path.abspath(library.path))
#... (populate files_to_process) ...
if library_path_obj.is_dir():
for item in library_path_obj.iterdir():
if item.is_file() and item.suffix.lower() == '.blend': files_to_process.append(str(item))
if not files_to_process: print(f"Warning: No .blend files found in dir: {library_path_obj}")
elif library_path_obj.is_file() and library_path_obj.suffix.lower() == '.blend':
files_to_process.append(str(library_path_obj))
else: print(f"Error: Library path not dir or .blend: {library_path_obj}"); return
print(f"Found {len(files_to_process)} .blend file(s) to inspect.")
# Initialize counters and flag for limit
created_in_this_run = 0
limit_reached_flag = False
for blend_file_path in files_to_process: # ... (inspect loop) ...
print(f"\nInspecting library file: {os.path.basename(blend_file_path)}...")
potential_nodegroups = []; # ... (inspection logic) ...
try:
with bpy.data.libraries.load(blend_file_path, link=False) as (data_from, data_to): potential_nodegroups = list(data_from.node_groups)
except Exception as e_load_inspect: print(f" Error inspecting file '{blend_file_path}': {e_load_inspect}"); continue
print(f" Found {len(potential_nodegroups)} NGs. Checking for '{ASSET_NAME_PREFIX}'...")
for asset_nodegroup_name in potential_nodegroups: # ... (NG loop) ...
if not asset_nodegroup_name.startswith(ASSET_NAME_PREFIX): continue
nodegroups_processed += 1
base_name = asset_nodegroup_name.removeprefix(ASSET_NAME_PREFIX)
material_name = f"{MATERIAL_NAME_PREFIX}{base_name}"
if bpy.data.materials.get(material_name): materials_skipped += 1; continue
linked_nodegroup = None; preview_path = None
try: # --- Start Main Processing Block for NG ---
# (Linking logic...)
existing_group = bpy.data.node_groups.get(asset_nodegroup_name); #... etc linking ...
is_correctly_linked = (existing_group and existing_group.library and bpy.path.abspath(existing_group.library.filepath) == blend_file_path)
if is_correctly_linked: linked_nodegroup = existing_group
else: # Link it
with bpy.data.libraries.load(blend_file_path, link=True, relative=False) as (data_from, data_to):
if asset_nodegroup_name in data_from.node_groups: data_to.node_groups = [asset_nodegroup_name]
else: print(f" Error: NG '{asset_nodegroup_name}' not found during link."); continue # Skip NG
linked_nodegroup = bpy.data.node_groups.get(asset_nodegroup_name)
if not linked_nodegroup or not linked_nodegroup.library: print(f" Error: NG '{asset_nodegroup_name}' link failed."); linked_nodegroup = None; link_errors += 1
if not linked_nodegroup: print(f" Failed link NG '{asset_nodegroup_name}'. Skip."); continue # Skip NG
preview_path = linked_nodegroup.get(THUMBNAIL_PROPERTY_NAME) # Path to COL-1 1K
# (Duplicate, Rename, Replace Placeholder...)
new_material = template_mat.copy(); #... checks ...
if not new_material: print(f" Error: Failed copy template mat. Skip."); continue
new_material.name = material_name
if not new_material.use_nodes or not new_material.node_tree: print(f" Error: New mat '{material_name}' no nodes."); continue
placeholder_node = find_node_by_label(new_material.node_tree, PLACEHOLDER_NODE_LABEL, 'GROUP'); #... checks ...
if not placeholder_node: print(f" Error: Placeholder '{PLACEHOLDER_NODE_LABEL}' not found."); continue
placeholder_node.node_tree = linked_nodegroup
print(f" Created material '{material_name}' and linked NG '{linked_nodegroup.name}'.")
# --- Load base COL-1 image once ---
thumbnail_image = None
if preview_path and Path(preview_path).is_file():
try: thumbnail_image = bpy.data.images.load(preview_path, check_existing=True)
except Exception as e_load_base: print(f" Error loading base thumbnail '{preview_path}': {e_load_base}")
# --- Set Viewport Color (Median) ---
median_color = None
if thumbnail_image: median_color = calculate_value_from_image(thumbnail_image, target_size=SCALED_SIZE, mode='color', method='median')
if median_color: new_material.diffuse_color = median_color; print(f" Set viewport color: {median_color[:3]}")
else: print(f" Warn: Could not set viewport color.")
# --- Determine Paths and Metal Map Existence ---
roughness_path = None; metallic_path = None; metal_map_found = False; #... etc ...
if preview_path and "_COL-1" in preview_path:
try: # ... path derivation logic ...
base_path_obj=Path(preview_path); directory=base_path_obj.parent; base_stem=base_path_obj.stem
if "_COL-1" in base_stem:
rough_stem=base_stem.replace("_COL-1", "_ROUGH")
for ext in DERIVED_MAP_EXTENSIONS:
potential_path=directory/f"{rough_stem}{ext}";
if potential_path.is_file(): roughness_path=str(potential_path); break
metal_stem=base_stem.replace("_COL-1", "_METAL")
for ext in DERIVED_MAP_EXTENSIONS:
potential_path=directory/f"{metal_stem}{ext}";
if potential_path.is_file(): metallic_path=str(potential_path); metal_map_found=True; break
except Exception as e_derive: print(f" Error deriving paths: {e_derive}")
if not metal_map_found: print(f" Info: No METAL map found. Assuming Spec/Gloss.")
# --- Set Viewport Roughness (Median, Conditional Inversion) ---
median_roughness = None; # ... etc ...
if roughness_path:
try: rough_img = bpy.data.images.load(roughness_path, check_existing=True)
except Exception as e_load_rough: print(f" Error loading rough image: {e_load_rough}")
if rough_img: median_roughness = calculate_value_from_image(rough_img, target_size=SCALED_SIZE, mode='grayscale', method='median')
else: print(f" Error: load None for rough path.")
if median_roughness is not None:
final_roughness_value = median_roughness
if not metal_map_found: final_roughness_value = 1.0 - median_roughness; print(f" Inverting ROUGH->Gloss: {median_roughness:.3f} -> {final_roughness_value:.3f}")
new_material.roughness = min(max(0.0, final_roughness_value), 1.0); print(f" Set viewport roughness: {new_material.roughness:.3f}")
else: print(f" Warn: Could not set viewport roughness.")
# --- Set Viewport Metallic (Median) ---
median_metallic = None; # ... etc ...
if metal_map_found:
try: metal_img = bpy.data.images.load(metallic_path, check_existing=True)
except Exception as e_load_metal: print(f" Error loading metal image: {e_load_metal}")
if metal_img: median_metallic = calculate_value_from_image(metal_img, target_size=SCALED_SIZE, mode='grayscale', method='median')
else: print(f" Error: load None for metal path.")
if median_metallic is not None: new_material.metallic = median_metallic; print(f" Set viewport metallic: {median_metallic:.3f}")
else: new_material.metallic = 0.0; # Default
if metal_map_found: print(f" Warn: Could not calc viewport metallic. Set 0.0.")
else: print(f" Set viewport metallic to default: 0.0")
# --- Mark Material as Asset ---
mat_asset_data = None; # ... (logic remains same) ...
try: # ... asset marking ...
if not new_material.asset_data: new_material.asset_mark(); print(f" Marked material as asset.")
mat_asset_data = new_material.asset_data
except Exception as e_asset: print(f" Error marking mat asset: {e_asset}")
# --- Copy Asset Tags ---
if mat_asset_data and linked_nodegroup.asset_data: # ... (logic remains same) ...
try: # ... tag copying ...
source_tags=linked_nodegroup.asset_data.tags; target_tags=mat_asset_data.tags
tags_copied_count=0; existing_target_tag_names={t.name for t in target_tags}
for src_tag in source_tags:
if src_tag.name not in existing_target_tag_names: target_tags.new(name=src_tag.name); tags_copied_count += 1
if tags_copied_count > 0: print(f" Copied {tags_copied_count} asset tags.")
except Exception as e_tags: print(f" Error copying tags: {e_tags}")
# --- Set Custom Preview for Material ---
if preview_path and Path(preview_path).is_file(): # ... (logic remains same) ...
try: # ... preview setting ...
with bpy.context.temp_override(id=new_material): bpy.ops.ed.lib_id_load_custom_preview(filepath=preview_path)
except RuntimeError as e_op: print(f" Error running preview op for mat '{new_material.name}': {e_op}")
except Exception as e_prev: print(f" Unexpected preview error for mat: {e_prev}")
elif preview_path: print(f" Warn: Thumb path not found for preview step: '{preview_path}'")
# --- Increment Counters & Check Limit ---
materials_created += 1 # Overall counter for summary
created_in_this_run += 1 # Counter for this run's limit
# Check limit AFTER successful creation
if MATERIAL_CREATION_LIMIT > 0 and created_in_this_run >= MATERIAL_CREATION_LIMIT:
print(f"\n--- Material Creation Limit ({MATERIAL_CREATION_LIMIT}) Reached ---")
limit_reached_flag = True
break # Exit inner loop
except Exception as e: # Catch errors for the whole NG processing block
print(f" An unexpected error occurred processing NG '{asset_nodegroup_name}': {e}")
# --- End Main Processing Block for NG ---
# Check flag to stop outer loop
if limit_reached_flag:
print("Stopping library file iteration due to limit.")
break # Exit outer loop
# (Completion summary...)
end_time = time.time(); duration = end_time - start_time; print("\n--- Material Creation Finished ---"); # ... etc ...
print(f"Duration: {duration:.2f} seconds")
print(f"Summary: Processed {nodegroups_processed} NGs. Created {materials_created} Mats this run. Skipped {materials_skipped}. Link Errors {link_errors}.")
if limit_reached_flag: print(f"NOTE: Script stopped early due to creation limit ({MATERIAL_CREATION_LIMIT}). Run again to process more.")
# --- How to Run ---
# 1. Rerun Script 1 to add "thumbnail_filepath" property.
# 2. Setup Asset Library in Prefs. Set ASSET_LIBRARY_NAME below.
# 3. In current file, create "Template_PBRMaterial" with "PBRSET_PLACEHOLDER" node.
# 4. Set MATERIAL_CREATION_LIMIT in Config section above (0 for unlimited).
# 5. Paste script & Run (Alt+P).
if __name__ == "__main__":
# Only need ASSET_LIBRARY_NAME configuration here now
if ASSET_LIBRARY_NAME == "My Asset Library": # Default check
print("\nERROR: Please update the 'ASSET_LIBRARY_NAME' variable in the script's Configuration section.")
print(" Set it to the name of your asset library in Blender Preferences before running.\n")
elif not bpy.data.materials.get(TEMPLATE_MATERIAL_NAME):
print(f"\nERROR: Template material '{TEMPLATE_MATERIAL_NAME}' not found in current file.\n")
else:
create_materials_for_library_assets(ASSET_LIBRARY_NAME)