Files
ChildHood/tools/asset-catalog/analyze_blueprints.py
T
starmen1265 9f6572600b 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枚举发现)
2026-06-09 19:01:50 +08:00

489 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""UE5 蓝图结构提取脚本。
对 index.json 中发现的蓝图资产提取:
- 是否为 Actor 类型蓝图
- 组件层次结构(类型、名称、相对变换)
- StaticMeshComponent 引用的模型路径
- 触发区域组件(BoxComponent / SphereComponent)
- 非 Actor 类型资产列表(Curve、Sound、Struct 等)
- AI 占位字段(interaction_type_tags, mechanism_tags, description)
输出: output/blueprints/<SubfolderName>.json,每个子目录一个文件
用法:
交互模式: exec(open(r"...\analyze_blueprints.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 = "[BlueprintAnalysis]"
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
# ============================================================
# 自动 Tag 生成
# ============================================================
import re
_BP_NAME_RULES = [
(r"Door", ["door", "interactive"]),
(r"Button|PushButton|FloorButton|Lever|Valve", ["button", "interactive"]),
(r"Lamp|CeilingLight|RoadLamp|RoadLight|FlamingTorch|BlockingLights", ["lighting"]),
(r"Decal|Zone|Circle|Octagon|Shape|Stripe|Arrow|Square", ["decal", "marker"]),
(r"Procedural|BarrierWall|StairPlane|SpeedBump|Ladder", ["procedural"]),
(r"Tooltip|DisplayPanel|CameraPreview", ["ui", "display"]),
(r"Human|Scale", ["reference"]),
(r"Spline", ["spline", "utility"]),
]
def _generate_blueprint_tags(name: str) -> list:
"""根据蓝图名生成 tags。"""
tags = []
for pattern, rule_tags in _BP_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
# 已知的非 Actor 蓝图类型
NON_ACTOR_TYPES = {
"CurveVector", "CurveLinearColor", "CurveFloat",
"SoundCue", "SoundWave", "SoundClass",
"UserDefinedStruct", "UserDefinedEnum",
"NiagaraSystem", "NiagaraEmitter", "NiagaraModule",
"Texture2D", "TextureCube",
"DataTable",
"StringTable",
"AnimSequence", "AnimMontage", "BlendSpace",
"WidgetBlueprint",
}
def _is_actor_blueprint(obj) -> bool:
"""判断加载的对象是否为 Actor 类型的蓝图。
纯 API 检测,多层回退:
1. 尝试 obj.generated_class() → 检查父类链
2. 尝试 obj.get_editor_property("generated_class") → 检查父类链
3. 尝试 obj.get_class() 的父类链(对 Blueprint 资产通常到不了 Actor)
"""
import unreal
# 方法 1 & 2: 通过 generated_class 属性获取生成的类
gen_class = None
for getter in [
lambda: obj.generated_class(),
lambda: obj.get_editor_property("generated_class"),
lambda: getattr(obj, "generated_class", None),
]:
try:
gen_class = getter()
if gen_class:
break
except Exception:
continue
if gen_class:
# 检查生成类的父类链是否到 Actor
if _check_class_is_actor(gen_class):
return True
# 方法 3: 直接加载 _C 后缀的生成类
try:
obj_path = obj.get_path_name()
# /Game/.../B_AutoDoor.B_AutoDoor → /Game/.../B_AutoDoor.B_AutoDoor_C
generated_path = obj_path.rsplit(".", 1)
if len(generated_path) == 2:
gen_path = generated_path[0] + "." + generated_path[1] + "_C"
gen_asset = unreal.load_asset(gen_path)
if gen_asset and _check_class_is_actor(gen_asset):
return True
except Exception:
pass
# 方法 4: 对 obj.get_class() 做父类链检查(通常失败但值得尝试)
try:
obj_class = obj.get_class()
if _check_class_is_actor(obj_class):
return True
except Exception:
pass
return False
def _check_class_is_actor(ue_class) -> bool:
"""检查 UE 类的父类链中是否包含 Actor。"""
try:
current = ue_class
visited = set()
while current:
name = current.get_name()
if name in visited:
break
visited.add(name)
if name == "Actor":
return True
if name in ("Object", "Interface", "None", "Object"):
break
current = current.get_super_class()
except Exception:
pass
return False
def _extract_component_info(comp) -> dict:
"""从组件提取信息。"""
import unreal
comp_info = {
"type": comp.get_class().get_name(),
"name": "",
"relative_location": None,
"relative_rotation": None,
"relative_scale": None,
}
try:
comp_info["name"] = comp.get_name()
except Exception:
pass
try:
rel_loc = comp.get_editor_property("relative_location")
if rel_loc:
comp_info["relative_location"] = _round3([rel_loc.x, rel_loc.y, rel_loc.z])
except Exception:
pass
try:
rel_rot = comp.get_editor_property("relative_rotation")
if rel_rot:
comp_info["relative_rotation"] = _round3([rel_rot.pitch, rel_rot.yaw, rel_rot.roll])
except Exception:
pass
try:
rel_scale = comp.get_editor_property("relative_scale3d") or comp.get_editor_property("relative_scale")
if rel_scale:
comp_info["relative_scale"] = _round3([rel_scale.x, rel_scale.y, rel_scale.z])
except Exception:
pass
# StaticMeshComponent: 提取引用的模型
if "StaticMeshComponent" in comp_info["type"] or "StaticMesh" in comp_info["type"]:
try:
sm = comp.get_static_mesh()
if sm:
comp_info["static_mesh_path"] = sm.get_path_name()
except Exception:
pass
# 尝试另一种方式
if "static_mesh_path" not in comp_info:
try:
sm = comp.get_editor_property("static_mesh")
if sm:
comp_info["static_mesh_path"] = sm.get_path_name()
except Exception:
pass
# SkeletalMeshComponent: 提取引用的骨骼模型
if "SkeletalMeshComponent" in comp_info["type"] or "SkeletalMesh" in comp_info["type"]:
try:
skm = comp.get_editor_property("skeletal_mesh")
if skm:
comp_info["skeletal_mesh_path"] = skm.get_path_name()
except Exception:
pass
return comp_info
def _analyze_single_blueprint(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,
"is_actor_blueprint": False,
"components": [],
"analysis_notes": [],
# AI Pass 2 占位
"interaction_type_tags": [],
"mechanism_tags": [],
"description": "",
}
try:
obj = unreal.load_asset(ue_path)
except Exception as e:
result["analysis_notes"].append(f"加载失败: {e}")
return result
if obj is None:
result["analysis_notes"].append("加载返回 None")
result["style_tags"] = _generate_blueprint_tags(name)
return result
# 检查是否为已知非 Actor 类型
actual_class = type(obj).__name__
if actual_class in NON_ACTOR_TYPES:
result["analysis_notes"].append(f"非 Actor 类型: {actual_class}")
result["style_tags"] = _generate_blueprint_tags(name)
return result
# 尝试判断是否为 Actor 蓝图
is_actor = _is_actor_blueprint(obj)
result["is_actor_blueprint"] = is_actor
if not is_actor:
result["analysis_notes"].append(f"非 Actor 蓝图: {actual_class}")
result["style_tags"] = _generate_blueprint_tags(name)
return result
# ---- 提取组件 ----
try:
# 优先通过 generated_class 获取 CDO
bp_class = None
for getter in [
lambda: obj.generated_class(),
lambda: obj.get_editor_property("generated_class"),
lambda: obj.get_class(),
]:
try:
bp_class = getter()
if bp_class:
break
except Exception:
continue
if bp_class:
cdo = bp_class.get_default_object()
if cdo:
components = _extract_all_components(cdo)
result["components"] = components
result["component_count"] = len(components)
else:
result["analysis_notes"].append("CDO 获取返回 None")
else:
result["analysis_notes"].append("无法获取 Blueprint 类")
except Exception as e:
result["analysis_notes"].append(f"组件提取失败: {e}")
# ---- 自动 Tags ----
result["style_tags"] = _generate_blueprint_tags(name)
# ---- 基于 Tags 推断 interaction_type_tags ----
interaction_tags = []
if result["components"]:
comp_types = [c.get("type", "") for c in result["components"]]
comp_names = [c.get("name", "") for c in result["components"]]
has_trigger = any("Box" in t or "Sphere" in t or "Capsule" in t or
"Trigger" in n for t, n in zip(comp_types, comp_names))
has_timeline = any("Timeline" in n for n in comp_names)
has_smc = any("StaticMesh" in t for t in comp_types)
if has_trigger and "door" in str(result["style_tags"]):
interaction_tags.extend(["proximity_trigger", "auto_door"])
elif has_trigger and "button" in str(result["style_tags"]):
interaction_tags.extend(["press_interact", "button_trigger"])
elif has_trigger:
interaction_tags.append("proximity_trigger")
if has_timeline:
interaction_tags.append("animated")
if has_smc and not has_trigger and "interactive" not in str(result["style_tags"]):
interaction_tags.append("static_prop")
result["interaction_type_tags"] = interaction_tags
return result
def _extract_all_components(cdo) -> list:
"""从 CDO 提取所有组件信息。"""
import unreal
components = []
try:
# 方法 1: get_all_components (某些版本可用)
all_comps = cdo.get_all_components()
if all_comps:
for comp in all_comps:
comp_info = _extract_component_info(comp)
components.append(comp_info)
return components
except Exception:
pass
try:
# 方法 2: 通过 BlueprintCreatedComponents 或实例化组件
# 尝试获取组件列表
comp_prop = cdo.get_editor_property("blueprint_created_components")
if comp_prop:
for item in comp_prop:
try:
comp = item.get_editor_property("component")
if comp:
comp_info = _extract_component_info(comp)
components.append(comp_info)
except Exception:
pass
except Exception:
pass
try:
# 方法 3: 通过 InstanceComponents
inst_comps = cdo.get_editor_property("instance_components")
if inst_comps:
for comp in inst_comps:
comp_info = _extract_component_info(comp)
components.append(comp_info)
except Exception:
pass
# 去重(某些方法可能重复获取)
seen = set()
unique = []
for c in components:
key = (c.get("type", ""), c.get("name", ""))
if key not in seen:
seen.add(key)
unique.append(c)
return unique
# ============================================================
# 主流程
# ============================================================
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)
blueprints_by_subfolder = index_data.get("blueprints", {})
if not blueprints_by_subfolder:
print(f"{TAG} 无蓝图资产需要分析")
return {"status": "ok", "analyzed": 0}
# 创建输出目录
blueprints_dir = output_dir / "blueprints"
blueprints_dir.mkdir(parents=True, exist_ok=True)
total_all = sum(len(assets) for assets in blueprints_by_subfolder.values())
print(f"{TAG} 共 {total_all} 个蓝图资产,分布在 {len(blueprints_by_subfolder)} 个子目录")
analyzed = 0
skipped_non_actor = 0
for subfolder, assets in blueprints_by_subfolder.items():
safe_name = safe_subfolder_name(subfolder)
output_file = blueprints_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 += len(existing.get("actor_blueprints", {}))
skipped_non_actor += len(existing.get("skipped_non_actor", []))
continue
except Exception:
pass
print(f"{TAG} 分析子目录: {subfolder} ({len(assets)} 个资产)")
actor_blueprints = {}
non_actor = []
sub_analyzed = 0
for i, asset_info in enumerate(assets):
progress_log(i, len(assets), f"{asset_info['name']}")
result = _analyze_single_blueprint(asset_info)
if result.get("is_actor_blueprint"):
actor_blueprints[asset_info["name"]] = result
sub_analyzed += 1
else:
non_actor.append({
"name": asset_info["name"],
"ue_class_type": result.get("ue_class_type", asset_info.get("ue_class_type", "Unknown")),
"note": result.get("analysis_notes", []),
})
# 写入分组文件
group_data = {
"subfolder": subfolder,
"source_path": f"{get_scan_root()}/Blueprints/{subfolder}" if subfolder else f"{get_scan_root()}/Blueprints",
"actor_blueprint_count": len(actor_blueprints),
"non_actor_count": len(non_actor),
"completed": True,
"actor_blueprints": actor_blueprints,
"skipped_non_actor": non_actor,
}
write_json(group_data, output_file)
analyzed += sub_analyzed
skipped_non_actor += len(non_actor)
print(f"{TAG} ✅ {safe_name}: {sub_analyzed} Actor 蓝图, {len(non_actor)} 非 Actor → {output_file}")
print(f"{TAG} 全部完成: {analyzed} Actor 蓝图, {skipped_non_actor} 非 Actor / {total_all} 总计")
return {
"status": "ok",
"total": total_all,
"actor_blueprints": analyzed,
"skipped_non_actor": skipped_non_actor,
}
# ---- 入口 ----
if __name__ == "__main__":
if not wait_for_editor():
sys.exit(1)
result = run()
if result.get("status") != "ok":
sys.exit(1)