9f6572600b
- 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枚举发现)
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""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)
|