Files
ChildHood/tools/asset-catalog/analyze_meshes.py
T
starmen1265 7a2436a612 feat(asset-catalog): 简化分析工具+新增汇总和AI推理脚本
- analyze_meshes: 移除失败的 LineTrace 和六面分析,只保留基础属性
  (包围盒、锚点、复杂度、Socket、材质槽位、自动Tag)
- build_database: 新增离线汇总脚本,将 meshes/*.json 扁平化为
  统一的 asset_database.json
- ai_inference: 新增 GLM API 批量推理脚本,按类别分批推断语义信息
  (朝向、表面高度、功能描述、交互属性、降级方案)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:50:56 +08:00

427 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""UE5 静态模型几何提取脚本。
对 index.json 中发现的每个 StaticMesh 提取:
- 整体包围盒(min, max, size)
- 锚点类型和 XY 偏移
- 网格复杂度(顶点数、三角形数)
- Socket 列表
- 材质槽位
- AI 占位字段(key_surfaces, style_tags, scene_tags 等)
输出: output/meshes/<SubfolderName>.json,每个子目录一个文件
用法:
交互模式: exec(open(r"...\analyze_meshes.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, get_steps,
write_json, read_json, wait_for_editor, progress_log,
iso_now, safe_subfolder_name, classify_pivot,
)
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": [],
"analysis_notes": [],
# AI Pass 2 占位
"key_surfaces": [],
"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)
except Exception as e:
result["analysis_notes"].append(f"包围盒提取失败: {e}")
return result
# ---- 3. 网格复杂度 ----
result["mesh_complexity"] = _try_get_complexity(mesh)
# ---- 4. Sockets ----
result["sockets"] = _try_get_sockets(mesh)
# ---- 5. 材质槽位 ----
result["material_slots"] = _try_get_material_slots(mesh)
# ---- 6. 自动 Tags ----
style_tags, scene_tags = _generate_mesh_tags(name, subfolder)
result["style_tags"] = style_tags
result["scene_tags"] = scene_tags
return result
# ============================================================
# 主流程
# ============================================================
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)