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:
@@ -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
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主流程
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user