Character ages and Phase 8 checkpoint (interiors)

This commit is contained in:
2026-10-31 15:50:44 +03:00
parent 214b05b40f
commit ea281bf913
117 changed files with 13301 additions and 432 deletions
+163 -30
View File
@@ -33,6 +33,18 @@ def prepare_armature(mapping):
orig_armature = bpy.data.objects[mapping.armature_name]
armature_name = orig_armature.name
orig_armature.name = orig_armature.name + "_orig"
# Rename the original data block too, so the baked armature can use the
# exact stem as its data name (and therefore the exported skeleton file
# is `<stem>.skeleton`, not `<stem>.005.skeleton`).
orig_armature.data.name = armature_name + "_orig"
# Orphaned armature data blocks left by removed duplicate clothing-stage
# rigs still occupy the stem name; move them out of the way.
for ad in bpy.data.armatures:
if ad is orig_armature.data or ad is new_armature.data:
continue
if ad.name == armature_name or ad.name.startswith(
armature_name + "."):
ad.name = ad.name + "_unused"
new_armature.name = armature_name
queue = []
if new_armature.animation_data is None:
@@ -276,6 +288,19 @@ for mapping in[CommandLineMapping()]:
else:
print(f"No hair with own skeleton detected")
# ---------------------------------------------
# Helper/reference meshes (Body, Tops, Bottoms, Shoes, HairBack,
# Accessory, Body_shapes) are build-time weight-transfer aids and
# must not be exported: they are not catalog parts, and exporting
# them bloats the resource tree and creates texture-name collisions
# across age directories.
allowed = set(mapping.objs)
allowed.update(o.name for o in hair_own_skel_objs)
for ob in list(bpy.data.objects):
if ob.type != 'MESH':
continue
if ob.name not in allowed:
print(f"Removing non-catalog helper mesh '{ob.name}'")
bpy.data.objects.remove(ob, do_unlink=True)
else:
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
@@ -390,8 +415,46 @@ for mapping in[CommandLineMapping()]:
hair_arm.hide_render = False
obj_names = mapping.objs
prefix = mapping.armature_name + "_"
material_obj_names = list(obj_names)
material_obj_names += [o.name for o in hair_own_skel_objs]
for name in obj_names:
# Deduplicate image datablocks that came from multiple clothing stages
# but point at the same source file. Without this, the exporter writes
# `foo.png`, `foo.001.png`, ... for one texture.
def dedupe_images():
canonical = {}
for img in list(bpy.data.images):
if not img.filepath:
continue
try:
key = os.path.normpath(bpy.path.abspath(img.filepath))
except Exception:
continue
if not key:
continue
if key not in canonical:
canonical[key] = img
continue
keep = canonical[key]
for mat in bpy.data.materials:
if not mat.node_tree:
continue
for node in mat.node_tree.nodes:
if node.type == 'TEX_IMAGE' and node.image is img:
node.image = keep
try:
bpy.data.images.remove(img)
except Exception:
pass
dedupe_images()
try:
bpy.ops.outliner.orphans_purge(do_local_ids=True,
do_linked_ids=True,
do_recursive=True)
except Exception:
pass
for name in material_obj_names:
obj = bpy.data.objects.get(name)
if obj and obj.type == 'MESH':
@@ -499,6 +562,7 @@ for mapping in[CommandLineMapping()]:
lod_obj_names = []
if mapping_lods_path and os.path.exists(mapping_lods_path):
print("Appending character LOD objects from: " + mapping_lods_path)
before_lod_objects = set(bpy.data.objects)
with bpy.data.libraries.load(mapping_lods_path) as (data_from, data_to):
data_to.objects = [
n for n in data_from.objects
@@ -525,6 +589,16 @@ for mapping in[CommandLineMapping()]:
if mod.type == 'ARMATURE':
mod.object = body_arm
print(f" LOD object '{obj.name}' -> mesh '{obj.data.name}'")
# The lods blend also contains its own body armature. Drop appended
# armature objects so their data blocks cannot steal the stem
# skeleton name during export.
for obj in list(bpy.data.objects):
if obj in before_lod_objects or obj.type != 'ARMATURE':
continue
bpy.data.objects.remove(obj, do_unlink=True)
for ad in list(bpy.data.armatures):
if ad.users == 0:
bpy.data.armatures.remove(ad)
# -------------------------------------------------------------
# Triangulate body part meshes before OGRE export to prevent the exporter's
@@ -560,16 +634,53 @@ for mapping in[CommandLineMapping()]:
armobj = bpy.data.objects.get(mapping.armature_name)
armobj.data.name = armobj.name
print("EXPORT ARMATURE DATA: obj=%s data=%s all=%s" % (
armobj.name, armobj.data.name,
[(a.name, a.users) for a in bpy.data.armatures]))
# ---- Fix: sync Image filepath to Image name ----
# Blender silently reuses existing Images when appending
# from libraries, so the Image's filepath may still point
# to the old texture even after the user renamed it.
# blender2ogre writes the filepath to .material files,
# so we must update filepath to match the Image name.
# ---- Prefix image resources per stem ----
# All age directories share the "Characters" resource group, so texture
# file names must be unique across ages/sexes. Adult gets
# `male-...`/`female-...`; new ages get `baby-male-...` etc. The prefix
# is idempotent (an image already prefixed with the stem is left alone).
import re as _re
seen_imgs = set()
for name in obj_names:
stem = mapping.armature_name
def _unique_image_name(base):
if bpy.data.images.get(base) is None:
return base
i = 1
while bpy.data.images.get(f"{base}.{i:03d}") is not None:
i += 1
return f"{base}.{i:03d}"
image_obj_names = list(obj_names)
image_obj_names += [o.name for o in hair_own_skel_objs]
# LOD impostors use the already-unique `charlod_<stem>-atlas.png` /
# `charlod_<stem>` material created by generate_lods.py; prefixing them
# again would make the material reference a different file/format.
# Normalize their image format to the filename extension, though; some
# Blender versions keep the image blob as TARGA even when saved as PNG.
for name in lod_obj_names:
obj = bpy.data.objects.get(name)
if not obj or obj.type != 'MESH':
continue
for slot in obj.material_slots:
if not slot.material or not slot.material.node_tree:
continue
for node in slot.material.node_tree.nodes:
if node.type == 'TEX_IMAGE' and node.image:
fp = node.image.filepath.lower()
if fp.endswith(".png"):
node.image.file_format = 'PNG'
elif fp.endswith(".jpg") or fp.endswith(".jpeg"):
node.image.file_format = 'JPEG'
elif fp.endswith(".tga"):
node.image.file_format = 'TARGA'
output_dir = os.path.dirname(mapping.gltf_path)
for name in image_obj_names:
obj = bpy.data.objects.get(name)
if obj and obj.type == 'MESH':
for slot in obj.material_slots:
@@ -577,28 +688,50 @@ for mapping in[CommandLineMapping()]:
for node in slot.material.node_tree.nodes:
if node.type == 'TEX_IMAGE' and node.image:
img = node.image
if img.name not in seen_imgs:
seen_imgs.add(img.name)
# Strip Blender auto-suffix (.NNN)
clean = _re.sub(
r'\.\d{3}$', '', img.name)
# Extract just the filename from
# the current filepath
old_name = os.path.basename(
img.filepath)
if old_name != clean and clean:
# Rebuild filepath with new
# filename
dirpart = os.path.dirname(
img.filepath)
new_path = os.path.join(
dirpart, clean)
print(f" Updating Image "
f"'{img.name}': "
f"filepath "
f"'{old_name}' -> "
f"'{clean}'")
img.filepath = new_path
if img.name in seen_imgs:
continue
seen_imgs.add(img.name)
clean = _re.sub(r'\.\d{3}$', '', img.name)
old_name = os.path.basename(img.filepath)
if not old_name:
# Packed/generated image: use the datablock
# name as the file base.
old_name = clean
if not os.path.splitext(old_name)[1]:
old_name += ".png"
# Strip Blender auto-suffix before the extension.
base, ext = os.path.splitext(old_name)
base = _re.sub(r'\.\d{3}$', '', base)
if not ext:
ext = ".png"
if (base.startswith(stem + "-") or
base.startswith(stem + "_")):
new_base = base
else:
new_base = f"{stem}-{base}"
image_name = _unique_image_name(new_base)
new_file = image_name + ext
new_path = os.path.join(output_dir, new_file)
if img.name != image_name:
print(f" Prefixing Image "
f"'{img.name}' -> '{image_name}' "
f"({new_file})")
img.name = image_name
if new_file.lower().endswith(".png"):
img.file_format = 'PNG'
elif new_file.lower().endswith(".jpg") or \
new_file.lower().endswith(".jpeg"):
img.file_format = 'JPEG'
elif new_file.lower().endswith(".tga"):
img.file_format = 'TARGA'
img.filepath = new_path
img.filepath_raw = new_path
try:
img.save()
except Exception as exc:
print(f" WARNING: could not save image "
f"'{img.name}': {exc}")
seen_imgs.add(img.name)
# ---------------------------------------------------
bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')