Gap filling, improvements for character pipeline

This commit is contained in:
2026-05-25 01:42:28 +03:00
parent c7ef9283cd
commit eea50adfcb
10 changed files with 507 additions and 26 deletions
+28 -2
View File
@@ -425,9 +425,35 @@ 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)
# 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.
function(add_shape_key_propagation INPUT_BLEND OUTPUT_BLEND)
add_custom_command(
OUTPUT ${OUTPUT_BLEND}
DEPENDS ${INPUT_BLEND}
${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py
${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
COMMAND ${CMAKE_COMMAND} -E copy ${INPUT_BLEND} ${OUTPUT_BLEND}
COMMAND ${BLENDER} -b -Y ${OUTPUT_BLEND}
-P ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py --
${OUTPUT_BLEND} ${OUTPUT_BLEND}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endfunction()
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend
)
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend
)
# male
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND
"${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
)
@@ -446,7 +472,7 @@ add_clothes_pipeline(
# female
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND
"${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
)
@@ -0,0 +1,139 @@
"""
Add missing shape keys from Body_shapes to body part objects.
Preserves existing shape keys.
Usage: blender -b --python add_missing_shape_keys.py -- <blend_file> <output_file>
"""
import bpy
import sys
import os
import mathutils
from mathutils.bvhtree import BVHTree
# Import functions from transfer_shape_keys.py
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from transfer_shape_keys import (
compute_robust_surface_mapping,
compute_side_preserving_position,
smooth_boundary_areas,
smooth_penetration_areas,
create_bvh_for_source,
load_source_data,
fix_seams_across_objects
)
def get_source_object_name(target_obj_info):
if target_obj_info.get('ref_shapes'):
return target_obj_info['ref_shapes']
return None
def add_missing_shape_keys(source_data, target_obj):
"""Add only missing shape keys to target object, preserving existing ones."""
if not target_obj.data.shape_keys:
target_obj.shape_key_add(name='Basis', from_mix=False)
existing_names = {kb.name for kb in target_obj.data.shape_keys.key_blocks}
missing_names = [n for n in source_data['names'] if n != 'Basis' and n not in existing_names]
if not missing_names:
print(f" {target_obj.name}: all shape keys present, skipping")
return
print(f" {target_obj.name}: adding missing keys: {missing_names}")
# Create missing shape keys
for sk_name in missing_names:
target_obj.shape_key_add(name=sk_name, from_mix=False)
target_obj.data.shape_keys.use_relative = True
# Compute mapping
mapping = compute_robust_surface_mapping(target_obj, source_data)
# Set values for missing shape keys
for sk_name in missing_names:
print(f" Setting {sk_name}...")
sk = target_obj.data.shape_keys.key_blocks[sk_name]
target_obj.data.shape_keys.use_relative = False
for i, v in enumerate(sk.data):
if i < len(mapping['target_verts']):
pos = compute_side_preserving_position(i, sk_name, mapping, source_data, 1.0)
v.co = pos
sk.data.update()
target_obj.data.update_tag()
bpy.context.view_layer.update()
target_obj.data.shape_keys.use_relative = True
target_obj.data.update_tag()
bpy.context.view_layer.update()
smooth_boundary_areas(target_obj, sk_name, mapping, source_data)
smooth_penetration_areas(target_obj, sk_name, mapping, source_data)
print(f" ✓ Added {sk_name}")
def process_blend(blend_path, output_path):
print(f"Processing: {blend_path}")
bpy.ops.wm.open_mainfile(filepath=blend_path)
# Find source object
source_obj = None
for name in ['Body_shapes', 'Body']:
if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH':
obj = bpy.data.objects[name]
if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1:
source_obj = obj
print(f"Found source: {name}")
break
if not source_obj:
print("ERROR: No source object with shape keys found")
return False
# Build source data
source_data = {
'names': [kb.name for kb in source_obj.data.shape_keys.key_blocks],
'vertex_positions': {},
'polygons': [],
'is_relative': source_obj.data.shape_keys.use_relative,
'source_obj_name': source_obj.name
}
for poly in source_obj.data.polygons:
source_data['polygons'].append([v for v in poly.vertices])
for sk in source_obj.data.shape_keys.key_blocks:
source_data['vertex_positions'][sk.name] = [(v.co.x, v.co.y, v.co.z) for v in sk.data]
print(f"Source shape keys: {source_data['names']}")
# Process body part objects
required_props = {'age', 'sex', 'slot'}
modified = False
for obj in bpy.data.objects:
if obj.type == 'MESH' and all(p in obj for p in required_props):
if obj == source_obj:
continue
add_missing_shape_keys(source_data, obj)
modified = True
# Fix seams: synchronize shape key offsets for vertices sharing
# the same basis position across body parts.
fix_seams_across_objects()
bpy.ops.wm.save_as_mainfile(filepath=output_path)
print(f"Saved: {output_path}")
return True
if __name__ == "__main__":
try:
args = sys.argv[sys.argv.index("--") + 1:]
if len(args) >= 2:
process_blend(args[0], args[1])
else:
print("Usage: blender -b -P script.py -- <input.blend> <output.blend>")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+5 -2
View File
@@ -66,8 +66,11 @@ def raycast_and_adjust_vertices(target_body, bvh_cloth, out_ray_length=0.15):
new_co = m_body_inv @ (v_world + offset)
v.co = new_co
# Update shape keys if they exist
if has_shape_keys:
# Update shape keys if they exist.
# In relative mode, Blender automatically uses v.co as basis,
# so shape key offsets don't need updating. Only update if
# the shape keys are in absolute mode (which we don't use).
if has_shape_keys and not target_body.data.shape_keys.use_relative:
for kb in target_body.data.shape_keys.key_blocks:
kb.data[i].co = new_co
@@ -0,0 +1,91 @@
import bpy
import sys
import os
def propagate_shape_keys(blend_path, output_path):
"""Copy missing shape keys from Body_shapes to all body part objects."""
print(f"Propagating shape keys from {blend_path} to {output_path}")
bpy.ops.wm.open_mainfile(filepath=blend_path)
# Find source object with all shape keys
source_obj = None
for name in ['Body_shapes', 'Body']:
if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH':
obj = bpy.data.objects[name]
if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1:
source_obj = obj
print(f"Found source object: {name}")
break
if not source_obj:
print("ERROR: No source object with shape keys found")
return False
source_keys = source_obj.data.shape_keys.key_blocks
source_basis = source_keys['Basis'].data
modified = False
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
# Only process body part objects
if not all(p in obj for p in ['age', 'sex', 'slot']):
continue
# Skip the source object itself
if obj == source_obj:
continue
if not obj.data.shape_keys:
# Create basis if missing
obj.shape_key_add(name='Basis', from_mix=False)
existing_names = {kb.name for kb in obj.data.shape_keys.key_blocks}
for sk in source_keys:
if sk.name == 'Basis':
continue
if sk.name in existing_names:
print(f" {obj.name}: already has '{sk.name}', skipping")
continue
# Copy shape key
print(f" {obj.name}: adding '{sk.name}' from {source_obj.name}")
new_sk = obj.shape_key_add(name=sk.name, from_mix=False)
# Transfer vertex positions relative to basis
# We need to map source vertices to target vertices
# For now, assume same topology (body parts were separated from same mesh)
num_verts = min(len(new_sk.data), len(sk.data), len(source_basis))
for i in range(num_verts):
# The shape key position in source
src_pos = sk.data[i].co
src_basis_pos = source_basis[i].co
# Offset from basis
offset = src_pos - src_basis_pos
# Apply to target using target's basis position
new_sk.data[i].co = obj.data.shape_keys.key_blocks['Basis'].data[i].co + offset
new_sk.data.update()
modified = True
if modified:
obj.data.update_tag()
bpy.context.view_layer.update()
bpy.ops.wm.save_as_mainfile(filepath=output_path)
print(f"Saved: {output_path}")
return True
if __name__ == "__main__":
try:
args = sys.argv[sys.argv.index("--") + 1:]
if len(args) >= 2:
propagate_shape_keys(args[0], args[1])
else:
print("Usage: blender -b -P script.py -- <input.blend> <output.blend>")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+123 -11
View File
@@ -243,12 +243,16 @@ def compute_signed_distance_and_direction(bvh, point, reference_normal=None):
if location is None:
return None, None, None, None
# Determine sign using reference normal
# Determine sign using reference normal.
# For points outside (in the direction of the normal), signed_distance
# must be positive. For points inside, negative.
if reference_normal is not None:
to_surface = location - point
if to_surface.length > 0:
dot = to_surface.normalized().dot(reference_normal)
signed_distance = distance if dot > 0 else -distance
# Inverted relative to the no-reference-normal branch because
# to_surface here points *toward* the surface, not away.
signed_distance = -distance if dot > 0 else distance
else:
signed_distance = 0
else:
@@ -743,15 +747,21 @@ def smooth_penetration_areas(target_obj, sk_name, mapping, source_data, threshol
problem_vertices = set()
bvh_deformed, _ = create_bvh_for_source(source_data, sk_name)
# Shape keys are in relative mode here, so current_positions are offsets.
# The BVH is built from absolute source positions, so we must convert
# relative offsets to absolute positions before querying it.
basis_positions = mapping['target_verts']
for i, pos in enumerate(current_positions):
if i < len(mapping['surface_points']) and i < len(mapping['side_flags']):
surface_point = mapping['surface_points'][i]
target_side = mapping['side_flags'][i]
if target_side != 0:
location, _, _, _ = bvh_deformed.find_nearest(pos)
if target_side != 0 and i < len(basis_positions):
abs_pos = basis_positions[i] + pos
location, _, _, _ = bvh_deformed.find_nearest(abs_pos)
if location and i < len(mapping['direction_vectors']):
to_surface = pos - location
to_surface = abs_pos - location
current_side = 1 if to_surface.dot(mapping['direction_vectors'][i]) > 0 else -1
if current_side != target_side:
@@ -1009,16 +1019,109 @@ def process_target_object(target_obj_info, source_data, current_file_path):
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 get_body_part_objects():
"""Find all mesh objects that are body parts (have age/sex/slot)"""
result = []
required_props = {'age', 'sex', 'slot'}
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data and all(p in obj for p in required_props):
result.append(obj)
return result
def round_pos(co, tolerance=0.0001):
"""Round a coordinate to tolerance-based buckets for spatial hashing"""
return (round(co.x / tolerance),
round(co.y / tolerance),
round(co.z / tolerance))
def fix_seams_across_objects(tolerance=0.0001):
"""
Post-process shape keys to ensure vertices sharing the same basis
position (within tolerance) get identical offsets. This fixes seams
between body parts and UV seams within the same part.
"""
body_parts = get_body_part_objects()
if not body_parts:
print("No body part objects found for seam fixing")
return
print(f"\n{'=' * 60}")
print("SEAM FIX: Synchronizing shape key offsets across body parts")
print(f"{'=' * 60}")
print(f"Found {len(body_parts)} body part objects")
# Collect shape key names from first object that has them
shape_key_names = []
for obj in body_parts:
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
for kb in obj.data.shape_keys.key_blocks:
if kb.name != 'Basis':
shape_key_names.append(kb.name)
break
if not shape_key_names:
print("No shape keys found, skipping seam fix")
return
print(f"Synchronizing {len(shape_key_names)} shape key(s): {', '.join(shape_key_names)}")
fixed_vertices = 0
fixed_groups = 0
for sk_name in shape_key_names:
# Build spatial hash: position_key -> list of (obj, vert_idx, offset)
pos_map = {}
for obj in body_parts:
if not obj.data.shape_keys or sk_name not in obj.data.shape_keys.key_blocks:
continue
sk = obj.data.shape_keys.key_blocks[sk_name]
for i, v in enumerate(obj.data.vertices):
if i >= len(sk.data):
continue
pos_key = round_pos(v.co, tolerance)
offset = sk.data[i].co.copy()
if pos_key not in pos_map:
pos_map[pos_key] = []
pos_map[pos_key].append((obj, i, offset))
# Average offsets for each position group with multiple vertices
for pos_key, entries in pos_map.items():
if len(entries) < 2:
continue
# Compute average offset
avg_offset = mathutils.Vector((0.0, 0.0, 0.0))
for obj, idx, offset in entries:
avg_offset += offset
avg_offset /= len(entries)
# Apply averaged offset to all vertices in this group
for obj, idx, offset in entries:
sk = obj.data.shape_keys.key_blocks[sk_name]
sk.data[idx].co = avg_offset.copy()
fixed_vertices += 1
fixed_groups += 1
# Update all meshes
for obj in body_parts:
if obj.data.shape_keys:
for kb in obj.data.shape_keys.key_blocks:
kb.data.update()
obj.data.update_tag()
bpy.context.view_layer.update()
print(f"Fixed {fixed_vertices} vertices across {fixed_groups} position groups")
print(f"Seam fix complete")
def main():
print("=" * 60)
print("Blender Shape Key Transfer Script - Boundary Velocity Limiting")
@@ -1077,6 +1180,15 @@ def main():
print(f"\nProgress saved to: {temp_progress_file}")
# Post-process: fix seams by synchronizing offsets across body parts
print(f"\nLoading final working file for seam fix...")
bpy.ops.wm.open_mainfile(filepath=working_file)
fix_seams_across_objects()
# Save after seam fix
print(f"\nSaving after seam fix...")
bpy.ops.wm.save_as_mainfile(filepath=working_file)
# Copy the final working file to the output location
print(f"\nCopying final result to: {output_file}")
shutil.copy2(working_file, output_file)