# This is ancient code from Project Heist a.k.a. Charge. Maybe still useful somehow, but can probably be removed any time. from typing import Any, Dict, List, Set, Union, Optional import bpy import mathutils import bmesh import numpy as np from asset_pipeline.api import ( AssetTransferMapping, TaskLayer, BuildContext, ) class TransferSettings(bpy.types.PropertyGroup): pass # imp_mat: bpy.props.BoolProperty(name="Materials", default=True) # type: ignore # imp_uv: bpy.props.BoolProperty(name="UVs", default=True) # type: ignore # imp_vcol: bpy.props.BoolProperty(name="Vertex Colors", default=True) # type: ignore # transfer_type: bpy.props.EnumProperty( # type: ignore # name="Transfer Type", # items=[("VERTEX_ORDER", "Vertex Order", ""), ("PROXIMITY", "Proximity", "")], # ) class RiggingTaskLayer(TaskLayer): name = "Rigging" order = 0 @classmethod def transfer_data( cls, context: bpy.types.Context, build_context: BuildContext, transfer_mapping: AssetTransferMapping, transfer_settings: bpy.types.PropertyGroup, ) -> None: print(f"\n\033[1mProcessing data from {cls.__name__}...\033[0m") transfer_mapping.generate_mapping() # add prefixes to existing modifiers for obj_source, obj_target in transfer_mapping.object_map.items(): prefix_modifiers(obj_target, 0) class ModelingTaskLayer(TaskLayer): name = "Modeling" order = 1 ''' Only affects objects of the target inside collections ending with '.geometry'. New objects can be created anywhere. New modifiers are automatically prefixed with 'GEO-'. Any modifier that is given the prefix 'APL-' will be automatically applied after push. The order of the modifier stack is generally owned by the rigging task layer. Newly created modifiers in the modeling task layer are an exception. ''' @classmethod def transfer_data( cls, context: bpy.types.Context, build_context: BuildContext, transfer_mapping: AssetTransferMapping, transfer_settings: bpy.types.PropertyGroup, ) -> None: print(f"\n\033[1mProcessing data from {cls.__name__}...\033[0m") transfer_mapping.generate_mapping() # identify geometry collections in source and target geometry_colls_source = [] for coll in transfer_mapping.collection_map.keys(): if coll.name.split('.')[-2] == 'geometry': geometry_colls_source += [coll] geometry_objs_source = { ob for coll in geometry_colls_source for ob in list(coll.all_objects) } geometry_colls_target = [] for coll in transfer_mapping.collection_map.keys(): if coll.name.split('.')[-2] == 'geometry': geometry_colls_target += [transfer_mapping.collection_map[coll]] geometry_objs_target = { ob for coll in geometry_colls_target for ob in list(coll.all_objects) } # handle new objects for ob in transfer_mapping.no_match_source_objs: # link new object to target parent collection for coll_source in transfer_mapping.collection_map.keys(): if ob in set(coll_source.objects): transfer_mapping.collection_map[coll_source].objects.link(ob) # (replace object dependencies) pass # handle removed objects for ob in transfer_mapping.no_match_target_objs: # delete objects inside the target .geometry collections if ob in geometry_objs_target: print(info_text(f"DELETING {ob.name}")) bpy.data.objects.remove(ob) # transfer data between object geometries for obj_source, obj_target in transfer_mapping.object_map.items(): if obj_source not in geometry_objs_source: continue # transfer object transformation (world space) con_vis = [] for con in obj_target.constraints: con_vis += [con.enabled] con.enabled = False for con in obj_source.constraints: con.enabled = False obj_target.matrix_world = obj_source.matrix_world for con, vis in zip(obj_target.constraints, con_vis): con.enabled = vis # TODO: support object type change if obj_source.type != obj_target.type: print( warning_text( f"Mismatching object type. Skipping {obj_target.name}." ) ) continue # check for topology match (vertex, edge, loop count) (mesh, curve separately) topo_match = match_topology(obj_source, obj_target) if ( topo_match is None ): # TODO: support geometry types other than mesh or curve continue # if topology matches: transfer position attribute (keeping shapekeys intact) if topo_match: if obj_target.type == 'MESH': if len(obj_target.data.vertices) == 0: print( warning_text( f"Mesh object '{obj_target.name}' has empty object data" ) ) continue offset = [ obj_source.data.vertices[i].co - obj_target.data.vertices[i].co for i in range(len(obj_source.data.vertices)) ] offset_sum = 0 for x in offset: offset_sum += x.length offset_avg = offset_sum / len(offset) if offset_avg > 0.1: print( warning_text( f"Average Vertex offset is {offset_avg} for {obj_target.name}" ) ) for i, vec in enumerate(offset): obj_target.data.vertices[i].co += vec # update shapekeys if obj_target.data.shape_keys: for key in obj_target.data.shape_keys.key_blocks: for i, point in enumerate([dat.co for dat in key.data]): key.data[i].co = point + offset[i] elif ( obj_target.type == 'CURVE' ): # TODO: proper geometry transfer for curves obj_target.data = obj_source.data else: pass # if topology does not match replace geometry (throw warning) -> TODO: handle data transfer onto mesh for simple cases (trivial topological changes: e.g. added separate mesh island, added span) else: # replace the object data and do proximity transfer of all rigging data # generate new transfer source object from mesh data obj_target_original = bpy.data.objects.new( f"{obj_target.name}.original", obj_target.data ) if obj_target.data.shape_keys: sk_original = obj_target.data.shape_keys.copy() else: sk_original = None context.scene.collection.objects.link(obj_target_original) print( warning_text( f"Topology Mismatch! Replacing object data and transferring with potential data loss on '{obj_target.name}'" ) ) obj_target.data = obj_source.data # transfer weights bpy.ops.object.data_transfer( { "object": obj_target_original, "active_object": obj_target_original, "selected_editable_objects": [obj_target], }, data_type="VGROUP_WEIGHTS", use_create=True, vert_mapping='POLYINTERP_NEAREST', layers_select_src="ALL", layers_select_dst="NAME", mix_mode="REPLACE", ) # transfer shapekeys transfer_shapekeys_proximity(obj_target_original, obj_target) # transfer drivers copy_drivers(sk_original, obj_target.data.shape_keys) del sk_original bpy.data.objects.remove(obj_target_original) # sync modifier stack (those without prefix on the source are added and prefixed, those with matching/other prefix are synced/ignored based on their prefix) prefix_modifiers(obj_source, 1) remove_removed_modifiers(obj_source, obj_target) transfer_new_modifiers(obj_source, obj_target) sync_modifier_settings(obj_source, obj_target, transfer_mapping) rebind_modifiers(obj_target) # restore multiusers if not ( build_context.is_push or type(cls) in build_context.asset_context.task_layer_assembly.get_used_task_layers() ): meshes_dict = dict() for ob in transfer_mapping.object_map.keys(): if not ob.data: continue if ob.type not in ['MESH', 'CURVE']: continue if ob.data not in meshes_dict.keys(): meshes_dict[ob.data] = [ob] else: meshes_dict[ob.data] += [ob] for mesh, objects in meshes_dict.items(): main_mesh = transfer_mapping.object_map[objects[0]].data for ob in objects: transfer_mapping.object_map[ob].data = main_mesh def remove_removed_modifiers(obj_source, obj_target): for mod in obj_target.modifiers: if mod.name.split('-')[0] not in ['GEO', 'APL']: continue if mod.name not in [m.name for m in obj_source.modifiers]: print(info_text(f"Removing modifier {mod.name}")) obj_target.modifiers.remove(mod) def transfer_new_modifiers(obj_source, obj_target): for i, mod in enumerate(obj_source.modifiers): if mod.name.split('-')[0] not in ['GEO', 'APL']: continue if mod.name in [m.name for m in obj_target.modifiers]: continue mod_new = obj_target.modifiers.new(mod.name, mod.type) # sort new modifier at correct index (default to beginning of the stack) idx = 0 if i > 0: name_prev = obj_source.modifiers[i - 1].name for target_mod_i, target_mod in enumerate(obj_target.modifiers): if target_mod.name == name_prev: idx = target_mod_i + 1 bpy.ops.object.modifier_move_to_index( {'object': obj_target}, modifier=mod_new.name, index=idx ) def sync_modifier_settings(obj_source, obj_target, transfer_mapping): for i, mod_source in enumerate(obj_source.modifiers): mod_target = obj_target.modifiers.get(mod_source.name) if not mod_target: continue if mod_source.name.split('-')[0] not in ['GEO', 'APL']: continue for prop in [ p.identifier for p in mod_source.bl_rna.properties if not p.is_readonly ]: value = getattr(mod_source, prop) if ( type(value) == bpy.types.Object and value in transfer_mapping.object_map ): # If a modifier is referencing a .TASK object, # remap that reference to a .TARGET object. # (Eg. modeling Mirror modifier with a mirror object) value = transfer_mapping.object_map[value] setattr(mod_target, prop, value) def rebind_modifiers(obj): op_map = { 'SURFACE_DEFORM': bpy.ops.object.surfacedeform_bind, 'MESH_DEFORM': bpy.ops.object.meshdeform_bind, 'CORRECTIVE_SMOOTH': bpy.ops.object.correctivesmooth_bind, } for mod in obj.modifiers: op_func = op_map.get(mod.type) if not op_func or not mod.is_bound: continue for i in range(2): op_func( {"object": obj, "active_object": obj}, modifier=mod.name, ) def prefix_modifiers(obj: bpy.types.Object, idx: int, delimiter='-') -> None: prefixes = ['RIG', 'GEO', 'APL'] for mod in obj.modifiers: if not mod.name.split(delimiter)[0] in prefixes: mod.name = f'{prefixes[idx]}{delimiter}{mod.name}' # Not allowed: 2 TaskLayer Classes with the same ClassName (Note: note 'name' attribute) class ShadingTaskLayer(TaskLayer): name = "Shading" order = 3 @classmethod def transfer_data( cls, context: bpy.types.Context, build_context: BuildContext, transfer_mapping: AssetTransferMapping, transfer_settings: bpy.types.PropertyGroup, ) -> None: print(f"\n\033[1mProcessing data from {cls.__name__}...\033[0m") settings = transfer_settings depsgraph = context.evaluated_depsgraph_get() transfer_mapping.generate_mapping() for obj_source, obj_target in transfer_mapping.object_map.items(): if not obj_target.type in ["MESH", "CURVE"]: continue if obj_target.name.startswith("WGT-"): while obj_target.material_slots: obj_target.active_material_index = 0 bpy.ops.object.material_slot_remove({"object": obj_target}) continue # TODO: support object type change if obj_source.type != obj_target.type: print( warning_text( f"Mismatching object type. Skipping {obj_target.name}." ) ) continue # Transfer material slot assignments. # Delete all material slots of target object. while len(obj_target.material_slots) > len(obj_source.material_slots): obj_target.active_material_index = len(obj_source.material_slots) bpy.ops.object.material_slot_remove({"object": obj_target}) # Transfer material slots for idx in range(len(obj_source.material_slots)): if idx >= len(obj_target.material_slots): bpy.ops.object.material_slot_add({"object": obj_target}) obj_target.material_slots[idx].link = obj_source.material_slots[ idx ].link obj_target.material_slots[idx].material = obj_source.material_slots[ idx ].material # Transfer active material slot index obj_target.active_material_index = obj_source.active_material_index # Transfer material slot assignments for curve if obj_target.type == "CURVE": if len(obj_target.data.splines) == 0: print( warning_text( f"Curve object '{obj_target.name}' has empty object data" ) ) continue for spl_to, spl_from in zip( obj_target.data.splines, obj_source.data.splines ): spl_to.material_index = spl_from.material_index # Rest of the loop applies only to meshes. if obj_target.type != "MESH": continue if len(obj_target.data.vertices) == 0: print( warning_text( f"Mesh object '{obj_target.name}' has empty object data" ) ) continue topo_match = match_topology(obj_source, obj_target) if ( not topo_match ): # TODO: Support trivial topology changes in more solid way than proximity transfer print( warning_text( f"Mismatch in topology, falling back to proximity transfer. (Object '{obj_target.name}')" ) ) # generate new transfer source object from mesh data obj_source_original = bpy.data.objects.new( f"{obj_source.name}.original", obj_source.data ) context.scene.collection.objects.link(obj_source_original) # Transfer face data if topo_match: for pol_to, pol_from in zip( obj_target.data.polygons, obj_source.data.polygons ): pol_to.material_index = pol_from.material_index pol_to.use_smooth = pol_from.use_smooth else: obj_source_eval = obj_source.evaluated_get(depsgraph) for pol_target in obj_target.data.polygons: ( hit, loc, norm, face_index, ) = obj_source_eval.closest_point_on_mesh(pol_target.center) pol_source = obj_source_eval.data.polygons[face_index] pol_target.material_index = pol_source.material_index pol_target.use_smooth = pol_source.use_smooth # Transfer UV Seams if topo_match: for edge_from, edge_to in zip( obj_source.data.edges, obj_target.data.edges ): edge_to.use_seam = edge_from.use_seam else: bpy.ops.object.data_transfer( { "object": obj_source_original, "active_object": obj_source_original, "selected_editable_objects": [obj_target], }, data_type="SEAM", edge_mapping="NEAREST", mix_mode="REPLACE", ) # Transfer UV layers while len(obj_target.data.uv_layers) > 0: rem = obj_target.data.uv_layers[0] obj_target.data.uv_layers.remove(rem) if topo_match: for uv_from in obj_source.data.uv_layers: uv_to = obj_target.data.uv_layers.new( name=uv_from.name, do_init=False ) for loop in obj_target.data.loops: uv_to.data[loop.index].uv = uv_from.data[loop.index].uv else: for uv_from in obj_source.data.uv_layers: uv_to = obj_target.data.uv_layers.new( name=uv_from.name, do_init=False ) transfer_corner_data( obj_source, obj_target, uv_from.data, uv_to.data, data_suffix='uv', ) # Make sure correct layer is set to active for uv_l in obj_source.data.uv_layers: if uv_l.active_render: obj_target.data.uv_layers[uv_l.name].active_render = True break # Transfer Vertex Colors while len(obj_target.data.vertex_colors) > 0: rem = obj_target.data.vertex_colors[0] obj_target.data.vertex_colors.remove(rem) if topo_match: for vcol_from in obj_source.data.vertex_colors: vcol_to = obj_target.data.vertex_colors.new( name=vcol_from.name, do_init=False ) for loop in obj_target.data.loops: vcol_to.data[loop.index].color = vcol_from.data[ loop.index ].color else: for vcol_from in obj_source.data.vertex_colors: vcol_to = obj_target.data.vertex_colors.new( name=vcol_from.name, do_init=False ) transfer_corner_data( obj_source, obj_target, vcol_from.data, vcol_to.data, data_suffix='color', ) bpy.data.objects.remove(obj_source_original) ### Utilities def info_text(text: str) -> str: return f"\t\033[1mInfo\033[0m\t: " + text def warning_text(text: str) -> str: return f"\t\033[1m\033[93mWarning\033[0m\t: " + text def error_text(text: str) -> str: return f"\t\033[1m\033[91mError\033[0m\t: " + text def match_topology(a: bpy.types.Object, b: bpy.types.Object) -> bool: """Checks if two objects have matching topology (efficiency over exactness)""" if a.type != b.type: return False if a.type == 'MESH': if len(a.data.vertices) != len(b.data.vertices): return False if len(a.data.edges) != len(b.data.edges): return False if len(a.data.polygons) != len(b.data.polygons): return False for e1, e2 in zip(a.data.edges, b.data.edges): for v1, v2 in zip(e1.vertices, e2.vertices): if v1 != v2: return False return True elif a.type == 'CURVE': if len(a.data.splines) != len(b.data.splines): return False for spline1, spline2 in zip(a.data.splines, b.data.splines): if len(spline1.points) != len(spline2.points): return False return True return None def copy_parenting(source_ob: bpy.types.Object, target_ob: bpy.types.Object) -> None: """Copy parenting data from one object to another.""" target_ob.parent = source_ob.parent target_ob.parent_type = source_ob.parent_type target_ob.parent_bone = source_ob.parent_bone target_ob.matrix_parent_inverse = source_ob.matrix_parent_inverse.copy() def copy_attributes(a: Any, b: Any) -> None: keys = dir(a) for key in keys: if ( not key.startswith("_") and not key.startswith("error_") and key != "group" and key != "is_valid" and key != "rna_type" and key != "bl_rna" ): try: setattr(b, key, getattr(a, key)) except AttributeError: pass def copy_driver( source_fcurve: bpy.types.FCurve, target_obj: bpy.types.Object, data_path: Optional[str] = None, index: Optional[int] = None, ) -> bpy.types.FCurve: if not data_path: data_path = source_fcurve.data_path new_fc = None try: if index: new_fc = target_obj.driver_add(data_path, index) else: new_fc = target_obj.driver_add(data_path) except: print( warning_text( f"Couldn't copy driver {source_fcurve.data_path} to {target_obj.name}" ) ) return copy_attributes(source_fcurve, new_fc) copy_attributes(source_fcurve.driver, new_fc.driver) # Remove default modifiers, variables, etc. for m in new_fc.modifiers: new_fc.modifiers.remove(m) for v in new_fc.driver.variables: new_fc.driver.variables.remove(v) # Copy modifiers for m1 in source_fcurve.modifiers: m2 = new_fc.modifiers.new(type=m1.type) copy_attributes(m1, m2) # Copy variables for v1 in source_fcurve.driver.variables: v2 = new_fc.driver.variables.new() copy_attributes(v1, v2) for i in range(len(v1.targets)): copy_attributes(v1.targets[i], v2.targets[i]) return new_fc def copy_drivers(source_ob: bpy.types.Object, target_ob: bpy.types.Object) -> None: """Copy all drivers from one object to another.""" if not hasattr(source_ob, "animation_data") or not source_ob.animation_data: return for fc in source_ob.animation_data.drivers: copy_driver(fc, target_ob) def copy_rigging_object_data( source_ob: bpy.types.Object, target_ob: bpy.types.Object ) -> None: """Copy all object data that could be relevant to rigging.""" # TODO: Object constraints, if needed. copy_drivers(source_ob, target_ob) copy_parenting(source_ob, target_ob) # HACK: For some reason Armature constraints on grooming objects lose their target when updating? Very strange... for c in target_ob.constraints: if c.type == "ARMATURE": for t in c.targets: if t.target == None: t.target = target_ob.parent # mesh interpolation utilities def edge_data_split(edge, data_layer, data_suffix: str): for vert in edge.verts: vals = [] for loop in vert.link_loops: loops_edge_vert = set([loop for f in edge.link_faces for loop in f.loops]) if loop not in loops_edge_vert: continue dat = data_layer[loop.index] element = list(getattr(dat, data_suffix)) if not vals: vals.append(element) elif not vals[0] == element: vals.append(element) if len(vals) > 1: return True return False def closest_edge_on_face_to_line(face, p1, p2, skip_edges=None): '''Returns edge of a face which is closest to line.''' for edge in face.edges: if skip_edges: if edge in skip_edges: continue res = mathutils.geometry.intersect_line_line( p1, p2, *[edge.verts[i].co for i in range(2)] ) if not res: continue (p_traversal, p_edge) = res frac_1 = (edge.verts[1].co - edge.verts[0].co).dot( p_edge - edge.verts[0].co ) / (edge.verts[1].co - edge.verts[0].co).length ** 2.0 frac_2 = (p2 - p1).dot(p_traversal - p1) / (p2 - p1).length ** 2.0 if (frac_1 >= 0 and frac_1 <= 1) and (frac_2 >= 0 and frac_2 <= 1): return edge return None def interpolate_data_from_face( bm_source, tris_dict, face, p, data_layer_source, data_suffix='' ): '''Returns interpolated value of a data layer within a face closest to a point.''' (tri, point) = closest_tri_on_face(tris_dict, face, p) if not tri: return None weights = mathutils.interpolate.poly_3d_calc( [tri[i].vert.co for i in range(3)], point ) if not data_suffix: cols_weighted = [ weights[i] * np.array(data_layer_source[tri[i].index]) for i in range(3) ] col = sum(np.array(cols_weighted)) else: cols_weighted = [ weights[i] * np.array(getattr(data_layer_source[tri[i].index], data_suffix)) for i in range(3) ] col = sum(np.array(cols_weighted)) return col def closest_face_to_point(bm_source, p_target, bvh_tree=None): if not bvh_tree: bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source) (loc, norm, index, distance) = bvh_tree.find_nearest(p_target) return bm_source.faces[index] def tris_per_face(bm_source): tris_source = bm_source.calc_loop_triangles() tris_dict = dict() for face in bm_source.faces: tris_face = [] for i in range(len(tris_source))[::-1]: if tris_source[i][0] in face.loops: tris_face.append(tris_source.pop(i)) tris_dict[face] = tris_face return tris_dict def closest_tri_on_face(tris_dict, face, p): points = [] dist = [] tris = [] for tri in tris_dict[face]: point = mathutils.geometry.closest_point_on_tri( p, *[tri[i].vert.co for i in range(3)] ) tris.append(tri) points.append(point) dist.append((point - p).length) min_idx = np.argmin(np.array(dist)) point = points[min_idx] tri = tris[min_idx] return (tri, point) def transfer_corner_data( obj_source, obj_target, data_layer_source, data_layer_target, data_suffix='' ): ''' Transfers interpolated face corner data from data layer of a source object to data layer of a target object, while approximately preserving data seams (e.g. necessary for UV Maps). The transfer is face interpolated per target corner within the source face that is closest to the target corner point and does not have any data seams on the way back to the source face that is closest to the target face's center. ''' bm_source = bmesh.new() bm_source.from_mesh(obj_source.data) bm_source.faces.ensure_lookup_table() bm_target = bmesh.new() bm_target.from_mesh(obj_target.data) bm_target.faces.ensure_lookup_table() bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source) tris_dict = tris_per_face(bm_source) for face_target in bm_target.faces: face_target_center = face_target.calc_center_median() face_source = closest_face_to_point(bm_source, face_target_center, bvh_tree) for corner_target in face_target.loops: # find nearest face on target compared to face that loop belongs to p = corner_target.vert.co face_source_closest = closest_face_to_point(bm_source, p, bvh_tree) enclosed = face_source_closest is face_source face_source_int = face_source if not enclosed: # traverse faces between point and face center traversed_faces = set() traversed_edges = set() while face_source_int is not face_source_closest: traversed_faces.add(face_source_int) edge = closest_edge_on_face_to_line( face_source_int, face_target_center, p, skip_edges=traversed_edges, ) if edge == None: break if len(edge.link_faces) != 2: break traversed_edges.add(edge) split = edge_data_split(edge, data_layer_source, data_suffix) if split: break # set new source face to other face belonging to edge face_source_int = ( edge.link_faces[1] if edge.link_faces[1] is not face_source_int else edge.link_faces[0] ) # avoid looping behaviour if face_source_int in traversed_faces: face_source_int = face_source break # interpolate data from selected face col = interpolate_data_from_face( bm_source, tris_dict, face_source_int, p, data_layer_source, data_suffix ) if col is None: continue if not data_suffix: data_layer_target.data[corner_target.index] = col else: setattr(data_layer_target[corner_target.index], data_suffix, list(col)) return def transfer_shapekeys_proximity(obj_source, obj_target) -> None: ''' Transfers shapekeys from one object to another based on the mesh proximity with face interpolation. ''' # copy shapekey layout if not obj_source.data.shape_keys: return for sk_source in obj_source.data.shape_keys.key_blocks: if obj_target.data.shape_keys: sk_target = obj_target.data.shape_keys.key_blocks.get(sk_source.name) if sk_target: continue sk_target = obj_target.shape_key_add() sk_target.value = 0 sk_target.name = sk_source.name for sk_target in obj_target.data.shape_keys.key_blocks: sk_source = obj_source.data.shape_keys.key_blocks[sk_target.name] sk_target.vertex_group = sk_source.vertex_group sk_target.relative_key = obj_target.data.shape_keys.key_blocks[ sk_source.relative_key.name ] bm_source = bmesh.new() bm_source.from_mesh(obj_source.data) bm_source.faces.ensure_lookup_table() bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source) tris_dict = tris_per_face(bm_source) for i, vert in enumerate(obj_target.data.vertices): p = vert.co face = closest_face_to_point(bm_source, p, bvh_tree) (tri, point) = closest_tri_on_face(tris_dict, face, p) if not tri: continue weights = mathutils.interpolate.poly_3d_calc( [tri[i].vert.co for i in range(3)], point ) for sk_target in obj_target.data.shape_keys.key_blocks: sk_source = obj_source.data.shape_keys.key_blocks.get(sk_target.name) vals_weighted = [ weights[i] * ( sk_source.data[tri[i].vert.index].co - obj_source.data.vertices[tri[i].vert.index].co ) for i in range(3) ] val = mathutils.Vector(sum(np.array(vals_weighted))) sk_target.data[i].co = vert.co + val class GroomingTaskLayer(TaskLayer): name = "Grooming" order = 2 @classmethod def transfer_data( cls, context: bpy.types.Context, build_context: BuildContext, transfer_mapping: AssetTransferMapping, transfer_settings: bpy.types.PropertyGroup, ) -> None: print(f"\n\033[1mProcessing data from {cls.__name__}...\033[0m") coll_source = transfer_mapping.source_coll coll_target = transfer_mapping.target_coll for obj_source, obj_target in transfer_mapping.object_map.items(): if not "PARTICLE_SYSTEM" in [mod.type for mod in obj_source.modifiers]: continue l = [] for mod in obj_source.modifiers: if not mod.type == "PARTICLE_SYSTEM": l += [mod.show_viewport] mod.show_viewport = False bpy.ops.particle.copy_particle_systems( {"object": obj_source, "selected_editable_objects": [obj_target]} ) c = 0 for mod in obj_source.modifiers: if mod.type == "PARTICLE_SYSTEM": continue mod.show_viewport = l[c] c += 1 # TODO: handle cases where collections with exact naming convention cannot be found try: coll_from_hair = next( c for name, c in coll_source.children.items() if ".hair" in name ) coll_from_part = next( c for name, c in coll_from_hair.children.items() if ".hair.particles" in name ) coll_from_part_proxy = next( c for name, c in coll_from_part.children.items() if ".hair.particles.proxy" in name ) except: print( warning_text( f"Could not find existing particle hair collection. Make sure you are following the exact naming and structuring convention!" ) ) return # link 'from' hair.particles collection in 'to' try: coll_to_hair = next( c for name, c in coll_target.children.items() if ".hair" in name ) except: coll_target.children.link(coll_from_hair) return coll_to_hair.children.link(coll_from_part) try: coll_to_part = next( c for name, c in coll_to_hair.children.items() if ".hair.particles" in name ) except: print( warning_text( f"Failed to find particle hair collections in target collection" ) ) coll_to_part.user_clear() bpy.data.collections.remove(coll_to_part) return # transfer shading # transfer_dict = map_objects_by_name(coll_to_part, coll_from_part) # transfer_shading_data(context, transfer_dict) ShadingTaskLayer.transfer_data(context, transfer_mapping, transfer_settings) # transfer modifiers for obj_source, obj_target in transfer_mapping.object_map.items(): if not "PARTICLE_SYSTEM" in [m.type for m in obj_target.modifiers]: bpy.ops.object.make_links_data( {"object": obj_source, "selected_editable_objects": [obj_target]}, type="MODIFIERS", ) # We want to rig the hair base mesh with an Armature modifier, so transfer vertex groups by proximity. bpy.ops.object.data_transfer( {"object": obj_source, "selected_editable_objects": [obj_target]}, data_type="VGROUP_WEIGHTS", use_create=True, vert_mapping="NEAREST", layers_select_src="ALL", layers_select_dst="NAME", mix_mode="REPLACE", ) # We used to want to rig the auto-generated hair particle proxy meshes with Surface Deform, so re-bind those. # NOTE: Surface Deform probably won't be used for final rigging for mod in obj_target.modifiers: if mod.type == "SURFACE_DEFORM" and mod.is_bound: for i in range(2): bpy.ops.object.surfacedeform_bind( {"object": obj_target}, modifier=mod.name ) copy_rigging_object_data(obj_source, obj_target) # remove 'to' hair.particles collection coll_to_part.user_clear() bpy.data.collections.remove(coll_to_part) return