Files
ChildHood/tools/asset-catalog/analyze_materials.py
T
starmen1265 9f6572600b 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枚举发现)
2026-06-09 19:01:50 +08:00

349 lines
12 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 中发现的每个材质资产提取:
- 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)