Skip to content
Snippets Groups Projects
vector_clothing_replicator.py 6.77 KiB
Newer Older
  • Learn to ignore specific revisions
  • Pregmodder's avatar
    Pregmodder committed
    #!/usr/bin/env python3
    
    
    Pregmodder's avatar
    Pregmodder committed
    Application for procedural content adaption
    *THIS IS VERY EXPERIMENTAL*
    Contains a very poor man's implementation of spline mesh warping.
    
    This application parses SVG path data of an outfit sample aligned to a body part.
    The outfit is then replicated for other shapes of the same body part.
    
    Example data is geared towards generating a strap outfit for boobs and torso
    for all sizes of boobs and all shapes of torsos based on a single outfit for
    boobs of size 2 and a hourglass torso respectively.
    
    Limitations:
    * handles paths only
    * only svg.path Line and CubicBezier are tested
    * only the aforementioned examples are tested
    
    Usage:
    python3 vector_clothing_replicator.py infile clothing bodypart destinationfile
    
    Usage Example:
    python3 vector_clothing_replicator.py vector_source.svg Straps Boob vector_destination.svg
    python3 vector_clothing_replicator.py vector_source.svg Straps Torso vector_destination.svg
    
    Pregmodder's avatar
    Pregmodder committed
    
    import copy
    import sys
    
    
    import lxml.etree as etree
    from svg.path import parse_path
    
    
    Pregmodder's avatar
    Pregmodder committed
    REFERENCE_PATH_SAMPLES = 200
    
    EMBED_REPLICATIONS = True  # whether to embed all replications into the input file or output separate files
    
    Pregmodder's avatar
    Pregmodder committed
    
    input_file = sys.argv[1]
    clothing = sys.argv[2]
    bodypart = sys.argv[3]
    output_file_embed = sys.argv[4]
    
    # TODO: make these configurable
    
    output_file_pattern = '%s_%s_%s.svg'  # bodypart, target_id, clothing
    
    if 'Torso' == bodypart:
        xpath_shape = './svg:g[@id="Torso_"]/svg:g[@id="Torso_%s"]/svg:path[@class="skin torso"]/@d'  # TODO: formulate more general, independent of style
        xpath_outfit_container = '//svg:g[@id="Torso_Outfit_%s_"]' % (clothing)
        xpath_outfit = '//svg:g[@id="Torso_Outfit_%s_%s"]' % (clothing, '%s')
        target_ids = "Unnatural,Hourglass,Normal".split(",")
        reference_id = "Hourglass"
    
    Pregmodder's avatar
    Pregmodder committed
    else:
    
        raise RuntimeError("Please specify a bodypart for clothing to replicate.")
    
    Pregmodder's avatar
    Pregmodder committed
    
    tree = etree.parse(input_file)
    
    ns = {'svg': 'http://www.w3.org/2000/svg'}
    
    Pregmodder's avatar
    Pregmodder committed
    
    canvas = copy.deepcopy(tree)
    
    for e in canvas.xpath('./svg:g', namespaces=ns) + canvas.xpath('./svg:path', namespaces=ns):
        # TODO: this should be "remove all objects, preserve document properties"
        e.getparent().remove(e)
    
    
    Pregmodder's avatar
    Pregmodder committed
    
    def get_points(xpath_shape):
    
        """
        This function extracts reference paths by the given xpath selector.
        Each path is used to sample a fixed number of points.
        """
        paths_data = tree.xpath(xpath_shape, namespaces=ns)
        points = []
        path_length = None
        for path_data in paths_data:
            p = parse_path(path_data)
            points += [
                p.point(1.0 / float(REFERENCE_PATH_SAMPLES) * i)
                for i in range(REFERENCE_PATH_SAMPLES)
            ]
        if not points:
            raise RuntimeError(
                'No paths for reference points found by selector "%s".' % xpath_shape
            )
        return points
    
    
    Pregmodder's avatar
    Pregmodder committed
    
    def point_movement(point, reference_points, target_points):
    
        """
        For a given point, finds the nearest point in the reference path.
        Gives distance vector from the nearest reference point to the
        respective target reference point.
        """
        distances = [abs(point - reference_point) for reference_point in reference_points]
        min_ref_dist_idx = min(enumerate(distances), key=lambda x: x[1])[0]
        movement = target_points[min_ref_dist_idx] - reference_points[min_ref_dist_idx]
        return movement
    
    
    reference_points = get_points(xpath_shape % reference_id)
    container = tree.xpath(xpath_outfit_container, namespaces=ns)
    if len(container) != 1:
        raise RuntimeError('Outfit container selector "%s" does not yield exactly one layer.' % xpath_outfit_container)
    
    Pregmodder's avatar
    Pregmodder committed
    container = container[0]
    
    outfit_source = container.xpath(xpath_outfit % reference_id, namespaces=ns)
    if len(outfit_source) != 1:
        raise RuntimeError(
            'Outfit source selector "%s" does not yield exactly one outfit layer in container selected by "%s".' % (
                xpath_outfit % reference_id, xpath_outfit_container))
    
    Pregmodder's avatar
    Pregmodder committed
    outfit_source = outfit_source[0]
    
    for target_id in target_ids:
    
        print(
            'Generating variant "%s" of clothing "%s" for bodypart "%s"...' %
            (target_id, clothing, bodypart)
        )
        outfit = copy.deepcopy(outfit_source)
        paths = outfit.xpath('./svg:path', namespaces=ns)
        if target_id == reference_id:
            print("This is the source variant. Skipping...")
        else:
            layerid = outfit.get('id').replace('_%s' % reference_id, '_%s' % target_id)
            outfit.set('id', layerid)
            outfit.set(etree.QName('http://www.inkscape.org/namespaces/inkscape', 'label'),
                       layerid)  # for the Inkscape-users
            target_points = get_points(xpath_shape % target_id)
            if len(reference_points) != len(target_points):
                raise RuntimeError(
                    ('Different amounts of sampled points in reference "%s" and target "%s" paths. ' +
                     'Selector "%s" probably matches different number of paths in the two layers.') %
                    (reference_id, target_id, xpath_shape)
                )
            for path in paths:
                path_data = path.get("d")
                p = parse_path(path_data)
                for segment in p:
                    original_distance = abs(segment.end - segment.start)
                    start_movement = point_movement(segment.start, reference_points, target_points)
                    segment.start += start_movement
                    end_movement = point_movement(segment.end, reference_points, target_points)
                    segment.end += end_movement
                    distance = abs(segment.end - segment.start)
                    try:
                        # enhance position of CubicBezier control points
                        # amplification is relative to the distance gained by movement
                        segment.control1 += start_movement
                        segment.control1 += (segment.control1 - segment.start) * (distance / original_distance - 1.0)
                        segment.control2 += end_movement
                        segment.control2 += (segment.control2 - segment.end) * (distance / original_distance - 1.0)
                    except AttributeError as ae:
                        # segment is not a CubicBezier
                        pass
                path.set("d", p.d())
            if EMBED_REPLICATIONS:
                container.append(outfit)
        if not EMBED_REPLICATIONS:
            container = copy.deepcopy(canvas).xpath('.', namespaces=ns)[0]
            container.append(outfit)
    
        if not EMBED_REPLICATIONS:
            svg = etree.tostring(container, pretty_print=True)
            with open((output_file_pattern % (bodypart, target_id, clothing)).lower(), 'wb') as f:
                f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
                f.write(svg)
    
    Pregmodder's avatar
    Pregmodder committed
    
    if EMBED_REPLICATIONS:
    
        svg = etree.tostring(tree, pretty_print=True)
        with open(output_file_embed, 'wb') as f:
            f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
            f.write(svg)