Skip to content
Snippets Groups Projects
vector_clothing_replicator.py 6.08 KiB
Newer Older
  • Learn to ignore specific revisions
  • Pregmodder's avatar
    Pregmodder committed
    #!/usr/bin/env python3
    
    '''
    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
    '''
    
    from svg.path import parse_path
    import copy
    import lxml.etree as etree
    import sys
    
    REFERENCE_PATH_SAMPLES = 200
    
    klorpa's avatar
    klorpa committed
    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"
    else:
      raise RuntimeError("Please specify a bodypart for clothing to replicate.")
    
    tree = etree.parse(input_file)
    ns = {'svg' : 'http://www.w3.org/2000/svg'}
    
    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)
    
    def get_points(xpath_shape):
      '''
    
    klorpa's avatar
    klorpa committed
      This function extracts reference paths by the given xpath selector.
    
    Pregmodder's avatar
    Pregmodder committed
      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:
    
    klorpa's avatar
    klorpa committed
    	p = parse_path(path_data)
    	points += [
    	  p.point(1.0/float(REFERENCE_PATH_SAMPLES)*i)
    	  for i in range(REFERENCE_PATH_SAMPLES)
    	]
    
    Pregmodder's avatar
    Pregmodder committed
      if (not points):
    
    klorpa's avatar
    klorpa committed
    	raise RuntimeError(
    	  'No paths for reference points found by selector "%s".'%(xpath_shape)
    	)
    
    Pregmodder's avatar
    Pregmodder committed
      return points
    
    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))
    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))
    outfit_source = outfit_source[0]
    
    for target_id in target_ids:
      print(
    
    klorpa's avatar
    klorpa committed
    	'Generating variant "%s" of clothing "%s" for bodypart "%s"...'%
    	(target_id, clothing, bodypart)
    
    Pregmodder's avatar
    Pregmodder committed
      )
      outfit = copy.deepcopy(outfit_source)
      paths = outfit.xpath('./svg:path',namespaces=ns)
      if target_id == reference_id:
    
    klorpa's avatar
    klorpa committed
    	print("This is the source variant. Skipping...")
    
    Pregmodder's avatar
    Pregmodder committed
      else:
    
    klorpa's avatar
    klorpa committed
    	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)
    
    Pregmodder's avatar
    Pregmodder committed
      if not EMBED_REPLICATIONS:
    
    klorpa's avatar
    klorpa committed
    	container = copy.deepcopy(canvas).xpath('.',namespaces=ns)[0]
    	container.append(outfit)
    
    Pregmodder's avatar
    Pregmodder committed
    
      if not EMBED_REPLICATIONS:
    
    klorpa's avatar
    klorpa committed
    	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:
    
    klorpa's avatar
    klorpa committed
    	f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
    	f.write(svg)