Skip to content

Commit

Permalink
add an ability to copy/paste/clear armature mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
igelbox committed Dec 17, 2017
1 parent 02cc540 commit 777b8b4
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 9 deletions.
6 changes: 4 additions & 2 deletions animation_retarget/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@


def register():
from . import core, ui
from . import core, ui, ops
core.register()
ops.register()
ui.register()


def unregister():
from . import core, ui
from . import core, ui, ops
ui.unregister()
ops.unregister()
core.unregister()
114 changes: 107 additions & 7 deletions animation_retarget/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,102 @@
from collections import OrderedDict
import configparser
from io import StringIO

import bpy
import mathutils


__CONFIG_PREFIX_BONE__ = 'bone:'


def mapping_to_text(target_obj):
def put_nonempty_value(data, name, value):
if value:
data[name] = value

def put_nonzero_tuple(data, name, value):
for val in value:
if val:
data[name] = value
break

def prepare_value(value):
value = round(value, 6)
ivalue = int(value)
return ivalue if ivalue == value else value

config = configparser.ConfigParser()
config['object'] = {
'source': target_obj.animation_retarget.source,
}

for bone in target_obj.pose.bones:
prop = bone.animation_retarget
data = OrderedDict()
for name in ('source', 'use_location', 'use_rotation'):
put_nonempty_value(data, name, getattr(prop, name))
for name in ('source_to_target_rest', 'delta_transform'):
value = tuple(map(prepare_value, getattr(prop, name)))
put_nonzero_tuple(data, name, value)
if data:
config[__CONFIG_PREFIX_BONE__ + bone.name] = data

buffer = StringIO()
config.write(buffer)
return buffer.getvalue()


def text_to_mapping(text, target_obj):
def parse_boolean(text):
return {
'True': True,
'False': False,
}[text]

def parse_tuple(text):
text = text.replace('(', '').replace(')', '')
return tuple(map(float, text.split(',')))

config = configparser.ConfigParser()
config.read_string(text)

source = config['object']['source']
target_obj.animation_retarget.source = source

for key, value in config.items():
if not key.startswith(__CONFIG_PREFIX_BONE__):
continue
name = key[len(__CONFIG_PREFIX_BONE__):]
target_bone = target_obj.pose.bones.get(name)
if not target_bone:
print('bone ' + name + ' is not found')
continue
prop = target_bone.animation_retarget
if 'source' in value:
prop.source = value['source']
for name in ('use_location', 'use_rotation'):
if name in value:
setattr(prop, name, parse_boolean(value[name]))
for name in ('source_to_target_rest', 'delta_transform'):
if name in value:
setattr(prop, name, parse_tuple(value[name]))
prop.invalidate_cache()
bpy.context.scene.update()


__ZERO_V16__ = (0,) * 16

def clear_mapping(target_obj):
target_obj.animation_retarget.source = ''
for bone in target_obj.pose.bones:
prop = bone.animation_retarget
prop.source = ''
prop.use_location = prop.use_rotation = False
prop.source_to_target_rest = prop.delta_transform = __ZERO_V16__
prop.invalidate_cache()
bpy.context.scene.update()


class RelativeObjectTransform(bpy.types.PropertyGroup):
b_type = bpy.types.Object

Expand All @@ -25,18 +121,19 @@ def _prop_to_pose_bone(obj, prop):
def _fvec16_to_matrix4(fvec):
return mathutils.Matrix((fvec[0:4], fvec[4:8], fvec[8:12], fvec[12:16]))

def _fvec9_to_matrix3(fvec):
return mathutils.Matrix((fvec[0:3], fvec[3:6], fvec[6:9]))

__ROTATION_MODES__ = ('quaternion', 'euler', 'axis_angle')

class RelativeBoneTransform(bpy.types.PropertyGroup):
b_type = bpy.types.PoseBone

def _update_source(self, _context):
if self.source:
self.update_link()

source = bpy.props.StringProperty(
name='Source Bone',
description='A bone whose animation will be used',
update=lambda self, _: self.update_link(),
update=_update_source,
)

