ModelTranslator: support pure-mesh FBX via naming-convention texture discovery and auto material

This commit is contained in:
ud18010
2026-07-15 16:39:00 +08:00
parent 78b250355b
commit 253f03273d
3 changed files with 82 additions and 14 deletions
+78 -13
View File
@@ -38,6 +38,17 @@ def decode_sphere_map(enc01):
IMG_EXTS = ('.png', '.jpg', '.jpeg', '.tga', '.exr', '.bmp', '.tif', '.tiff')
# 磁盘贴图命名约定:文件名末段 token -> 贴图槽位
DISK_MAP_TOKENS = {
"color": "basecolor", "basecolor": "basecolor", "albedo": "basecolor",
"diffuse": "basecolor",
"normal": "normal", "nrm": "normal",
"metallic": "metallic", "metalness": "metallic", "metal": "metallic",
"roughness": "roughness", "rough": "roughness",
"ao": "ao", "occlusion": "ao", "ambient": "ao",
"alpha": "alpha", "opacity": "alpha",
}
def _looks_like_ao(name):
return bool(AO_NAME_RE.search(name))
@@ -54,6 +65,54 @@ def _image_stem(img):
return os.path.splitext(os.path.basename(path))[0]
def _load_disk_image(src_dir, fname, warnings):
"""加载 FBX 同目录的贴图文件,失败返回 None。"""
import bpy
try:
img = bpy.data.images.load(os.path.join(src_dir, fname), check_existing=True)
except RuntimeError as e:
warnings.append("贴图 %s 加载失败:%s" % (fname, e))
return None
if _image_broken(img):
warnings.append("贴图 %s 加载失败(0 尺寸)" % fname)
return None
return img
def _find_maps_on_disk(src_dir, model_name, maps, warnings):
"""材质无任何贴图(如纯网格 FBX)时,按命名约定从 FBX 同目录识别整套贴图。
文件按 <前缀>_<token>.<ext> 归组;多套前缀时取与模型同名的一组。"""
try:
files = sorted(os.listdir(src_dir))
except OSError:
return
groups = {} # 前缀 -> {槽位: 文件名}
for f in files:
stem, ext = os.path.splitext(f)
if ext.lower() not in IMG_EXTS or "_" not in stem:
continue
prefix, token = stem.rsplit("_", 1)
key = DISK_MAP_TOKENS.get(token.lower())
if key:
groups.setdefault(prefix, {}).setdefault(key, f)
if not groups:
return
if model_name in groups:
chosen = groups[model_name]
elif len(groups) == 1:
chosen = next(iter(groups.values()))
else:
warnings.append("同目录有多套命名约定贴图(前缀:%s),无法确定归属,忽略"
% ", ".join(sorted(groups)))
return
for key, fname in chosen.items():
if maps.get(key):
continue
img = _load_disk_image(src_dir, fname, warnings)
if img is not None:
maps[key] = (img, None)
def _find_ao_on_disk(src_dir, maps, warnings):
"""材质未引用 AO 时,从 FBX 同目录按文件名兜底搜索(烘焙工具常只把 _ao.png 放旁边)。
多候选时优先与 basecolor 贴图同前缀(xxx_color.png -> xxx_ao.png)。"""
@@ -78,15 +137,7 @@ def _find_ao_on_disk(src_dir, maps, warnings):
else:
warnings.append("同目录有多张疑似 AO 贴图无法确定归属,忽略:%s" % ", ".join(cands))
return None
try:
img = bpy.data.images.load(os.path.join(src_dir, pick), check_existing=True)
except RuntimeError as e:
warnings.append("AO 贴图 %s 加载失败:%s" % (pick, e))
return None
if _image_broken(img):
warnings.append("AO 贴图 %s 加载失败(0 尺寸)" % pick)
return None
return img
return _load_disk_image(src_dir, pick, warnings)
def _image_pixels(img):
@@ -129,7 +180,7 @@ def _trace_image(socket, warnings, what):
return None, None
def _material_maps(mat, warnings, src_dir):
def _material_maps(mat, warnings, src_dir, model_name):
"""从材质 Principled BSDF 提取贴图与常量。"""
maps = {"basecolor": None, "alpha": None, "normal": None,
"metallic": None, "roughness": None, "ao": None}
@@ -170,6 +221,9 @@ def _material_maps(mat, warnings, src_dir):
and not _image_broken(n.image) and _looks_like_ao(n.image.name):
maps["ao"] = (n.image, None)
break
# 纯网格 FBX:材质一张贴图都没有时,按命名约定从同目录识别整套
if not any(maps.values()):
_find_maps_on_disk(src_dir, model_name, maps, warnings)
if maps["ao"] is None:
img = _find_ao_on_disk(src_dir, maps, warnings)
if img is not None:
@@ -205,7 +259,7 @@ def _save_png(path, rgba):
def _convert_material(mat, model_name, outdir, max_size, used_snames, src_dir):
warnings = []
maps, consts = _material_maps(mat, warnings, src_dir)
maps, consts = _material_maps(mat, warnings, src_dir, model_name)
tw, th = _target_size(maps, max_size)
# --- 基础贴图 ---
@@ -258,8 +312,10 @@ def _convert_material(mat, model_name, outdir, max_size, used_snames, src_dir):
used_snames.add(sname)
if sname != base_sname:
warnings.append("材质名 %s 清洗后与其它材质重名,输出改用 %s" % (mat.name, sname))
base_png = os.path.abspath(os.path.join(outdir, "%s_%s_base.png" % (model_name, sname)))
mix_png = os.path.abspath(os.path.join(outdir, "%s_%s_mix.png" % (model_name, sname)))
# 材质名与模型名相同(如纯网格 FBX 的兜底材质)时不重复拼接
stem = model_name if sname == model_name else "%s_%s" % (model_name, sname)
base_png = os.path.abspath(os.path.join(outdir, "%s_base.png" % stem))
mix_png = os.path.abspath(os.path.join(outdir, "%s_mix.png" % stem))
_save_png(base_png, base)
_save_png(mix_png, mix)
@@ -299,6 +355,15 @@ def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=src)
# 纯网格 FBX(无材质槽):以模型名新建材质挂上,否则 Unity 侧没有重映射挂点
fallback_mat = None
for obj in bpy.data.objects:
if obj.type == 'MESH' and not any(s.material for s in obj.material_slots):
if fallback_mat is None:
fallback_mat = bpy.data.materials.new(model_name)
fallback_mat.use_nodes = True
obj.data.materials.append(fallback_mat)
used_mats = []
for obj in bpy.data.objects:
if obj.type != 'MESH':