"""UE5 材质参数提取脚本。 对 index.json 中发现的每个材质资产提取: - UE5 类型(MaterialInstanceConstant / Material) - 父材质路径 - 所有 scalar / vector 参数(动态发现,不硬编码参数名) - AI 占位字段(base_color_hex, style_tags) 输出: output/materials/.json,每个子目录一个文件 用法: 交互模式: exec(open(r"...\analyze_materials.py").read()) 命令行: 通过 run_all.py 调用 """ import sys from pathlib import Path _SCRIPT_DIR = Path(__file__).resolve().parent if str(_SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPT_DIR)) from _common import ( HAS_UNREAL, get_scan_root, get_output_dir, write_json, read_json, wait_for_editor, progress_log, iso_now, safe_subfolder_name, ) TAG = "[MaterialAnalysis]" def _round3(v): """将浮点数或列表保留 3 位小数。""" if isinstance(v, (list, tuple)): return [round(x, 3) if isinstance(x, float) else x for x in v] return round(v, 3) if isinstance(v, float) else v def _rgb_to_hex(r: float, g: float, b: float) -> str: """将 0-1 范围的 RGB 转为 hex 字符串。""" ri = max(0, min(255, round(r * 255))) gi = max(0, min(255, round(g * 255))) bi = max(0, min(255, round(b * 255))) return f"#{ri:02X}{gi:02X}{bi:02X}" # ============================================================ # 自动 Tag 生成 # ============================================================ import re _MATERIAL_NAME_RULES = [ (r"DevRed$|DevOrange$|DevGray$|DevBrown$|DevWhite$|DevBlue$|DevGreen$|" r"DevSkyBlue$|DevYellow$|DevFoliage$|DevPink$|DevBlack$|DevPurple$|DevLightGreen$", ["basic_color"]), (r"Glass", ["glass", "transparent"]), (r"DevLight_", ["emissive", "light_color"]), (r"Grid", ["grid", "debug"]), (r"Mirror", ["mirror", "reflective"]), (r"TPLA", ["tri_planar", "local_aligned"]), (r"TPWA", ["tri_planar", "world_aligned"]), (r"StainDecal", ["decal", "stain"]), (r"DevArrow|DevTrigger", ["editor", "debug"]), (r"Emissive|FullyBlack", ["special"]), (r"DisplayPanel", ["display"]), ] def _generate_material_tags(name: str) -> list: """根据材质名生成 tags。""" tags = [] for pattern, rule_tags in _MATERIAL_NAME_RULES: if re.search(pattern, name): for t in rule_tags: if t not in tags: tags.append(t) if not tags: tags = ["unclassified"] return tags def _analyze_single_material(asset_info: dict) -> dict: """分析单个材质资产。""" import unreal ue_path = asset_info["ue_path"] name = asset_info["name"] ue_class_type = asset_info.get("ue_class_type", "Unknown") result = { "ue_path": ue_path, "ue_class_type": ue_class_type, "parent_material": None, "parameters": {"vector": [], "scalar": []}, "analysis_notes": [], # AI Pass 2 占位 "base_color_hex": "", "style_tags": [], } try: mat = unreal.load_asset(ue_path) except Exception as e: result["analysis_notes"].append(f"加载失败: {e}") return result if mat is None: result["analysis_notes"].append("加载返回 None") return result # ---- 父材质 ---- try: parent = mat.get_editor_property("parent") if parent: result["parent_material"] = parent.get_path_name() except Exception: try: base_mat = mat.get_base_material() if base_mat: result["parent_material"] = base_mat.get_path_name() except Exception: pass # ---- 参数提取 ---- # MaterialInstanceConstant 有参数覆盖 if ue_class_type == "MaterialInstanceConstant": _extract_instance_params(mat, result) else: # 基础 Material: 尝试读取静态参数 _extract_material_params(mat, result) # ---- 自动 Tags ---- result["style_tags"] = _generate_material_tags(name) # ---- 自动 base_color_hex ---- # 从 vector 参数中找名称含 "Color" 的参数 for vp in result["parameters"]["vector"]: if "color" in vp["name"].lower() and not result["base_color_hex"]: val = vp["value"] result["base_color_hex"] = _rgb_to_hex(val["r"], val["g"], val["b"]) return result def _extract_instance_params(mat, result: dict): """从 MaterialInstanceConstant 提取参数。""" import unreal # 尝试方法 1: get_editor_property 获取参数列表 try: # scalar 参数 scalar_params = mat.get_editor_property("scalar_parameter_values") if scalar_params: for sp in scalar_params: try: param_info = sp.get_editor_property("parameter_info") param_value = sp.get_editor_property("parameter_value") param_name = param_info.get_editor_property("name") if param_info else None if param_name and param_value is not None: result["parameters"]["scalar"].append({ "name": str(param_name), "value": round(float(param_value), 6), }) except Exception: pass except Exception: pass try: # vector 参数 vector_params = mat.get_editor_property("vector_parameter_values") if vector_params: for vp in vector_params: try: param_info = vp.get_editor_property("parameter_info") param_value = vp.get_editor_property("parameter_value") param_name = param_info.get_editor_property("name") if param_info else None if param_name and param_value is not None: result["parameters"]["vector"].append({ "name": str(param_name), "value": { "r": round(float(param_value.r), 4), "g": round(float(param_value.g), 4), "b": round(float(param_value.b), 4), "a": round(float(param_value.a), 4), }, }) except Exception: pass except Exception: pass # 尝试方法 2: MaterialEditingLibrary if not result["parameters"]["vector"] and not result["parameters"]["scalar"]: _try_material_editing_lib(mat, result) def _try_material_editing_lib(mat, result: dict): """尝试通过 MaterialEditingLibrary 读取参数。""" import unreal try: # 获取 scalar 参数 scalar_params = unreal.MaterialEditingLibrary.get_material_instance_scalar_parameter_values(mat) if scalar_params: for sp in scalar_params: try: name = sp.get_editor_property("parameter_name") if hasattr(sp, 'get_editor_property') else str(sp) value = sp.get_editor_property("default_value") if hasattr(sp, 'get_editor_property') else None if name and value is not None: result["parameters"]["scalar"].append({ "name": str(name), "value": round(float(value), 6), }) except Exception: pass except Exception: pass try: # 获取 vector 参数 vector_params = unreal.MaterialEditingLibrary.get_material_instance_vector_parameter_values(mat) if vector_params: for vp in vector_params: try: name = vp.get_editor_property("parameter_name") if hasattr(vp, 'get_editor_property') else str(vp) value = vp.get_editor_property("default_value") if hasattr(vp, 'get_editor_property') else None if name and value is not None: result["parameters"]["vector"].append({ "name": str(name), "value": { "r": round(float(value.r), 4), "g": round(float(value.g), 4), "b": round(float(value.b), 4), "a": round(float(value.a), 4), }, }) except Exception: pass except Exception: pass def _extract_material_params(mat, result: dict): """从基础 Material 尝试提取参数。""" # 基础 Material 的参数提取更困难 # 标记为需要 AI 判断 result["analysis_notes"].append("基础 Material,参数需从实例推断") # ============================================================ # 主流程 # ============================================================ def run(index_data: dict = None) -> dict: """执行材质分析。""" if not HAS_UNREAL: print(f"{TAG} 错误: unreal 模块不可用") return {"status": "error", "reason": "no unreal"} output_dir = get_output_dir() # 加载 index.json if index_data is None: index_path = output_dir / "index.json" if not index_path.exists(): print(f"{TAG} 错误: index.json 不存在,请先运行 discover") return {"status": "error", "reason": "no index.json"} index_data = read_json(index_path) materials_by_subfolder = index_data.get("materials", {}) if not materials_by_subfolder: print(f"{TAG} 无材质资产需要分析") return {"status": "ok", "analyzed": 0} # 创建输出目录 materials_dir = output_dir / "materials" materials_dir.mkdir(parents=True, exist_ok=True) total_all = sum(len(assets) for assets in materials_by_subfolder.values()) print(f"{TAG} 共 {total_all} 个材质,分布在 {len(materials_by_subfolder)} 个子目录") analyzed = 0 failed = 0 for subfolder, assets in materials_by_subfolder.items(): safe_name = safe_subfolder_name(subfolder) output_file = materials_dir / f"{safe_name}.json" # 断点续跑 if output_file.exists(): try: existing = read_json(output_file) if existing.get("completed"): print(f"{TAG} 跳过已完成的: {safe_name}") analyzed += existing.get("count", len(assets)) continue except Exception: pass print(f"{TAG} 分析子目录: {subfolder} ({len(assets)} 个材质)") mat_results = {} subfolder_analyzed = 0 subfolder_failed = 0 for i, asset_info in enumerate(assets): progress_log(i, len(assets), f"{asset_info['name']}") result = _analyze_single_material(asset_info) mat_results[asset_info["name"]] = result if result.get("analysis_notes") and "加载失败" in str(result.get("analysis_notes")): subfolder_failed += 1 else: subfolder_analyzed += 1 # 写入分组文件 group_data = { "subfolder": subfolder, "source_path": f"{get_scan_root()}/Materials/{subfolder}" if subfolder else f"{get_scan_root()}/Materials", "count": len(assets), "completed": True, "materials": mat_results, } write_json(group_data, output_file) analyzed += subfolder_analyzed failed += subfolder_failed print(f"{TAG} ✅ {safe_name}: {subfolder_analyzed} 成功, {subfolder_failed} 失败 → {output_file}") print(f"{TAG} 全部完成: {analyzed} 成功, {failed} 失败 / {total_all} 总计") return { "status": "ok", "total": total_all, "analyzed": analyzed, "failed": failed, } # ---- 入口 ---- if __name__ == "__main__": if not wait_for_editor(): sys.exit(1) result = run() if result.get("status") != "ok": sys.exit(1)