def _get_use_rotation(self):
Expand All @@ -61,7 +158,7 @@ def _set_use_rotation(self, value):
for fcurve in bone.driver_add('rotation_' + mode):
driver = fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var = driver.variables[0] if driver.variables else driver.variables.new()
tgt = var.targets[0]
tgt.id = self.id_data
tgt.data_path = 'pose.bones["%s"].animation_retarget.transform[%d]' % (
Expand Down Expand Up @@ -91,7 +188,7 @@ def _set_use_location(self, value):
for fcurve in bone.driver_add('location'):
driver = fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var = driver.variables[0] if driver.variables else driver.variables.new()
tgt = var.targets[0]
tgt.id = self.id_data
tgt.data_path = 'pose.bones["%s"].animation_retarget.transform[%d]' % (
Expand Down Expand Up @@ -147,9 +244,12 @@ def update_link(self):
)

def _invalidate(self):
self.frame_cache[7] = 0
self.invalidate_cache()
bpy.context.scene.update()

def invalidate_cache(self):
self.frame_cache[7] = 0

def _get_transform(self):
frame = bpy.context.scene.frame_current + 1 # to workaround the default 0 value
cache = tuple(self.frame_cache)
Expand Down
77 changes: 77 additions & 0 deletions animation_retarget/ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import bpy

from .core import mapping_to_text, text_to_mapping, clear_mapping

WM = bpy.context.window_manager


class OBJECT_OT_CopyMapping(bpy.types.Operator):
bl_idname = "animation_retarget.copy_mapping"
bl_label = "Copy Mapping"
bl_description = "Copy the current mapping to the clipboard"

def execute(self, context):
target_obj = context.active_object
WM.clipboard = mapping_to_text(target_obj)
return {'FINISHED'}

@classmethod
def poll(cls, context):
target_obj = context.active_object
if (not target_obj) or (target_obj.type not in {'ARMATURE'}):
return False
if not target_obj.animation_retarget.source:
return False
return True


class OBJECT_OT_PasteMapping(bpy.types.Operator):
bl_idname = "animation_retarget.paste_mapping"
bl_label = "Paste Mapping"
bl_description = "Paste the current mapping from the clipboard"

def execute(self, context):
target_obj = context.active_object
text_to_mapping(WM.clipboard, target_obj)
return {'FINISHED'}

@classmethod
def poll(cls, context):
target_obj = context.active_object
if (not target_obj) or (target_obj.type not in {'ARMATURE'}):
return False
if not WM.clipboard:
return False
return True


class OBJECT_OT_ClearMapping(bpy.types.Operator):
bl_idname = "animation_retarget.clear_mapping"
bl_label = "Clear Mapping"
bl_description = "Clear the current mapping"

def execute(self, context):
target_obj = context.active_object
clear_mapping(target_obj)
return {'FINISHED'}

@classmethod
def poll(cls, context):
target_obj = context.active_object
if (not target_obj) or (target_obj.type not in {'ARMATURE'}):
return False
return True


__CLASSES__ = (
OBJECT_OT_CopyMapping,
OBJECT_OT_PasteMapping,
OBJECT_OT_ClearMapping,
)

def register():
for clas in __CLASSES__:
bpy.utils.register_class(clas)
def unregister():
for clas in reversed(__CLASSES__):
bpy.utils.unregister_class(clas)
7 changes: 7 additions & 0 deletions animation_retarget/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def poll(cls, context):
def draw(self, context):
layout = self.layout
data = context.object.animation_retarget

col = layout.column(align=True)
row = col.row(align=True)
row.operator('animation_retarget.copy_mapping', icon='COPYDOWN', text='Copy')
row.operator('animation_retarget.paste_mapping', icon='PASTEDOWN', text='Paste')
row.operator('animation_retarget.clear_mapping', icon='X', text='Clear')

layout.prop_search(data, 'source', bpy.data, 'objects')

source = bpy.data.objects.get(data.source)
Expand Down
117 changes: 117 additions & 0 deletions tests/cases/test_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from tests import utils

import bpy

from animation_retarget import ops


class WM:
def __init__(self):
self.clipboard = ''


class TestOperations(utils.BaseTestCase):
def setUp(self):
super().setUp()
ops.WM = WM()

def tearDown(self):
ops.WM = bpy.context.window_manager
super().tearDown()

def test_copy(self):
operator = bpy.ops.animation_retarget.copy_mapping
# no armature
self.assertFalse(operator.poll())

src = create_armature('src')
tgt = create_armature('tgt')
# no source
self.assertFalse(operator.poll())

tgt.animation_retarget.source = src.name
prop = tgt.pose.bones['root'].animation_retarget
prop.source = 'root'
prop.use_location = True
# all ok
self.assertTrue(operator.poll())

operator()
self.assertEqual(ops.WM.clipboard, """[object]
source = src
[bone:root]
source = root
use_location = True
source_to_target_rest = (1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
delta_transform = (1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
""")

def test_paste(self):
operator = bpy.ops.animation_retarget.paste_mapping
# no armature
self.assertFalse(operator.poll())

create_armature('src')
tgt = create_armature('tgt')
ops.WM.clipboard = ''
# the clipboard is empty
self.assertFalse(operator.poll())
ops.WM.clipboard = """
[object]
source=src
[bone:root]
source = root
use_rotation = True
source_to_target_rest = (1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
"""
# all ok
self.assertTrue(operator.poll())

operator()
self.assertEqual(tgt.animation_retarget.source, 'src')
prop = tgt.pose.bones['root'].animation_retarget
self.assertEqual(prop.source, 'root')
self.assertFalse(prop.use_location)
self.assertTrue(prop.use_rotation)

def test_clear(self):
operator = bpy.ops.animation_retarget.clear_mapping
# no armature
self.assertFalse(operator.poll())

src = create_armature('src')
tgt = create_armature('tgt')
tgt.animation_retarget.source = src.name
prop = tgt.pose.bones['root'].animation_retarget
prop.source = 'root'
prop.use_location = True
prop.use_rotation = True
# all ok
self.assertTrue(operator.poll())

operator()
self.assertEqual(tgt.animation_retarget.source, '')
self.assertEqual(prop.source, '')
self.assertFalse(prop.use_location)
self.assertFalse(prop.use_rotation)


def create_armature(name):
arm = bpy.data.armatures.new(name)
obj = bpy.data.objects.new(name, arm)
bpy.context.scene.objects.link(obj)
bpy.context.scene.objects.active = obj

bpy.ops.object.mode_set(mode='EDIT')
try:
root = arm.edit_bones.new('root')
root.tail = (0, 0, 1)
child = arm.edit_bones.new('child')
child.parent = root
child.head = root.tail
child.tail = (0, 1, 1)
finally:
bpy.ops.object.mode_set(mode='OBJECT')
return obj

0 comments on commit 777b8b4

Please sign in to comment.