291 lines
19 KiB
Python
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) |