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:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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!
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user