ModelTranslator: support external-texture FBX (disk AO fallback search, missing-texture graceful degradation)
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user