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>
This commit is contained in:
2026-06-10 00:50:56 +08:00
parent a951009dae
commit 7a2436a612
3 changed files with 518 additions and 222 deletions
+5 -222
View File
@@ -6,8 +6,6 @@
- 网格复杂度(顶点数、三角形数)
- Socket 列表
- 材质槽位
- 包围盒 6 面候选
- 射线检测细节包围盒(spawn + Line Trace)
- AI 占位字段(key_surfaces, style_tags, scene_tags 等)
输出: output/meshes/<SubfolderName>.json,每个子目录一个文件
@@ -18,7 +16,6 @@
"""
import sys
import math
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
@@ -28,7 +25,7 @@ if str(_SCRIPT_DIR) not in sys.path:
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,
iso_now, safe_subfolder_name, classify_pivot,
)
TAG = "[MeshAnalysis]"
@@ -263,12 +260,9 @@ def _analyze_single_mesh(asset_info: dict, subfolder: str = "") -> dict:
"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": [],
@@ -301,39 +295,20 @@ def _analyze_single_mesh(asset_info: dict, subfolder: str = "") -> dict:
# ---- 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. 网格复杂度 ----
# ---- 3. 网格复杂度 ----
result["mesh_complexity"] = _try_get_complexity(mesh)
# ---- 5. Sockets ----
# ---- 4. Sockets ----
result["sockets"] = _try_get_sockets(mesh)
# ---- 6. 材质槽位 ----
# ---- 5. 材质槽位 ----
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 ----
# ---- 6. 自动 Tags ----
style_tags, scene_tags = _generate_mesh_tags(name, subfolder)
result["style_tags"] = style_tags
result["scene_tags"] = scene_tags
@@ -341,198 +316,6 @@ def _analyze_single_mesh(asset_info: dict, subfolder: str = "") -> dict:
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"]
# Set collision to BlockAll so traces can hit this mesh
smc = actor.get_component_by_class(unreal.StaticMeshComponent)
if smc:
try:
smc.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
smc.set_collision_profile_name("BlockAll")
except Exception as e:
debug.append(f"collision_set_warn: {e}")
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
# ============================================================
# 主流程
# ============================================================