Asset-Frameworker/.lh/blenderscripts/create_materials.py.json
2025-04-29 18:26:13 +02:00

46 lines
137 KiB
JSON

{
"sourceFile": "blenderscripts/create_materials.py",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 7,
"patches": [
{
"date": 1745258817112,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1745259821809,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,10 +1,15 @@\n # blenderscripts/create_materials.py\r\n-# Version: 1.0\r\n+# Version: 1.1\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n-# reads metadata.json files, finds corresponding PBRSET node groups,\r\n-# and creates/updates Blender materials linking to those node groups.\r\n+# reads metadata.json files, finds corresponding PBRSET node groups\r\n+# in a specified Blender Asset Library, and creates Blender materials\r\n+# linking to those node groups. Skips assets if material already exists.\r\n # Sets material viewport properties and custom previews based on metadata.\r\n+# Changes v1.1:\r\n+# - Added logic to link PBRSET node groups from an external asset library.\r\n+# - Added logic to skip processing if the target material already exists.\r\n+# - Added configuration for the PBRSET asset library name.\r\n \r\n import bpy\r\n import os\r\n import json\r\n@@ -18,8 +23,12 @@\n # Example: r\"G:\\Assets\\Processed\"\r\n # IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n \r\n+# Name of the Blender Asset Library (configured in Blender Preferences)\r\n+# that contains the PBRSET node groups created by create_nodegroups.py\r\n+PBRSET_ASSET_LIBRARY_NAME = \"Nodes-Linked\" # <<< CHANGE THIS NAME!\r\n+\r\n # Name of the required template material in the Blender file\r\n TEMPLATE_MATERIAL_NAME = \"Template_PBRMaterial\"\r\n \r\n # Label of the placeholder Group node within the template material's node tree\r\n@@ -194,27 +203,46 @@\n # --- Core Logic ---\r\n \r\n def process_library_for_materials(context):\r\n \"\"\"\r\n- Scans the library, reads metadata, finds PBRSET node groups,\r\n- and creates/updates materials linking to them.\r\n+ Scans the library, reads metadata, finds PBRSET node groups in the specified\r\n+ asset library, and creates/updates materials linking to them.\r\n \"\"\"\r\n start_time = time.time()\r\n print(f\"\\n--- Starting Material Creation from Node Groups ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n \r\n # --- Pre-run Checks ---\r\n print(\"Performing pre-run checks...\")\r\n valid_setup = True\r\n- # 1. Check Library Root Path\r\n+ # 1. Check Processed Asset Library Root Path\r\n root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n if not root_path.is_dir():\r\n print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n print(f\"!!! '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n valid_setup = False\r\n else:\r\n- print(f\" Asset Library Root: '{root_path}'\")\r\n+ print(f\" Processed Asset Library Root: '{root_path}'\")\r\n \r\n- # 2. Check Template Material and Placeholder Node\r\n+ # 2. Check PBRSET Asset Library Name and Path\r\n+ pbrset_library = context.preferences.filepaths.asset_libraries.get(PBRSET_ASSET_LIBRARY_NAME)\r\n+ pbrset_library_path = None\r\n+ if not pbrset_library:\r\n+ print(f\"!!! ERROR: PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}' not found in Blender Preferences.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ pbrset_library_path_str = bpy.path.abspath(pbrset_library.path)\r\n+ pbrset_library_path = Path(pbrset_library_path_str)\r\n+ # Check if the library path points to a single .blend file or a directory\r\n+ if not pbrset_library_path.exists() or (not pbrset_library_path.is_file() and not pbrset_library_path.is_dir()):\r\n+ print(f\"!!! ERROR: Path for PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}' is invalid or does not exist:\")\r\n+ print(f\"!!! '{pbrset_library_path_str}'\")\r\n+ valid_setup = False\r\n+ pbrset_library_path = None # Ensure it's None if invalid\r\n+ else:\r\n+ print(f\" PBRSET Asset Library: '{PBRSET_ASSET_LIBRARY_NAME}' -> '{pbrset_library_path_str}'\")\r\n+\r\n+\r\n+ # 3. Check Template Material and Placeholder Node\r\n template_mat = bpy.data.materials.get(TEMPLATE_MATERIAL_NAME)\r\n placeholder_node_found_in_template = False\r\n if not template_mat:\r\n print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' not found in this Blender file.\")\r\n@@ -239,18 +267,20 @@\n \r\n # --- Initialize Counters ---\r\n metadata_files_found = 0\r\n assets_processed = 0\r\n+ assets_skipped = 0\r\n materials_created = 0\r\n- materials_updated = 0\r\n+ # materials_updated = 0 # Not updating existing materials anymore\r\n node_groups_linked = 0\r\n previews_set = 0\r\n viewport_colors_set = 0\r\n viewport_roughness_set = 0\r\n viewport_metallic_set = 0\r\n errors_encountered = 0\r\n- pbrset_groups_missing = 0\r\n+ pbrset_groups_missing_in_library = 0\r\n placeholder_nodes_missing = 0\r\n+ library_link_errors = 0\r\n # --- End Counters ---\r\n \r\n print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n \r\n@@ -271,8 +301,40 @@\n print(\"No metadata files found. Nothing to process.\")\r\n print(\"--- Script Finished ---\")\r\n return True # No work needed is considered success\r\n \r\n+ # --- Determine the .blend file path for the PBRSET library ---\r\n+ # If the library path is a directory, we assume the node groups are in a file named like the library\r\n+ # This might need adjustment based on how the user organizes their library file(s)\r\n+ pbrset_blend_file_path = None\r\n+ if pbrset_library_path:\r\n+ if pbrset_library_path.is_file() and pbrset_library_path.suffix.lower() == '.blend':\r\n+ pbrset_blend_file_path = str(pbrset_library_path)\r\n+ elif pbrset_library_path.is_dir():\r\n+ # Attempt to find a .blend file named like the library or a common name\r\n+ potential_blend_name = f\"{PBRSET_ASSET_LIBRARY_NAME}.blend\"\r\n+ potential_path = pbrset_library_path / potential_blend_name\r\n+ if potential_path.is_file():\r\n+ pbrset_blend_file_path = str(potential_path)\r\n+ else:\r\n+ # Fallback: look for any .blend file (take the first one found) - less reliable\r\n+ found_blend = next(pbrset_library_path.glob('*.blend'), None)\r\n+ if found_blend:\r\n+ pbrset_blend_file_path = str(found_blend)\r\n+ print(f\" Warning: PBRSET library path is a directory. Using first found .blend file: {found_blend.name}\")\r\n+ else:\r\n+ print(f\"!!! ERROR: PBRSET library path is a directory, but no .blend file found inside.\")\r\n+ errors_encountered += 1 # Count as error if no blend file found\r\n+\r\n+ if pbrset_blend_file_path:\r\n+ print(f\" Using PBRSET library file: '{pbrset_blend_file_path}'\")\r\n+ else:\r\n+ print(f\"!!! ERROR: Could not determine the .blend file for the PBRSET library '{PBRSET_ASSET_LIBRARY_NAME}'.\")\r\n+ # No point continuing if we can't find the source blend file\r\n+ print(\"\\n--- Script aborted: Cannot find PBRSET library .blend file. ---\")\r\n+ return False\r\n+\r\n+\r\n # --- Process Each Metadata File ---\r\n for metadata_path in metadata_paths:\r\n asset_dir_path = metadata_path.parent\r\n print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n@@ -283,18 +345,15 @@\n # --- Extract Key Info ---\r\n asset_name = metadata.get(\"asset_name\")\r\n supplier_name = metadata.get(\"supplier_name\")\r\n archetype = metadata.get(\"archetype\")\r\n- # Get map info needed for preview path reconstruction\r\n processed_resolutions = metadata.get(\"processed_map_resolutions\", {})\r\n merged_resolutions = metadata.get(\"merged_map_resolutions\", {})\r\n map_details = metadata.get(\"map_details\", {})\r\n- image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n+ image_stats_1k = metadata.get(\"image_stats_1k\")\r\n \r\n- # Combine processed and merged maps to find reference map\r\n all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n \r\n- # Validate essential data\r\n if not asset_name:\r\n print(f\" !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n errors_encountered += 1\r\n continue\r\n@@ -304,83 +363,112 @@\n # --- Determine Target Names ---\r\n target_material_name = f\"{MATERIAL_NAME_PREFIX}{asset_name}\"\r\n target_pbrset_group_name = f\"{PBRSET_GROUP_PREFIX}{asset_name}\"\r\n \r\n- # --- Find PBRSET Node Group ---\r\n- pbrset_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n- if not pbrset_group:\r\n- print(f\" !!! WARNING: PBRSET Node Group '{target_pbrset_group_name}' not found. Cannot create/update material. Run create_nodegroups.py first?\")\r\n- pbrset_groups_missing += 1\r\n- continue # Skip this asset if the required node group doesn't exist\r\n+ # --- Check if Material Already Exists (Skip Logic) ---\r\n+ if bpy.data.materials.get(target_material_name):\r\n+ print(f\" Skipping asset '{asset_name}': Material '{target_material_name}' already exists.\")\r\n+ assets_skipped += 1\r\n+ continue # Move to the next metadata file\r\n \r\n- # --- Find or Create Material ---\r\n- material = bpy.data.materials.get(target_material_name)\r\n- is_new_material = False\r\n+ # --- Create New Material ---\r\n+ print(f\" Creating new material: '{target_material_name}'\")\r\n+ material = template_mat.copy()\r\n+ if not material:\r\n+ print(f\" !!! ERROR: Failed to copy template material '{TEMPLATE_MATERIAL_NAME}'. Skipping asset '{asset_name}'.\")\r\n+ errors_encountered += 1\r\n+ continue\r\n+ material.name = target_material_name\r\n+ materials_created += 1\r\n \r\n- if material is None:\r\n- print(f\" Creating new material: '{target_material_name}'\")\r\n- material = template_mat.copy()\r\n- if not material:\r\n- print(f\" !!! ERROR: Failed to copy template material '{TEMPLATE_MATERIAL_NAME}'. Skipping asset '{asset_name}'.\")\r\n- errors_encountered += 1\r\n- continue\r\n- material.name = target_material_name\r\n- materials_created += 1\r\n- is_new_material = True\r\n- else:\r\n- print(f\" Updating existing material: '{target_material_name}'\")\r\n- materials_updated += 1\r\n-\r\n- # Ensure material uses nodes\r\n+ # --- Find Placeholder Node ---\r\n if not material.use_nodes or not material.node_tree:\r\n- print(f\" !!! ERROR: Material '{material.name}' does not use nodes or has no node tree. Skipping node linking.\")\r\n- # Continue to set other properties if possible\r\n+ print(f\" !!! ERROR: Newly created material '{material.name}' does not use nodes or has no node tree. Skipping node linking.\")\r\n+ placeholder_node = None # Ensure it's None\r\n else:\r\n- # --- Find Placeholder Node ---\r\n placeholder_nodes = find_nodes_by_label(material.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n if not placeholder_nodes:\r\n print(f\" !!! WARNING: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in material '{material.name}'. Cannot link PBRSET group.\")\r\n placeholder_nodes_missing += 1\r\n+ placeholder_node = None # Ensure it's None\r\n else:\r\n placeholder_node = placeholder_nodes[0] # Assume first is correct\r\n- # --- Link Node Group to Placeholder ---\r\n- if placeholder_node.node_tree != pbrset_group:\r\n- try:\r\n- placeholder_node.node_tree = pbrset_group\r\n- print(f\" Linked PBRSET group '{pbrset_group.name}' to placeholder '{placeholder_node.label}'.\")\r\n- node_groups_linked += 1\r\n- except TypeError as e_assign:\r\n- print(f\" !!! ERROR: Could not assign PBRSET group to placeholder '{placeholder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n- errors_encountered += 1\r\n- except Exception as e_link:\r\n- print(f\" !!! UNEXPECTED ERROR linking PBRSET group: {e_link}\")\r\n- errors_encountered += 1\r\n \r\n+ # --- Find and Link PBRSET Node Group from Library ---\r\n+ linked_pbrset_group = None\r\n+ if placeholder_node and pbrset_blend_file_path: # Only proceed if placeholder exists and library file is known\r\n+ # Check if the group is already linked in the current file\r\n+ existing_linked_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n+ if existing_linked_group and existing_linked_group.library and bpy.path.abspath(existing_linked_group.library.filepath) == pbrset_blend_file_path:\r\n+ linked_pbrset_group = existing_linked_group\r\n+ print(f\" Found existing linked PBRSET group: '{linked_pbrset_group.name}'\")\r\n+ else:\r\n+ # Link the node group from the external file\r\n+ print(f\" Attempting to link PBRSET group '{target_pbrset_group_name}' from '{Path(pbrset_blend_file_path).name}'...\")\r\n+ try:\r\n+ with bpy.data.libraries.load(pbrset_blend_file_path, link=True, relative=False) as (data_from, data_to):\r\n+ if target_pbrset_group_name in data_from.node_groups:\r\n+ data_to.node_groups = [target_pbrset_group_name]\r\n+ else:\r\n+ print(f\" !!! ERROR: Node group '{target_pbrset_group_name}' not found in library file.\")\r\n+ pbrset_groups_missing_in_library += 1\r\n+\r\n+ # Verify linking was successful\r\n+ linked_pbrset_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n+ if not linked_pbrset_group or not linked_pbrset_group.library:\r\n+ print(f\" !!! ERROR: Failed to link node group '{target_pbrset_group_name}'.\")\r\n+ library_link_errors += 1\r\n+ linked_pbrset_group = None # Ensure it's None on failure\r\n+ else:\r\n+ print(f\" Successfully linked node group: '{linked_pbrset_group.name}'\")\r\n+\r\n+ except Exception as e_lib_load:\r\n+ print(f\" !!! ERROR loading library or linking node group: {e_lib_load}\")\r\n+ library_link_errors += 1\r\n+ linked_pbrset_group = None # Ensure it's None on failure\r\n+\r\n+ # --- Link Linked Node Group to Placeholder ---\r\n+ if placeholder_node and linked_pbrset_group:\r\n+ if placeholder_node.node_tree != linked_pbrset_group:\r\n+ try:\r\n+ placeholder_node.node_tree = linked_pbrset_group\r\n+ print(f\" Linked PBRSET group '{linked_pbrset_group.name}' to placeholder '{placeholder_node.label}'.\")\r\n+ node_groups_linked += 1\r\n+ except TypeError as e_assign:\r\n+ print(f\" !!! ERROR: Could not assign linked PBRSET group to placeholder '{placeholder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_link:\r\n+ print(f\" !!! UNEXPECTED ERROR linking PBRSET group to placeholder: {e_link}\")\r\n+ errors_encountered += 1\r\n+ elif placeholder_node and not linked_pbrset_group:\r\n+ print(f\" Info: Cannot link node group as it was not found or failed to link.\")\r\n+ # No 'else' needed if placeholder_node is None, error already logged\r\n+\r\n+\r\n # --- Mark Material as Asset ---\r\n if not material.asset_data:\r\n try:\r\n material.asset_mark()\r\n print(f\" Marked material '{material.name}' as asset.\")\r\n except Exception as e_mark:\r\n print(f\" !!! ERROR: Failed to mark material '{material.name}' as asset: {e_mark}\")\r\n- # Continue if possible\r\n \r\n # --- Copy Asset Tags ---\r\n- if material.asset_data and pbrset_group.asset_data:\r\n+ if material.asset_data and linked_pbrset_group and linked_pbrset_group.asset_data:\r\n tags_copied_count = 0\r\n if supplier_name:\r\n if add_tag_if_new(material.asset_data, supplier_name): tags_copied_count += 1\r\n if archetype:\r\n if add_tag_if_new(material.asset_data, archetype): tags_copied_count += 1\r\n- # Copy other tags from PBRSET group if needed\r\n- for ng_tag in pbrset_group.asset_data.tags:\r\n+ # Copy other tags from PBRSET group\r\n+ for ng_tag in linked_pbrset_group.asset_data.tags:\r\n if add_tag_if_new(material.asset_data, ng_tag.name): tags_copied_count += 1\r\n # if tags_copied_count > 0: print(f\" Copied {tags_copied_count} asset tags to material.\") # Optional info\r\n- # else: print(f\" Warn: Cannot copy tags. Material asset_data: {material.asset_data is not None}, Group asset_data: {pbrset_group.asset_data is not None}\") # Debug\r\n+ # else: print(f\" Warn: Cannot copy tags. Material asset_data: {material.asset_data is not None}, Linked Group: {linked_pbrset_group}, Group asset_data: {linked_pbrset_group.asset_data if linked_pbrset_group else None}\") # Debug\r\n \r\n+\r\n # --- Set Custom Preview ---\r\n ref_image_path = None\r\n- # Find reference image path (similar logic to create_nodegroups.py)\r\n for ref_map_type in REFERENCE_MAP_TYPES:\r\n if ref_map_type in all_map_resolutions:\r\n available_resolutions = all_map_resolutions[ref_map_type]\r\n lowest_res = None\r\n@@ -398,14 +486,13 @@\n resolution=lowest_res,\r\n primary_format=ref_format\r\n )\r\n if ref_image_path:\r\n- break # Found a suitable reference image path\r\n+ break\r\n \r\n if ref_image_path and material.asset_data:\r\n print(f\" Attempting to set preview from: {Path(ref_image_path).name}\")\r\n try:\r\n- # Ensure the ID (material) is the active one for the operator context\r\n with context.temp_override(id=material):\r\n bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n print(f\" Successfully set custom preview for material.\")\r\n previews_set += 1\r\n@@ -425,63 +512,49 @@\n if image_stats_1k and isinstance(image_stats_1k, dict):\r\n # Viewport Color\r\n color_mean = get_stat_value(image_stats_1k, VIEWPORT_COLOR_MAP_TYPES, 'mean')\r\n if isinstance(color_mean, list) and len(color_mean) >= 3:\r\n- # Add alpha if missing (default to 1.0)\r\n color_rgba = (*color_mean[:3], 1.0)\r\n- # Check if update is needed (compare first 3 elements)\r\n if tuple(material.diffuse_color[:3]) != tuple(color_rgba[:3]):\r\n material.diffuse_color = color_rgba\r\n print(f\" Set viewport color: {color_rgba[:3]}\")\r\n viewport_colors_set += 1\r\n- # else: print(f\" Info: Could not find valid 'mean' stat for viewport color in {VIEWPORT_COLOR_MAP_TYPES}.\") # Optional\r\n \r\n # Viewport Roughness & Metallic Check\r\n roughness_mean = get_stat_value(image_stats_1k, VIEWPORT_ROUGHNESS_MAP_TYPES, 'mean')\r\n metallic_mean = get_stat_value(image_stats_1k, VIEWPORT_METALLIC_MAP_TYPES, 'mean')\r\n metal_map_found = metallic_mean is not None\r\n \r\n # Roughness\r\n if roughness_mean is not None:\r\n- # Extract float value if it's a list (e.g., [0.5])\r\n rough_val = roughness_mean[0] if isinstance(roughness_mean, list) else roughness_mean\r\n if isinstance(rough_val, (float, int)):\r\n final_roughness = float(rough_val)\r\n- # Invert if metal map is NOT found (Gloss workflow)\r\n if not metal_map_found:\r\n final_roughness = 1.0 - final_roughness\r\n- # print(f\" Inverting roughness (no metal map): {rough_val:.3f} -> {final_roughness:.3f}\") # Debug\r\n- final_roughness = max(0.0, min(1.0, final_roughness)) # Clamp\r\n- # Check if update is needed\r\n+ final_roughness = max(0.0, min(1.0, final_roughness))\r\n if abs(material.roughness - final_roughness) > 0.001:\r\n material.roughness = final_roughness\r\n print(f\" Set viewport roughness: {final_roughness:.3f}\")\r\n viewport_roughness_set += 1\r\n- # else: print(f\" Info: Roughness 'mean' stat is not a valid number: {roughness_mean}\") # Optional\r\n- # else: print(f\" Info: Could not find 'mean' stat for viewport roughness in {VIEWPORT_ROUGHNESS_MAP_TYPES}.\") # Optional\r\n \r\n # Metallic\r\n if metal_map_found:\r\n metal_val = metallic_mean[0] if isinstance(metallic_mean, list) else metallic_mean\r\n if isinstance(metal_val, (float, int)):\r\n- final_metallic = max(0.0, min(1.0, float(metal_val))) # Clamp\r\n- # Check if update is needed\r\n+ final_metallic = max(0.0, min(1.0, float(metal_val)))\r\n if abs(material.metallic - final_metallic) > 0.001:\r\n material.metallic = final_metallic\r\n print(f\" Set viewport metallic: {final_metallic:.3f}\")\r\n viewport_metallic_set += 1\r\n- # else: print(f\" Info: Metallic 'mean' stat is not a valid number: {metallic_mean}\") # Optional\r\n else:\r\n- # If metal map wasn't found, ensure metallic is 0.0\r\n if material.metallic != 0.0:\r\n material.metallic = 0.0\r\n print(f\" Set viewport metallic to default: 0.0 (No metal map found)\")\r\n- viewport_metallic_set += 1 # Count setting default as a set action\r\n+ viewport_metallic_set += 1\r\n \r\n- # else: print(f\" Warn: 'image_stats_1k' missing or invalid in metadata. Cannot set viewport properties.\") # Optional\r\n+ assets_processed += 1 # Count assets where processing was attempted (even if errors occurred later)\r\n \r\n- assets_processed += 1\r\n-\r\n except FileNotFoundError:\r\n print(f\" !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n errors_encountered += 1\r\n except json.JSONDecodeError:\r\n@@ -489,11 +562,10 @@\n errors_encountered += 1\r\n except Exception as e_main_loop:\r\n print(f\" !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n import traceback\r\n- traceback.print_exc() # Print detailed traceback for debugging\r\n+ traceback.print_exc()\r\n errors_encountered += 1\r\n- # Continue to the next asset\r\n \r\n # --- End Metadata File Loop ---\r\n \r\n # --- Final Summary ---\r\n@@ -502,17 +574,20 @@\n print(\"\\n--- Material Creation Script Finished ---\")\r\n print(f\"Duration: {duration:.2f} seconds\")\r\n print(f\"Metadata Files Found: {metadata_files_found}\")\r\n print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+ print(f\"Assets Skipped (Already Exist): {assets_skipped}\")\r\n print(f\"Materials Created: {materials_created}\")\r\n- print(f\"Materials Updated: {materials_updated}\")\r\n+ # print(f\"Materials Updated: {materials_updated}\") # Removed as we skip existing\r\n print(f\"PBRSET Node Groups Linked: {node_groups_linked}\")\r\n print(f\"Material Previews Set: {previews_set}\")\r\n print(f\"Viewport Colors Set: {viewport_colors_set}\")\r\n print(f\"Viewport Roughness Set: {viewport_roughness_set}\")\r\n print(f\"Viewport Metallic Set: {viewport_metallic_set}\")\r\n- if pbrset_groups_missing > 0:\r\n- print(f\"!!! PBRSET Node Groups Missing: {pbrset_groups_missing} !!!\")\r\n+ if pbrset_groups_missing_in_library > 0:\r\n+ print(f\"!!! PBRSET Node Groups Missing in Library '{PBRSET_ASSET_LIBRARY_NAME}': {pbrset_groups_missing_in_library} !!!\")\r\n+ if library_link_errors > 0:\r\n+ print(f\"!!! Library Link Errors: {library_link_errors} !!!\")\r\n if placeholder_nodes_missing > 0:\r\n print(f\"!!! Placeholder Nodes Missing in Materials: {placeholder_nodes_missing} !!!\")\r\n if errors_encountered > 0:\r\n print(f\"!!! Other Errors Encountered: {errors_encountered} !!!\")\r\n@@ -526,10 +601,8 @@\n if __name__ == \"__main__\":\r\n # Ensure we are running within Blender\r\n try:\r\n import bpy\r\n- # import base64 # Already imported above\r\n except ImportError:\r\n print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n else:\r\n- # Pass Blender's current context to the processing function\r\n process_library_for_materials(bpy.context)\n\\ No newline at end of file\n"
},
{
"date": 1745261978731,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -201,16 +201,26 @@\n \r\n \r\n # --- Core Logic ---\r\n \r\n-def process_library_for_materials(context):\r\n+def process_library_for_materials(context, asset_library_root_override=None): # Add override parameter\r\n+ global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n \"\"\"\r\n Scans the library, reads metadata, finds PBRSET node groups in the specified\r\n asset library, and creates/updates materials linking to them.\r\n \"\"\"\r\n start_time = time.time()\r\n print(f\"\\n--- Starting Material Creation from Node Groups ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n \r\n+ # --- Determine Asset Library Root ---\r\n+ if asset_library_root_override:\r\n+ PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+ print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+ elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+ print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+ print(\"--- Script aborted. ---\")\r\n+ return False\r\n+\r\n # --- Pre-run Checks ---\r\n print(\"Performing pre-run checks...\")\r\n valid_setup = True\r\n # 1. Check Processed Asset Library Root Path\r\n@@ -513,8 +523,9 @@\n # Viewport Color\r\n color_mean = get_stat_value(image_stats_1k, VIEWPORT_COLOR_MAP_TYPES, 'mean')\r\n if isinstance(color_mean, list) and len(color_mean) >= 3:\r\n color_rgba = (*color_mean[:3], 1.0)\r\n+ print(f\" Debug: Raw color_mean from metadata: {color_mean[:3]}\") # Added logging\r\n if tuple(material.diffuse_color[:3]) != tuple(color_rgba[:3]):\r\n material.diffuse_color = color_rgba\r\n print(f\" Set viewport color: {color_rgba[:3]}\")\r\n viewport_colors_set += 1\r\n@@ -601,8 +612,25 @@\n if __name__ == \"__main__\":\r\n # Ensure we are running within Blender\r\n try:\r\n import bpy\r\n+ import sys\r\n except ImportError:\r\n print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n else:\r\n\\ No newline at end of file\n- process_library_for_materials(bpy.context)\n+ # --- Argument Parsing for Asset Library Root ---\r\n+ asset_root_arg = None\r\n+ try:\r\n+ # Blender arguments passed after '--' appear in sys.argv\r\n+ if \"--\" in sys.argv:\r\n+ args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+ if len(args_after_dash) >= 1:\r\n+ asset_root_arg = args_after_dash[0]\r\n+ print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+ else:\r\n+ print(\"Info: '--' found but no arguments after it.\")\r\n+ # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+ except Exception as e:\r\n+ print(f\"Error parsing command line arguments: {e}\")\r\n+ # --- End Argument Parsing ---\r\n+\r\n+ process_library_for_materials(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
},
{
"date": 1745261995905,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -15,15 +15,17 @@\n import json\r\n from pathlib import Path\r\n import time\r\n import base64 # Although not directly used here, keep for consistency if reusing more code later\r\n+import sys # <<< ADDED IMPORT\r\n \r\n # --- USER CONFIGURATION ---\r\n \r\n # Path to the root output directory of the Asset Processor Tool\r\n # Example: r\"G:\\Assets\\Processed\"\r\n # IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n \r\n # Name of the Blender Asset Library (configured in Blender Preferences)\r\n # that contains the PBRSET node groups created by create_nodegroups.py\r\n PBRSET_ASSET_LIBRARY_NAME = \"Nodes-Linked\" # <<< CHANGE THIS NAME!\r\n"
},
{
"date": 1745263542717,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -607,9 +607,19 @@\n print(\"---------------------------------------\")\r\n \r\n return True\r\n \r\n+ # --- Explicit Save ---\r\n+ try:\r\n+ bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+ print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+ except Exception as e_save:\r\n+ print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+ # Note: We don't have an errors_encountered counter in this script currently.\r\n+ # If needed, we could add one or just rely on the printout.\r\n+ # For now, just printing the error is sufficient for debugging.\r\n \r\n+\r\n # --- Execution Block ---\r\n \r\n if __name__ == \"__main__\":\r\n # Ensure we are running within Blender\r\n"
},
{
"date": 1745264210616,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,678 @@\n+# blenderscripts/create_materials.py\r\n+# Version: 1.1\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+# reads metadata.json files, finds corresponding PBRSET node groups\r\n+# in a specified Blender Asset Library, and creates Blender materials\r\n+# linking to those node groups. Skips assets if material already exists.\r\n+# Sets material viewport properties and custom previews based on metadata.\r\n+# Changes v1.1:\r\n+# - Added logic to link PBRSET node groups from an external asset library.\r\n+# - Added logic to skip processing if the target material already exists.\r\n+# - Added configuration for the PBRSET asset library name.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import base64 # Although not directly used here, keep for consistency if reusing more code later\r\n+import sys # <<< ADDED IMPORT\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n+\r\n+# Name of the Blender Asset Library (configured in Blender Preferences)\r\n+# that contains the PBRSET node groups created by create_nodegroups.py\r\n+PBRSET_ASSET_LIBRARY_NAME = \"Nodes-Linked\" # <<< CHANGE THIS NAME!\r\n+\r\n+# Name of the required template material in the Blender file\r\n+TEMPLATE_MATERIAL_NAME = \"Template_PBRMaterial\"\r\n+\r\n+# Label of the placeholder Group node within the template material's node tree\r\n+# where the PBRSET node group will be linked\r\n+PLACEHOLDER_NODE_LABEL = \"PBRSET_PLACEHOLDER\"\r\n+\r\n+# Prefix for the created materials\r\n+MATERIAL_NAME_PREFIX = \"Mat_\"\r\n+\r\n+# Prefix used for the PBRSET node groups created by create_nodegroups.py\r\n+PBRSET_GROUP_PREFIX = \"PBRSET_\"\r\n+\r\n+# Map type(s) to use for finding a reference image for the material preview\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n+\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map types to check in metadata's 'image_stats_1k' for viewport diffuse color\r\n+VIEWPORT_COLOR_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n+\r\n+# Map types to check in metadata's 'image_stats_1k' for viewport roughness\r\n+VIEWPORT_ROUGHNESS_MAP_TYPES = [\"ROUGH\"]\r\n+\r\n+# Map types to check in metadata's 'image_stats_1k' for viewport metallic\r\n+VIEWPORT_METALLIC_MAP_TYPES = [\"METAL\"]\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+ \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+ if not node_tree:\r\n+ return []\r\n+ matching_nodes = []\r\n+ for node in node_tree.nodes:\r\n+ # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+ node_identifier = node.label if node.label else node.name\r\n+ if node_identifier and node_identifier == label:\r\n+ if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+ matching_nodes.append(node)\r\n+ return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+ \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+ if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+ return False\r\n+ cleaned_tag_name = tag_name.strip()\r\n+ if not cleaned_tag_name:\r\n+ return False\r\n+\r\n+ # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+ if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+ try:\r\n+ asset_data.tags.new(cleaned_tag_name)\r\n+ print(f\" + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+ return True\r\n+ except Exception as e:\r\n+ print(f\" Error adding tag '{cleaned_tag_name}': {e}\")\r\n+ return False\r\n+ return False # Tag already existed\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n+ \"\"\"\r\n+ Constructs the expected image file path.\r\n+ If primary_format is provided, tries that first.\r\n+ Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n+ Returns the found path as a string, or None if not found.\r\n+ \"\"\"\r\n+ if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n+ print(f\" !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+ return None\r\n+\r\n+ found_path = None\r\n+\r\n+ # 1. Try the primary format if provided\r\n+ if primary_format:\r\n+ try:\r\n+ filename = IMAGE_FILENAME_PATTERN.format(\r\n+ asset_name=asset_name,\r\n+ map_type=map_type,\r\n+ resolution=resolution,\r\n+ format=primary_format.lower() # Ensure format is lowercase\r\n+ )\r\n+ primary_path = asset_dir_path / filename\r\n+ if primary_path.is_file():\r\n+ # print(f\" Found primary path: {str(primary_path)}\") # Verbose\r\n+ return str(primary_path)\r\n+ # else: print(f\" Primary path not found: {str(primary_path)}\") # Verbose\r\n+ except KeyError as e:\r\n+ print(f\" !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+ return None # Cannot proceed without valid pattern\r\n+ except Exception as e:\r\n+ print(f\" !!! ERROR reconstructing primary image path: {e}\")\r\n+ # Continue to fallback\r\n+\r\n+ # 2. Try fallback extensions\r\n+ # print(f\" Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+ for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+ # Skip if we already tried this extension as primary (and it failed)\r\n+ if primary_format and ext.lower() == primary_format.lower():\r\n+ continue\r\n+ try:\r\n+ fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+ asset_name=asset_name,\r\n+ map_type=map_type,\r\n+ resolution=resolution,\r\n+ format=ext.lower()\r\n+ )\r\n+ fallback_path = asset_dir_path / fallback_filename\r\n+ if fallback_path.is_file():\r\n+ print(f\" Found fallback path: {str(fallback_path)}\")\r\n+ return str(fallback_path) # Found it!\r\n+ except KeyError:\r\n+ # Should not happen if primary format worked, but handle defensively\r\n+ print(f\" !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+ return None\r\n+ except Exception as e_fallback:\r\n+ print(f\" !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+ continue # Try next extension\r\n+\r\n+ # If we get here, neither primary nor fallbacks worked\r\n+ if primary_format:\r\n+ print(f\" !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+ else:\r\n+ print(f\" !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+ return None # Not found after all checks\r\n+\r\n+\r\n+def get_stat_value(stats_dict, map_type_list, stat_key):\r\n+ \"\"\"\r\n+ Safely retrieves a specific statistic (e.g., 'mean') for the first matching\r\n+ map type from the provided list within the image_stats_1k dictionary.\r\n+\r\n+ Args:\r\n+ stats_dict (dict): The 'image_stats_1k' dictionary from metadata.\r\n+ map_type_list (list): List of map type strings to check (e.g., [\"COL\", \"COL-1\"]).\r\n+ stat_key (str): The statistic key to retrieve (e.g., \"mean\", \"min\", \"max\").\r\n+\r\n+ Returns:\r\n+ The found statistic value (can be float, list, etc.), or None if not found.\r\n+ \"\"\"\r\n+ if not stats_dict or not isinstance(stats_dict, dict):\r\n+ return None\r\n+\r\n+ for map_type in map_type_list:\r\n+ if map_type in stats_dict:\r\n+ map_stats = stats_dict[map_type]\r\n+ if isinstance(map_stats, dict) and stat_key in map_stats:\r\n+ return map_stats[stat_key] # Return the value for the first match\r\n+ else:\r\n+ # print(f\" Debug: Stats for '{map_type}' found but key '{stat_key}' or format is invalid.\") # Optional debug\r\n+ pass # Continue checking other map types in the list\r\n+ # else: print(f\" Debug: Map type '{map_type}' not found in stats_dict.\") # Optional debug\r\n+\r\n+ return None # Return None if no matching map type or stat key was found\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library_for_materials(context, asset_library_root_override=None): # Add override parameter\r\n+ global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n+ \"\"\"\r\n+ Scans the library, reads metadata, finds PBRSET node groups in the specified\r\n+ asset library, and creates/updates materials linking to them.\r\n+ \"\"\"\r\n+ start_time = time.time()\r\n+ print(f\"\\n--- Starting Material Creation from Node Groups ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+ print(f\" DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n+\r\n+ # --- Determine Asset Library Root ---\r\n+ if asset_library_root_override:\r\n+ PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+ print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+ elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+ print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+ print(\"--- Script aborted. ---\")\r\n+ return False\r\n+ print(f\" DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Pre-run Checks ---\r\n+ print(\"Performing pre-run checks...\")\r\n+ valid_setup = True\r\n+ # 1. Check Processed Asset Library Root Path\r\n+ root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+ if not root_path.is_dir():\r\n+ print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+ print(f\"!!! '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+ valid_setup = False\r\n+ else:\r\n+ print(f\" Processed Asset Library Root: '{root_path}'\")\r\n+ print(f\" DEBUG: Checking PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}'\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # 2. Check PBRSET Asset Library Name and Path\r\n+ pbrset_library = context.preferences.filepaths.asset_libraries.get(PBRSET_ASSET_LIBRARY_NAME)\r\n+ pbrset_library_path = None\r\n+ if not pbrset_library:\r\n+ print(f\"!!! ERROR: PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}' not found in Blender Preferences.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ pbrset_library_path_str = bpy.path.abspath(pbrset_library.path)\r\n+ pbrset_library_path = Path(pbrset_library_path_str)\r\n+ # Check if the library path points to a single .blend file or a directory\r\n+ if not pbrset_library_path.exists() or (not pbrset_library_path.is_file() and not pbrset_library_path.is_dir()):\r\n+ print(f\"!!! ERROR: Path for PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}' is invalid or does not exist:\")\r\n+ print(f\"!!! '{pbrset_library_path_str}'\")\r\n+ valid_setup = False\r\n+ pbrset_library_path = None # Ensure it's None if invalid\r\n+ else:\r\n+ print(f\" PBRSET Asset Library: '{PBRSET_ASSET_LIBRARY_NAME}' -> '{pbrset_library_path_str}'\")\r\n+ print(f\" DEBUG: PBRSET Library Path: {pbrset_library_path}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # 3. Check Template Material and Placeholder Node\r\n+ template_mat = bpy.data.materials.get(TEMPLATE_MATERIAL_NAME)\r\n+ placeholder_node_found_in_template = False\r\n+ if not template_mat:\r\n+ print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' not found in this Blender file.\")\r\n+ valid_setup = False\r\n+ elif not template_mat.use_nodes:\r\n+ print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' does not use nodes.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ placeholder_nodes = find_nodes_by_label(template_mat.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n+ if not placeholder_nodes:\r\n+ print(f\"!!! ERROR: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in template material '{TEMPLATE_MATERIAL_NAME}'.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ placeholder_node_found_in_template = True\r\n+ print(f\" Found Template Material: '{TEMPLATE_MATERIAL_NAME}' with placeholder '{PLACEHOLDER_NODE_LABEL}'\")\r\n+ print(f\" DEBUG: Template Material Found: {template_mat is not None}\") # DEBUG LOG (Indented)\r\n+ print(f\" DEBUG: Placeholder Node Found in Template: {placeholder_node_found_in_template}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ if not valid_setup:\r\n+ print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+ return False\r\n+ print(\"Pre-run checks passed.\")\r\n+ # --- End Pre-run Checks ---\r\n+\r\n+ # --- Initialize Counters ---\r\n+ metadata_files_found = 0\r\n+ assets_processed = 0\r\n+ assets_skipped = 0\r\n+ materials_created = 0\r\n+ # materials_updated = 0 # Not updating existing materials anymore\r\n+ node_groups_linked = 0\r\n+ previews_set = 0\r\n+ viewport_colors_set = 0\r\n+ viewport_roughness_set = 0\r\n+ viewport_metallic_set = 0\r\n+ errors_encountered = 0\r\n+ pbrset_groups_missing_in_library = 0\r\n+ placeholder_nodes_missing = 0\r\n+ library_link_errors = 0\r\n+ # --- End Counters ---\r\n+\r\n+ print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+ # --- Scan for metadata.json ---\r\n+ metadata_paths = []\r\n+ for supplier_dir in root_path.iterdir():\r\n+ if supplier_dir.is_dir():\r\n+ for asset_dir in supplier_dir.iterdir():\r\n+ if asset_dir.is_dir():\r\n+ metadata_file = asset_dir / 'metadata.json'\r\n+ if metadata_file.is_file():\r\n+ metadata_paths.append(metadata_file)\r\n+\r\n+ metadata_files_found = len(metadata_paths)\r\n+ print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+ print(f\" DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ if metadata_files_found == 0:\r\n+ print(\"No metadata files found. Nothing to process.\")\r\n+ print(\"--- Script Finished ---\")\r\n+ return True # No work needed is considered success\r\n+\r\n+ # --- Determine the .blend file path for the PBRSET library ---\r\n+ # If the library path is a directory, we assume the node groups are in a file named like the library\r\n+ # This might need adjustment based on how the user organizes their library file(s)\r\n+ pbrset_blend_file_path = None\r\n+ if pbrset_library_path:\r\n+ if pbrset_library_path.is_file() and pbrset_library_path.suffix.lower() == '.blend':\r\n+ pbrset_blend_file_path = str(pbrset_library_path)\r\n+ elif pbrset_library_path.is_dir():\r\n+ # Attempt to find a .blend file named like the library or a common name\r\n+ potential_blend_name = f\"{PBRSET_ASSET_LIBRARY_NAME}.blend\"\r\n+ potential_path = pbrset_library_path / potential_blend_name\r\n+ if potential_path.is_file():\r\n+ pbrset_blend_file_path = str(potential_path)\r\n+ else:\r\n+ # Fallback: look for any .blend file (take the first one found) - less reliable\r\n+ found_blend = next(pbrset_library_path.glob('*.blend'), None)\r\n+ if found_blend:\r\n+ pbrset_blend_file_path = str(found_blend)\r\n+ print(f\" Warning: PBRSET library path is a directory. Using first found .blend file: {found_blend.name}\")\r\n+ else:\r\n+ print(f\"!!! ERROR: PBRSET library path is a directory, but no .blend file found inside.\")\r\n+ errors_encountered += 1 # Count as error if no blend file found\r\n+\r\n+ if pbrset_blend_file_path:\r\n+ print(f\" Using PBRSET library file: '{pbrset_blend_file_path}'\")\r\n+ else:\r\n+ print(f\"!!! ERROR: Could not determine the .blend file for the PBRSET library '{PBRSET_ASSET_LIBRARY_NAME}'.\")\r\n+ # No point continuing if we can't find the source blend file\r\n+ print(\"\\n--- Script aborted: Cannot find PBRSET library .blend file. ---\")\r\n+ return False\r\n+\r\n+\r\n+ # --- Process Each Metadata File ---\r\n+ print(f\" DEBUG: Starting metadata file loop. Found {len(metadata_paths)} files.\") # DEBUG LOG (Indented)\r\n+ for metadata_path in metadata_paths:\r\n+ asset_dir_path = metadata_path.parent\r\n+ print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+ print(f\" DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n+ try:\r\n+ with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+ metadata = json.load(f)\r\n+\r\n+ # --- Extract Key Info ---\r\n+ asset_name = metadata.get(\"asset_name\")\r\n+ supplier_name = metadata.get(\"supplier_name\")\r\n+ archetype = metadata.get(\"archetype\")\r\n+ processed_resolutions = metadata.get(\"processed_map_resolutions\", {})\r\n+ merged_resolutions = metadata.get(\"merged_map_resolutions\", {})\r\n+ map_details = metadata.get(\"map_details\", {})\r\n+ image_stats_1k = metadata.get(\"image_stats_1k\")\r\n+\r\n+ all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n+ if not asset_name:\r\n+ print(f\" !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+ errors_encountered += 1\r\n+ continue\r\n+ print(f\" DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ print(f\" Asset Name: {asset_name}\")\r\n+\r\n+ # --- Determine Target Names ---\r\n+ target_material_name = f\"{MATERIAL_NAME_PREFIX}{asset_name}\"\r\n+ target_pbrset_group_name = f\"{PBRSET_GROUP_PREFIX}{asset_name}\"\r\n+ print(f\" DEBUG: Target Material Name: {target_material_name}\") # DEBUG LOG (Indented)\r\n+ print(f\" DEBUG: Target PBRSET Group Name: {target_pbrset_group_name}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Check if Material Already Exists (Skip Logic) ---\r\n+ if bpy.data.materials.get(target_material_name):\r\n+ print(f\" Skipping asset '{asset_name}': Material '{target_material_name}' already exists.\")\r\n+ assets_skipped += 1\r\n+ continue # Move to the next metadata file\r\n+ print(f\" DEBUG: Material '{target_material_name}' does not exist. Proceeding with creation.\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Create New Material ---\r\n+ print(f\" Creating new material: '{target_material_name}'\")\r\n+ print(f\" DEBUG: Copying template material '{TEMPLATE_MATERIAL_NAME}'\") # DEBUG LOG (Indented)\r\n+ material = template_mat.copy()\r\n+ if not material:\r\n+ print(f\" !!! ERROR: Failed to copy template material '{TEMPLATE_MATERIAL_NAME}'. Skipping asset '{asset_name}'.\")\r\n+ errors_encountered += 1\r\n+ continue\r\n+ material.name = target_material_name\r\n+ materials_created += 1\r\n+ print(f\" DEBUG: Material '{material.name}' created.\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Find Placeholder Node ---\r\n+ if not material.use_nodes or not material.node_tree:\r\n+ print(f\" !!! ERROR: Newly created material '{material.name}' does not use nodes or has no node tree. Skipping node linking.\")\r\n+ placeholder_node = None # Ensure it's None\r\n+ else:\r\n+ placeholder_nodes = find_nodes_by_label(material.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n+ if not placeholder_nodes:\r\n+ print(f\" !!! WARNING: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in material '{material.name}'. Cannot link PBRSET group.\")\r\n+ placeholder_nodes_missing += 1\r\n+ placeholder_node = None # Ensure it's None\r\n+ else:\r\n+ placeholder_node = placeholder_nodes[0] # Assume first is correct\r\n+ print(f\" DEBUG: Found placeholder node '{placeholder_node.label}' in material '{material.name}'.\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Find and Link PBRSET Node Group from Library ---\r\n+ linked_pbrset_group = None\r\n+ if placeholder_node and pbrset_blend_file_path: # Only proceed if placeholder exists and library file is known\r\n+ print(f\" DEBUG: Placeholder node exists and PBRSET library file path is known: {pbrset_blend_file_path}\") # DEBUG LOG (Indented)\r\n+ # Check if the group is already linked in the current file\r\n+ existing_linked_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n+ if existing_linked_group and existing_linked_group.library and bpy.path.abspath(existing_linked_group.library.filepath) == pbrset_blend_file_path:\r\n+ linked_pbrset_group = existing_linked_group\r\n+ print(f\" Found existing linked PBRSET group: '{linked_pbrset_group.name}'\")\r\n+ else:\r\n+ # Link the node group from the external file\r\n+ print(f\" Attempting to link PBRSET group '{target_pbrset_group_name}' from '{Path(pbrset_blend_file_path).name}'...\")\r\n+ try:\r\n+ with bpy.data.libraries.load(pbrset_blend_file_path, link=True, relative=False) as (data_from, data_to):\r\n+ if target_pbrset_group_name in data_from.node_groups:\r\n+ data_to.node_groups = [target_pbrset_group_name]\r\n+ else:\r\n+ print(f\" !!! ERROR: Node group '{target_pbrset_group_name}' not found in library file.\")\r\n+ pbrset_groups_missing_in_library += 1\r\n+\r\n+ # Verify linking was successful\r\n+ linked_pbrset_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n+ if not linked_pbrset_group or not linked_pbrset_group.library:\r\n+ print(f\" !!! ERROR: Failed to link node group '{target_pbrset_group_name}'.\")\r\n+ library_link_errors += 1\r\n+ linked_pbrset_group = None # Ensure it's None on failure\r\n+ else:\r\n+ print(f\" Successfully linked node group: '{linked_pbrset_group.name}'\")\r\n+\r\n+ except Exception as e_lib_load:\r\n+ print(f\" !!! ERROR loading library or linking node group: {e_lib_load}\")\r\n+ library_link_errors += 1\r\n+ linked_pbrset_group = None # Ensure it's None on failure\r\n+\r\n+ # --- Link Linked Node Group to Placeholder ---\r\n+ if placeholder_node and linked_pbrset_group:\r\n+ print(f\" DEBUG: Attempting to link PBRSET group '{linked_pbrset_group.name}' to placeholder '{placeholder_node.label}'.\") # DEBUG LOG (Indented)\r\n+ if placeholder_node.node_tree != linked_pbrset_group:\r\n+ try:\r\n+ placeholder_node.node_tree = linked_pbrset_group\r\n+ print(f\" Linked PBRSET group '{linked_pbrset_group.name}' to placeholder '{placeholder_node.label}'.\")\r\n+ node_groups_linked += 1\r\n+ except TypeError as e_assign:\r\n+ print(f\" !!! ERROR: Could not assign linked PBRSET group to placeholder '{placeholder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_link:\r\n+ print(f\" !!! UNEXPECTED ERROR linking PBRSET group to placeholder: {e_link}\")\r\n+ errors_encountered += 1\r\n+ elif placeholder_node and not linked_pbrset_group:\r\n+ print(f\" Info: Cannot link node group as it was not found or failed to link.\")\r\n+ # No 'else' needed if placeholder_node is None, error already logged\r\n+\r\n+\r\n+ # --- Mark Material as Asset ---\r\n+ if not material.asset_data:\r\n+ print(f\" DEBUG: Marking material '{material.name}' as asset.\") # DEBUG LOG (Indented)\r\n+ try:\r\n+ material.asset_mark()\r\n+ print(f\" Marked material '{material.name}' as asset.\")\r\n+ except Exception as e_mark:\r\n+ print(f\" !!! ERROR: Failed to mark material '{material.name}' as asset: {e_mark}\")\r\n+\r\n+ # --- Copy Asset Tags ---\r\n+ if material.asset_data and linked_pbrset_group and linked_pbrset_group.asset_data:\r\n+ print(f\" DEBUG: Copying asset tags from PBRSET group to material.\") # DEBUG LOG (Indented)\r\n+ tags_copied_count = 0\r\n+ if supplier_name:\r\n+ if add_tag_if_new(material.asset_data, supplier_name): tags_copied_count += 1\r\n+ if archetype:\r\n+ if add_tag_if_new(material.asset_data, archetype): tags_copied_count += 1\r\n+ # Copy other tags from PBRSET group\r\n+ for ng_tag in linked_pbrset_group.asset_data.tags:\r\n+ if add_tag_if_new(material.asset_data, ng_tag.name): tags_copied_count += 1\r\n+ # if tags_copied_count > 0: print(f\" Copied {tags_copied_count} asset tags to material.\") # Optional info\r\n+ # else: print(f\" Warn: Cannot copy tags. Material asset_data: {material.asset_data is not None}, Linked Group: {linked_pbrset_group}, Group asset_data: {linked_pbrset_group.asset_data if linked_pbrset_group else None}\") # Debug\r\n+\r\n+\r\n+ # --- Set Custom Preview ---\r\n+ ref_image_path = None\r\n+ for ref_map_type in REFERENCE_MAP_TYPES:\r\n+ if ref_map_type in all_map_resolutions:\r\n+ available_resolutions = all_map_resolutions[ref_map_type]\r\n+ lowest_res = None\r\n+ for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+ if res_pref in available_resolutions:\r\n+ lowest_res = res_pref\r\n+ break\r\n+ if lowest_res:\r\n+ ref_map_details = map_details.get(ref_map_type, {})\r\n+ ref_format = ref_map_details.get(\"output_format\")\r\n+ ref_image_path = reconstruct_image_path_with_fallback(\r\n+ asset_dir_path=asset_dir_path,\r\n+ asset_name=asset_name,\r\n+ map_type=ref_map_type,\r\n+ resolution=lowest_res,\r\n+ primary_format=ref_format\r\n+ )\r\n+ if ref_image_path:\r\n+ break\r\n+\r\n+ if ref_image_path and material.asset_data:\r\n+ print(f\" Attempting to set preview from: {Path(ref_image_path).name}\")\r\n+ try:\r\n+ with context.temp_override(id=material):\r\n+ bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+ print(f\" Successfully set custom preview for material.\")\r\n+ previews_set += 1\r\n+ except RuntimeError as e_op:\r\n+ print(f\" !!! ERROR running preview operator for material '{material.name}': {e_op}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_preview:\r\n+ print(f\" !!! UNEXPECTED ERROR setting custom preview for material: {e_preview}\")\r\n+ errors_encountered += 1\r\n+ elif not material.asset_data:\r\n+ print(f\" Info: Cannot set preview for '{material.name}' as it's not marked as an asset.\")\r\n+ else:\r\n+ print(f\" Info: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for preview.\")\r\n+\r\n+\r\n+ # --- Set Viewport Properties from Stats ---\r\n+ if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+ print(f\" DEBUG: Applying viewport properties from stats.\") # DEBUG LOG (Indented)\r\n+ # Viewport Color\r\n+ color_mean = get_stat_value(image_stats_1k, VIEWPORT_COLOR_MAP_TYPES, 'mean')\r\n+ if isinstance(color_mean, list) and len(color_mean) >= 3:\r\n+ color_rgba = (*color_mean[:3], 1.0)\r\n+ print(f\" Debug: Raw color_mean from metadata: {color_mean[:3]}\") # Added logging\r\n+ if tuple(material.diffuse_color[:3]) != tuple(color_rgba[:3]):\r\n+ material.diffuse_color = color_rgba\r\n+ print(f\" Set viewport color: {color_rgba[:3]}\")\r\n+ viewport_colors_set += 1\r\n+\r\n+ # Viewport Roughness & Metallic Check\r\n+ roughness_mean = get_stat_value(image_stats_1k, VIEWPORT_ROUGHNESS_MAP_TYPES, 'mean')\r\n+ metallic_mean = get_stat_value(image_stats_1k, VIEWPORT_METALLIC_MAP_TYPES, 'mean')\r\n+ metal_map_found = metallic_mean is not None\r\n+\r\n+ # Roughness\r\n+ if roughness_mean is not None:\r\n+ rough_val = roughness_mean[0] if isinstance(roughness_mean, list) else roughness_mean\r\n+ if isinstance(rough_val, (float, int)):\r\n+ final_roughness = float(rough_val)\r\n+ if not metal_map_found:\r\n+ final_roughness = 1.0 - final_roughness\r\n+ final_roughness = max(0.0, min(1.0, final_roughness))\r\n+ if abs(material.roughness - final_roughness) > 0.001:\r\n+ material.roughness = final_roughness\r\n+ print(f\" Set viewport roughness: {final_roughness:.3f}\")\r\n+ viewport_roughness_set += 1\r\n+\r\n+ # Metallic\r\n+ if metal_map_found:\r\n+ metal_val = metallic_mean[0] if isinstance(metallic_mean, list) else metallic_mean\r\n+ if isinstance(metal_val, (float, int)):\r\n+ final_metallic = max(0.0, min(1.0, float(metal_val)))\r\n+ if abs(material.metallic - final_metallic) > 0.001:\r\n+ material.metallic = final_metallic\r\n+ print(f\" Set viewport metallic: {final_metallic:.3f}\")\r\n+ viewport_metallic_set += 1\r\n+ else:\r\n+ if material.metallic != 0.0:\r\n+ material.metallic = 0.0\r\n+ print(f\" Set viewport metallic to default: 0.0 (No metal map found)\")\r\n+ viewport_metallic_set += 1\r\n+\r\n+ assets_processed += 1 # Count assets where processing was attempted (even if errors occurred later)\r\n+\r\n+ except FileNotFoundError:\r\n+ print(f\" !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+ errors_encountered += 1\r\n+ except json.JSONDecodeError:\r\n+ print(f\" !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_main_loop:\r\n+ print(f\" !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+ import traceback\r\n+ traceback.print_exc()\r\n+ errors_encountered += 1\r\n+\r\n+ # --- End Metadata File Loop ---\r\n+\r\n+ # --- Final Summary ---\r\n+ end_time = time.time()\r\n+ duration = end_time - start_time\r\n+ print(\"\\n--- Material Creation Script Finished ---\")\r\n+ print(f\"Duration: {duration:.2f} seconds\")\r\n+ print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+ print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+ print(f\"Assets Skipped (Already Exist): {assets_skipped}\")\r\n+ print(f\"Materials Created: {materials_created}\")\r\n+ # print(f\"Materials Updated: {materials_updated}\") # Removed as we skip existing\r\n+ print(f\"PBRSET Node Groups Linked: {node_groups_linked}\")\r\n+ print(f\"Material Previews Set: {previews_set}\")\r\n+ print(f\"Viewport Colors Set: {viewport_colors_set}\")\r\n+ print(f\"Viewport Roughness Set: {viewport_roughness_set}\")\r\n+ print(f\"Viewport Metallic Set: {viewport_metallic_set}\")\r\n+ if pbrset_groups_missing_in_library > 0:\r\n+ print(f\"!!! PBRSET Node Groups Missing in Library '{PBRSET_ASSET_LIBRARY_NAME}': {pbrset_groups_missing_in_library} !!!\")\r\n+ if library_link_errors > 0:\r\n+ print(f\"!!! Library Link Errors: {library_link_errors} !!!\")\r\n+ if placeholder_nodes_missing > 0:\r\n+ print(f\"!!! Placeholder Nodes Missing in Materials: {placeholder_nodes_missing} !!!\")\r\n+ if errors_encountered > 0:\r\n+ print(f\"!!! Other Errors Encountered: {errors_encountered} !!!\")\r\n+ print(\"---------------------------------------\")\r\n+\r\n+ # --- Explicit Save ---\r\n+ print(f\" DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+ try:\r\n+ bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+ print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+ except Exception as e_save:\r\n+ print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+ # Note: We don't have an errors_encountered counter in this script currently.\r\n+ # If needed, we could add one or just rely on the printout.\r\n+ # For now, just printing the error is sufficient for debugging.\r\n+\r\n+ return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+ # Ensure we are running within Blender\r\n+ try:\r\n+ import bpy\r\n+ import sys\r\n+ except ImportError:\r\n+ print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+ else:\r\n+ # --- Argument Parsing for Asset Library Root ---\r\n+ asset_root_arg = None\r\n+ try:\r\n+ # Blender arguments passed after '--' appear in sys.argv\r\n+ if \"--\" in sys.argv:\r\n+ args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+ if len(args_after_dash) >= 1:\r\n+ asset_root_arg = args_after_dash[0]\r\n+ print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+ else:\r\n+ print(\"Info: '--' found but no arguments after it.\")\r\n+ # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+ except Exception as e:\r\n+ print(f\"Error parsing command line arguments: {e}\")\r\n+ # --- End Argument Parsing ---\r\n+\r\n+ process_library_for_materials(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
},
{
"date": 1745264936593,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -675,5 +675,5 @@\n except Exception as e:\r\n print(f\"Error parsing command line arguments: {e}\")\r\n # --- End Argument Parsing ---\r\n \r\n- process_library_for_materials(bpy.context, asset_library_root_override=asset_root_ar\n\\ No newline at end of file\n+ process_library_for_materials(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
},
{
"date": 1745266523931,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,680 @@\n+# blenderscripts/create_materials.py\r\n+# Version: 1.1\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+# reads metadata.json files, finds corresponding PBRSET node groups\r\n+# in a specified Blender Asset Library, and creates Blender materials\r\n+# linking to those node groups. Skips assets if material already exists.\r\n+# Sets material viewport properties and custom previews based on metadata.\r\n+# Changes v1.1:\r\n+# - Added logic to link PBRSET node groups from an external asset library.\r\n+# - Added logic to skip processing if the target material already exists.\r\n+# - Added configuration for the PBRSET asset library name.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import base64 # Although not directly used here, keep for consistency if reusing more code later\r\n+import sys # <<< ADDED IMPORT\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n+\r\n+# Name of the Blender Asset Library (configured in Blender Preferences)\r\n+# that contains the PBRSET node groups created by create_nodegroups.py\r\n+PBRSET_ASSET_LIBRARY_NAME = \"Nodes-Linked\" # <<< CHANGE THIS NAME!\r\n+\r\n+# Name of the required template material in the Blender file\r\n+TEMPLATE_MATERIAL_NAME = \"Template_PBRMaterial\"\r\n+\r\n+# Label of the placeholder Group node within the template material's node tree\r\n+# where the PBRSET node group will be linked\r\n+PLACEHOLDER_NODE_LABEL = \"PBRSET_PLACEHOLDER\"\r\n+\r\n+# Prefix for the created materials\r\n+MATERIAL_NAME_PREFIX = \"Mat_\"\r\n+\r\n+# Prefix used for the PBRSET node groups created by create_nodegroups.py\r\n+PBRSET_GROUP_PREFIX = \"PBRSET_\"\r\n+\r\n+# Map type(s) to use for finding a reference image for the material preview\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n+\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map types to check in metadata's 'image_stats_1k' for viewport diffuse color\r\n+VIEWPORT_COLOR_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n+\r\n+# Map types to check in metadata's 'image_stats_1k' for viewport roughness\r\n+VIEWPORT_ROUGHNESS_MAP_TYPES = [\"ROUGH\"]\r\n+\r\n+# Map types to check in metadata's 'image_stats_1k' for viewport metallic\r\n+VIEWPORT_METALLIC_MAP_TYPES = [\"METAL\"]\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+ \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+ if not node_tree:\r\n+ return []\r\n+ matching_nodes = []\r\n+ for node in node_tree.nodes:\r\n+ # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+ node_identifier = node.label if node.label else node.name\r\n+ if node_identifier and node_identifier == label:\r\n+ if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+ matching_nodes.append(node)\r\n+ return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+ \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+ if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+ return False\r\n+ cleaned_tag_name = tag_name.strip()\r\n+ if not cleaned_tag_name:\r\n+ return False\r\n+\r\n+ # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+ if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+ try:\r\n+ asset_data.tags.new(cleaned_tag_name)\r\n+ print(f\" + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+ return True\r\n+ except Exception as e:\r\n+ print(f\" Error adding tag '{cleaned_tag_name}': {e}\")\r\n+ return False\r\n+ return False # Tag already existed\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n+ \"\"\"\r\n+ Constructs the expected image file path.\r\n+ If primary_format is provided, tries that first.\r\n+ Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n+ Returns the found path as a string, or None if not found.\r\n+ \"\"\"\r\n+ if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n+ print(f\" !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+ return None\r\n+\r\n+ found_path = None\r\n+\r\n+ # 1. Try the primary format if provided\r\n+ if primary_format:\r\n+ try:\r\n+ filename = IMAGE_FILENAME_PATTERN.format(\r\n+ asset_name=asset_name,\r\n+ map_type=map_type,\r\n+ resolution=resolution,\r\n+ format=primary_format.lower() # Ensure format is lowercase\r\n+ )\r\n+ primary_path = asset_dir_path / filename\r\n+ if primary_path.is_file():\r\n+ # print(f\" Found primary path: {str(primary_path)}\") # Verbose\r\n+ return str(primary_path)\r\n+ # else: print(f\" Primary path not found: {str(primary_path)}\") # Verbose\r\n+ except KeyError as e:\r\n+ print(f\" !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+ return None # Cannot proceed without valid pattern\r\n+ except Exception as e:\r\n+ print(f\" !!! ERROR reconstructing primary image path: {e}\")\r\n+ # Continue to fallback\r\n+\r\n+ # 2. Try fallback extensions\r\n+ # print(f\" Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+ for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+ # Skip if we already tried this extension as primary (and it failed)\r\n+ if primary_format and ext.lower() == primary_format.lower():\r\n+ continue\r\n+ try:\r\n+ fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+ asset_name=asset_name,\r\n+ map_type=map_type,\r\n+ resolution=resolution,\r\n+ format=ext.lower()\r\n+ )\r\n+ fallback_path = asset_dir_path / fallback_filename\r\n+ if fallback_path.is_file():\r\n+ print(f\" Found fallback path: {str(fallback_path)}\")\r\n+ return str(fallback_path) # Found it!\r\n+ except KeyError:\r\n+ # Should not happen if primary format worked, but handle defensively\r\n+ print(f\" !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+ return None\r\n+ except Exception as e_fallback:\r\n+ print(f\" !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+ continue # Try next extension\r\n+\r\n+ # If we get here, neither primary nor fallbacks worked\r\n+ if primary_format:\r\n+ print(f\" !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+ else:\r\n+ print(f\" !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+ return None # Not found after all checks\r\n+\r\n+\r\n+def get_stat_value(stats_dict, map_type_list, stat_key):\r\n+ \"\"\"\r\n+ Safely retrieves a specific statistic (e.g., 'mean') for the first matching\r\n+ map type from the provided list within the image_stats_1k dictionary.\r\n+\r\n+ Args:\r\n+ stats_dict (dict): The 'image_stats_1k' dictionary from metadata.\r\n+ map_type_list (list): List of map type strings to check (e.g., [\"COL\", \"COL-1\"]).\r\n+ stat_key (str): The statistic key to retrieve (e.g., \"mean\", \"min\", \"max\").\r\n+\r\n+ Returns:\r\n+ The found statistic value (can be float, list, etc.), or None if not found.\r\n+ \"\"\"\r\n+ if not stats_dict or not isinstance(stats_dict, dict):\r\n+ return None\r\n+\r\n+ for map_type in map_type_list:\r\n+ if map_type in stats_dict:\r\n+ map_stats = stats_dict[map_type]\r\n+ if isinstance(map_stats, dict) and stat_key in map_stats:\r\n+ return map_stats[stat_key] # Return the value for the first match\r\n+ else:\r\n+ # print(f\" Debug: Stats for '{map_type}' found but key '{stat_key}' or format is invalid.\") # Optional debug\r\n+ pass # Continue checking other map types in the list\r\n+ # else: print(f\" Debug: Map type '{map_type}' not found in stats_dict.\") # Optional debug\r\n+\r\n+ return None # Return None if no matching map type or stat key was found\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library_for_materials(context, asset_library_root_override=None): # Add override parameter\r\n+ global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n+ \"\"\"\r\n+ Scans the library, reads metadata, finds PBRSET node groups in the specified\r\n+ asset library, and creates/updates materials linking to them.\r\n+ \"\"\"\r\n+ print(\"DEBUG: Script started.\") # DEBUG LOG\r\n+ start_time = time.time()\r\n+ print(f\"\\n--- Starting Material Creation from Node Groups ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+ print(f\" DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Determine Asset Library Root ---\r\n+ if asset_library_root_override:\r\n+ PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+ print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+ elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+ print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+ print(\"--- Script aborted. ---\")\r\n+ return False\r\n+ print(f\" DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Pre-run Checks ---\r\n+ print(\"Performing pre-run checks...\")\r\n+ valid_setup = True\r\n+ # 1. Check Processed Asset Library Root Path\r\n+ root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+ if not root_path.is_dir():\r\n+ print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+ print(f\"!!! '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+ valid_setup = False\r\n+ else:\r\n+ print(f\" Processed Asset Library Root: '{root_path}'\")\r\n+ print(f\" DEBUG: Checking PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}'\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # 2. Check PBRSET Asset Library Name and Path\r\n+ pbrset_library = context.preferences.filepaths.asset_libraries.get(PBRSET_ASSET_LIBRARY_NAME)\r\n+ pbrset_library_path = None\r\n+ if not pbrset_library:\r\n+ print(f\"!!! ERROR: PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}' not found in Blender Preferences.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ pbrset_library_path_str = bpy.path.abspath(pbrset_library.path)\r\n+ pbrset_library_path = Path(pbrset_library_path_str)\r\n+ # Check if the library path points to a single .blend file or a directory\r\n+ if not pbrset_library_path.exists() or (not pbrset_library_path.is_file() and not pbrset_library_path.is_dir()):\r\n+ print(f\"!!! ERROR: Path for PBRSET Asset Library '{PBRSET_ASSET_LIBRARY_NAME}' is invalid or does not exist:\")\r\n+ print(f\"!!! '{pbrset_library_path_str}'\")\r\n+ valid_setup = False\r\n+ pbrset_library_path = None # Ensure it's None if invalid\r\n+ else:\r\n+ print(f\" PBRSET Asset Library: '{PBRSET_ASSET_LIBRARY_NAME}' -> '{pbrset_library_path_str}'\")\r\n+ print(f\" DEBUG: PBRSET Library Path: {pbrset_library_path}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # 3. Check Template Material and Placeholder Node\r\n+ template_mat = bpy.data.materials.get(TEMPLATE_MATERIAL_NAME)\r\n+ placeholder_node_found_in_template = False\r\n+ if not template_mat:\r\n+ print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' not found in this Blender file.\")\r\n+ valid_setup = False\r\n+ elif not template_mat.use_nodes:\r\n+ print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' does not use nodes.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ placeholder_nodes = find_nodes_by_label(template_mat.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n+ if not placeholder_nodes:\r\n+ print(f\"!!! ERROR: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in template material '{TEMPLATE_MATERIAL_NAME}'.\")\r\n+ valid_setup = False\r\n+ else:\r\n+ placeholder_node_found_in_template = True\r\n+ print(f\" Found Template Material: '{TEMPLATE_MATERIAL_NAME}' with placeholder '{PLACEHOLDER_NODE_LABEL}'\")\r\n+ print(f\" DEBUG: Template Material Found: {template_mat is not None}\") # DEBUG LOG (Indented)\r\n+ print(f\" DEBUG: Placeholder Node Found in Template: {placeholder_node_found_in_template}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ if not valid_setup:\r\n+ print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+ return False\r\n+ print(\"Pre-run checks passed.\")\r\n+ # --- End Pre-run Checks ---\r\n+\r\n+ # --- Initialize Counters ---\r\n+ metadata_files_found = 0\r\n+ assets_processed = 0\r\n+ assets_skipped = 0\r\n+ materials_created = 0\r\n+ # materials_updated = 0 # Not updating existing materials anymore\r\n+ node_groups_linked = 0\r\n+ previews_set = 0\r\n+ viewport_colors_set = 0\r\n+ viewport_roughness_set = 0\r\n+ viewport_metallic_set = 0\r\n+ errors_encountered = 0\r\n+ pbrset_groups_missing_in_library = 0\r\n+ placeholder_nodes_missing = 0\r\n+ library_link_errors = 0\r\n+ # --- End Counters ---\r\n+\r\n+ print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+ # --- Scan for metadata.json ---\r\n+ metadata_paths = []\r\n+ for supplier_dir in root_path.iterdir():\r\n+ if supplier_dir.is_dir():\r\n+ for asset_dir in supplier_dir.iterdir():\r\n+ if asset_dir.is_dir():\r\n+ metadata_file = asset_dir / 'metadata.json'\r\n+ if metadata_file.is_file():\r\n+ metadata_paths.append(metadata_file)\r\n+\r\n+ metadata_files_found = len(metadata_paths)\r\n+ print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+ print(f\" DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ if metadata_files_found == 0:\r\n+ print(\"No metadata files found. Nothing to process.\")\r\n+ print(\"--- Script Finished ---\")\r\n+ return True # No work needed is considered success\r\n+\r\n+ # --- Determine the .blend file path for the PBRSET library ---\r\n+ # If the library path is a directory, we assume the node groups are in a file named like the library\r\n+ # This might need adjustment based on how the user organizes their library file(s)\r\n+ pbrset_blend_file_path = None\r\n+ if pbrset_library_path:\r\n+ if pbrset_library_path.is_file() and pbrset_library_path.suffix.lower() == '.blend':\r\n+ pbrset_blend_file_path = str(pbrset_library_path)\r\n+ elif pbrset_library_path.is_dir():\r\n+ # Attempt to find a .blend file named like the library or a common name\r\n+ potential_blend_name = f\"{PBRSET_ASSET_LIBRARY_NAME}.blend\"\r\n+ potential_path = pbrset_library_path / potential_blend_name\r\n+ if potential_path.is_file():\r\n+ pbrset_blend_file_path = str(potential_path)\r\n+ else:\r\n+ # Fallback: look for any .blend file (take the first one found) - less reliable\r\n+ found_blend = next(pbrset_library_path.glob('*.blend'), None)\r\n+ if found_blend:\r\n+ pbrset_blend_file_path = str(found_blend)\r\n+ print(f\" Warning: PBRSET library path is a directory. Using first found .blend file: {found_blend.name}\")\r\n+ else:\r\n+ print(f\"!!! ERROR: PBRSET library path is a directory, but no .blend file found inside.\")\r\n+ errors_encountered += 1 # Count as error if no blend file found\r\n+\r\n+ if pbrset_blend_file_path:\r\n+ print(f\" Using PBRSET library file: '{pbrset_blend_file_path}'\")\r\n+ else:\r\n+ print(f\"!!! ERROR: Could not determine the .blend file for the PBRSET library '{PBRSET_ASSET_LIBRARY_NAME}'.\")\r\n+ # No point continuing if we can't find the source blend file\r\n+ print(\"\\n--- Script aborted: Cannot find PBRSET library .blend file. ---\")\r\n+ return False\r\n+\r\n+\r\n+ # --- Process Each Metadata File ---\r\n+ print(f\" DEBUG: Starting metadata file loop. Found {len(metadata_paths)} files.\") # DEBUG LOG (Indented)\r\n+ for metadata_path in metadata_paths:\r\n+ asset_dir_path = metadata_path.parent\r\n+ print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+ print(f\" DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n+ try:\r\n+ with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+ metadata = json.load(f)\r\n+\r\n+ # --- Extract Key Info ---\r\n+ asset_name = metadata.get(\"asset_name\")\r\n+ supplier_name = metadata.get(\"supplier_name\")\r\n+ archetype = metadata.get(\"archetype\")\r\n+ processed_resolutions = metadata.get(\"processed_map_resolutions\", {})\r\n+ merged_resolutions = metadata.get(\"merged_map_resolutions\", {})\r\n+ map_details = metadata.get(\"map_details\", {})\r\n+ image_stats_1k = metadata.get(\"image_stats_1k\")\r\n+\r\n+ all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n+ if not asset_name:\r\n+ print(f\" !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+ errors_encountered += 1\r\n+ continue\r\n+ print(f\" DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ print(f\" Asset Name: {asset_name}\")\r\n+\r\n+ # --- Determine Target Names ---\r\n+ target_material_name = f\"{MATERIAL_NAME_PREFIX}{asset_name}\"\r\n+ target_pbrset_group_name = f\"{PBRSET_GROUP_PREFIX}{asset_name}\"\r\n+ print(f\" DEBUG: Target Material Name: {target_material_name}\") # DEBUG LOG (Indented)\r\n+ print(f\" DEBUG: Target PBRSET Group Name: {target_pbrset_group_name}\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Check if Material Already Exists (Skip Logic) ---\r\n+ if bpy.data.materials.get(target_material_name):\r\n+ print(f\" Skipping asset '{asset_name}': Material '{target_material_name}' already exists.\")\r\n+ assets_skipped += 1\r\n+ continue # Move to the next metadata file\r\n+ print(f\" DEBUG: Material '{target_material_name}' does not exist. Proceeding with creation.\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Create New Material ---\r\n+ print(f\" Creating new material: '{target_material_name}'\")\r\n+ print(f\" DEBUG: Copying template material '{TEMPLATE_MATERIAL_NAME}'\") # DEBUG LOG (Indented)\r\n+ material = template_mat.copy()\r\n+ if not material:\r\n+ print(f\" !!! ERROR: Failed to copy template material '{TEMPLATE_MATERIAL_NAME}'. Skipping asset '{asset_name}'.\")\r\n+ errors_encountered += 1\r\n+ continue\r\n+ material.name = target_material_name\r\n+ materials_created += 1\r\n+ print(f\" DEBUG: Material '{material.name}' created.\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Find Placeholder Node ---\r\n+ if not material.use_nodes or not material.node_tree:\r\n+ print(f\" !!! ERROR: Newly created material '{material.name}' does not use nodes or has no node tree. Skipping node linking.\")\r\n+ placeholder_node = None # Ensure it's None\r\n+ else:\r\n+ placeholder_nodes = find_nodes_by_label(material.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n+ if not placeholder_nodes:\r\n+ print(f\" !!! WARNING: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in material '{material.name}'. Cannot link PBRSET group.\")\r\n+ placeholder_nodes_missing += 1\r\n+ placeholder_node = None # Ensure it's None\r\n+ else:\r\n+ placeholder_node = placeholder_nodes[0] # Assume first is correct\r\n+ print(f\" DEBUG: Found placeholder node '{placeholder_node.label}' in material '{material.name}'.\") # DEBUG LOG (Indented)\r\n+\r\n+\r\n+ # --- Find and Link PBRSET Node Group from Library ---\r\n+ linked_pbrset_group = None\r\n+ if placeholder_node and pbrset_blend_file_path: # Only proceed if placeholder exists and library file is known\r\n+ print(f\" DEBUG: Placeholder node exists and PBRSET library file path is known: {pbrset_blend_file_path}\") # DEBUG LOG (Indented)\r\n+ # Check if the group is already linked in the current file\r\n+ existing_linked_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n+ if existing_linked_group and existing_linked_group.library and bpy.path.abspath(existing_linked_group.library.filepath) == pbrset_blend_file_path:\r\n+ linked_pbrset_group = existing_linked_group\r\n+ print(f\" Found existing linked PBRSET group: '{linked_pbrset_group.name}'\")\r\n+ else:\r\n+ # Link the node group from the external file\r\n+ print(f\" Attempting to link PBRSET group '{target_pbrset_group_name}' from '{Path(pbrset_blend_file_path).name}'...\")\r\n+ try:\r\n+ with bpy.data.libraries.load(pbrset_blend_file_path, link=True, relative=False) as (data_from, data_to):\r\n+ if target_pbrset_group_name in data_from.node_groups:\r\n+ data_to.node_groups = [target_pbrset_group_name]\r\n+ else:\r\n+ print(f\" !!! ERROR: Node group '{target_pbrset_group_name}' not found in library file.\")\r\n+ pbrset_groups_missing_in_library += 1\r\n+\r\n+ # Verify linking was successful\r\n+ linked_pbrset_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n+ if not linked_pbrset_group or not linked_pbrset_group.library:\r\n+ print(f\" !!! ERROR: Failed to link node group '{target_pbrset_group_name}'.\")\r\n+ library_link_errors += 1\r\n+ linked_pbrset_group = None # Ensure it's None on failure\r\n+ else:\r\n+ print(f\" Successfully linked node group: '{linked_pbrset_group.name}'\")\r\n+\r\n+ except Exception as e_lib_load:\r\n+ print(f\" !!! ERROR loading library or linking node group: {e_lib_load}\")\r\n+ library_link_errors += 1\r\n+ linked_pbrset_group = None # Ensure it's None on failure\r\n+\r\n+ # --- Link Linked Node Group to Placeholder ---\r\n+ if placeholder_node and linked_pbrset_group:\r\n+ print(f\" DEBUG: Attempting to link PBRSET group '{linked_pbrset_group.name}' to placeholder '{placeholder_node.label}'.\") # DEBUG LOG (Indented)\r\n+ if placeholder_node.node_tree != linked_pbrset_group:\r\n+ try:\r\n+ placeholder_node.node_tree = linked_pbrset_group\r\n+ print(f\" Linked PBRSET group '{linked_pbrset_group.name}' to placeholder '{placeholder_node.label}'.\")\r\n+ node_groups_linked += 1\r\n+ except TypeError as e_assign:\r\n+ print(f\" !!! ERROR: Could not assign linked PBRSET group to placeholder '{placeholder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_link:\r\n+ print(f\" !!! UNEXPECTED ERROR linking PBRSET group to placeholder: {e_link}\")\r\n+ errors_encountered += 1\r\n+ elif placeholder_node and not linked_pbrset_group:\r\n+ print(f\" Info: Cannot link node group as it was not found or failed to link.\")\r\n+ # No 'else' needed if placeholder_node is None, error already logged\r\n+\r\n+\r\n+ # --- Mark Material as Asset ---\r\n+ if not material.asset_data:\r\n+ print(f\" DEBUG: Marking material '{material.name}' as asset.\") # DEBUG LOG (Indented)\r\n+ try:\r\n+ material.asset_mark()\r\n+ print(f\" Marked material '{material.name}' as asset.\")\r\n+ except Exception as e_mark:\r\n+ print(f\" !!! ERROR: Failed to mark material '{material.name}' as asset: {e_mark}\")\r\n+\r\n+ # --- Copy Asset Tags ---\r\n+ if material.asset_data and linked_pbrset_group and linked_pbrset_group.asset_data:\r\n+ print(f\" DEBUG: Copying asset tags from PBRSET group to material.\") # DEBUG LOG (Indented)\r\n+ tags_copied_count = 0\r\n+ if supplier_name:\r\n+ if add_tag_if_new(material.asset_data, supplier_name): tags_copied_count += 1\r\n+ if archetype:\r\n+ if add_tag_if_new(material.asset_data, archetype): tags_copied_count += 1\r\n+ # Copy other tags from PBRSET group\r\n+ for ng_tag in linked_pbrset_group.asset_data.tags:\r\n+ if add_tag_if_new(material.asset_data, ng_tag.name): tags_copied_count += 1\r\n+ # if tags_copied_count > 0: print(f\" Copied {tags_copied_count} asset tags to material.\") # Optional info\r\n+ # else: print(f\" Warn: Cannot copy tags. Material asset_data: {material.asset_data is not None}, Linked Group: {linked_pbrset_group}, Group asset_data: {linked_pbrset_group.asset_data if linked_pbrset_group else None}\") # Debug\r\n+\r\n+\r\n+ # --- Set Custom Preview ---\r\n+ ref_image_path = None\r\n+ for ref_map_type in REFERENCE_MAP_TYPES:\r\n+ if ref_map_type in all_map_resolutions:\r\n+ available_resolutions = all_map_resolutions[ref_map_type]\r\n+ lowest_res = None\r\n+ for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+ if res_pref in available_resolutions:\r\n+ lowest_res = res_pref\r\n+ break\r\n+ if lowest_res:\r\n+ ref_map_details = map_details.get(ref_map_type, {})\r\n+ ref_format = ref_map_details.get(\"output_format\")\r\n+ ref_image_path = reconstruct_image_path_with_fallback(\r\n+ asset_dir_path=asset_dir_path,\r\n+ asset_name=asset_name,\r\n+ map_type=ref_map_type,\r\n+ resolution=lowest_res,\r\n+ primary_format=ref_format\r\n+ )\r\n+ if ref_image_path:\r\n+ break\r\n+\r\n+ if ref_image_path and material.asset_data:\r\n+ print(f\" Attempting to set preview from: {Path(ref_image_path).name}\")\r\n+ try:\r\n+ with context.temp_override(id=material):\r\n+ bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+ print(f\" Successfully set custom preview for material.\")\r\n+ previews_set += 1\r\n+ except RuntimeError as e_op:\r\n+ print(f\" !!! ERROR running preview operator for material '{material.name}': {e_op}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_preview:\r\n+ print(f\" !!! UNEXPECTED ERROR setting custom preview for material: {e_preview}\")\r\n+ errors_encountered += 1\r\n+ elif not material.asset_data:\r\n+ print(f\" Info: Cannot set preview for '{material.name}' as it's not marked as an asset.\")\r\n+ else:\r\n+ print(f\" Info: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for preview.\")\r\n+\r\n+\r\n+ # --- Set Viewport Properties from Stats ---\r\n+ if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+ print(f\" DEBUG: Applying viewport properties from stats.\") # DEBUG LOG (Indented)\r\n+ # Viewport Color\r\n+ color_mean = get_stat_value(image_stats_1k, VIEWPORT_COLOR_MAP_TYPES, 'mean')\r\n+ if isinstance(color_mean, list) and len(color_mean) >= 3:\r\n+ color_rgba = (*color_mean[:3], 1.0)\r\n+ print(f\" Debug: Raw color_mean from metadata: {color_mean[:3]}\") # Added logging\r\n+ if tuple(material.diffuse_color[:3]) != tuple(color_rgba[:3]):\r\n+ material.diffuse_color = color_rgba\r\n+ print(f\" Set viewport color: {color_rgba[:3]}\")\r\n+ viewport_colors_set += 1\r\n+\r\n+ # Viewport Roughness & Metallic Check\r\n+ roughness_mean = get_stat_value(image_stats_1k, VIEWPORT_ROUGHNESS_MAP_TYPES, 'mean')\r\n+ metallic_mean = get_stat_value(image_stats_1k, VIEWPORT_METALLIC_MAP_TYPES, 'mean')\r\n+ metal_map_found = metallic_mean is not None\r\n+\r\n+ # Roughness\r\n+ if roughness_mean is not None:\r\n+ rough_val = roughness_mean[0] if isinstance(roughness_mean, list) else roughness_mean\r\n+ if isinstance(rough_val, (float, int)):\r\n+ final_roughness = float(rough_val)\r\n+ if not metal_map_found:\r\n+ final_roughness = 1.0 - final_roughness\r\n+ final_roughness = max(0.0, min(1.0, final_roughness))\r\n+ if abs(material.roughness - final_roughness) > 0.001:\r\n+ material.roughness = final_roughness\r\n+ print(f\" Set viewport roughness: {final_roughness:.3f}\")\r\n+ viewport_roughness_set += 1\r\n+\r\n+ # Metallic\r\n+ if metal_map_found:\r\n+ metal_val = metallic_mean[0] if isinstance(metallic_mean, list) else metallic_mean\r\n+ if isinstance(metal_val, (float, int)):\r\n+ final_metallic = max(0.0, min(1.0, float(metal_val)))\r\n+ if abs(material.metallic - final_metallic) > 0.001:\r\n+ material.metallic = final_metallic\r\n+ print(f\" Set viewport metallic: {final_metallic:.3f}\")\r\n+ viewport_metallic_set += 1\r\n+ else:\r\n+ if material.metallic != 0.0:\r\n+ material.metallic = 0.0\r\n+ print(f\" Set viewport metallic to default: 0.0 (No metal map found)\")\r\n+ viewport_metallic_set += 1\r\n+\r\n+ assets_processed += 1 # Count assets where processing was attempted (even if errors occurred later)\r\n+\r\n+ except FileNotFoundError:\r\n+ print(f\" !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+ errors_encountered += 1\r\n+ except json.JSONDecodeError:\r\n+ print(f\" !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+ errors_encountered += 1\r\n+ except Exception as e_main_loop:\r\n+ print(f\" !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+ import traceback\r\n+ traceback.print_exc()\r\n+ errors_encountered += 1\r\n+\r\n+ # --- End Metadata File Loop ---\r\n+\r\n+ # --- Final Summary ---\r\n+ end_time = time.time()\r\n+ duration = end_time - start_time\r\n+ print(\"\\n--- Material Creation Script Finished ---\")\r\n+ print(f\"Duration: {duration:.2f} seconds\")\r\n+ print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+ print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+ print(f\"Assets Skipped (Already Exist): {assets_skipped}\")\r\n+ print(f\"Materials Created: {materials_created}\")\r\n+ # print(f\"Materials Updated: {materials_updated}\") # Removed as we skip existing\r\n+ print(f\"PBRSET Node Groups Linked: {node_groups_linked}\")\r\n+ print(f\"Material Previews Set: {previews_set}\")\r\n+ print(f\"Viewport Colors Set: {viewport_colors_set}\")\r\n+ print(f\"Viewport Roughness Set: {viewport_roughness_set}\")\r\n+ print(f\"Viewport Metallic Set: {viewport_metallic_set}\")\r\n+ if pbrset_groups_missing_in_library > 0:\r\n+ print(f\"!!! PBRSET Node Groups Missing in Library '{PBRSET_ASSET_LIBRARY_NAME}': {pbrset_groups_missing_in_library} !!!\")\r\n+ if library_link_errors > 0:\r\n+ print(f\"!!! Library Link Errors: {library_link_errors} !!!\")\r\n+ if placeholder_nodes_missing > 0:\r\n+ print(f\"!!! Placeholder Nodes Missing in Materials: {placeholder_nodes_missing} !!!\")\r\n+ if errors_encountered > 0:\r\n+ print(f\"!!! Other Errors Encountered: {errors_encountered} !!!\")\r\n+ print(\"---------------------------------------\")\r\n+\r\n+ # --- Explicit Save ---\r\n+ print(f\" DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+ try:\r\n+ bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+ print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+ except Exception as e_save:\r\n+ print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+ # Note: We don't have an errors_encountered counter in this script currently.\r\n+ # If needed, we could add one or just rely on the printout.\r\n+ # For now, just printing the error is sufficient for debugging.\r\n+\r\n+ return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+ # Ensure we are running within Blender\r\n+ try:\r\n+ import bpy\r\n+ import sys\r\n+ except ImportError:\r\n+ print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+ else:\r\n+ # --- Argument Parsing for Asset Library Root ---\r\n+ asset_root_arg = None\r\n+ try:\r\n+ # Blender arguments passed after '--' appear in sys.argv\r\n+ if \"--\" in sys.argv:\r\n+ args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+ if len(args_after_dash) >= 1:\r\n+ asset_root_arg = args_after_dash[0]\r\n+ print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+ else:\r\n+ print(\"Info: '--' found but no arguments after it.\")\r\n+ # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+ except Exception as e:\r\n+ print(f\"Error parsing command line arguments: {e}\")\r\n+ # --- End Argument Parsing ---\r\n+\r\n+ process_library_for_materials(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
}
],
"date": 1745258817112,
"name": "Commit-0",
"content": "# blenderscripts/create_materials.py\r\n# Version: 1.0\r\n# Description: Scans a library processed by the Asset Processor Tool,\r\n# reads metadata.json files, finds corresponding PBRSET node groups,\r\n# and creates/updates Blender materials linking to those node groups.\r\n# Sets material viewport properties and custom previews based on metadata.\r\n\r\nimport bpy\r\nimport os\r\nimport json\r\nfrom pathlib import Path\r\nimport time\r\nimport base64 # Although not directly used here, keep for consistency if reusing more code later\r\n\r\n# --- USER CONFIGURATION ---\r\n\r\n# Path to the root output directory of the Asset Processor Tool\r\n# Example: r\"G:\\Assets\\Processed\"\r\n# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\nPROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n\r\n# Name of the required template material in the Blender file\r\nTEMPLATE_MATERIAL_NAME = \"Template_PBRMaterial\"\r\n\r\n# Label of the placeholder Group node within the template material's node tree\r\n# where the PBRSET node group will be linked\r\nPLACEHOLDER_NODE_LABEL = \"PBRSET_PLACEHOLDER\"\r\n\r\n# Prefix for the created materials\r\nMATERIAL_NAME_PREFIX = \"Mat_\"\r\n\r\n# Prefix used for the PBRSET node groups created by create_nodegroups.py\r\nPBRSET_GROUP_PREFIX = \"PBRSET_\"\r\n\r\n# Map type(s) to use for finding a reference image for the material preview\r\n# The script will look for these in order and use the first one found.\r\nREFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n\r\n# Preferred resolution order for reference image (lowest first is often faster)\r\nREFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n\r\n# Assumed filename pattern for processed images.\r\n# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\nIMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n\r\n# Fallback extensions to try if the primary format from metadata is not found\r\n# Order matters - first found will be used.\r\nFALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n\r\n# Map types to check in metadata's 'image_stats_1k' for viewport diffuse color\r\nVIEWPORT_COLOR_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n\r\n# Map types to check in metadata's 'image_stats_1k' for viewport roughness\r\nVIEWPORT_ROUGHNESS_MAP_TYPES = [\"ROUGH\"]\r\n\r\n# Map types to check in metadata's 'image_stats_1k' for viewport metallic\r\nVIEWPORT_METALLIC_MAP_TYPES = [\"METAL\"]\r\n\r\n# --- END USER CONFIGURATION ---\r\n\r\n\r\n# --- Helper Functions ---\r\n\r\ndef find_nodes_by_label(node_tree, label, node_type=None):\r\n \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n if not node_tree:\r\n return []\r\n matching_nodes = []\r\n for node in node_tree.nodes:\r\n # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n node_identifier = node.label if node.label else node.name\r\n if node_identifier and node_identifier == label:\r\n if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n matching_nodes.append(node)\r\n return matching_nodes\r\n\r\ndef add_tag_if_new(asset_data, tag_name):\r\n \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n return False\r\n cleaned_tag_name = tag_name.strip()\r\n if not cleaned_tag_name:\r\n return False\r\n\r\n # Check if tag already exists (case-insensitive check might be better sometimes)\r\n if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n try:\r\n asset_data.tags.new(cleaned_tag_name)\r\n print(f\" + Added Asset Tag: '{cleaned_tag_name}'\")\r\n return True\r\n except Exception as e:\r\n print(f\" Error adding tag '{cleaned_tag_name}': {e}\")\r\n return False\r\n return False # Tag already existed\r\n\r\ndef reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n \"\"\"\r\n Constructs the expected image file path.\r\n If primary_format is provided, tries that first.\r\n Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n Returns the found path as a string, or None if not found.\r\n \"\"\"\r\n if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n print(f\" !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n return None\r\n\r\n found_path = None\r\n\r\n # 1. Try the primary format if provided\r\n if primary_format:\r\n try:\r\n filename = IMAGE_FILENAME_PATTERN.format(\r\n asset_name=asset_name,\r\n map_type=map_type,\r\n resolution=resolution,\r\n format=primary_format.lower() # Ensure format is lowercase\r\n )\r\n primary_path = asset_dir_path / filename\r\n if primary_path.is_file():\r\n # print(f\" Found primary path: {str(primary_path)}\") # Verbose\r\n return str(primary_path)\r\n # else: print(f\" Primary path not found: {str(primary_path)}\") # Verbose\r\n except KeyError as e:\r\n print(f\" !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n return None # Cannot proceed without valid pattern\r\n except Exception as e:\r\n print(f\" !!! ERROR reconstructing primary image path: {e}\")\r\n # Continue to fallback\r\n\r\n # 2. Try fallback extensions\r\n # print(f\" Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n # Skip if we already tried this extension as primary (and it failed)\r\n if primary_format and ext.lower() == primary_format.lower():\r\n continue\r\n try:\r\n fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n asset_name=asset_name,\r\n map_type=map_type,\r\n resolution=resolution,\r\n format=ext.lower()\r\n )\r\n fallback_path = asset_dir_path / fallback_filename\r\n if fallback_path.is_file():\r\n print(f\" Found fallback path: {str(fallback_path)}\")\r\n return str(fallback_path) # Found it!\r\n except KeyError:\r\n # Should not happen if primary format worked, but handle defensively\r\n print(f\" !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n return None\r\n except Exception as e_fallback:\r\n print(f\" !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n continue # Try next extension\r\n\r\n # If we get here, neither primary nor fallbacks worked\r\n if primary_format:\r\n print(f\" !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n else:\r\n print(f\" !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n return None # Not found after all checks\r\n\r\n\r\ndef get_stat_value(stats_dict, map_type_list, stat_key):\r\n \"\"\"\r\n Safely retrieves a specific statistic (e.g., 'mean') for the first matching\r\n map type from the provided list within the image_stats_1k dictionary.\r\n\r\n Args:\r\n stats_dict (dict): The 'image_stats_1k' dictionary from metadata.\r\n map_type_list (list): List of map type strings to check (e.g., [\"COL\", \"COL-1\"]).\r\n stat_key (str): The statistic key to retrieve (e.g., \"mean\", \"min\", \"max\").\r\n\r\n Returns:\r\n The found statistic value (can be float, list, etc.), or None if not found.\r\n \"\"\"\r\n if not stats_dict or not isinstance(stats_dict, dict):\r\n return None\r\n\r\n for map_type in map_type_list:\r\n if map_type in stats_dict:\r\n map_stats = stats_dict[map_type]\r\n if isinstance(map_stats, dict) and stat_key in map_stats:\r\n return map_stats[stat_key] # Return the value for the first match\r\n else:\r\n # print(f\" Debug: Stats for '{map_type}' found but key '{stat_key}' or format is invalid.\") # Optional debug\r\n pass # Continue checking other map types in the list\r\n # else: print(f\" Debug: Map type '{map_type}' not found in stats_dict.\") # Optional debug\r\n\r\n # print(f\" Debug: Stat key '{stat_key}' not found for any map types in {map_type_list}.\") # Optional debug\r\n return None # Return None if no matching map type or stat key was found\r\n\r\n\r\n# --- Core Logic ---\r\n\r\ndef process_library_for_materials(context):\r\n \"\"\"\r\n Scans the library, reads metadata, finds PBRSET node groups,\r\n and creates/updates materials linking to them.\r\n \"\"\"\r\n start_time = time.time()\r\n print(f\"\\n--- Starting Material Creation from Node Groups ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n\r\n # --- Pre-run Checks ---\r\n print(\"Performing pre-run checks...\")\r\n valid_setup = True\r\n # 1. Check Library Root Path\r\n root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n if not root_path.is_dir():\r\n print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n print(f\"!!! '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n valid_setup = False\r\n else:\r\n print(f\" Asset Library Root: '{root_path}'\")\r\n\r\n # 2. Check Template Material and Placeholder Node\r\n template_mat = bpy.data.materials.get(TEMPLATE_MATERIAL_NAME)\r\n placeholder_node_found_in_template = False\r\n if not template_mat:\r\n print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' not found in this Blender file.\")\r\n valid_setup = False\r\n elif not template_mat.use_nodes:\r\n print(f\"!!! ERROR: Template material '{TEMPLATE_MATERIAL_NAME}' does not use nodes.\")\r\n valid_setup = False\r\n else:\r\n placeholder_nodes = find_nodes_by_label(template_mat.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n if not placeholder_nodes:\r\n print(f\"!!! ERROR: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in template material '{TEMPLATE_MATERIAL_NAME}'.\")\r\n valid_setup = False\r\n else:\r\n placeholder_node_found_in_template = True\r\n print(f\" Found Template Material: '{TEMPLATE_MATERIAL_NAME}' with placeholder '{PLACEHOLDER_NODE_LABEL}'\")\r\n\r\n if not valid_setup:\r\n print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n return False\r\n print(\"Pre-run checks passed.\")\r\n # --- End Pre-run Checks ---\r\n\r\n # --- Initialize Counters ---\r\n metadata_files_found = 0\r\n assets_processed = 0\r\n materials_created = 0\r\n materials_updated = 0\r\n node_groups_linked = 0\r\n previews_set = 0\r\n viewport_colors_set = 0\r\n viewport_roughness_set = 0\r\n viewport_metallic_set = 0\r\n errors_encountered = 0\r\n pbrset_groups_missing = 0\r\n placeholder_nodes_missing = 0\r\n # --- End Counters ---\r\n\r\n print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n\r\n # --- Scan for metadata.json ---\r\n metadata_paths = []\r\n for supplier_dir in root_path.iterdir():\r\n if supplier_dir.is_dir():\r\n for asset_dir in supplier_dir.iterdir():\r\n if asset_dir.is_dir():\r\n metadata_file = asset_dir / 'metadata.json'\r\n if metadata_file.is_file():\r\n metadata_paths.append(metadata_file)\r\n\r\n metadata_files_found = len(metadata_paths)\r\n print(f\"Found {metadata_files_found} metadata.json files.\")\r\n\r\n if metadata_files_found == 0:\r\n print(\"No metadata files found. Nothing to process.\")\r\n print(\"--- Script Finished ---\")\r\n return True # No work needed is considered success\r\n\r\n # --- Process Each Metadata File ---\r\n for metadata_path in metadata_paths:\r\n asset_dir_path = metadata_path.parent\r\n print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n try:\r\n with open(metadata_path, 'r', encoding='utf-8') as f:\r\n metadata = json.load(f)\r\n\r\n # --- Extract Key Info ---\r\n asset_name = metadata.get(\"asset_name\")\r\n supplier_name = metadata.get(\"supplier_name\")\r\n archetype = metadata.get(\"archetype\")\r\n # Get map info needed for preview path reconstruction\r\n processed_resolutions = metadata.get(\"processed_map_resolutions\", {})\r\n merged_resolutions = metadata.get(\"merged_map_resolutions\", {})\r\n map_details = metadata.get(\"map_details\", {})\r\n image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n\r\n # Combine processed and merged maps to find reference map\r\n all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n\r\n # Validate essential data\r\n if not asset_name:\r\n print(f\" !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n errors_encountered += 1\r\n continue\r\n\r\n print(f\" Asset Name: {asset_name}\")\r\n\r\n # --- Determine Target Names ---\r\n target_material_name = f\"{MATERIAL_NAME_PREFIX}{asset_name}\"\r\n target_pbrset_group_name = f\"{PBRSET_GROUP_PREFIX}{asset_name}\"\r\n\r\n # --- Find PBRSET Node Group ---\r\n pbrset_group = bpy.data.node_groups.get(target_pbrset_group_name)\r\n if not pbrset_group:\r\n print(f\" !!! WARNING: PBRSET Node Group '{target_pbrset_group_name}' not found. Cannot create/update material. Run create_nodegroups.py first?\")\r\n pbrset_groups_missing += 1\r\n continue # Skip this asset if the required node group doesn't exist\r\n\r\n # --- Find or Create Material ---\r\n material = bpy.data.materials.get(target_material_name)\r\n is_new_material = False\r\n\r\n if material is None:\r\n print(f\" Creating new material: '{target_material_name}'\")\r\n material = template_mat.copy()\r\n if not material:\r\n print(f\" !!! ERROR: Failed to copy template material '{TEMPLATE_MATERIAL_NAME}'. Skipping asset '{asset_name}'.\")\r\n errors_encountered += 1\r\n continue\r\n material.name = target_material_name\r\n materials_created += 1\r\n is_new_material = True\r\n else:\r\n print(f\" Updating existing material: '{target_material_name}'\")\r\n materials_updated += 1\r\n\r\n # Ensure material uses nodes\r\n if not material.use_nodes or not material.node_tree:\r\n print(f\" !!! ERROR: Material '{material.name}' does not use nodes or has no node tree. Skipping node linking.\")\r\n # Continue to set other properties if possible\r\n else:\r\n # --- Find Placeholder Node ---\r\n placeholder_nodes = find_nodes_by_label(material.node_tree, PLACEHOLDER_NODE_LABEL, 'ShaderNodeGroup')\r\n if not placeholder_nodes:\r\n print(f\" !!! WARNING: Placeholder node '{PLACEHOLDER_NODE_LABEL}' not found in material '{material.name}'. Cannot link PBRSET group.\")\r\n placeholder_nodes_missing += 1\r\n else:\r\n placeholder_node = placeholder_nodes[0] # Assume first is correct\r\n # --- Link Node Group to Placeholder ---\r\n if placeholder_node.node_tree != pbrset_group:\r\n try:\r\n placeholder_node.node_tree = pbrset_group\r\n print(f\" Linked PBRSET group '{pbrset_group.name}' to placeholder '{placeholder_node.label}'.\")\r\n node_groups_linked += 1\r\n except TypeError as e_assign:\r\n print(f\" !!! ERROR: Could not assign PBRSET group to placeholder '{placeholder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n errors_encountered += 1\r\n except Exception as e_link:\r\n print(f\" !!! UNEXPECTED ERROR linking PBRSET group: {e_link}\")\r\n errors_encountered += 1\r\n\r\n # --- Mark Material as Asset ---\r\n if not material.asset_data:\r\n try:\r\n material.asset_mark()\r\n print(f\" Marked material '{material.name}' as asset.\")\r\n except Exception as e_mark:\r\n print(f\" !!! ERROR: Failed to mark material '{material.name}' as asset: {e_mark}\")\r\n # Continue if possible\r\n\r\n # --- Copy Asset Tags ---\r\n if material.asset_data and pbrset_group.asset_data:\r\n tags_copied_count = 0\r\n if supplier_name:\r\n if add_tag_if_new(material.asset_data, supplier_name): tags_copied_count += 1\r\n if archetype:\r\n if add_tag_if_new(material.asset_data, archetype): tags_copied_count += 1\r\n # Copy other tags from PBRSET group if needed\r\n for ng_tag in pbrset_group.asset_data.tags:\r\n if add_tag_if_new(material.asset_data, ng_tag.name): tags_copied_count += 1\r\n # if tags_copied_count > 0: print(f\" Copied {tags_copied_count} asset tags to material.\") # Optional info\r\n # else: print(f\" Warn: Cannot copy tags. Material asset_data: {material.asset_data is not None}, Group asset_data: {pbrset_group.asset_data is not None}\") # Debug\r\n\r\n # --- Set Custom Preview ---\r\n ref_image_path = None\r\n # Find reference image path (similar logic to create_nodegroups.py)\r\n for ref_map_type in REFERENCE_MAP_TYPES:\r\n if ref_map_type in all_map_resolutions:\r\n available_resolutions = all_map_resolutions[ref_map_type]\r\n lowest_res = None\r\n for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n if res_pref in available_resolutions:\r\n lowest_res = res_pref\r\n break\r\n if lowest_res:\r\n ref_map_details = map_details.get(ref_map_type, {})\r\n ref_format = ref_map_details.get(\"output_format\")\r\n ref_image_path = reconstruct_image_path_with_fallback(\r\n asset_dir_path=asset_dir_path,\r\n asset_name=asset_name,\r\n map_type=ref_map_type,\r\n resolution=lowest_res,\r\n primary_format=ref_format\r\n )\r\n if ref_image_path:\r\n break # Found a suitable reference image path\r\n\r\n if ref_image_path and material.asset_data:\r\n print(f\" Attempting to set preview from: {Path(ref_image_path).name}\")\r\n try:\r\n # Ensure the ID (material) is the active one for the operator context\r\n with context.temp_override(id=material):\r\n bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n print(f\" Successfully set custom preview for material.\")\r\n previews_set += 1\r\n except RuntimeError as e_op:\r\n print(f\" !!! ERROR running preview operator for material '{material.name}': {e_op}\")\r\n errors_encountered += 1\r\n except Exception as e_preview:\r\n print(f\" !!! UNEXPECTED ERROR setting custom preview for material: {e_preview}\")\r\n errors_encountered += 1\r\n elif not material.asset_data:\r\n print(f\" Info: Cannot set preview for '{material.name}' as it's not marked as an asset.\")\r\n else:\r\n print(f\" Info: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for preview.\")\r\n\r\n\r\n # --- Set Viewport Properties from Stats ---\r\n if image_stats_1k and isinstance(image_stats_1k, dict):\r\n # Viewport Color\r\n color_mean = get_stat_value(image_stats_1k, VIEWPORT_COLOR_MAP_TYPES, 'mean')\r\n if isinstance(color_mean, list) and len(color_mean) >= 3:\r\n # Add alpha if missing (default to 1.0)\r\n color_rgba = (*color_mean[:3], 1.0)\r\n # Check if update is needed (compare first 3 elements)\r\n if tuple(material.diffuse_color[:3]) != tuple(color_rgba[:3]):\r\n material.diffuse_color = color_rgba\r\n print(f\" Set viewport color: {color_rgba[:3]}\")\r\n viewport_colors_set += 1\r\n # else: print(f\" Info: Could not find valid 'mean' stat for viewport color in {VIEWPORT_COLOR_MAP_TYPES}.\") # Optional\r\n\r\n # Viewport Roughness & Metallic Check\r\n roughness_mean = get_stat_value(image_stats_1k, VIEWPORT_ROUGHNESS_MAP_TYPES, 'mean')\r\n metallic_mean = get_stat_value(image_stats_1k, VIEWPORT_METALLIC_MAP_TYPES, 'mean')\r\n metal_map_found = metallic_mean is not None\r\n\r\n # Roughness\r\n if roughness_mean is not None:\r\n # Extract float value if it's a list (e.g., [0.5])\r\n rough_val = roughness_mean[0] if isinstance(roughness_mean, list) else roughness_mean\r\n if isinstance(rough_val, (float, int)):\r\n final_roughness = float(rough_val)\r\n # Invert if metal map is NOT found (Gloss workflow)\r\n if not metal_map_found:\r\n final_roughness = 1.0 - final_roughness\r\n # print(f\" Inverting roughness (no metal map): {rough_val:.3f} -> {final_roughness:.3f}\") # Debug\r\n final_roughness = max(0.0, min(1.0, final_roughness)) # Clamp\r\n # Check if update is needed\r\n if abs(material.roughness - final_roughness) > 0.001:\r\n material.roughness = final_roughness\r\n print(f\" Set viewport roughness: {final_roughness:.3f}\")\r\n viewport_roughness_set += 1\r\n # else: print(f\" Info: Roughness 'mean' stat is not a valid number: {roughness_mean}\") # Optional\r\n # else: print(f\" Info: Could not find 'mean' stat for viewport roughness in {VIEWPORT_ROUGHNESS_MAP_TYPES}.\") # Optional\r\n\r\n # Metallic\r\n if metal_map_found:\r\n metal_val = metallic_mean[0] if isinstance(metallic_mean, list) else metallic_mean\r\n if isinstance(metal_val, (float, int)):\r\n final_metallic = max(0.0, min(1.0, float(metal_val))) # Clamp\r\n # Check if update is needed\r\n if abs(material.metallic - final_metallic) > 0.001:\r\n material.metallic = final_metallic\r\n print(f\" Set viewport metallic: {final_metallic:.3f}\")\r\n viewport_metallic_set += 1\r\n # else: print(f\" Info: Metallic 'mean' stat is not a valid number: {metallic_mean}\") # Optional\r\n else:\r\n # If metal map wasn't found, ensure metallic is 0.0\r\n if material.metallic != 0.0:\r\n material.metallic = 0.0\r\n print(f\" Set viewport metallic to default: 0.0 (No metal map found)\")\r\n viewport_metallic_set += 1 # Count setting default as a set action\r\n\r\n # else: print(f\" Warn: 'image_stats_1k' missing or invalid in metadata. Cannot set viewport properties.\") # Optional\r\n\r\n assets_processed += 1\r\n\r\n except FileNotFoundError:\r\n print(f\" !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n errors_encountered += 1\r\n except json.JSONDecodeError:\r\n print(f\" !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n errors_encountered += 1\r\n except Exception as e_main_loop:\r\n print(f\" !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n import traceback\r\n traceback.print_exc() # Print detailed traceback for debugging\r\n errors_encountered += 1\r\n # Continue to the next asset\r\n\r\n # --- End Metadata File Loop ---\r\n\r\n # --- Final Summary ---\r\n end_time = time.time()\r\n duration = end_time - start_time\r\n print(\"\\n--- Material Creation Script Finished ---\")\r\n print(f\"Duration: {duration:.2f} seconds\")\r\n print(f\"Metadata Files Found: {metadata_files_found}\")\r\n print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n print(f\"Materials Created: {materials_created}\")\r\n print(f\"Materials Updated: {materials_updated}\")\r\n print(f\"PBRSET Node Groups Linked: {node_groups_linked}\")\r\n print(f\"Material Previews Set: {previews_set}\")\r\n print(f\"Viewport Colors Set: {viewport_colors_set}\")\r\n print(f\"Viewport Roughness Set: {viewport_roughness_set}\")\r\n print(f\"Viewport Metallic Set: {viewport_metallic_set}\")\r\n if pbrset_groups_missing > 0:\r\n print(f\"!!! PBRSET Node Groups Missing: {pbrset_groups_missing} !!!\")\r\n if placeholder_nodes_missing > 0:\r\n print(f\"!!! Placeholder Nodes Missing in Materials: {placeholder_nodes_missing} !!!\")\r\n if errors_encountered > 0:\r\n print(f\"!!! Other Errors Encountered: {errors_encountered} !!!\")\r\n print(\"---------------------------------------\")\r\n\r\n return True\r\n\r\n\r\n# --- Execution Block ---\r\n\r\nif __name__ == \"__main__\":\r\n # Ensure we are running within Blender\r\n try:\r\n import bpy\r\n # import base64 # Already imported above\r\n except ImportError:\r\n print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n else:\r\n # Pass Blender's current context to the processing function\r\n process_library_for_materials(bpy.context)"
}
]
}