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
+1
View File
@@ -38,6 +38,7 @@ out/tong/
- 贴图识别:沿材质 Principled BSDF 连线找 Base Color / Alpha / Normal / Metallic / Roughness;内嵌与外置贴图(FBX 旁的松散文件)均支持
- AO 按贴图名匹配(含 `ao`/`occlusion`/`ambient`):先找材质内引用的贴图;FBX 未引用时自动搜索 FBX 同目录的图片文件,多候选优先与颜色贴图同前缀(`xxx_color.png``xxx_ao.png`
- **纯网格 FBX**(无材质或材质无贴图,常见于烘焙工具导出):自动以模型名新建材质,并按命名约定从 FBX 同目录识别整套贴图——`<前缀>_<token>.png`token 支持 `color/basecolor/albedo/diffuse``normal/nrm``metallic/metalness/metal``roughness/rough``ao/occlusion/ambient``alpha/opacity`;多套前缀时取与模型同名的一组
- 基础贴图 A 通道自动判定:**透明贴图 > 颜色贴图自带 alpha > AO 贴图 > 填白(255)**;判定为透明时 `.mat` 自动勾选 `Alpha As Transparency`AlphaTest 裁剪)
- 无对应贴图的通道用 Principled 常量值填充(会在日志中标注 `(常量)`
- 目标尺寸 = min(2048, 源贴图最大边),所有贴图缩放到统一尺寸
+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':
+3 -1
View File
@@ -51,7 +51,9 @@ def write_unity_assets(outdir, summary, max_size):
with open(mix_png + ".meta", "w", encoding="utf-8", newline="\n") as f:
f.write(ua.texture_meta(mix_guid, srgb=False, max_size=max_size))
mat_name = "%s_%s" % (summary["model"], m["safe_name"])
# 与 bl_convert 的贴图命名一致:材质名等于模型名时不重复拼接
mat_name = summary["model"] if m["safe_name"] == summary["model"] \
else "%s_%s" % (summary["model"], m["safe_name"])
mat_path = os.path.join(outdir, mat_name + ".mat")
mat_guid = ua.guid_for(mat_path)
with open(mat_path, "w", encoding="utf-8", newline="\n") as f: