Transfer Shape Key
authorCampbell Barton <ideasman42@gmail.com>
Sun, 27 Dec 2009 11:14:06 +0000 (11:14 +0000)
committerCampbell Barton <ideasman42@gmail.com>
Sun, 27 Dec 2009 11:14:06 +0000 (11:14 +0000)
Useful if you have 2 different characters with the same base mesh (matching indicies), and want to copy a facial expression for eg, from one to another.
Durian request to re-use shapes between characters.

* Copies the active shape to other selected objects
* Different methods to apply the shape
* * OFFSET, simple translation offset
* * RELATIVE (EDGE/FACE), Use Barycentric transformation to copy the shape. This means the target mesh can be a different orientation and scale and the shape should still apply since the surrounding geometry is used as a basis for the offset.

bug: barycentric transform's depth was inverted.

Note:
* This isnt added into a menu yet,
* This cant be redone since adding a shape key messes up the redo stack. needs fixing for other scripts too.

release/scripts/modules/rna_info.py
release/scripts/op/object.py
source/blender/blenlib/intern/math_geom.c

index 0a1b02d57597758f41b53dc7966213214ad986a7..86da0b5694397b5ac2354d43c4ae0358b9bbc3a0 100644 (file)
@@ -232,7 +232,7 @@ class InfoPropertyRNA:
 
         if self.is_never_none:
             type_str += ", (never None)"
-        
+
         return type_str
 
     def __repr__(self):
index 08fac5d4d3ca74b283134ce9a280a746756dde00..c510981ebc253c5532ccfd722fc2a9736945ca53 100644 (file)
@@ -149,6 +149,170 @@ class Retopo(bpy.types.Operator):
         return {'FINISHED'}
 
 
