"""Blender headless script: extract asset catalog from a .blend library file. Usage: blender --background --python catalog_assets.py -- Outputs a single JSON line to stdout: {"materials": [{"name": "Mat1", "base_color": [R,G,B], "metallic": 0.0, ...}, ...], "node_groups": ["NG1", ...]} Only assets marked via Blender's asset system (asset_data is not None) are included. PBR properties are extracted from the Principled BSDF node of each material. """ from __future__ import annotations import json import math import sys import traceback def _linear_to_srgb(v: float) -> float: """Convert a single linear color channel to sRGB.""" if v <= 0.0031308: return v * 12.92 return 1.055 * math.pow(v, 1.0 / 2.4) - 0.055 def _extract_pbr(mat) -> dict: """Extract PBR properties from a Blender material's Principled BSDF node. Returns dict with: name, base_color (sRGB), metallic, roughness, transmission, ior. Falls back to diffuse_color if no Principled BSDF found. """ entry = { "name": mat.name, "base_color": [0.5, 0.5, 0.5], "metallic": 0.0, "roughness": 0.5, "transmission": 0.0, "ior": 1.45, } # Find Principled BSDF node bsdf = None if mat.use_nodes and mat.node_tree: for node in mat.node_tree.nodes: if node.type == "BSDF_PRINCIPLED": bsdf = node break if bsdf: # Base Color — convert linear → sRGB bc = bsdf.inputs["Base Color"].default_value entry["base_color"] = [ round(_linear_to_srgb(bc[0]), 4), round(_linear_to_srgb(bc[1]), 4), round(_linear_to_srgb(bc[2]), 4), ] entry["metallic"] = round(bsdf.inputs["Metallic"].default_value, 4) entry["roughness"] = round(bsdf.inputs["Roughness"].default_value, 4) # Transmission Weight (Blender 4.0+) or Transmission (older) for tx_name in ("Transmission Weight", "Transmission"): if tx_name in bsdf.inputs: entry["transmission"] = round(bsdf.inputs[tx_name].default_value, 4) break if "IOR" in bsdf.inputs: entry["ior"] = round(bsdf.inputs["IOR"].default_value, 4) else: # Fallback: use viewport diffuse_color (already sRGB) dc = mat.diffuse_color entry["base_color"] = [round(dc[0], 4), round(dc[1], 4), round(dc[2], 4)] return entry def main() -> None: argv = sys.argv if "--" not in argv: print(json.dumps({"error": "No blend path provided after --"})) sys.exit(1) blend_path = argv[argv.index("--") + 1] import bpy # type: ignore[import] bpy.ops.wm.open_mainfile(filepath=blend_path) materials = [ _extract_pbr(m) for m in bpy.data.materials if m.asset_data is not None ] node_groups = [ng.name for ng in bpy.data.node_groups if ng.asset_data is not None] catalog = {"materials": materials, "node_groups": node_groups} print(json.dumps(catalog)) try: main() except SystemExit: raise except Exception: traceback.print_exc() sys.exit(1)