Hair Card to line conversion script

Needed to modify hair cards and wanted to remesh them with the fancy geometry node hair tools now available. Problem was that I needed to convert the hair card meshes into lines that follow the flow of the cards. With a bit of trial and error and a lot oh help from our old friend Chargpt, got this script working. It uses shortest edges to determine which lines should be intersected, idk if that’s gonna work for everyone, but it works well enouph for my models. Enjoy, and I hope this helps someone!

import bpy
import bmesh

def find_shortest_edges(face):
    """
    Identify the shortest pair of opposite edges in a quad face.
    """
    if len(face.edges) != 4:
        return None

    edges = list(face.edges)
    edge_lengths = [(edge, (edge.verts[0].co - edge.verts[1].co).length) for edge in edges]
    edge_lengths.sort(key=lambda x: x[1])  # Sort by length, ascending order

    # Shortest two edges are most likely opposing
    edge_1, edge_2 = edge_lengths[0][0], edge_lengths[1][0]
    return edge_1, edge_2

def bisect_and_connect_lines(midpoint_a, midpoint_b):
    """
    Bisect the center line and return the midpoint of the bisected line.
    """
    bisect_point = (midpoint_a + midpoint_b) / 2.0
    return bisect_point

def clean_mesh(bm, merge_distance=0.0001):
    """
    Clean the mesh by merging vertices by distance and removing vertices
    connected to one or fewer edges, including isolated vertex pairs.
    """
    # Merge vertices by distance
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=merge_distance)
    
    # Delete vertices connected to one or fewer edges
    for vert in bm.verts:
        if len(vert.link_edges) <= 1:  # If a vertex has one or fewer edges
            bm.verts.remove(vert)
    
    # Check for bi-connected vertices (vertices connected exactly to two other vertices)
    for vert in bm.verts:
        if len(vert.link_edges) == 2:  # Exactly two connections
            edge1, edge2 = vert.link_edges
            # If both edges connect only back to the original vertex, we can remove it
            other_vert_1 = edge1.other_vert(vert)
            other_vert_2 = edge2.other_vert(vert)
            if len(other_vert_1.link_edges) == 1 and len(other_vert_2.link_edges) == 1:
                bm.verts.remove(vert)
                break  # Only remove the first found bi-connected vertex

def convert_hair_cards_to_center_lines(obj):
    if not obj or obj.type != 'MESH':
        print(f"Skipping {obj.name}: Not a mesh object.")
        return

    print(f"Operating on: {obj.name}")

    # Access mesh data and create bmesh
    mesh = obj.data
    bm = bmesh.new()
    bm.from_mesh(mesh)

    center_lines = []

    # Process each quad face
    for face in bm.faces:
        if len(face.edges) != 4:
            continue  # Skip non-quad faces

        # Find the shortest pair of opposite edges
        shortest_edges = find_shortest_edges(face)
        if not shortest_edges:
            continue

        edge_1, edge_2 = shortest_edges

        # Calculate midpoints for the shortest edges
        midpoint_a = (edge_1.verts[0].co + edge_1.verts[1].co) / 2.0
        midpoint_b = (edge_2.verts[0].co + edge_2.verts[1].co) / 2.0

        # Bisect the center line
        bisected_point = bisect_and_connect_lines(midpoint_a, midpoint_b)

        # Create a centerline segment with the bisected point
        center_lines.append((midpoint_a, bisected_point))
        center_lines.append((midpoint_b, bisected_point))

    # Create a new object for the center lines
    line_mesh = bpy.data.meshes.new(f"{obj.name}_HairCardCenterLines")
    line_obj = bpy.data.objects.new(f"{obj.name}_HairCardCenterLines", line_mesh)
    bpy.context.collection.objects.link(line_obj)

    # Generate the new mesh data
    vertices = [point for line in center_lines for point in line]
    edges = [(i * 2, i * 2 + 1) for i in range(len(center_lines))]
    line_mesh.from_pydata(vertices, edges, [])
    line_mesh.update()

    # Now clean up the mesh
    bm = bmesh.new()
    bm.from_mesh(line_mesh)
    clean_mesh(bm)
    bm.to_mesh(line_mesh)
    bm.free()

    print(f"Converted hair cards to center lines and cleaned up for {obj.name}.")

# Iterate over all selected objects
for obj in bpy.context.selected_objects:
    convert_hair_cards_to_center_lines(obj)

1 Like