Files
ChildHood/tools/asset-catalog/discover_assets.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

220 lines
6.8 KiB
Python

"""UE5 资产发现脚本。
扫描指定 Content 路径下所有资产,记录名称、路径、UE5 类型。
不做任何推断,仅记录 UE5 原始数据。
输出: output/index.json
用法:
交互模式: exec(open(r"...\discover_assets.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 = "[Discover]"
# 已知类型分类
MESH_TYPES = {"StaticMesh"}
MATERIAL_TYPES = {"MaterialInstanceConstant", "Material"}
# 其余全部归入 others
def _get_subfolder(asset_path: str, scan_root: str) -> str:
"""从资产完整路径中提取相对于 scan_root/Meshes|Materials|... 的子目录。
例:
path = "/Game/BlockingStarterPack/Meshes/Architecture/Walls/SM_Wall_400x300.SM_Wall_400x300"
scan_root = "/Game/BlockingStarterPack"
→ "Architecture/Walls"
"""
# 去掉末尾的 .XXX
base = asset_path.rsplit(".", 1)[0]
# 找到 scan_root 之后的路径
if base.startswith(scan_root + "/"):
relative = base[len(scan_root) + 1:]
else:
relative = base
# 去掉第一级目录和最后一级(资产名)
# Meshes/Architecture/Walls/SM_Wall_400x300 → Architecture/Walls
parts = relative.split("/")
if len(parts) > 2:
return "/".join(parts[1:-1]) # 去掉首段(Meshes 等)和末段(资产名)
return ""
def _get_asset_name(asset_path: str) -> str:
"""从 UE 资产路径提取资产名。
例: "/Game/.../SM_Wall_400x300.SM_Wall_400x300" → "SM_Wall_400x300"
"""
# 取最后一个 / 后、第一个 . 前
last_part = asset_path.rsplit("/", 1)[-1]
return last_part.rsplit(".", 1)[0]
def _load_and_classify(asset_path: str) -> dict:
"""加载资产并记录其 UE5 类型名。
Returns:
{"name": str, "ue_path": str, "ue_class_type": str}
加载失败时 ue_class_type 为 "LOAD_FAILED"
"""
name = _get_asset_name(asset_path)
try:
import unreal
obj = unreal.load_asset(asset_path)
if obj is None:
return {"name": name, "ue_path": asset_path, "ue_class_type": "LOAD_FAILED"}
class_name = type(obj).__name__
return {"name": name, "ue_path": asset_path, "ue_class_type": class_name}
except Exception as e:
return {"name": name, "ue_path": asset_path, "ue_class_type": f"ERROR:{e}"}
def run() -> dict:
"""执行资产发现。
Returns:
统计信息 dict
"""
if not HAS_UNREAL:
print(f"{TAG} 错误: unreal 模块不可用")
return {"status": "error", "reason": "no unreal"}
import unreal
scan_root = get_scan_root()
output_dir = get_output_dir()
print(f"{TAG} 扫描根目录: {scan_root}")
print(f"{TAG} 输出目录: {output_dir}")
# ---- 枚举所有资产 ----
print(f"{TAG} 开始枚举资产...")
all_assets = []
try:
asset_list = unreal.EditorAssetLibrary.list_assets(scan_root, recursive=True, include_folder=False)
all_assets = list(asset_list)
except Exception as e:
print(f"{TAG} 错误: list_assets 失败: {e}")
return {"status": "error", "reason": str(e)}
total = len(all_assets)
print(f"{TAG} 发现 {total} 个资产")
if total == 0:
print(f"{TAG} 未发现任何资产,退出")
return {"status": "ok", "total": 0}
# ---- 分类 ----
meshes = {} # subfolder -> [asset_info, ...]
materials = {} # subfolder -> [asset_info, ...]
others = {} # subfolder -> [asset_info, ...]
by_type = {}
for i, asset_path in enumerate(all_assets):
progress_log(i, total, "资产分类")
info = _load_and_classify(asset_path)
class_type = info["ue_class_type"]
subfolder = _get_subfolder(asset_path, scan_root)
# 统计
by_type[class_type] = by_type.get(class_type, 0) + 1
# 分类
if class_type in MESH_TYPES:
meshes.setdefault(subfolder, []).append(info)
elif class_type in MATERIAL_TYPES:
materials.setdefault(subfolder, []).append(info)
else:
others.setdefault(subfolder, []).append(info)
# ---- 蓝图归类 ----
# 从 others 中提取蓝图相关(Blueprint 类型或目录含 Blueprint 的)
blueprints = {}
# UE5 的蓝图资产 class type 可能是各种名称
# 我们把 ue_path 中包含 /Blueprints/ 的单独归类
remaining_others = {}
for subfolder, assets in others.items():
bp_assets = []
other_assets = []
for a in assets:
if "/Blueprints/" in a["ue_path"] or a["ue_class_type"] in {
"Blueprint", "BlueprintGeneratedClass", "WidgetBlueprint",
"AnimBlueprint", "BlueprintType",
}:
bp_assets.append(a)
else:
other_assets.append(a)
if bp_assets:
blueprints[subfolder] = bp_assets
if other_assets:
remaining_others[subfolder] = other_assets
# ---- 写入 index.json ----
index = {
"metadata": {
"generated_at": iso_now(),
"scan_root": scan_root,
"total_assets": total,
"by_type": by_type,
},
"meshes": meshes,
"materials": materials,
"blueprints": blueprints,
"others": remaining_others,
}
output_path = output_dir / "index.json"
write_json(index, output_path)
print(f"{TAG} ✅ 已写入: {output_path}")
# ---- 统计 ----
mesh_count = sum(len(v) for v in meshes.values())
mat_count = sum(len(v) for v in materials.values())
bp_count = sum(len(v) for v in blueprints.values())
other_count = sum(len(v) for v in remaining_others.values())
stats = {
"status": "ok",
"total": total,
"meshes": mesh_count,
"materials": mat_count,
"blueprints": bp_count,
"others": other_count,
"mesh_subfolders": list(meshes.keys()),
"material_subfolders": list(materials.keys()),
"blueprint_subfolders": list(blueprints.keys()),
}
print(f"{TAG} 统计:")
print(f"{TAG} StaticMesh: {mesh_count} ({len(meshes)} 子目录)")
print(f"{TAG} Materials: {mat_count} ({len(materials)} 子目录)")
print(f"{TAG} Blueprints: {bp_count} ({len(blueprints)} 子目录)")
print(f"{TAG} Others: {other_count}")
return stats
# ---- 入口 ----
if __name__ == "__main__":
if not wait_for_editor():
sys.exit(1)
result = run()
if result.get("status") != "ok":
sys.exit(1)