Clothes and hairs
This commit is contained in:
@@ -62,6 +62,7 @@ add_custom_command(
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
|
||||
${VRM_IMPORTED_BLENDS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
|
||||
# ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-hair.stamp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
VERBATIM
|
||||
@@ -418,12 +419,28 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
|
||||
)
|
||||
endfunction()
|
||||
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-top.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-feet.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-top.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-feet.blend)
|
||||
set(SEX_LIST "male" "female")
|
||||
foreach(SEX ${SEX_LIST})
|
||||
set(HAIR_WEIGHTED_STAMP "${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-hair_weighted.stamp")
|
||||
add_custom_command(
|
||||
OUTPUT ${HAIR_WEIGHTED_STAMP}
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-hair.blend
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/clothes
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py --
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-hair.blend
|
||||
./ ${CMAKE_CURRENT_BINARY_DIR}/clothes
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${HAIR_WEIGHTED_STAMP}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-hair_weighted.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMENT "Processing hair meshes (weight transfer)"
|
||||
)
|
||||
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-top.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-feet.blend)
|
||||
endforeach()
|
||||
|
||||
# Propagate missing shape keys (like "fat") from Body_shapes to base body parts.
|
||||
# This ensures all body part meshes have consistent shape keys, preventing seams.
|
||||
@@ -452,40 +469,31 @@ add_shape_key_propagation(
|
||||
)
|
||||
|
||||
# male
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # BOTTOM_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-top_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-feet_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
set(SLOT_LIST "bottom" "top" "feet" "hair")
|
||||
set(SLOT_INPUT "edited-normal-male-shapes.blend")
|
||||
list(GET SLOT_LIST -1 LAST_SLOT)
|
||||
foreach (SLOT ${SLOT_LIST})
|
||||
set(SLOT_OUTPUT "edited-normal-male-consolidated-${SLOT}.blend")
|
||||
if (SLOT STREQUAL LAST_SLOT)
|
||||
set(SLOT_OUTPUT "edited-normal-male-consolidated.blend")
|
||||
endif()
|
||||
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-${SLOT}_weighted.blend"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
|
||||
)
|
||||
set(SLOT_INPUT ${SLOT_OUTPUT})
|
||||
endforeach()
|
||||
|
||||
# female
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-top_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-feet_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
set(SLOT_INPUT "edited-normal-female-shapes.blend")
|
||||
foreach (SLOT ${SLOT_LIST})
|
||||
set(SLOT_OUTPUT "edited-normal-female-consolidated-${SLOT}.blend")
|
||||
if (SLOT STREQUAL LAST_SLOT)
|
||||
set(SLOT_OUTPUT "edited-normal-female-consolidated.blend")
|
||||
endif()
|
||||
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-${SLOT}_weighted.blend"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
|
||||
)
|
||||
set(SLOT_INPUT ${SLOT_OUTPUT})
|
||||
endforeach()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -203,8 +203,41 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
|
||||
clothing_obj.select_set(True)
|
||||
new_target.select_set(True)
|
||||
bpy.context.view_layer.objects.active = new_target
|
||||
|
||||
# Remember clothing's material BEFORE join so we can verify
|
||||
# after Blender potentially remaps faces to a stale copy.
|
||||
clothing_mat = clothing_obj.active_material
|
||||
bpy.ops.object.join()
|
||||
|
||||
# ---- Fix: ensure clothing faces keep the clothing's material ----
|
||||
# After join, Blender may have assigned clothing faces to a
|
||||
# pre-existing slot with the same base name but older texture
|
||||
# references (loaded from the body blend). Find the slot that
|
||||
# actually holds the clothing's original material data block
|
||||
# and reassign any faces that landed in a stale look-alike slot.
|
||||
if clothing_mat:
|
||||
clothing_slot = None
|
||||
stale_slot = None
|
||||
for i, slot in enumerate(new_target.material_slots):
|
||||
if slot.material == clothing_mat:
|
||||
clothing_slot = i
|
||||
elif (slot.material and
|
||||
slot.material != clothing_mat and
|
||||
slot.material.name == clothing_mat.name):
|
||||
# Same name, different data block -> stale
|
||||
stale_slot = i
|
||||
if clothing_slot is not None and stale_slot is not None:
|
||||
mesh = new_target.data
|
||||
fixed = 0
|
||||
for poly in mesh.polygons:
|
||||
if poly.material_index == stale_slot:
|
||||
poly.material_index = clothing_slot
|
||||
fixed += 1
|
||||
if fixed:
|
||||
print(f" Fixed {fixed} faces: stale mat slot "
|
||||
f"{stale_slot} -> clothing slot {clothing_slot}")
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
# NEW CODE: Copy stored custom properties to combined object
|
||||
# After joining, new_target is the active object and contains the combined mesh
|
||||
for prop_name, prop_value in clothing_props.items():
|
||||
|
||||
@@ -10,27 +10,57 @@ from transfer_shape_keys import fix_seams_across_objects, fix_normals_across_obj
|
||||
|
||||
def process_append(source_files, output_path):
|
||||
required_props = {"age", "sex", "slot"}
|
||||
|
||||
|
||||
for file_path in source_files:
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Warning: Source file not found: {file_path}")
|
||||
continue
|
||||
|
||||
# ---- Pre-import materials from source blend ----
|
||||
# Blender silently reuses existing materials when appending
|
||||
# objects, even if the incoming one has different properties
|
||||
# (texture references). Force materials to be imported first
|
||||
# so texture changes in the source .blend survive.
|
||||
with bpy.data.libraries.load(file_path, link=False) as (data_from, data_to):
|
||||
data_to.materials = data_from.materials
|
||||
imported_materials = {}
|
||||
for mat in data_to.materials:
|
||||
if mat is None:
|
||||
continue
|
||||
existing = bpy.data.materials.get(mat.name)
|
||||
if existing and existing != mat:
|
||||
print(f" Material '{mat.name}' already exists with "
|
||||
f"different data; incoming material will "
|
||||
f"replace it on appended objects.")
|
||||
imported_materials[mat.name] = mat
|
||||
# ----------------------------------------------------
|
||||
|
||||
with bpy.data.libraries.load(file_path) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
|
||||
for obj in data_to.objects:
|
||||
if obj is None: continue
|
||||
|
||||
|
||||
# Check criteria
|
||||
has_props = all(p in obj.keys() for p in required_props)
|
||||
if obj.type == 'MESH' and has_props:
|
||||
# 1. Link to the scene root temporarily
|
||||
bpy.context.collection.objects.link(obj)
|
||||
|
||||
|
||||
# ---- Remap material slots to imported versions ----
|
||||
for slot in obj.material_slots:
|
||||
if slot.material and slot.material.name in imported_materials:
|
||||
imported = imported_materials[slot.material.name]
|
||||
if imported != slot.material:
|
||||
print(f" Remapping slot "
|
||||
f"'{slot.material.name}' -> "
|
||||
f"'{imported.name}' on '{obj.name}'")
|
||||
slot.material = imported
|
||||
# ---------------------------------------------------
|
||||
|
||||
# 2. Synchronize Names
|
||||
obj.data.name = obj.name
|
||||
|
||||
|
||||
# 3. Find Target Armature
|
||||
arm_name = obj.get("sex")
|
||||
arm_obj = bpy.data.objects.get(arm_name)
|
||||
@@ -40,19 +70,19 @@ def process_append(source_files, output_path):
|
||||
# Remove from all current collections first
|
||||
for col in obj.users_collection:
|
||||
col.objects.unlink(obj)
|
||||
|
||||
|
||||
# Link to every collection the armature belongs to
|
||||
for col in arm_obj.users_collection:
|
||||
col.objects.link(obj)
|
||||
|
||||
|
||||
# B. Parent to Armature
|
||||
obj.parent = arm_obj
|
||||
|
||||
|
||||
# C. Handle Armature Modifier
|
||||
arm_mod = next((m for m in obj.modifiers if m.type == 'ARMATURE'), None)
|
||||
if not arm_mod:
|
||||
arm_mod = obj.modifiers.new(name="Armature", type='ARMATURE')
|
||||
|
||||
|
||||
arm_mod.object = arm_obj
|
||||
print(f"Processed {obj.name}: Parented and Modset to {arm_name}")
|
||||
else:
|
||||
@@ -90,4 +120,3 @@ if __name__ == "__main__":
|
||||
process_append(sources, output)
|
||||
except ValueError:
|
||||
print("Error: Use '--' to separate Blender args from script args.")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -355,10 +355,27 @@ for mapping in[CommandLineMapping()]:
|
||||
for slot in obj.material_slots:
|
||||
if slot.material:
|
||||
mat = slot.material
|
||||
# 3. Check if material already has the prefix
|
||||
# 3. Normalize material name: strip Blender auto-suffixes
|
||||
# (.001,.002 from name collisions, .### from dupes)
|
||||
# before applying the armature prefix. This prevents
|
||||
# duplicate .material files with unpredictable names
|
||||
# (e.g. male_male-clothes-ed.001 vs male_male-clothes-ed)
|
||||
# that cause submeshes to reference missing materials
|
||||
# at runtime.
|
||||
import re
|
||||
clean = re.sub(r'\.\d{3}$', '', mat.name)
|
||||
if clean != mat.name:
|
||||
old = mat.name
|
||||
mat.name = clean
|
||||
# If Blender added .001 again because the clean
|
||||
# name is already taken, keep the suffixed name.
|
||||
print(f"Normalized material '{old}' -> '{mat.name}'"
|
||||
f" on object '{name}'")
|
||||
# 4. Check if material already has the prefix
|
||||
if not mat.name.startswith(prefix):
|
||||
mat.name = prefix + mat.name
|
||||
print(f"Renamed material '{mat.name}' on object '{name}'")
|
||||
print(f"Renamed material '{mat.name}'"
|
||||
f" on object '{name}'")
|
||||
# 3. Export custom properties to json
|
||||
save_data = {}
|
||||
for key in obj.keys():
|
||||
@@ -446,15 +463,81 @@ for mapping in[CommandLineMapping()]:
|
||||
|
||||
armobj = bpy.data.objects.get(mapping.armature_name)
|
||||
armobj.data.name = armobj.name
|
||||
|
||||
# ---- 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.
|
||||
import re as _re
|
||||
seen_imgs = set()
|
||||
for name in obj_names:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if obj and obj.type == 'MESH':
|
||||
for slot in obj.material_slots:
|
||||
if slot.material and slot.material.node_tree:
|
||||
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
|
||||
# ---------------------------------------------------
|
||||
|
||||
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')
|
||||
|
||||
ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))
|
||||
|
||||
# Fix alpha_rejection values in ALL generated .material files.
|
||||
# blender2ogre 0.9.0 writes float values (e.g. "127.5") for
|
||||
# alpha_rejection thresholds, but OGRE 14 expects an integer
|
||||
# in the range 0-255. Float values are truncated to integer
|
||||
# during material compilation, causing 127.5 -> 127 when 128
|
||||
# was intended. This makes submeshes with alpha < 128 in the
|
||||
# texture atlas completely invisible.
|
||||
import glob as _glob
|
||||
_mat_files = _glob.glob(os.path.join(ogre_export_dir, "*.material"))
|
||||
for _mf in _mat_files:
|
||||
with open(_mf, 'r') as _f:
|
||||
_content = _f.read()
|
||||
if 'alpha_rejection' in _content:
|
||||
import re as _re
|
||||
_new = _re.sub(
|
||||
r'alpha_rejection (\S+) (\d+\.?\d*)',
|
||||
lambda m: 'alpha_rejection ' + m.group(1) +
|
||||
' ' + str(int(float(m.group(2)))),
|
||||
_content)
|
||||
if _new != _content:
|
||||
with open(_mf, 'w') as _f:
|
||||
_f.write(_new)
|
||||
print(f" Fixed alpha_rejection in {_mf}")
|
||||
|
||||
# Post-process exported OGRE meshes to fix normal/tangent seams between
|
||||
# body parts. Even with identical custom normals in Blender, the OGRE
|
||||
# exporter's calc_tangents() produces slightly different tangents for
|
||||
# matching vertices because BodyTop and BodyBottom have different face
|
||||
# topologies. This causes a visible lighting seam with normal mapping,
|
||||
# especially when shape keys (like "fat") are applied.
|
||||
ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))
|
||||
fix_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fix_ogre_mesh_seams.py")
|
||||
if os.path.exists(fix_script):
|
||||
print(f"\nPost-processing OGRE meshes in {ogre_export_dir}...")
|
||||
|
||||
Reference in New Issue
Block a user