+class ShapeTransfer(bpy.types.Operator):
+    '''Copy the active objects current shape to other selected objects with the same number of verts'''
+
+    bl_idname = "object.shape_key_transfer"
+    bl_label = "Transfer Shape Key"
+    bl_register = True
+    bl_undo = True
+
+    mode = EnumProperty(items=(
+                        ('OFFSET', "Offset", "Apply the relative positional offset"),
+                        ('RELATIVE_FACE', "Relative Face", "Calculate the geometricly relative position (using faces)."),
+                        ('RELATIVE_EDGE', "Relative Edge", "Calculate the geometricly relative position (using edges).")),
+                name="Transformation Mode",
+                description="Method to apply relative shape positions to the new shape",
+                default='OFFSET')
+
+    use_clamp = BoolProperty(name="Clamp Offset",
+                description="Clamp the transformation to the distance each vertex moves in the original shape.",
+                default=False)
+
+    def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False):
+
+        def me_nos(verts):
+            return [v.normal.copy() for v in verts]
+
+        def me_cos(verts):
+            return [v.co.copy() for v in verts]
+
+        def ob_add_shape(ob):
+            C_tmp = {"object": ob}
+            me = ob.data
+            if me.shape_keys is None: # add basis
+                bpy.ops.object.shape_key_add(C_tmp)
+            bpy.ops.object.shape_key_add(C_tmp)
+            ob.active_shape_key_index = len(me.shape_keys.keys) - 1
+            ob.shape_key_lock = True
+
+        from Geometry import BarycentricTransform
+        from Mathutils import Vector
+
+        if use_clamp and mode == 'OFFSET':
+            use_clamp = False
+
+        me = ob_act.data
+
+        orig_shape_coords = me_cos(ob_act.active_shape_key.data)
+
+        orig_normals = me_nos(me.verts)
+        orig_coords = me_cos(me.verts)
+
+        for ob_other in objects:
+            me_other = ob_other.data
+            if len(me_other.verts) != len(me.verts):
+                self.report({'WARNING'}, "Skipping '%s', vertex count differs" % ob_other.name)
+                continue
+
+            target_normals = me_nos(me_other.verts)
+            target_coords = me_cos(me_other.verts)
+
+            ob_add_shape(ob_other)
+
+            # editing the final coords, only list that stores wrapped coords
+            target_shape_coords = [v.co for v in ob_other.active_shape_key.data]
+
+            median_coords = [[] for i in range(len(me.verts))]
+
+            # Method 1, edge
+            if mode == 'OFFSET':
+                for i, vert_cos in enumerate(median_coords):
+                    vert_cos.append(target_coords[i] + (orig_shape_coords[i] - orig_coords[i]))
+
+            elif mode == 'RELATIVE_FACE':
+                for face in me.faces:
+                    i1, i2, i3, i4 = face.verts_raw
+                    if i4 != 0:
+                        pt = BarycentricTransform(orig_shape_coords[i1],
+                            orig_coords[i4], orig_coords[i1], orig_coords[i2],
+                            target_coords[i4], target_coords[i1], target_coords[i2])
+                        median_coords[i1].append(pt)
+
+                        pt = BarycentricTransform(orig_shape_coords[i2],
+                            orig_coords[i1], orig_coords[i2], orig_coords[i3],
+                            target_coords[i1], target_coords[i2], target_coords[i3])
+                        median_coords[i2].append(pt)
+
+                        pt = BarycentricTransform(orig_shape_coords[i3],
+                            orig_coords[i2], orig_coords[i3], orig_coords[i4],
+                            target_coords[i2], target_coords[i3], target_coords[i4])
+                        median_coords[i3].append(pt)
+
+                        pt = BarycentricTransform(orig_shape_coords[i4],
+                            orig_coords[i3], orig_coords[i4], orig_coords[i1],
+                            target_coords[i3], target_coords[i4], target_coords[i1])
+                        median_coords[i4].append(pt)
+
+                    else:
+                        pt = BarycentricTransform(orig_shape_coords[i1],
+                            orig_coords[i3], orig_coords[i1], orig_coords[i2],
+                            target_coords[i3], target_coords[i1], target_coords[i2])
+                        median_coords[i1].append(pt)
+
+                        pt = BarycentricTransform(orig_shape_coords[i2],
+                            orig_coords[i1], orig_coords[i2], orig_coords[i3],
+                            target_coords[i1], target_coords[i2], target_coords[i3])
+                        median_coords[i2].append(pt)
+
+                        pt = BarycentricTransform(orig_shape_coords[i3],
+                            orig_coords[i2], orig_coords[i3], orig_coords[i1],
+                            target_coords[i2], target_coords[i3], target_coords[i1])
+                        median_coords[i3].append(pt)
+
+            elif mode == 'RELATIVE_EDGE':
+                for ed in me.edges:
+                    i1, i2 = ed.verts
+                    v1, v2 = orig_coords[i1], orig_coords[i2]
+                    edge_length = (v1 - v2).length
+                    n1loc = v1 + orig_normals[i1] * edge_length
+                    n2loc = v2 + orig_normals[i2] * edge_length
+
+
+                    # now get the target nloc's
+                    v1_to, v2_to = target_coords[i1], target_coords[i2]
+                    edlen_to = (v1_to - v2_to).length
+                    n1loc_to = v1_to + target_normals[i1] * edlen_to
+                    n2loc_to = v2_to + target_normals[i2] * edlen_to
+
+                    pt = BarycentricTransform(orig_shape_coords[i1],
+                        v2, v1, n1loc,
+                        v2_to, v1_to, n1loc_to)
+                    median_coords[i1].append(pt)
+
+                    pt = BarycentricTransform(orig_shape_coords[i2],
+                        v1, v2, n2loc,
+                        v1_to, v2_to, n2loc_to)
+                    median_coords[i2].append(pt)
+
+            # apply the offsets to the new shape
+            from functools import reduce
+            VectorAdd = Vector.__add__
+
+            for i, vert_cos in enumerate(median_coords):
+                if vert_cos:
+                    co = reduce(VectorAdd, vert_cos) / len(vert_cos)
+
+                    if use_clamp:
+                        # clamp to the same movement as the original
+                        # breaks copy between different scaled meshes.
+                        len_from = (orig_shape_coords[i] - orig_coords[i]).length
+                        ofs = co - target_coords[i]
+                        ofs.length = len_from
+                        co = target_coords[i] + ofs
+
+                    target_shape_coords[i][:] = co
+
+        return {'FINISHED'}
+
+    def execute(self, context):
+        C = bpy.context
+        ob_act = C.active_object
+        objects = [ob for ob in C.selected_editable_objects if ob != ob_act]
+        return self._main(ob_act, objects, self.properties.mode, self.properties.use_clamp)
+
+
 bpy.types.register(SelectPattern)
 bpy.types.register(SubdivisionSet)
 bpy.types.register(Retopo)
+bpy.types.register(ShapeTransfer)
index dd773cb00c17b187d303edf8c75f38bfd64ce96c..470e5d71a4503855b39e999ff69953277371cb87 100644 (file)
@@ -1404,7 +1404,7 @@ void barycentric_transform(float pt_tar[3], float const pt_src[3],
        area_tar= sqrtf(area_tri_v3(tri_tar_p1, tri_tar_p2, tri_tar_p3));
        area_src= sqrtf(area_tri_v2(tri_xy_src[0], tri_xy_src[1], tri_xy_src[2]));
 
-       z_ofs_src= tri_xy_src[0][2] - pt_src_xy[2];
+       z_ofs_src= pt_src_xy[2] - tri_xy_src[0][2];
        madd_v3_v3v3fl(pt_tar, pt_tar, no_tar, (z_ofs_src / area_src) * area_tar);
 }