9f6572600b
- 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枚举发现)
489 lines
16 KiB
Python
489 lines
16 KiB
Python
"""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)
|