feat(tools): 添加UE5资产探索脚本系统asset-catalog

- discover_assets: 枚举Content资产,按UE5类型分类,输出index.json
- analyze_meshes: 包围盒/锚点/bbox_faces(area+orientation)/Line Trace细节包围盒/规则Tags
- analyze_materials: 动态参数提取/base_color_hex自动转换/规则Tags
- analyze_blueprints: generated_class API Actor检测/组件提取/interaction推断Tags
- run_all编排器: 断点续跑/步骤控制/错误traceback/UE5 JSON序列化兼容
- probe脚本: UE5.7 API探测(TraceTypeQuery/DrawDebugTrace枚举发现)
This commit is contained in:
2026-06-09 19:01:50 +08:00
parent 0646d8c349
commit 9f6572600b
12 changed files with 2600 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
"""UE5 材质参数提取脚本。
对 index.json 中发现的每个材质资产提取:
- UE5 类型(MaterialInstanceConstant / Material)
- 父材质路径
- 所有 scalar / vector 参数(动态发现,不硬编码参数名)
- AI 占位字段(base_color_hex, style_tags)
输出: output/materials/<SubfolderName>.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)