Fixed inflation
This commit is contained in:
@@ -19,7 +19,8 @@ from transfer_shape_keys import (
|
||||
smooth_penetration_areas,
|
||||
create_bvh_for_source,
|
||||
load_source_data,
|
||||
fix_seams_across_objects
|
||||
fix_seams_across_objects,
|
||||
fix_normals_across_objects
|
||||
)
|
||||
|
||||
def get_source_object_name(target_obj_info):
|
||||
@@ -120,7 +121,8 @@ def process_blend(blend_path, output_path):
|
||||
# Fix seams: synchronize shape key offsets for vertices sharing
|
||||
# the same basis position across body parts.
|
||||
fix_seams_across_objects()
|
||||
|
||||
fix_normals_across_objects()
|
||||
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output_path)
|
||||
print(f"Saved: {output_path}")
|
||||
return True
|
||||
|
||||
@@ -43,37 +43,27 @@ def get_transformation_matrices(obj):
|
||||
return m, m_inv, m_normal
|
||||
|
||||
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)
|
||||
|
||||
"""Raycast from body to cloth and mark vertices that are covered.
|
||||
Returns a list where 1 = vertex is covered by clothing, 0 = visible.
|
||||
Does NOT modify vertex positions -- that used to cause visible steps
|
||||
at clothing boundaries because border vertices kept the inward offset
|
||||
while exposed neighbours did not."""
|
||||
m_body, _, m_body_normal = get_transformation_matrices(target_body)
|
||||
|
||||
num_verts = len(target_body.data.vertices)
|
||||
hit_values = [0] * num_verts
|
||||
has_shape_keys = target_body.data.shape_keys is not None
|
||||
|
||||
# Forward raycast (into cloth)
|
||||
|
||||
for i, v in enumerate(target_body.data.vertices):
|
||||
v_world = m_body @ v.co
|
||||
n_world = (m_body_normal @ v.normal).normalized()
|
||||
|
||||
# Raycast forward (into cloth) and backward (from inside cloth)
|
||||
|
||||
# Raycast forward (outward) and backward (inward)
|
||||
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:
|
||||
hit_values[i] = 1
|
||||
# Adjust vertex position to be slightly outside cloth
|
||||
offset = -n_world * (0.005 if hit_f else 0.01)
|
||||
new_co = m_body_inv @ (v_world + offset)
|
||||
v.co = new_co
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
return hit_values
|
||||
|
||||
def protect_and_remove_hidden_geometry(target_body, hit_values, threshold=4.0):
|
||||
|
||||
@@ -185,22 +185,45 @@ def get_target_object_by_name(obj_name):
|
||||
return None
|
||||
|
||||
def delete_existing_shape_keys(target_obj):
|
||||
"""Delete all existing shape keys from target object"""
|
||||
"""Delete all existing shape keys from target object.
|
||||
CRITICAL: Blender's shape_key_remove() leaves mesh vertices in their
|
||||
*deformed* state rather than restoring basis positions. We must save
|
||||
the Basis positions before deletion and restore them afterwards."""
|
||||
if not target_obj.data.shape_keys:
|
||||
return False
|
||||
|
||||
|
||||
# Save Basis positions before deletion
|
||||
basis_positions = None
|
||||
for kb in target_obj.data.shape_keys.key_blocks:
|
||||
if kb.name == 'Basis':
|
||||
basis_positions = [v.co.copy() for v in kb.data]
|
||||
break
|
||||
|
||||
# Fallback: if no Basis exists, use current vertex positions
|
||||
if basis_positions is None:
|
||||
basis_positions = [v.co.copy() for v in target_obj.data.vertices]
|
||||
|
||||
num_keys = len(target_obj.data.shape_keys.key_blocks)
|
||||
print(f" Deleting {num_keys} existing shape keys...")
|
||||
|
||||
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
target_obj.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
|
||||
while target_obj.data.shape_keys:
|
||||
target_obj.active_shape_key_index = 0
|
||||
bpy.ops.object.shape_key_remove()
|
||||
|
||||
|
||||
target_obj.select_set(False)
|
||||
|
||||
# RESTORE BASIS POSITIONS - Blender leaves mesh deformed after removal
|
||||
for i, v in enumerate(target_obj.data.vertices):
|
||||
if i < len(basis_positions):
|
||||
v.co = basis_positions[i]
|
||||
|
||||
target_obj.data.update_tag()
|
||||
bpy.context.view_layer.update()
|
||||
print(f" Restored basis positions for {len(basis_positions)} vertices")
|
||||
return True
|
||||
|
||||
def ensure_shape_keys_structure(shape_key_names, target_obj):
|
||||
@@ -1122,6 +1145,94 @@ def fix_seams_across_objects(tolerance=0.0001):
|
||||
print(f"Fixed {fixed_vertices} vertices across {fixed_groups} position groups")
|
||||
print(f"Seam fix complete")
|
||||
|
||||
def fix_normals_across_objects(tolerance=0.0001):
|
||||
"""
|
||||
Post-process normals to ensure vertices sharing the same basis
|
||||
position (within tolerance) get identical normals. This fixes
|
||||
lighting seams between body parts.
|
||||
"""
|
||||
body_parts = get_body_part_objects()
|
||||
if not body_parts:
|
||||
print("No body part objects found for normal fixing")
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("NORMAL FIX: Synchronizing normals across body parts")
|
||||
print(f"{'=' * 60}")
|
||||
print(f"Found {len(body_parts)} body part objects")
|
||||
|
||||
# Build spatial hash: position_key -> list of normals
|
||||
pos_map = {}
|
||||
|
||||
for obj in body_parts:
|
||||
obj.data.calc_normals()
|
||||
for v in obj.data.vertices:
|
||||
pos_key = round_pos(v.co, tolerance)
|
||||
if pos_key not in pos_map:
|
||||
pos_map[pos_key] = []
|
||||
pos_map[pos_key].append(v.normal.copy())
|
||||
|
||||
# Compute averaged normals for positions with multiple vertices
|
||||
avg_normals = {}
|
||||
for pos_key, normals in pos_map.items():
|
||||
if len(normals) < 2:
|
||||
continue
|
||||
avg = mathutils.Vector((0.0, 0.0, 0.0))
|
||||
for n in normals:
|
||||
avg += n
|
||||
if avg.length > 0.001:
|
||||
avg.normalize()
|
||||
avg_normals[pos_key] = avg
|
||||
|
||||
if not avg_normals:
|
||||
print("No matching vertices found, skipping normal fix")
|
||||
return
|
||||
|
||||
print(f"Found {len(avg_normals)} position groups with matching vertices")
|
||||
|
||||
# Apply averaged normals to each object
|
||||
fixed_loops = 0
|
||||
fixed_verts = 0
|
||||
|
||||
for obj in body_parts:
|
||||
obj.data.use_auto_smooth = True
|
||||
|
||||
if not obj.data.has_custom_normals:
|
||||
obj.data.create_normals_split()
|
||||
|
||||
obj.data.calc_normals_split()
|
||||
|
||||
vert_normal_map = {}
|
||||
for i, v in enumerate(obj.data.vertices):
|
||||
pos_key = round_pos(v.co, tolerance)
|
||||
if pos_key in avg_normals:
|
||||
vert_normal_map[i] = avg_normals[pos_key]
|
||||
|
||||
if not vert_normal_map:
|
||||
continue
|
||||
|
||||
custom_normals = []
|
||||
for loop in obj.data.loops:
|
||||
if loop.vertex_index in vert_normal_map:
|
||||
n = vert_normal_map[loop.vertex_index]
|
||||
custom_normals.append((n.x, n.y, n.z))
|
||||
fixed_loops += 1
|
||||
else:
|
||||
if hasattr(obj.data, 'corner_normals'):
|
||||
cn = obj.data.corner_normals[loop.index]
|
||||
custom_normals.append((cn.vector.x, cn.vector.y, cn.vector.z))
|
||||
else:
|
||||
custom_normals.append((loop.normal.x, loop.normal.y, loop.normal.z))
|
||||
|
||||
obj.data.normals_split_custom_set(custom_normals)
|
||||
fixed_verts += len(vert_normal_map)
|
||||
obj.data.update_tag()
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
print(f"Fixed {fixed_verts} vertices ({fixed_loops} loops) across {len(body_parts)} objects")
|
||||
print(f"Normal fix complete")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Blender Shape Key Transfer Script - Boundary Velocity Limiting")
|
||||
@@ -1184,7 +1295,8 @@ def main():
|
||||
print(f"\nLoading final working file for seam fix...")
|
||||
bpy.ops.wm.open_mainfile(filepath=working_file)
|
||||
fix_seams_across_objects()
|
||||
|
||||
fix_normals_across_objects()
|
||||
|
||||
# Save after seam fix
|
||||
print(f"\nSaving after seam fix...")
|
||||
bpy.ops.wm.save_as_mainfile(filepath=working_file)
|
||||
|
||||
Reference in New Issue
Block a user