From b71b599d9c0907b2bdff5fdd963502987fc92314 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 25 May 2026 04:10:44 +0300 Subject: [PATCH] Fixed inflation --- .../characters/add_missing_shape_keys.py | 6 +- assets/blender/characters/combine_clothes.py | 34 ++--- .../blender/characters/transfer_shape_keys.py | 124 +++++++++++++++++- 3 files changed, 134 insertions(+), 30 deletions(-) diff --git a/assets/blender/characters/add_missing_shape_keys.py b/assets/blender/characters/add_missing_shape_keys.py index 44e138c..ba4c4b2 100644 --- a/assets/blender/characters/add_missing_shape_keys.py +++ b/assets/blender/characters/add_missing_shape_keys.py @@ -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 diff --git a/assets/blender/characters/combine_clothes.py b/assets/blender/characters/combine_clothes.py index 6a4d176..323a908 100644 --- a/assets/blender/characters/combine_clothes.py +++ b/assets/blender/characters/combine_clothes.py @@ -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): diff --git a/assets/blender/characters/transfer_shape_keys.py b/assets/blender/characters/transfer_shape_keys.py index 883baee..c6ac4fe 100644 --- a/assets/blender/characters/transfer_shape_keys.py +++ b/assets/blender/characters/transfer_shape_keys.py @@ -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)