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
View File
+284
View File
@@ -0,0 +1,284 @@
"""UE5 资产探索工具 — 共享工具模块。
提供环境变量读取、JSON I/O、UE5 编辑器等待、进度日志等基础功能。
所有分析脚本共享此模块。
环境变量:
AC_SCAN_ROOT — 要扫描的 UE5 Content 路径(默认 /Game/BlockingStarterPack)
AC_OUTPUT_DIR — JSON 输出目录(绝对路径,默认 <project>/output/asset-catalog/)
AC_NO_QUIT — 设为 "1" 运行后不退出编辑器
AC_STEPS — 逗号分隔的步骤列表(默认 discover,mesh,material,blueprint)
"""
import json
import os
import sys
import time
from pathlib import Path
# ---- Unreal import guard ----
try:
import unreal
HAS_UNREAL = True
except ImportError:
HAS_UNREAL = False
# ---- 项目根目录 ----
_SCRIPT_DIR = Path(__file__).resolve().parent
# ============================================================
# 环境变量读取
# ============================================================
def get_scan_root() -> str:
"""返回要扫描的 UE5 Content 路径。"""
return os.environ.get("AC_SCAN_ROOT", "/Game/BlockingStarterPack")
def get_output_dir() -> Path:
"""返回 JSON 输出目录(Path),自动创建。"""
raw = os.environ.get("AC_OUTPUT_DIR", "")
if raw:
p = Path(raw)
else:
# 默认放在项目 output/asset-catalog/ 下
# 尝试通过 unreal API 获取项目目录
if HAS_UNREAL:
try:
project_dir = Path(unreal.Paths.project_dir()).parent
p = project_dir / "output" / "asset-catalog"
except Exception:
p = Path("output") / "asset-catalog"
else:
p = Path("output") / "asset-catalog"
p.mkdir(parents=True, exist_ok=True)
return p
def should_no_quit() -> bool:
"""运行完成后是否不退出编辑器。"""
return os.environ.get("AC_NO_QUIT", "0") == "1"
def get_steps() -> list:
"""返回要执行的步骤列表。"""
raw = os.environ.get("AC_STEPS", "discover,mesh,material,blueprint")
return [s.strip() for s in raw.split(",") if s.strip()]
# ============================================================
# JSON I/O
# ============================================================
class _UE5JsonEncoder(json.JSONEncoder):
"""JSON encoder that handles UE5 Python types (unreal.Name, etc.)."""
def default(self, obj):
try:
return str(obj)
except Exception:
return super().default(obj)
def write_json(data, filepath):
"""写入 JSON 文件,ensure_ascii=False, indent=2。
自动处理 UE5 Python 类型(unreal.Name 等)。
Args:
data: 可序列化的 Python 对象
filepath: Path 或 str,绝对路径
"""
p = Path(filepath)
p.parent.mkdir(parents=True, exist_ok=True)
with open(p, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2, cls=_UE5JsonEncoder)
def read_json(filepath) -> dict:
"""读取 JSON 文件。
Args:
filepath: Path 或 str
Returns:
解析后的 dict
"""
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
# ============================================================
# UE5 编辑器等待
# ============================================================
def wait_for_editor(timeout=90):
"""轮询等待 UE5 EditorActorSubsystem 可用。
Args:
timeout: 最大等待秒数
Returns:
True 如果编辑器就绪,False 如果超时
"""
if not HAS_UNREAL:
print("[AssetCatalog] 错误: unreal 模块不可用")
return False
print("[AssetCatalog] 等待编辑器初始化...")
start = time.time()
while time.time() - start < timeout:
try:
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
if subsys is not None:
elapsed = time.time() - start
print(f"[AssetCatalog] 编辑器就绪 ({elapsed:.1f}s)")
return True
except Exception:
pass
time.sleep(1)
print(f"[AssetCatalog] 错误: 编辑器初始化超时 ({timeout}s)")
return False
def quit_editor():
"""退出 UE5 编辑器。"""
if HAS_UNREAL:
unreal.SystemLibrary.execute_console_command(None, "quit")
# ============================================================
# 工具函数
# ============================================================
def setup_paths():
"""将 tools/ 目录加入 sys.path(如果尚未加入)。"""
tools_dir = str(_SCRIPT_DIR.parent)
if tools_dir not in sys.path:
sys.path.insert(0, tools_dir)
ac_dir = str(_SCRIPT_DIR)
if ac_dir not in sys.path:
sys.path.insert(0, ac_dir)
def progress_log(current: int, total: int, label: str = "", interval: int = 20):
"""每 interval 条打印一次进度。
Args:
current: 当前索引(0-based)
total: 总数
label: 附加标签
interval: 打印间隔
"""
if current % interval == 0 or current == total - 1:
pct = (current + 1) / total * 100
print(f"[AssetCatalog] 进度: {current + 1}/{total} ({pct:.0f}%) {label}")
def safe_subfolder_name(subfolder: str) -> str:
"""将子目录路径转为安全的文件名。
例: "Architecture/Walls" -> "Architecture_Walls"
"Blocking/Basics" -> "Blocking_Basics"
"" -> "root"
"""
if not subfolder:
return "root"
return subfolder.replace("/", "_").replace("\\", "_")
def iso_now() -> str:
"""返回当前时间的 ISO 格式字符串。"""
import datetime
return datetime.datetime.now().isoformat()
def classify_pivot(bmin, bmax, tol=5.0):
"""从包围盒 min/max 推断锚点类型。
Args:
bmin: (x, y, z) 最小值元组
bmax: (x, y, z) 最大值元组
tol: 判断容差(cm)
Returns:
dict: {"type": str, "offset_xy": [float, float]}
type 为 CORNER / CENTER / CENTER_BOTTOM / EDGE_BOTTOM / OTHER
"""
mn_x, mn_y, mn_z = bmin
mx_x, mx_y, mx_z = bmax
size_x = mx_x - mn_x
size_y = mx_y - mn_y
size_z = mx_z - mn_z
half_x, half_y, half_z = size_x / 2, size_y / 2, size_z / 2
# 判断逻辑
is_corner = (abs(mn_x) < tol and abs(mn_y) < tol and abs(mn_z) < tol)
is_center = (abs(mn_x + half_x) < tol and abs(mn_y + half_y) < tol
and abs(mn_z + half_z) < tol)
is_center_bot = (abs(mn_x + half_x) < tol and abs(mn_y + half_y) < tol
and abs(mn_z) < tol)
# EDGE_BOTTOM: 一端在 0, 一端在 max, 底部在 0
is_edge_bot_x = (abs(mn_x) < tol and abs(mn_z) < tol and abs(mx_y - size_y) < tol)
is_edge_bot_y = (abs(mn_y) < tol and abs(mn_z) < tol and abs(mx_x - size_x) < tol)
offset_xy = [(mn_x + mx_x) / 2, (mn_y + mx_y) / 2]
if is_center:
pivot_type = "CENTER"
elif is_center_bot:
pivot_type = "CENTER_BOTTOM"
elif is_corner:
pivot_type = "CORNER"
elif is_edge_bot_x or is_edge_bot_y:
pivot_type = "EDGE_BOTTOM"
else:
pivot_type = "OTHER"
return {"type": pivot_type, "offset_xy": offset_xy}
def compute_bbox_faces(bmin, bmax):
"""根据包围盒 6 个面生成表面信息,含面积和方向标签。
Args:
bmin: (x, y, z) 最小值
bmax: (x, y, z) 最大值
Returns:
list of dict, 按面积降序排列
"""
mn_x, mn_y, mn_z = bmin
mx_x, mx_y, mx_z = bmax
cx = (mn_x + mx_x) / 2
cy = (mn_y + mx_y) / 2
cz = (mn_z + mx_z) / 2
sx = mx_x - mn_x
sy = mx_y - mn_y
sz = mx_z - mn_z
raw_faces = [
{"face": "top", "center": [cx, cy, mx_z], "normal": [0, 0, 1],
"size": [sx, sy], "orientation": "horizontal_up"},
{"face": "bottom", "center": [cx, cy, mn_z], "normal": [0, 0, -1],
"size": [sx, sy], "orientation": "horizontal_down"},
{"face": "front", "center": [cx, mx_y, cz], "normal": [0, 1, 0],
"size": [sx, sz], "orientation": "vertical_y+"},
{"face": "back", "center": [cx, mn_y, cz], "normal": [0, -1, 0],
"size": [sx, sz], "orientation": "vertical_y-"},
{"face": "left", "center": [mn_x, cy, cz], "normal": [-1, 0, 0],
"size": [sy, sz], "orientation": "vertical_x-"},
{"face": "right", "center": [mx_x, cy, cz], "normal": [1, 0, 0],
"size": [sy, sz], "orientation": "vertical_x+"},
]
# 计算面积和宽高比
for f in raw_faces:
w, h = f["size"]
f["area"] = round(abs(w * h), 1)
f["aspect_ratio"] = round(max(w, h) / min(w, h), 2) if min(w, h) > 0 else 0
# 按面积降序排列
raw_faces.sort(key=lambda f: f["area"], reverse=True)
return raw_faces
+488
View File
@@ -0,0 +1,488 @@
"""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)
+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)
+634
View File
@@ -0,0 +1,634 @@
"""UE5 静态模型几何提取脚本。
对 index.json 中发现的每个 StaticMesh 提取:
- 整体包围盒(min, max, size)
- 锚点类型和 XY 偏移
- 网格复杂度(顶点数、三角形数)
- Socket 列表
- 材质槽位
- 包围盒 6 面候选
- 射线检测细节包围盒(spawn + Line Trace)
- AI 占位字段(key_surfaces, style_tags, scene_tags 等)
输出: output/meshes/<SubfolderName>.json,每个子目录一个文件
用法:
交互模式: exec(open(r"...\analyze_meshes.py").read())
命令行: 通过 run_all.py 调用
"""
import sys
import math
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, get_steps,
write_json, read_json, wait_for_editor, progress_log,
iso_now, safe_subfolder_name, classify_pivot, compute_bbox_faces,
)
TAG = "[MeshAnalysis]"
# ============================================================
# 自动 Tag 生成
# ============================================================
# (pattern, [style_tags])
_MESH_NAME_RULES = [
# 家具
(r"Table", ["furniture", "table"]),
(r"Chair|Armchair", ["furniture", "seating"]),
(r"Bed", ["furniture", "bed"]),
(r"Monitor|Laptop|Keyboard", ["furniture", "electronics"]),
(r"Broom|Radiator", ["furniture", "appliance"]),
# 建筑
(r"Wall", ["architecture", "wall"]),
(r"Floor", ["architecture", "floor"]),
(r"Door", ["architecture", "door"]),
(r"Window", ["architecture", "window"]),
(r"Stair", ["architecture", "stair"]),
(r"Beam|Ladder", ["architecture", "structure"]),
(r"Barrier|Fence", ["architecture", "barrier"]),
# 灯光
(r"Lamp|Light|Spot|Flash|Torch", ["lighting"]),
# 车辆
(r"\bCar\b|Truck|Bus|Boat|Plane|Metro|Trailer", ["vehicle"]),
# 工业
(r"Barrel|WoodenBox|WoodenCrate|Pallet|Container", ["industrial"]),
# 道路
(r"Road|Barricade|SpeedBump|Cone", ["outdoor", "road"]),
# 基础体
(r"\bCube\b|\bCylinder\b|\bSphere\b|\bCone\b|\bRamp\b|\bPipe\b|Quarter", ["primitive"]),
# 浴室
(r"Toilet|Urinal", ["bathroom"]),
# 装饰
(r"Book|Bottle|Mug|Plate|Can|Flask|Padlock|Bucket|Trash", ["decoration", "prop"]),
# 自然
(r"Tree|Foliage|Mount", ["vegetation"]),
# 武器
(r"Pistol|Sniper|Sword|Dagger|Target", ["weapon"]),
# 人物
(r"Human|Camera|Cage", ["character", "reference"]),
# 展示
(r"DisplayPanel", ["display"]),
]
# (subdirectory keyword, [scene_tags])
_MESH_SCENE_RULES = [
(r"Furnitures|Bedroom|Bathroom|Decorations", ["indoor", "home"]),
(r"Lights", ["indoor", "outdoor"]),
(r"Vehicles|Roads", ["outdoor", "street"]),
(r"Industrials|Pipes|Gears", ["outdoor", "industrial"]),
(r"SmallBuildings|BigBuildings", ["outdoor", "building"]),
(r"Architecture", ["architecture", "construction"]),
(r"Vegetations", ["outdoor", "nature"]),
(r"Primitives|Extra", ["utility"]),
(r"Weapons|Interactions", ["gameplay"]),
]
def _generate_mesh_tags(name: str, subfolder: str) -> tuple:
"""根据模型名和目录生成 style_tags 和 scene_tags。
Returns:
(style_tags, scene_tags)
"""
import re
style_tags = []
scene_tags = []
# 基于名称的 style tags
for pattern, tags in _MESH_NAME_RULES:
if re.search(pattern, name):
for t in tags:
if t not in style_tags:
style_tags.append(t)
# 基于目录的 scene tags
for pattern, tags in _MESH_SCENE_RULES:
if re.search(pattern, subfolder):
for t in tags:
if t not in scene_tags:
scene_tags.append(t)
# 如果没有任何 style tag,标记为 unknown
if not style_tags:
style_tags = ["unknown"]
if not scene_tags:
scene_tags = ["unclassified"]
return style_tags, scene_tags
# ============================================================
# 单个 Mesh 分析
# ============================================================
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 _try_get_complexity(mesh) -> dict:
"""尝试获取网格复杂度信息。"""
info = {"num_verts": None, "num_triangles": None}
try:
import unreal
# 尝试获取 LOD 数量
num_lod = mesh.get_num_lods()
info["num_lods"] = num_lod
except Exception:
pass
try:
# 尝试获取 section 信息
num_sections = mesh.get_num_sections(0)
info["num_sections"] = num_sections
except Exception:
pass
try:
# 尝试通过 render data 获取顶点数
render_data = mesh.get_render_data()
if render_data:
info["num_verts"] = render_data.num_vertices
info["num_triangles"] = render_data.num_triangles
except Exception:
pass
return info
def _try_get_sockets(mesh) -> list:
"""尝试获取 Socket 名称列表。"""
sockets = []
try:
import unreal
socket_names = mesh.find_socket_names() if hasattr(mesh, 'find_socket_names') else []
for name in socket_names:
try:
sock = mesh.find_socket(name)
if sock:
loc = sock.relative_location
rot = sock.relative_rotation
sockets.append({
"name": str(name),
"relative_location": _round3([loc.x, loc.y, loc.z]),
"relative_rotation": _round3([rot.pitch, rot.yaw, rot.roll]),
})
except Exception:
sockets.append({"name": str(name), "relative_location": None, "relative_rotation": None})
except Exception:
pass
return sockets
def _try_get_material_slots(mesh) -> list:
"""尝试获取材质槽位。"""
import unreal
slots = []
# 方法 1: get_material(i)
try:
num_mat = mesh.get_num_materials()
for i in range(num_mat):
slot_info = {"index": i, "slot_name": None, "material_name": None}
try:
mat = mesh.get_material(i)
if mat:
slot_info["material_name"] = mat.get_name()
except Exception:
pass
slots.append(slot_info)
except Exception:
pass
# 方法 2: get_editor_property("static_materials")
if not slots:
try:
static_mats = mesh.get_editor_property("static_materials")
if static_mats:
for i, sm in enumerate(static_mats):
slot_info = {"index": i, "slot_name": None, "material_name": None}
try:
slot_info["slot_name"] = sm.get_editor_property("material_slot_name")
mi = sm.get_editor_property("material_interface")
if mi:
slot_info["material_name"] = mi.get_name()
except Exception:
pass
slots.append(slot_info)
except Exception:
pass
# 方法 3: get_material_slot_names() — 至少获取槽位名
if not slots:
try:
slot_names = mesh.get_material_slot_names()
for i, name in enumerate(slot_names):
slots.append({"index": i, "slot_name": str(name), "material_name": None})
except Exception:
pass
# 方法 4: 用 section 数作为 fallback
if not slots:
try:
num_sections = mesh.get_num_sections(0)
for i in range(num_sections):
slots.append({"index": i, "slot_name": None, "material_name": None})
except Exception:
pass
return slots
def _analyze_single_mesh(asset_info: dict, subfolder: str = "") -> dict:
"""分析单个 StaticMesh,返回完整信息。"""
import unreal
ue_path = asset_info["ue_path"]
name = asset_info["name"]
result = {
"ue_path": ue_path,
"bounding_box": None,
"pivot": None,
"mesh_complexity": {},
"sockets": [],
"material_slots": [],
"detail_boxes": [],
"bbox_faces": [],
"analysis_notes": [],
# AI Pass 2 占位
"key_surfaces": [],
"detail_boxes_annotated": [],
"ground_sink_estimate": None,
"style_tags": [],
"scene_tags": [],
}
# 加载 mesh
try:
mesh = unreal.load_asset(ue_path)
except Exception as e:
result["analysis_notes"].append(f"加载失败: {e}")
return result
if mesh is None:
result["analysis_notes"].append("加载返回 None")
return result
# ---- 1. 包围盒 ----
try:
bb = mesh.get_bounding_box()
bmin = (bb.min.x, bb.min.y, bb.min.z)
bmax = (bb.max.x, bb.max.y, bb.max.z)
size = (bmax[0] - bmin[0], bmax[1] - bmin[1], bmax[2] - bmin[2])
result["bounding_box"] = {
"min": _round3(list(bmin)),
"max": _round3(list(bmax)),
"size": _round3(list(size)),
}
# ---- 2. 锚点 ----
result["pivot"] = classify_pivot(bmin, bmax)
# ---- 3. 包围盒面 ----
result["bbox_faces"] = compute_bbox_faces(bmin, bmax)
except Exception as e:
result["analysis_notes"].append(f"包围盒提取失败: {e}")
return result
# ---- 4. 网格复杂度 ----
result["mesh_complexity"] = _try_get_complexity(mesh)
# ---- 5. Sockets ----
result["sockets"] = _try_get_sockets(mesh)
# ---- 6. 材质槽位 ----
result["material_slots"] = _try_get_material_slots(mesh)
# ---- 7. 细节包围盒 ----
detail_boxes, trace_debug = _try_trace_detail_boxes(mesh, bmin, bmax)
if trace_debug:
result["analysis_notes"].extend(trace_debug)
if detail_boxes:
result["detail_boxes"] = detail_boxes
else:
# 降级: 仅整体包围盒
result["detail_boxes"] = [{
"label": "overall",
"min": _round3(list(bmin)),
"max": _round3(list(bmax)),
"size": _round3(list(size)),
"source": "overall",
}]
# ---- 8. 自动 Tags ----
style_tags, scene_tags = _generate_mesh_tags(name, subfolder)
result["style_tags"] = style_tags
result["scene_tags"] = scene_tags
return result
# ============================================================
# Line Trace 细节包围盒
# ============================================================
# UE5.7 Python correct enum names (discovered via probe)
_TRACE_CHANNEL = None
_DRAW_DEBUG = None
def _get_trace_enums():
"""Lazy-init: get the correct UE5.7 trace enums."""
global _TRACE_CHANNEL, _DRAW_DEBUG
if _TRACE_CHANNEL is not None:
return _TRACE_CHANNEL is not None
import unreal
try:
_TRACE_CHANNEL = unreal.TraceTypeQuery.TRACE_TYPE_QUERY1
_DRAW_DEBUG = unreal.DrawDebugTrace.NONE
return True
except Exception:
return False
def _try_trace_detail_boxes(mesh, bmin: tuple, bmax: tuple) -> tuple:
"""通过 Line Trace 获取细节包围盒。
Returns:
(detail_boxes, debug_notes)
"""
import unreal
detail_boxes = []
debug = []
size = (bmax[0] - bmin[0], bmax[1] - bmin[1], bmax[2] - bmin[2])
if max(size) < 10:
return [], ["skip_trace: model too small"]
# Check trace API availability
if not _get_trace_enums():
return [], ["trace_unavailable: enums not found"]
# ---- Spawn temporary Actor ----
try:
editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
if not editor_subsystem:
return [], ["trace_fail: no subsystem"]
except Exception as e:
return [], [f"trace_fail: {e}"]
try:
actor = editor_subsystem.spawn_actor_from_object(mesh, unreal.Vector(0, 0, 0))
except Exception as e:
return [], [f"trace_fail: spawn {e}"]
if not actor:
return [], ["trace_fail: spawn None"]
try:
hit_points = _perform_line_traces(actor, bmin, bmax)
debug.append(f"trace_hits: {len(hit_points)}")
if len(hit_points) >= 4:
detail_boxes = _cluster_hit_points(hit_points, bmin, bmax)
if detail_boxes:
debug.append(f"trace_boxes: {len(detail_boxes)}")
finally:
try:
unreal.EditorLevelLibrary.destroy_actor(actor)
except Exception:
pass
return detail_boxes, debug
def _perform_line_traces(actor, bmin: tuple, bmax: tuple) -> list:
"""多方向 Line Trace 收集命中点。"""
import unreal
hit_points = []
mn_x, mn_y, mn_z = bmin
mx_x, mx_y, mx_z = bmax
sx, sy, sz = mx_x - mn_x, mx_y - mn_y, mx_z - mn_z
step = min(50, max(10, min(sx, sy) / 5))
margin = max(20, max(sx, sy, sz) * 0.1)
z_step = min(50, max(15, sz / 4))
trace_ch = _TRACE_CHANNEL
draw_dbg = _DRAW_DEBUG
def _trace(start_v, end_v):
try:
hit = unreal.SystemLibrary.line_trace_single(
actor, start_v, end_v,
trace_ch, False, [], draw_dbg,
)
if hit and hasattr(hit, 'impact_point'):
pt = hit.impact_point
hit_points.append((pt.x, pt.y, pt.z))
except Exception:
pass
try:
# Top-down (Z axis)
z = mn_z
while z <= mx_z + 0.1:
x = mn_x + step / 2
while x <= mx_x + 0.1:
y = mn_y + step / 2
while y <= mx_y + 0.1:
_trace(unreal.Vector(x, y, mx_z + margin),
unreal.Vector(x, y, mn_z - margin))
y += step
x += step
z += z_step
# Front (Y+)
x = mn_x + step / 2
while x <= mx_x + 0.1:
z = mn_z + step / 2
while z <= mx_z + 0.1:
_trace(unreal.Vector(x, mx_y + margin, z),
unreal.Vector(x, mn_y - margin, z))
z += step
x += step
# Right (X+)
y = mn_y + step / 2
while y <= mx_y + 0.1:
z = mn_z + step / 2
while z <= mx_z + 0.1:
_trace(unreal.Vector(mx_x + margin, y, z),
unreal.Vector(mn_x - margin, y, z))
z += step
y += step
except Exception:
pass
return hit_points
def _cluster_hit_points(hit_points: list, bmin: tuple, bmax: tuple) -> list:
"""将命中点按 Z 轴聚类为细节包围盒。"""
if not hit_points:
return []
sorted_pts = sorted(hit_points, key=lambda p: p[2])
z_slices = []
current_slice = [sorted_pts[0]]
z_gap = 10.0
for i in range(1, len(sorted_pts)):
if sorted_pts[i][2] - current_slice[-1][2] > z_gap:
z_slices.append(current_slice)
current_slice = [sorted_pts[i]]
else:
current_slice.append(sorted_pts[i])
z_slices.append(current_slice)
boxes = []
for idx, pts in enumerate(z_slices):
if len(pts) < 3:
continue
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
zs = [p[2] for p in pts]
bmn = [min(xs), min(ys), min(zs)]
bmx = [max(xs), max(ys), max(zs)]
bsz = [bmx[0]-bmn[0], bmx[1]-bmn[1], bmx[2]-bmn[2]]
if bsz[0] < 2 and bsz[1] < 2:
continue
boxes.append({
"label": f"part_{idx}",
"min": _round3(bmn), "max": _round3(bmx), "size": _round3(bsz),
"source": "trace", "hit_count": len(pts),
})
if len(boxes) > 8:
boxes.sort(key=lambda b: b["size"][0]*b["size"][1]*b["size"][2], reverse=True)
boxes = boxes[:8]
return boxes
# ============================================================
# 主流程
# ============================================================
def run(index_data: dict = None) -> dict:
"""执行模型分析。
Args:
index_data: 已加载的 index.json 数据,若为 None 则从文件读取
Returns:
统计信息 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)
meshes_by_subfolder = index_data.get("meshes", {})
if not meshes_by_subfolder:
print(f"{TAG} 无 StaticMesh 资产需要分析")
return {"status": "ok", "analyzed": 0}
# 创建输出目录
meshes_dir = output_dir / "meshes"
meshes_dir.mkdir(parents=True, exist_ok=True)
total_all = sum(len(assets) for assets in meshes_by_subfolder.values())
print(f"{TAG} 共 {total_all} 个 StaticMesh,分布在 {len(meshes_by_subfolder)} 个子目录")
analyzed = 0
failed = 0
global_idx = 0
for subfolder, assets in meshes_by_subfolder.items():
safe_name = safe_subfolder_name(subfolder)
output_file = meshes_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))
global_idx += len(assets)
continue
except Exception:
pass
print(f"{TAG} 分析子目录: {subfolder} ({len(assets)} 个模型)")
mesh_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_mesh(asset_info, subfolder)
mesh_results[asset_info["name"]] = result
if result.get("bounding_box") is not None:
subfolder_analyzed += 1
else:
subfolder_failed += 1
global_idx += 1
# 写入分组文件
group_data = {
"subfolder": subfolder,
"source_path": f"{get_scan_root()}/Meshes/{subfolder}" if subfolder else f"{get_scan_root()}/Meshes",
"count": len(assets),
"completed": True,
"meshes": mesh_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)
+219
View File
@@ -0,0 +1,219 @@
"""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)
+159
View File
@@ -0,0 +1,159 @@
"""UE5 Python API collision/trace 探测脚本。
测试哪些碰撞查询 API 在 UE5.7 Python 中可用。
用法: exec(open(r"...\probe_collision_api.py").read())
"""
import unreal
print("=" * 60)
print("UE5 Collision API Probe")
print("=" * 60)
# ---- 1. ETraceTypeQuery enum ----
print("\n--- ETraceTypeQuery ---")
try:
val = unreal.ETraceTypeQuery.TRACE_TYPE_QUERY1
print(f" ETraceTypeQuery.TRACE_TYPE_QUERY1 = {val}")
except Exception as e:
print(f" ETraceTypeQuery: FAIL - {e}")
try:
val = unreal.TraceTypeQuery.TRACE_TYPE_QUERY1
print(f" TraceTypeQuery.TRACE_TYPE_QUERY1 = {val}")
except Exception as e:
print(f" TraceTypeQuery: FAIL - {e}")
# Try integer directly
print("\n--- line_trace_single with integer channel ---")
try:
# Spawn a test cube
mesh = unreal.load_asset("/Game/BlockingStarterPack/Meshes/Primitives/SM_Cube.SM_Cube")
if mesh:
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actor = subsys.spawn_actor_from_object(mesh, unreal.Vector(0, 0, 0))
if actor:
bb = mesh.get_bounding_box()
print(f" Cube bbox: min=({bb.min.x},{bb.min.y},{bb.min.z}) max=({bb.max.x},{bb.max.y},{bb.max.z})")
# Try line_trace_single with int 0
try:
hit = unreal.SystemLibrary.line_trace_single(
actor,
unreal.Vector(50, 50, 200),
unreal.Vector(50, 50, -200),
0, # integer instead of enum
False, [], None,
)
if hit:
print(f" line_trace_single(int=0): HIT at ({hit.impact_point.x},{hit.impact_point.y},{hit.impact_point.z})")
else:
print(f" line_trace_single(int=0): no hit")
except Exception as e:
print(f" line_trace_single(int=0): FAIL - {e}")
# Try with None for draw_debug
try:
hit = unreal.SystemLibrary.line_trace_single(
actor,
unreal.Vector(50, 50, 200),
unreal.Vector(50, 50, -200),
0,
False, [],
)
if hit:
print(f" line_trace_single(6-arg): HIT at ({hit.impact_point.x},{hit.impact_point.y},{hit.impact_point.z})")
else:
print(f" line_trace_single(6-arg): no hit")
except Exception as e:
print(f" line_trace_single(6-arg): FAIL - {e}")
# ---- 2. sphere_overlap_components ----
print("\n--- sphere_overlap_components ---")
try:
result = unreal.SystemLibrary.sphere_overlap_components(
actor,
unreal.Vector(50, 50, 50),
20.0,
[], # object types
unreal.StaticMeshComponent,
)
print(f" sphere_overlap_components: found {len(result) if result else 0} components")
except Exception as e:
print(f" sphere_overlap_components: FAIL - {e}")
# ---- 3. box_overlap_components ----
print("\n--- box_overlap_components ---")
try:
result = unreal.SystemLibrary.box_overlap_components(
actor,
unreal.Vector(50, 50, 50),
unreal.Vector(10, 10, 10),
[],
unreal.StaticMeshComponent,
)
print(f" box_overlap_components: found {len(result) if result else 0} components")
except Exception as e:
print(f" box_overlap_components: FAIL - {e}")
# ---- 4. component-level queries ----
print("\n--- Component-level queries ---")
try:
smc = actor.get_component_by_class(unreal.StaticMeshComponent)
if smc:
print(f" SMC found: {smc.get_name()}")
# get_closest_point_on_collision
try:
cp = smc.get_closest_point_on_collision(unreal.Vector(0, 0, 200))
if cp:
print(f" get_closest_point_on_collision(0,0,200): ({cp.x},{cp.y},{cp.z})")
else:
print(f" get_closest_point_on_collision: returned None")
except Exception as e:
print(f" get_closest_point_on_collision: FAIL - {e}")
# get_bounding_box on component
try:
cbb = smc.get_bounding_box()
print(f" component.get_bounding_box: min=({cbb.min.x},{cbb.min.y},{cbb.min.z}) max=({cbb.max.x},{cbb.max.y},{cbb.max.z})")
except Exception as e:
print(f" component.get_bounding_box: FAIL - {e}")
except Exception as e:
print(f" Component queries: FAIL - {e}")
# Cleanup
unreal.EditorLevelLibrary.destroy_actor(actor)
except Exception as e:
print(f" Setup error: {e}")
# ---- 5. Check what SystemLibrary has ----
print("\n--- SystemLibrary overlap/trace methods ---")
methods = [m for m in dir(unreal.SystemLibrary) if 'overlap' in m.lower() or 'trace' in m.lower()]
for m in methods:
print(f" {m}")
print("\n--- EditorLevelLibrary methods ---")
methods = [m for m in dir(unreal.EditorLevelLibrary) if 'overlap' in m.lower() or 'trace' in m.lower()]
for m in methods:
print(f" {m}")
print("\n" + "=" * 60)
print("Probe complete.")
# Write results to file
try:
from pathlib import Path
out = Path(__file__).parent.parent.parent / "output" / "asset-catalog" / "probe_result.txt"
out.parent.mkdir(parents=True, exist_ok=True)
with open(out, "w", encoding="utf-8") as f:
f.write("See UE5 Output Log for probe results.\n")
print(f"Result marker written to {out}")
except Exception:
pass
# Quit editor
unreal.SystemLibrary.execute_console_command(None, "quit")
+102
View File
@@ -0,0 +1,102 @@
"""UE5 Line Trace API 精确探测。"""
import unreal
print("=" * 60)
print("Line Trace API Probe v2")
print("=" * 60)
# Spawn test cube
mesh = unreal.load_asset("/Game/BlockingStarterPack/Meshes/Primitives/SM_Cube.SM_Cube")
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actor = subsys.spawn_actor_from_object(mesh, unreal.Vector(0, 0, 0))
trace_ch = unreal.TraceTypeQuery.TRACE_TYPE_QUERY1
print(f"trace_channel = {trace_ch} (type: {type(trace_ch).__name__})")
# 1. Find draw_debug_type enum
print("\n--- Find draw_debug_type ---")
for name in ['EDrawDebugTrace', 'DrawDebugTrace']:
try:
enum_cls = getattr(unreal, name)
print(f" unreal.{name} found: {dir(enum_cls)}")
none_val = enum_cls.NONE
print(f" {name}.NONE = {none_val}")
break
except AttributeError:
print(f" unreal.{name}: not found")
# 2. Test line_trace_single with correct enum
print("\n--- Test line_trace_single ---")
try:
draw_debug = unreal.EDrawDebugTrace.NONE
except AttributeError:
draw_debug = None
# Attempt 1: 7 args with draw_debug
print(f" Attempt 1: 7 args, draw_debug={draw_debug}")
try:
hit = unreal.SystemLibrary.line_trace_single(
actor,
unreal.Vector(50, 50, 200),
unreal.Vector(50, 50, -200),
trace_ch,
False, [], draw_debug,
)
print(f" HIT: {hit}")
if hit and hasattr(hit, 'impact_point'):
print(f" impact: ({hit.impact_point.x},{hit.impact_point.y},{hit.impact_point.z})")
except Exception as e:
print(f" FAIL: {e}")
# Attempt 2: try line_trace_single_new
print(f" Attempt 2: line_trace_single_new")
try:
hit = unreal.SystemLibrary.line_trace_single_new(
actor,
unreal.Vector(50, 50, 200),
unreal.Vector(50, 50, -200),
trace_ch,
False, [],
)
print(f" HIT: {hit}")
if hit and hasattr(hit, 'impact_point'):
print(f" impact: ({hit.impact_point.x},{hit.impact_point.y},{hit.impact_point.z})")
except Exception as e:
print(f" FAIL: {e}")
# Attempt 3: line_trace_single_for_objects
print(f" Attempt 3: line_trace_single_for_objects")
try:
hit = unreal.SystemLibrary.line_trace_single_for_objects(
actor,
unreal.Vector(50, 50, 200),
unreal.Vector(50, 50, -200),
[unreal.ObjectTypeQuery1],
False, [], draw_debug,
)
print(f" HIT: {hit}")
except Exception as e:
print(f" FAIL: {e}")
# Attempt 4: line_trace_single_by_profile
print(f" Attempt 4: line_trace_single_by_profile")
try:
hit = unreal.SystemLibrary.line_trace_single_by_profile(
actor,
unreal.Vector(50, 50, 200),
unreal.Vector(50, 50, -200),
"Visibility",
False, [],
)
print(f" HIT: {hit}")
if hit and hasattr(hit, 'impact_point'):
print(f" impact: ({hit.impact_point.x},{hit.impact_point.y},{hit.impact_point.z})")
except Exception as e:
print(f" FAIL: {e}")
# Cleanup
unreal.EditorLevelLibrary.destroy_actor(actor)
print("\n" + "=" * 60)
print("Done.")
unreal.SystemLibrary.execute_console_command(None, "quit")
+61
View File
@@ -0,0 +1,61 @@
@echo off
setlocal enabledelayedexpansion
set "SCRIPT_DIR=%~dp0"
set "PROJECT_DIR=%SCRIPT_DIR%..\.."
for %%i in ("%PROJECT_DIR%\.") do set "PROJECT_ROOT=%%~fi"
set "UPROJECT=%PROJECT_ROOT%\ChildHood.uproject"
set "UE_EDITOR="
if defined UE5_ROOT (
if exist "%UE5_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe" (
set "UE_EDITOR=%UE5_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe"
)
)
if "%UE_EDITOR%"=="" (
for %%p in (
"C:\Program Files\Epic Games\UE_5.7"
"C:\Program Files (x86)\Epic Games\UE_5.7"
"D:\Program Files\Epic Games\UE_5.7"
) do (
if not defined UE_EDITOR (
if exist "%%~p\Engine\Binaries\Win64\UnrealEditor.exe" (
set "UE_EDITOR=%%~p\Engine\Binaries\Win64\UnrealEditor.exe"
)
)
)
)
if "%UE_EDITOR%"=="" (
echo [ERROR] UE5 not found. Set UE5_ROOT or install to standard path.
exit /b 1
)
if not defined AC_SCAN_ROOT set "AC_SCAN_ROOT=/Game/BlockingStarterPack"
if not defined AC_OUTPUT_DIR set "AC_OUTPUT_DIR=%PROJECT_ROOT%\output\asset-catalog"
if not defined AC_STEPS set "AC_STEPS=discover,mesh,material,blueprint"
if not defined AC_NO_QUIT set "AC_NO_QUIT=0"
set "AC_PROJECT_ROOT=%PROJECT_ROOT%"
set "SCRIPT_PATH=%SCRIPT_DIR%run_all.py"
set "SCRIPT_PATH=!SCRIPT_PATH:\=/!"
set "UPROJECT_FWD=!UPROJECT:\=/!"
set "AC_OUTPUT_DIR_FWD=!AC_OUTPUT_DIR:\=/!"
echo.
echo ================================================
echo UE5 Asset Catalog Tool
echo ================================================
echo UE5: !UE_EDITOR!
echo Project: !UPROJECT_FWD!
echo Script: !SCRIPT_PATH!
echo Scan Root: !AC_SCAN_ROOT!
echo Output: !AC_OUTPUT_DIR_FWD!
echo Steps: !AC_STEPS!
echo ================================================
echo.
"!UE_EDITOR!" "!UPROJECT_FWD!" -unattended -NoSound -NullRHI -ExecutePythonScript=!SCRIPT_PATH!
echo.
echo Done. Check output in: !AC_OUTPUT_DIR_FWD!
+213
View File
@@ -0,0 +1,213 @@
"""UE5 资产探索编排器。
在单次 UE5 编辑器会话内顺序执行所有分析步骤。
步骤:
1. discover — 资产发现
2. mesh — 模型几何提取
3. material — 材质参数提取
4. blueprint — 蓝图结构提取
通过 AC_STEPS 环境变量控制执行哪些步骤(逗号分隔)。
默认全部执行。
环境变量(由 run_all.bat 设置):
AC_SCAN_ROOT — 扫描路径
AC_OUTPUT_DIR — 输出目录
AC_NO_QUIT — 是否不退出
AC_STEPS — 步骤列表
AC_PROJECT_ROOT — 项目根目录(用于 sys.path)
"""
import json
import os
import sys
import time
from pathlib import Path
# ---- sys.path 设置 ----
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
# 项目根目录
_project_root = os.environ.get("AC_PROJECT_ROOT", "")
if _project_root:
PROJECT_ROOT = Path(_project_root)
else:
PROJECT_ROOT = _SCRIPT_DIR.parent.parent
# ---- Unreal import ----
try:
import unreal
HAS_UNREAL = True
except ImportError:
HAS_UNREAL = False
print("[AssetCatalog] 错误: unreal 模块不可用,必须在 UE5 环境中运行")
sys.exit(1)
from _common import (
get_scan_root, get_output_dir, get_steps, should_no_quit,
write_json, wait_for_editor, quit_editor, iso_now,
)
TAG = "[AssetCatalog]"
def run():
"""主流程:顺序执行所有启用的步骤。"""
print(f"{TAG} {'=' * 50}")
print(f"{TAG} UE5 资产探索工具")
print(f"{TAG} {'=' * 50}")
scan_root = get_scan_root()
output_dir = get_output_dir()
steps = get_steps()
print(f"{TAG} 扫描路径: {scan_root}")
print(f"{TAG} 输出目录: {output_dir}")
print(f"{TAG} 执行步骤: {', '.join(steps)}")
print(f"{TAG}")
# 确保输出目录存在
output_dir.mkdir(parents=True, exist_ok=True)
# 等待编辑器就绪
if not wait_for_editor():
print(f"{TAG} 错误: 编辑器初始化超时")
return
log = {
"started_at": iso_now(),
"scan_root": scan_root,
"output_dir": str(output_dir),
"steps": [],
}
index_data = None
# ---- Step 0: Discover ----
if "discover" in steps:
print(f"\n{TAG} {'-' * 40}")
print(f"{TAG} Step 0: Discover")
print(f"{TAG} {'-' * 40}")
t0 = time.time()
from discover_assets import run as run_discover
result = run_discover()
elapsed = time.time() - t0
result["elapsed_sec"] = round(elapsed, 1)
log["steps"].append({"name": "discover", **result})
if result.get("status") == "ok":
# load index.json for subsequent steps
try:
index_data = json.loads(
(output_dir / "index.json").read_text(encoding="utf-8")
)
except Exception as e:
print(f"{TAG} WARN: load index.json failed: {e}")
else:
print(f"{TAG} Discover failed, skipping remaining steps")
log["completed_at"] = iso_now()
log["status"] = "discover_failed"
_save_log(log, output_dir)
_maybe_quit()
return
# ---- Step 1: Meshes ----
if "mesh" in steps:
print(f"\n{TAG} {'-' * 40}")
print(f"{TAG} Step 1: Mesh Analysis")
print(f"{TAG} {'-' * 40}")
t0 = time.time()
try:
from analyze_meshes import run as run_mesh
result = run_mesh(index_data)
except Exception as e:
print(f"{TAG} ERROR in mesh analysis: {e}")
import traceback
traceback.print_exc()
result = {"status": "error", "reason": str(e)}
elapsed = time.time() - t0
result["elapsed_sec"] = round(elapsed, 1)
log["steps"].append({"name": "mesh", **result})
# ---- Step 2: Materials ----
if "material" in steps:
print(f"\n{TAG} {'-' * 40}")
print(f"{TAG} Step 2: Material Analysis")
print(f"{TAG} {'-' * 40}")
t0 = time.time()
try:
from analyze_materials import run as run_material
result = run_material(index_data)
except Exception as e:
print(f"{TAG} ERROR in material analysis: {e}")
import traceback
traceback.print_exc()
result = {"status": "error", "reason": str(e)}
elapsed = time.time() - t0
result["elapsed_sec"] = round(elapsed, 1)
log["steps"].append({"name": "material", **result})
# ---- Step 3: Blueprints ----
if "blueprint" in steps:
print(f"\n{TAG} {'-' * 40}")
print(f"{TAG} Step 3: Blueprint Analysis")
print(f"{TAG} {'-' * 40}")
t0 = time.time()
try:
from analyze_blueprints import run as run_blueprint
result = run_blueprint(index_data)
except Exception as e:
print(f"{TAG} ERROR in blueprint analysis: {e}")
import traceback
traceback.print_exc()
result = {"status": "error", "reason": str(e)}
elapsed = time.time() - t0
result["elapsed_sec"] = round(elapsed, 1)
log["steps"].append({"name": "blueprint", **result})
# ---- 汇总 ----
log["completed_at"] = iso_now()
log["status"] = "ok"
_save_log(log, output_dir)
print(f"\n{TAG} {'=' * 50}")
print(f"{TAG} 全部完成")
print(f"{TAG} {'=' * 50}")
for step in log["steps"]:
print(f"{TAG} {step['name']}: {step.get('status', '?')} "
f"({step.get('analyzed', step.get('total', '?'))} 成功, "
f"{step.get('elapsed_sec', '?')}s)")
_maybe_quit()
def _save_log(log: dict, output_dir: Path):
"""保存运行日志。"""
log_path = output_dir / "catalog_log.json"
write_json(log, log_path)
print(f"{TAG} 日志已保存: {log_path}")
def _maybe_quit():
"""根据 AC_NO_QUIT 决定是否退出编辑器。"""
if should_no_quit():
print(f"{TAG} AC_NO_QUIT=1,不退出编辑器")
else:
print(f"{TAG} 退出编辑器...")
quit_editor()
# ---- 入口 ----
if __name__ == "__main__":
run()
+46
View File
@@ -0,0 +1,46 @@
@echo off
setlocal enabledelayedexpansion
set "SCRIPT_DIR=%~dp0"
set "PROJECT_DIR=%SCRIPT_DIR%..\.."
for %%i in ("%PROJECT_DIR%\.") do set "PROJECT_ROOT=%%~fi"
set "UPROJECT=%PROJECT_ROOT%\ChildHood.uproject"
set "UE_EDITOR="
if defined UE5_ROOT (
if exist "%UE5_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe" (
set "UE_EDITOR=%UE5_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe"
)
)
if "%UE_EDITOR%"=="" (
for %%p in (
"C:\Program Files\Epic Games\UE_5.7"
"C:\Program Files (x86)\Epic Games\UE_5.7"
"D:\Program Files\Epic Games\UE_5.7"
) do (
if not defined UE_EDITOR (
if exist "%%~p\Engine\Binaries\Win64\UnrealEditor.exe" (
set "UE_EDITOR=%%~p\Engine\Binaries\Win64\UnrealEditor.exe"
)
)
)
)
if "%UE_EDITOR%"=="" (
echo [ERROR] UE5 not found.
exit /b 1
)
set "SCRIPT_PATH=%SCRIPT_DIR%probe_collision_api.py"
set "SCRIPT_PATH=!SCRIPT_PATH:\=/!"
set "UPROJECT_FWD=!UPROJECT:\=/!"
echo.
echo === Collision API Probe ===
echo UE5: !UE_EDITOR!
echo Script: !SCRIPT_PATH!
echo.
"!UE_EDITOR!" "!UPROJECT_FWD!" -unattended -NoSound -NullRHI -ExecutePythonScript=!SCRIPT_PATH!
echo.
echo Done.
+46
View File
@@ -0,0 +1,46 @@
@echo off
setlocal enabledelayedexpansion
set "SCRIPT_DIR=%~dp0"
set "PROJECT_DIR=%SCRIPT_DIR%..\.."
for %%i in ("%PROJECT_DIR%\.") do set "PROJECT_ROOT=%%~fi"
set "UPROJECT=%PROJECT_ROOT%\ChildHood.uproject"
set "UE_EDITOR="
if defined UE5_ROOT (
if exist "%UE5_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe" (
set "UE_EDITOR=%UE5_ROOT%\Engine\Binaries\Win64\UnrealEditor.exe"
)
)
if "%UE_EDITOR%"=="" (
for %%p in (
"C:\Program Files\Epic Games\UE_5.7"
"C:\Program Files (x86)\Epic Games\UE_5.7"
"D:\Program Files\Epic Games\UE_5.7"
) do (
if not defined UE_EDITOR (
if exist "%%~p\Engine\Binaries\Win64\UnrealEditor.exe" (
set "UE_EDITOR=%%~p\Engine\Binaries\Win64\UnrealEditor.exe"
)
)
)
)
if "%UE_EDITOR%"=="" (
echo [ERROR] UE5 not found.
exit /b 1
)
set "SCRIPT_PATH=%SCRIPT_DIR%probe_trace_v2.py"
set "SCRIPT_PATH=!SCRIPT_PATH:\=/!"
set "UPROJECT_FWD=!UPROJECT:\=/!"
echo.
echo === Collision API Probe ===
echo UE5: !UE_EDITOR!
echo Script: !SCRIPT_PATH!
echo.
"!UE_EDITOR!" "!UPROJECT_FWD!" -unattended -NoSound -NullRHI -ExecutePythonScript=!SCRIPT_PATH!
echo.
echo Done.