Refactoring editing tools
This commit is contained in:
@@ -74,7 +74,7 @@ add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
|
||||
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
"${FEMALE_OBJECTS}"
|
||||
"female"
|
||||
@@ -85,7 +85,7 @@ add_custom_command(
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
|
||||
${VRM_IMPORTED_BLENDS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
VERBATIM
|
||||
@@ -423,6 +423,7 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
|
||||
endfunction()
|
||||
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND
|
||||
@@ -430,3 +431,9 @@ add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
add_clothes_pipeline(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND
|
||||
)
|
||||
|
||||
|
||||
BIN
assets/blender/characters/clothes-female-bottom.blend
LFS
Normal file
BIN
assets/blender/characters/clothes-female-bottom.blend
LFS
Normal file
Binary file not shown.
Binary file not shown.
@@ -35,7 +35,7 @@ def get_transformation_matrices(obj):
|
||||
|
||||
return m, m_inv, m_normal
|
||||
|
||||
def raycast_and_adjust_vertices(target_body, bvh_cloth):
|
||||
def raycast_and_adjust_vertices(target_body, bvh_cloth, out_ray_length=0.15):
|
||||
"""Raycast from body to cloth and adjust vertices that intersect"""
|
||||
m_body, m_body_inv, m_body_normal = get_transformation_matrices(target_body)
|
||||
|
||||
@@ -49,7 +49,7 @@ def raycast_and_adjust_vertices(target_body, bvh_cloth):
|
||||
n_world = (m_body_normal @ v.normal).normalized()
|
||||
|
||||
# Raycast forward (into cloth) and backward (from inside cloth)
|
||||
hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, 0.015)
|
||||
hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, out_ray_length)
|
||||
hit_b, _, _, _ = bvh_cloth.ray_cast(v_world, -n_world, 0.005)
|
||||
|
||||
if hit_f or hit_b:
|
||||
@@ -151,9 +151,19 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
|
||||
|
||||
print(f"Processing: {clothing_name_for_final} -> {target_name} (using copy)")
|
||||
|
||||
# NEW CODE: Store custom properties from clothing before they might be lost
|
||||
clothing_props = {}
|
||||
for prop_name in ["ref_shapes", "ref_ray_length"]:
|
||||
if prop_name in clothing_obj:
|
||||
clothing_props[prop_name] = clothing_obj[prop_name]
|
||||
print(f" Stored property '{prop_name}' = {clothing_obj[prop_name]} from clothing")
|
||||
|
||||
# Step A: Raycast & adjust vertices
|
||||
out_ray_length = 0.015
|
||||
if "ref_ray_length" in clothing_obj:
|
||||
out_ray_length = clothing_obj["ref_ray_length"]
|
||||
bvh_cloth = setup_bvh_and_matrices(clothing_obj)
|
||||
hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth)
|
||||
hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth, out_ray_length)
|
||||
|
||||
# Step B: Remove hidden geometry
|
||||
protect_and_remove_hidden_geometry(new_target, hit_values)
|
||||
@@ -188,6 +198,12 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
|
||||
bpy.context.view_layer.objects.active = new_target
|
||||
bpy.ops.object.join()
|
||||
|
||||
# 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():
|
||||
new_target[prop_name] = prop_value
|
||||
print(f" Copied property '{prop_name}' = {prop_value} to combined object")
|
||||
|
||||
# Rename the combined object using the appropriate clothing name
|
||||
new_target.name = f"{target_name}_{clothing_name_for_final}"
|
||||
whitelist.add(new_target)
|
||||
|
||||
Binary file not shown.
@@ -29,35 +29,63 @@ def parse_args():
|
||||
print("Usage: blender -b --python transfer_shape_keys.py -- <source_file> <target_file> <output_file>")
|
||||
sys.exit(1)
|
||||
|
||||
def load_source_data(source_path):
|
||||
"""Load source file and extract shape key information"""
|
||||
def get_source_object_name(target_obj_info):
|
||||
"""Get source object name from target's custom property or fallback values"""
|
||||
# Check for custom property "ref_shapes" in the stored info
|
||||
if target_obj_info.get('ref_shapes'):
|
||||
ref_name = target_obj_info['ref_shapes']
|
||||
print(f" Using ref_shapes custom property: '{ref_name}'")
|
||||
return ref_name
|
||||
|
||||
# Fallback to "Body_shapes" then "Body"
|
||||
print(f" No ref_shapes property found, trying fallbacks")
|
||||
return None
|
||||
|
||||
def load_source_data(source_path, target_obj_info):
|
||||
"""Load source file and extract shape key information using object name from target info"""
|
||||
print(f"\nLoading source file: {source_path}")
|
||||
|
||||
if not os.path.exists(source_path):
|
||||
print(f"Error: Source file not found: {source_path}")
|
||||
sys.exit(1)
|
||||
|
||||
current_file = bpy.data.filepath if bpy.data.filepath else None
|
||||
# Get source object name based on target's custom property from stored info
|
||||
source_obj_name = get_source_object_name(target_obj_info)
|
||||
|
||||
# Open source file
|
||||
bpy.ops.wm.open_mainfile(filepath=source_path)
|
||||
|
||||
body_object = None
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name == "Body_shapes" and obj.type == 'MESH':
|
||||
body_object = obj
|
||||
break
|
||||
|
||||
# If we have a specific name from ref_shapes, try that first
|
||||
if source_obj_name:
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name == source_obj_name and obj.type == 'MESH':
|
||||
body_object = obj
|
||||
print(f" Found source object: '{source_obj_name}' from ref_shapes")
|
||||
break
|
||||
|
||||
# If not found or no ref_shapes, try fallbacks
|
||||
if not body_object:
|
||||
fallback_names = ["Body_shapes", "Body"]
|
||||
for fallback_name in fallback_names:
|
||||
print(f" Trying fallback: '{fallback_name}'")
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name == fallback_name and obj.type == 'MESH':
|
||||
body_object = obj
|
||||
source_obj_name = fallback_name
|
||||
print(f" Found source object: '{fallback_name}'")
|
||||
break
|
||||
if body_object:
|
||||
break
|
||||
|
||||
if not body_object:
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name == "Body" and obj.type == 'MESH':
|
||||
body_object = obj
|
||||
break
|
||||
if not body_object:
|
||||
print("Error: Could not find mesh object named 'Body' in source file")
|
||||
print("Error: Could not find mesh object in source file")
|
||||
print("Tried: ref_shapes property (if present), 'Body_shapes', 'Body'")
|
||||
sys.exit(1)
|
||||
|
||||
if not body_object.data.shape_keys:
|
||||
print("Error: Body object has no shape keys")
|
||||
print(f"Error: Source object '{source_obj_name}' has no shape keys")
|
||||
sys.exit(1)
|
||||
|
||||
shape_key_data = {
|
||||
@@ -73,7 +101,7 @@ def load_source_data(source_path):
|
||||
shape_key_data['polygons'].append([v for v in poly.vertices])
|
||||
|
||||
source_shape_keys = body_object.data.shape_keys.key_blocks
|
||||
print(f"Found Body object with {len(source_shape_keys)} shape keys")
|
||||
print(f"Found source object '{source_obj_name}' with {len(source_shape_keys)} shape keys")
|
||||
|
||||
for sk in source_shape_keys:
|
||||
shape_key_data['names'].append(sk.name)
|
||||
@@ -85,15 +113,13 @@ def load_source_data(source_path):
|
||||
|
||||
shape_key_data['vertex_positions'][sk.name] = vertex_positions
|
||||
|
||||
if current_file and os.path.exists(current_file):
|
||||
bpy.ops.wm.open_mainfile(filepath=current_file)
|
||||
elif current_file:
|
||||
bpy.ops.wm.read_homefile()
|
||||
# Store the source object name that was actually used
|
||||
shape_key_data['source_obj_name'] = source_obj_name
|
||||
|
||||
return shape_key_data
|
||||
|
||||
def load_target_file(target_path):
|
||||
"""Load the target .blend file and find objects with custom properties"""
|
||||
"""Load the target .blend file and collect information about target objects"""
|
||||
print(f"\nLoading target file: {target_path}")
|
||||
|
||||
if not os.path.exists(target_path):
|
||||
@@ -103,23 +129,41 @@ def load_target_file(target_path):
|
||||
temp_dir = os.path.join(os.path.dirname(target_path), "temp_blend_files")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
temp_target = os.path.join(temp_dir, os.path.basename(target_path))
|
||||
# Create a unique temp file path
|
||||
temp_target = os.path.join(temp_dir, f"temp_processing_{os.path.basename(target_path)}")
|
||||
shutil.copy2(target_path, temp_target)
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=temp_target)
|
||||
|
||||
target_objects = []
|
||||
# Store information about target objects without keeping references
|
||||
target_objects_info = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and obj.data:
|
||||
if all(prop in obj for prop in ['age', 'sex', 'slot']):
|
||||
target_objects.append(obj)
|
||||
print(f"Found target object: {obj.name}")
|
||||
print(f" - age: {obj['age']}")
|
||||
print(f" - sex: {obj['sex']}")
|
||||
print(f" - slot: {obj['slot']}")
|
||||
print(f" - vertices: {len(obj.data.vertices)}")
|
||||
obj_info = {
|
||||
'name': obj.name,
|
||||
'age': obj['age'],
|
||||
'sex': obj['sex'],
|
||||
'slot': obj['slot'],
|
||||
'vertex_count': len(obj.data.vertices),
|
||||
'ref_shapes': obj.get('ref_shapes', None) # Store ref_shapes if present
|
||||
}
|
||||
target_objects_info.append(obj_info)
|
||||
print(f"Found target object: {obj_info['name']}")
|
||||
print(f" - age: {obj_info['age']}")
|
||||
print(f" - sex: {obj_info['sex']}")
|
||||
print(f" - slot: {obj_info['slot']}")
|
||||
print(f" - vertices: {obj_info['vertex_count']}")
|
||||
if obj_info['ref_shapes']:
|
||||
print(f" - ref_shapes: '{obj_info['ref_shapes']}'")
|
||||
|
||||
return target_objects, temp_target
|
||||
return target_objects_info, temp_target
|
||||
|
||||
def get_target_object_by_name(obj_name):
|
||||
"""Get a valid reference to a target object by name"""
|
||||
if obj_name in bpy.data.objects:
|
||||
return bpy.data.objects[obj_name]
|
||||
return None
|
||||
|
||||
def delete_existing_shape_keys(target_obj):
|
||||
"""Delete all existing shape keys from target object"""
|
||||
@@ -921,6 +965,31 @@ def save_output_file(output_path, temp_target):
|
||||
|
||||
print(f"File saved successfully")
|
||||
|
||||
def process_target_object(target_obj_info, source_data, current_file_path):
|
||||
"""Process a single target object using its stored information"""
|
||||
print(f"\nProcessing: {target_obj_info['name']}")
|
||||
print(f" {'=' * 40}")
|
||||
|
||||
# Get a fresh reference to the target object
|
||||
target_obj = get_target_object_by_name(target_obj_info['name'])
|
||||
if not target_obj:
|
||||
print(f" Error: Could not find target object '{target_obj_info['name']}'")
|
||||
return False
|
||||
|
||||
ensure_shape_keys_structure(source_data['names'], target_obj)
|
||||
mapping = transfer_shape_keys(source_data, target_obj)
|
||||
verify_transfer(source_data['names'], target_obj)
|
||||
|
||||
# Test quality of 'fat' shape key if it exists
|
||||
for sk_name in source_data['names']:
|
||||
if sk_name == 'fat':
|
||||
test_shape_key_quality(target_obj, sk_name, mapping, source_data)
|
||||
break
|
||||
|
||||
print(f" {'=' * 40}")
|
||||
print(f" ✓ Completed")
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Blender Shape Key Transfer Script - Boundary Velocity Limiting")
|
||||
@@ -932,29 +1001,60 @@ def main():
|
||||
print(f"Output: {output_file}")
|
||||
|
||||
try:
|
||||
source_data = load_source_data(source_file)
|
||||
target_objects, temp_target = load_target_file(target_file)
|
||||
# First, load target file and collect information about target objects
|
||||
target_objects_info, temp_target = load_target_file(target_file)
|
||||
|
||||
if target_objects:
|
||||
for idx, target_obj in enumerate(target_objects, 1):
|
||||
print(f"\n[{idx}/{len(target_objects)}] Processing: {target_obj.name}")
|
||||
print(f" {'=' * 40}")
|
||||
|
||||
ensure_shape_keys_structure(source_data['names'], target_obj)
|
||||
mapping = transfer_shape_keys(source_data, target_obj)
|
||||
verify_transfer(source_data['names'], target_obj)
|
||||
|
||||
for sk_name in source_data['names']:
|
||||
if sk_name == 'fat':
|
||||
test_shape_key_quality(target_obj, sk_name, mapping, source_data)
|
||||
break
|
||||
|
||||
print(f" {'=' * 40}")
|
||||
print(f" ✓ Completed")
|
||||
|
||||
save_output_file(output_file, temp_target)
|
||||
else:
|
||||
if not target_objects_info:
|
||||
print("\nNo target objects found with required properties")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nFound {len(target_objects_info)} target objects to process")
|
||||
|
||||
# Create a working file that we'll update after each object
|
||||
working_file = temp_target
|
||||
|
||||
# Process each target object
|
||||
for idx, target_obj_info in enumerate(target_objects_info, 1):
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"[{idx}/{len(target_objects_info)}] Processing: {target_obj_info['name']}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# Load source data using the target object's information
|
||||
source_data = load_source_data(source_file, target_obj_info)
|
||||
|
||||
# Load the current working file
|
||||
bpy.ops.wm.open_mainfile(filepath=working_file)
|
||||
|
||||
# Process the target object
|
||||
success = process_target_object(target_obj_info, source_data, source_file)
|
||||
|
||||
if not success:
|
||||
print(f" Failed to process {target_obj_info['name']}")
|
||||
continue
|
||||
|
||||
# Save progress to a new temp file
|
||||
temp_progress_file = os.path.join(os.path.dirname(working_file), f"progress_{idx}_{os.path.basename(target_file)}")
|
||||
bpy.ops.wm.save_as_mainfile(filepath=temp_progress_file)
|
||||
|
||||
# Update working file for next iteration
|
||||
working_file = temp_progress_file
|
||||
|
||||
print(f"\nProgress saved to: {temp_progress_file}")
|
||||
|
||||
# Copy the final working file to the output location
|
||||
print(f"\nCopying final result to: {output_file}")
|
||||
shutil.copy2(working_file, output_file)
|
||||
|
||||
# Clean up temp files
|
||||
try:
|
||||
temp_dir = os.path.dirname(temp_target)
|
||||
for file in os.listdir(temp_dir):
|
||||
if file.startswith("temp_") or file.startswith("progress_"):
|
||||
os.remove(os.path.join(temp_dir, file))
|
||||
if os.path.exists(temp_dir) and not os.listdir(temp_dir):
|
||||
os.rmdir(temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Script completed successfully!")
|
||||
@@ -968,3 +1068,4 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user