ModelTranslator: support external-texture FBX (disk AO fallback search, missing-texture graceful degradation)

This commit is contained in:
ud18010
2026-07-15 12:16:24 +08:00
parent 0b068dbaa0
commit 78b250355b
2 changed files with 67 additions and 7 deletions
+64 -6
View File
@@ -36,10 +36,59 @@ def decode_sphere_map(enc01):
# ---------------- 以下仅在 Blender 内执行 ----------------
IMG_EXTS = ('.png', '.jpg', '.jpeg', '.tga', '.exr', '.bmp', '.tif', '.tiff')
def _looks_like_ao(name):
return bool(AO_NAME_RE.search(name))
def _image_broken(img):
"""外置贴图文件丢失/加载失败时,Blender 留下 0 尺寸空图。"""
return img.size[0] == 0 or img.size[1] == 0
def _image_stem(img):
"""贴图文件名(不含扩展名);外置贴图优先用 filepath,比 img.name 可靠。"""
path = img.filepath if img.filepath else img.name
return os.path.splitext(os.path.basename(path))[0]
def _find_ao_on_disk(src_dir, maps, warnings):
"""材质未引用 AO 时,从 FBX 同目录按文件名兜底搜索(烘焙工具常只把 _ao.png 放旁边)。
多候选时优先与 basecolor 贴图同前缀(xxx_color.png -> xxx_ao.png)。"""
import bpy
try:
files = sorted(os.listdir(src_dir))
except OSError:
return None
cands = [f for f in files if f.lower().endswith(IMG_EXTS)
and _looks_like_ao(os.path.splitext(f)[0])]
if not cands:
return None
pick = None
if maps["basecolor"] and len(cands) > 1:
prefix = _image_stem(maps["basecolor"][0]).rsplit("_", 1)[0]
matched = [f for f in cands if f.startswith(prefix)]
if matched:
pick = matched[0]
if pick is None:
if len(cands) == 1:
pick = cands[0]
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
def _image_pixels(img):
"""bpy Image -> float32 (h, w, 4),原始值(不做色彩变换)。"""
w, h = img.size
@@ -80,7 +129,7 @@ def _trace_image(socket, warnings, what):
return None, None
def _material_maps(mat, warnings):
def _material_maps(mat, warnings, src_dir):
"""从材质 Principled BSDF 提取贴图与常量。"""
maps = {"basecolor": None, "alpha": None, "normal": None,
"metallic": None, "roughness": None, "ao": None}
@@ -99,7 +148,11 @@ def _material_maps(mat, warnings):
return
img, ch = _trace_image(sock, warnings, key)
if img is not None:
maps[key] = (img, ch)
if _image_broken(img):
warnings.append("%s 贴图 %s 加载失败(外置文件缺失?),改用常量"
% (key, img.name))
else:
maps[key] = (img, ch)
grab('Base Color', 'basecolor')
grab('Alpha', 'alpha')
@@ -114,9 +167,13 @@ def _material_maps(mat, warnings):
used = {m[0] for m in maps.values() if m}
for n in mat.node_tree.nodes:
if n.type == 'TEX_IMAGE' and n.image and n.image not in used \
and _looks_like_ao(n.image.name):
and not _image_broken(n.image) and _looks_like_ao(n.image.name):
maps["ao"] = (n.image, None)
break
if maps["ao"] is None:
img = _find_ao_on_disk(src_dir, maps, warnings)
if img is not None:
maps["ao"] = (img, None)
return maps, consts
@@ -146,9 +203,9 @@ def _save_png(path, rgba):
bpy.data.images.remove(img)
def _convert_material(mat, model_name, outdir, max_size, used_snames):
def _convert_material(mat, model_name, outdir, max_size, used_snames, src_dir):
warnings = []
maps, consts = _material_maps(mat, warnings)
maps, consts = _material_maps(mat, warnings, src_dir)
tw, th = _target_size(maps, max_size)
# --- 基础贴图 ---
@@ -250,8 +307,9 @@ def main():
if slot.material and slot.material not in used_mats:
used_mats.append(slot.material)
src_dir = os.path.dirname(os.path.abspath(src))
used_snames = set()
results = [_convert_material(m, model_name, outdir, max_size, used_snames)
results = [_convert_material(m, model_name, outdir, max_size, used_snames, src_dir)
for m in used_mats]
_strip_textures()