Skip to content

Instantly share code, notes, and snippets.

@tinkerer-red
Last active November 5, 2024 05:10
Show Gist options
  • Save tinkerer-red/6905635eec8422bc7aa0fa4cb710a524 to your computer and use it in GitHub Desktop.
Save tinkerer-red/6905635eec8422bc7aa0fa4cb710a524 to your computer and use it in GitHub Desktop.
A collection of blender scripts
# Purpose is to bind all materials to a single object to allow for importing into unity, as unity will not import a material if it's not actively used on a model, despite being in the files.
import bpy
def create_carrier_mesh():
# Name for the carrier mesh
carrier_mesh_name = "Material_Carrier_Mesh"
# Create a new mesh and object for the carrier
mesh_data = bpy.data.meshes.new(carrier_mesh_name)
carrier_obj = bpy.data.objects.new(carrier_mesh_name, mesh_data)
# Link the carrier object to the current scene
bpy.context.collection.objects.link(carrier_obj)
# Debug: Print all keys in carrier_obj.data to locate material-related attributes
print("carrier_obj.data keys:")
for attribute in dir(carrier_obj.data):
print(attribute)
# Store vertices and faces for the polygons
vertices = []
faces = []
material_indices = []
# Size of each polygon
poly_size = 0.1
offset = 0
# Iterate over each material in the Blender file
for idx, material in enumerate(bpy.data.materials):
# Define vertices for a square polygon for each material
v_start = len(vertices)
vertices.extend([
(offset, 0, 0),
(offset + poly_size, 0, 0),
(offset + poly_size, poly_size, 0),
(offset, poly_size, 0)
])
faces.append((v_start, v_start + 1, v_start + 2, v_start + 3))
material_indices.append(idx)
# Add the material to the carrier object
if material.name not in carrier_obj.data.materials:
carrier_obj.data.materials.append(material)
# Move offset for the next polygon to avoid overlap
offset += poly_size * 2
# Assign vertices, faces, and materials to the mesh data
mesh_data.from_pydata(vertices, [], faces)
mesh_data.update()
# Set the material index for each face to match the corresponding material
for face, mat_index in zip(carrier_obj.data.polygons, material_indices):
face.material_index = mat_index
print(f"Carrier mesh '{carrier_mesh_name}' created with {len(bpy.data.materials)} materials.")
def export_carrier_mesh(filepath):
# Ensure forward slashes in filepath to avoid issues with backslashes
filepath = filepath.replace("\\", "/")
# Create the carrier mesh with all materials
create_carrier_mesh()
# Select and export the carrier mesh as FBX
bpy.ops.object.select_all(action='DESELECT')
bpy.data.objects["Material_Carrier_Mesh"].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects["Material_Carrier_Mesh"]
# Export as FBX
bpy.ops.export_scene.fbx(
filepath=filepath,
use_selection=True,
apply_unit_scale=True,
bake_space_transform=True
)
print(f"Exported carrier mesh to '{filepath}'.")
# Example usage: specify the path where you want to export the FBX
output_path = "E:\\vrc\\Rust Player Model\\MaterialCarrierMesh.fbx" # Replace with your desired output path
export_carrier_mesh(output_path)
# Usage Instructions
# Select All Source Objects First: In the 3D Viewport, select all the source meshes (hair models).
# Select the Target Object Last: The last selected (active) object will be treated as the target where the shape keys will be added.
# Run the Script: This will create a new shape key on the target for each source object, with deformations applied per vertex based on UV proximity.
import bpy
import bmesh
from mathutils import Vector
# Function to find exact matches between vertices
def find_exact_matches(source_verts, target_verts):
print(" - Finding exact matches between source and target vertices...")
unmatched_source = []
unchanged_target = set() # Set to track target vertices with exact matches
unmatched_target = set(target_verts) # Set to track vertices that need matching
for i, source_vert in enumerate(source_verts):
if i % 100 == 0:
print(f" - Processed {i}/{len(source_verts)} source vertices for exact matches...")
matched = False
for target_vert in target_verts:
if (source_vert.co - target_vert.co).length == 0:
unchanged_target.add(target_vert) # Mark target vertex as exact match
unmatched_target.discard(target_vert) # Remove from unmatched target set
matched = True
break
if not matched:
unmatched_source.append(source_vert) # Track only unmatched source vertices
print(" - Exact match search complete.")
return unmatched_source, list(unmatched_target), unchanged_target
# Function to group vertices by UV location, limited to unmatched vertices
def group_by_uv(mesh_obj, unmatched_verts):
print(f" - Grouping vertices by UV location for {mesh_obj.name}...")
uv_layer = mesh_obj.data.uv_layers.active.data
uv_groups = {}
for poly in mesh_obj.data.polygons:
for loop_index in poly.loop_indices:
uv_coord = tuple(uv_layer[loop_index].uv)
vert_index = mesh_obj.data.loops[loop_index].vertex_index
vert = mesh_obj.data.vertices[vert_index]
if vert in unmatched_verts: # Only group unmatched vertices
if uv_coord not in uv_groups:
uv_groups[uv_coord] = []
uv_groups[uv_coord].append(vert)
print(f" - UV grouping complete with {len(uv_groups)} unique UV locations.")
return uv_groups
# Function to find the best nearest vertex pairs within UV-matching groups with proper pairing tracking
def find_best_nearest(source_group, target_group):
print(" - Finding nearest matching vertices within UV groups...")
paired_verts = {}
paired_targets = set() # Track paired target vertices
paired_sources = set() # Track paired source vertices
for i, src_vert in enumerate(source_group):
if i % 10 == 0:
print(f" - Processed {i}/{len(source_group)} source vertices in UV group...")
# Skip if the source vertex is already paired
if src_vert in paired_sources:
continue
# Reset closest tracking for each source vertex
closest_vert = None
min_distance = float("inf")
for tgt_vert in target_group:
# Skip if this target vertex has already been paired
if tgt_vert in paired_targets:
continue
# Calculate the distance between source and target vertices
dist = (src_vert.co - tgt_vert.co).length
# If this distance is the smallest encountered, update closest_vert
if dist < min_distance:
min_distance = dist
closest_vert = tgt_vert
# Pair the source and target vertices if a closest match was found
if closest_vert:
print(f"Closest Target for Source {src_vert.index} is Target {closest_vert.index} with Distance: {min_distance}")
paired_verts[src_vert] = closest_vert
paired_targets.add(closest_vert) # Mark the target vertex as paired
paired_sources.add(src_vert) # Mark the source vertex as paired
print(" - Nearest neighbor pairing complete.")
return paired_verts
# Function to adaptively move unmatched, unchanged vertices
def adapt_unmatched_unchanged(target_shape_key, unmatched_unchanged, source_group):
print(" - Adapting unmatched, unchanged vertices with average movement of nearest neighbors...")
for tgt_vert in unmatched_unchanged:
# Find the 3 closest source vertices based on distance
distances = [(src_vert, (tgt_vert.co - src_vert.co).length) for src_vert in source_group]
distances.sort(key=lambda x: x[1]) # Sort by distance
# Get the 3 nearest vertices
nearest_three = distances[:3]
# Initialize the offset as a Vector and calculate the average offset based on the 3 nearest vertices
average_offset = Vector((0, 0, 0))
for src_vert, _ in nearest_three:
average_offset += (src_vert.co - tgt_vert.co)
average_offset /= len(nearest_three) # Divide by 3 to get the average
# Apply the offset to the target vertex in the shape key
target_shape_key.data[tgt_vert.index].co = tgt_vert.co + average_offset
print(" - Adaptive movement for unmatched, unchanged vertices complete.")
# Step 1: Set the target object as the active object, assuming it's the last selected object
target_obj = bpy.context.active_object
print(f"Target object for shape key transfer: {target_obj.name}")
# Step 2: Iterate over all selected objects, excluding the target
for source_obj in bpy.context.selected_objects:
if source_obj == target_obj:
continue
print(f"Processing source object: {source_obj.name}")
# Step 3: Get vertices and remove exact matches
source_verts = source_obj.data.vertices
target_verts = target_obj.data.vertices
source_unmatched, target_unmatched, unchanged_target = find_exact_matches(source_verts, target_verts)
# Step 4: Group unmatched vertices by UV location (only unmatched vertices are grouped)
source_uv_groups = group_by_uv(source_obj, set(source_unmatched))
target_uv_groups = group_by_uv(target_obj, set(target_unmatched))
# Step 5: Create a new shape key for each source object deformation
shape_key_name = f"TransferredDeform_{source_obj.name}"
print(f" - Creating new shape key: {shape_key_name}")
target_obj.shape_key_add(name=shape_key_name, from_mix=False)
target_shape_key = target_obj.data.shape_keys.key_blocks[shape_key_name]
# Step 6: Match vertices within UV groups and apply offsets
paired_verts = {}
for uv_coord, source_group in source_uv_groups.items():
if uv_coord in target_uv_groups:
target_group = target_uv_groups[uv_coord]
matched_pairs = find_best_nearest(source_group, target_group)
paired_verts.update(matched_pairs)
# Apply the nearest matching as shape key deformation
for src_vert, tgt_vert in matched_pairs.items():
if tgt_vert not in unchanged_target: # Skip vertices with exact matches
offset = src_vert.co - tgt_vert.co
target_shape_key.data[tgt_vert.index].co = tgt_vert.co + offset
print(f" - Applied deformation for UV group: {uv_coord}")
# Step 7: Adapt unmatched, unchanged vertices based on the 3 nearest neighbors
unmatched_unchanged = [tgt_vert for tgt_vert in target_verts if tgt_vert not in paired_verts.values() and tgt_vert not in unchanged_target]
adapt_unmatched_unchanged(target_shape_key, unmatched_unchanged, source_verts)
print(f"Completed shape key transfer for source object: {source_obj.name}")
print("Shape key transfer complete for all selected source objects.")
import bpy
import bmesh
from mathutils import Vector
# Function to match vertices by iterating through all vertices in the source and target based on UV coordinates, with tolerance
def find_uv_matched_vertices(source_obj, target_obj, tolerance=0.0005):
print("Finding matches for vertices based on UV coordinates with tolerance...")
matched_verts = {}
# Enter edit mode to access UV data
bpy.context.view_layer.objects.active = source_obj
bpy.ops.object.mode_set(mode='EDIT')
source_mesh = bmesh.from_edit_mesh(source_obj.data)
bpy.context.view_layer.objects.active = target_obj
bpy.ops.object.mode_set(mode='EDIT')
target_mesh = bmesh.from_edit_mesh(target_obj.data)
# Fetch UV layers for source and target
uv_layer_source = source_mesh.loops.layers.uv.active
uv_layer_target = target_mesh.loops.layers.uv.active
# Collect source UV coordinates mapped to vertex indices
source_uv_verts = {}
for face in source_mesh.faces:
for loop in face.loops:
uv = loop[uv_layer_source].uv
vert_index = loop.vert.index
if uv.to_tuple() not in source_uv_verts:
source_uv_verts[uv.to_tuple()] = []
source_uv_verts[uv.to_tuple()].append(vert_index)
print(f"Source vertex {vert_index} UV: {uv.to_tuple()}")
# Collect target UV coordinates mapped to vertex indices
target_uv_verts = {}
print("Collecting UVs for the target object...")
for face in target_mesh.faces:
for loop in face.loops:
uv = loop[uv_layer_target].uv
vert_index = loop.vert.index
if uv.to_tuple() not in target_uv_verts:
target_uv_verts[uv.to_tuple()] = []
target_uv_verts[uv.to_tuple()].append(vert_index)
print(f"Target vertex {vert_index} UV: {uv.to_tuple()}")
# Switch back to object mode
bpy.ops.object.mode_set(mode='OBJECT')
# Match each target UV with source UVs within tolerance
for target_uv, target_indices in target_uv_verts.items():
for target_idx in target_indices:
potential_matches = []
for source_uv, source_indices in source_uv_verts.items():
uv_distance = (Vector(target_uv) - Vector(source_uv)).length
if uv_distance <= tolerance:
for source_idx in source_indices:
potential_matches.append((source_idx, uv_distance))
# If potential matches are found, choose the closest one by world distance
if potential_matches:
closest_source_idx = min(
potential_matches,
key=lambda match: (match[1], (target_obj.data.vertices[target_idx].co - source_obj.data.vertices[match[0]].co).length)
)[0]
matched_verts[target_idx] = closest_source_idx
print(f"Matched Target Vertex {target_idx} with Source Vertex {closest_source_idx} at UV {target_uv}")
print(f"Total matched vertices: {len(matched_verts)}")
return matched_verts
# Function to calculate a position based on the closest point along the bone with the highest weight
def calculate_bone_closest_position(vertex, target_obj, armature_obj):
most_influential_group = max(vertex.groups, key=lambda g: g.weight, default=None)
if not most_influential_group or most_influential_group.weight < 0.2:
return vertex.co # Skip if no significant influence
bone_name = target_obj.vertex_groups[most_influential_group.group].name
bone = armature_obj.pose.bones.get(bone_name)
if not bone:
return vertex.co # Return the original position if the bone is not found
# Find the closest point on the line segment defined by the bone's head and tail
bone_vector = bone.tail - bone.head
proj_length = (vertex.co - bone.head).dot(bone_vector.normalized())
closest_point = bone.head + bone_vector.normalized() * max(0, min(proj_length, bone_vector.length))
return closest_point
# Function to apply matched vertices to a shape key
def apply_shape_key_from_matches(target_obj, source_obj, matched_verts, shape_key_name="TransferredShapeKey"):
print(f"Applying shape key '{shape_key_name}' to target '{target_obj.name}' using source '{source_obj.name}'...")
# Create or retrieve the shape key
if shape_key_name in target_obj.data.shape_keys.key_blocks:
shape_key = target_obj.data.shape_keys.key_blocks[shape_key_name]
else:
shape_key = target_obj.shape_key_add(name=shape_key_name, from_mix=False)
# Apply the matched vertices positions to the shape key
for target_idx, source_idx in matched_verts.items():
target_vert = shape_key.data[target_idx]
source_vert = source_obj.data.vertices[source_idx]
target_vert.co = source_vert.co
print(f"Shape key '{shape_key_name}' applied successfully.")
# Process unmatched vertices to apply bone closest point positioning
def process_unmatched_vertices(target_obj, matched_verts, armature_obj, shape_key):
print("Processing unmatched vertices to move them to the closest point along the most influential bone...")
for vert in target_obj.data.vertices:
if vert.index not in matched_verts:
new_position = calculate_bone_closest_position(vert, target_obj, armature_obj)
shape_key.data[vert.index].co = new_position
print(f"Updated unmatched vertex {vert.index} to closest bone point {new_position}")
# Example usage
def main():
target_obj = bpy.context.active_object
print(f"\nTarget object for shape key transfer: {target_obj.name}")
armature_obj = next((mod.object for mod in target_obj.modifiers if mod.type == 'ARMATURE'), None)
if not armature_obj:
print("No armature modifier found on the target object.")
return
for source_obj in bpy.context.selected_objects:
if source_obj == target_obj:
continue
print(f"\nProcessing source object: {source_obj.name}")
matched_verts = find_uv_matched_vertices(source_obj, target_obj, tolerance=0.0005)
if matched_verts:
shape_key_name = f"Transferred_{source_obj.name}"
apply_shape_key_from_matches(target_obj, source_obj, matched_verts, shape_key_name=shape_key_name)
shape_key = target_obj.data.shape_keys.key_blocks[shape_key_name]
process_unmatched_vertices(target_obj, matched_verts, armature_obj, shape_key)
else:
print("No matching vertices found based on UV coordinates within tolerance.")
print("Shape key transfer complete for all selected source objects.")
# Run the main function
main()
# Prints a list of users who are making use of a material, typically used if you are cleaning up a project with many oddly named materials and wish to get a comprahensive list to update assets
import bpy
def list_material_usage():
# Iterate over each material in the file
for material in bpy.data.materials:
print(f"Material: {material.name}")
# Find all objects that use this material
users = []
for obj in bpy.data.objects:
if obj.type == 'MESH':
# Check each material slot in the object
for slot in obj.material_slots:
if slot.material == material:
users.append(obj.name)
break # Move to the next object once the material is found
# Display the results
if users:
print(f" Used by objects: {', '.join(users)}")
else:
print(" Not used by any objects.")
# Run the debug script
list_material_usage()
# Select all objects you wish to fix weights for and press run
# This script is used for making sure verts which share a position bart are not connected have the same weights to prevent seems from showing between seporate parts
import bpy
from collections import defaultdict
def average_weights(weight_dict):
"""Average weights from multiple vertices and normalize them."""
total_weights = defaultdict(float)
# Sum weights for each bone
for weights in weight_dict.values():
for group, weight in weights.items():
total_weights[group] += weight
# Average the weights by dividing by the number of vertices
vertex_count = len(weight_dict)
for group in total_weights:
total_weights[group] /= vertex_count
# Normalize weights to ensure they sum to 1
total_weight = sum(total_weights.values())
if total_weight > 0:
for group in total_weights:
total_weights[group] /= total_weight
return total_weights
def apply_weights_to_vertex(vertex, weight_dict, obj):
"""Apply averaged weights to a vertex."""
# Clear all weights by setting existing weights to zero
for group in vertex.groups:
group.weight = 0.0
# Assign new weights, adding vertex group if necessary
for group_index, weight in weight_dict.items():
# Ensure the vertex is in the target group, adding it if not
if not any(g.group == group_index for g in vertex.groups):
obj.vertex_groups[group_index].add([vertex.index], 0.0, 'REPLACE')
# Set the weight for the group
for g in vertex.groups:
if g.group == group_index:
g.weight = weight
break
def average_vertex_weights_for_mesh(obj):
"""Find vertices with identical positions and average their weights."""
print(f"Processing mesh: {obj.name}")
# Dictionary to store vertices by exact positions
position_dict = defaultdict(list)
modified_pairs = 0
# Populate position_dict with vertices grouped by exact position
for vert in obj.data.vertices:
pos_key = tuple(obj.matrix_world @ vert.co) # Exact position as the key
position_dict[pos_key].append(vert)
# Average and update weights for each group of vertices at the same position
for verts in position_dict.values():
if len(verts) > 1: # Only average if there are multiple vertices at the same position
modified_pairs += 1
weight_dict = defaultdict(dict)
for vert in verts:
for group in vert.groups:
weight_dict[vert.index][group.group] = group.weight
# Calculate the averaged weights
averaged_weights = average_weights(weight_dict)
# Apply the averaged weights to each vertex in the group
for vert in verts:
apply_weights_to_vertex(vert, averaged_weights, obj)
print(f"Completed processing for mesh: {obj.name} - Modified {modified_pairs} pairs of vertices.")
# Run the script on all selected mesh objects
for obj in bpy.context.selected_objects:
if obj.type == 'MESH' and obj.vertex_groups:
average_vertex_weights_for_mesh(obj)
print("Weight averaging complete for all selected meshes.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment