Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

# For your local virtual environment
venv
.venv

# To have local stuff outside git
*.local.*

# For all of the automatically generated videos
MMS-Examples/*.mp4
Expand Down
4 changes: 2 additions & 2 deletions Docs/DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ In addition, there are binary requirements:

```
assets/ - binary files needed for the final blender scene
avasag_avatar_casual.glb - is our target character
avasag_avatar_casual.glb - is our target character, currently called Gloria.
controller_config.json - described what inflection strategy must be used for each target bone
defaults.blend - default blender scene, containing an imported version of the GLB. It contains some default rendering values and lights, and the timeline will be filled by the MMS animation.
gloria-YYMMDD.blend - default blender scene, containing an imported version of the GLB. It contains some default rendering values and lights. Its timeline will be filled by the MMS animation.
ignorelist.json - list of bones that must be deleted after loading an armature
```

Expand Down
2 changes: 1 addition & 1 deletion assets/LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ https://creativecommons.org/licenses/by-nc-sa/4.0/

## The default Blender scene

The file `defaults.blend` is the base Blender scene containing the imported Gloria character, default light setup, and some default render options.
The file `gloria-YYMMDD.blend` is the base Blender scene containing the imported Gloria character, default light setup, and some default render options.

The "default Blender scene" by "DFKI GmbH" (https://www.dfki.de) are is licensed under CC BY-NC-SA.
CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL-SHAREALIKE 4.0 INTERNATIONAL
Expand Down
Binary file added assets/gloria-260624.blend
Binary file not shown.
227 changes: 172 additions & 55 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
from typing import List, Optional


# The template Blender scene containing the character, the light setup, and some default rendering parameters
DEFAULT_BLEND_SCENE = "./assets/gloria-260624.blend"
# In the template scene, the name of the armature object to be animated.
TARGET_ARMATURE_NAME = "skeleton #5"
# In the template scene, the name of the camera object used for rendering.
RENDER_CAMERA_NAME = "Camera"
# The name of the final action containing the composed sign sequence
TARGET_ACTION_NAME = "final_action"
# Path to the JSON file with the list of bones to ignore during animation procedures
# BONES_IGNORE_LIST_PATH = "./assets/ignorelist.json"


def add_options(arg_parser: argparse.ArgumentParser):
"""Add command line options to the parser.

Expand Down Expand Up @@ -142,7 +154,12 @@ def add_options(arg_parser: argparse.ArgumentParser):
)

arg_parser.add_argument(
"--render-sentence", action="store_true", help="Render the sentence video."
"--render-sentence",
type=int,
help="Render the video of a si ngle specified sentence (AVASAG project)." \
" Provide an integer number X as parameter, it will be converted in 'SatzX.blend" \
"The file will be searched in the corpus in the folder 'generated/sentences/trimmed/'.",
required=False
)

arg_parser.add_argument(
Expand Down Expand Up @@ -219,7 +236,7 @@ def post_bake(
view3d.spaces[0].region_3d.view_perspective = "CAMERA"

# Setup the objects to be rendered
bpy.context.scene.camera = bpy.data.objects["Camera"]
bpy.context.scene.camera = bpy.data.objects[RENDER_CAMERA_NAME]

# Cleanup the animation curves
action = bpy.data.actions.get(action_name)
Expand Down Expand Up @@ -273,28 +290,94 @@ def post_bake(
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path))


def render_sentence(sentence_id: str, generated_root: Path, glue: Glue, arguments: argparse.Namespace) -> None:
"""Render the original sentence from the database.
def initialize_scene():
"""Initialize the current Blender context:
i) Copy all objects and worlds from the source template blender scene to the current context.
ii) Link all objects into the current context to work on them.
"""

This function performs no inflection. Therefore, rendering is as straightforward as it can be.
# Select all objects and delete them.
# Will leave only the armature data and the actions.
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete()

# Load objects from the reference scene (nice character and lights)
with bpy.data.libraries.load(DEFAULT_BLEND_SCENE) as (data_from, data_to):
data_to.objects = data_from.objects
data_to.worlds = data_from.worlds

# Link all objects to the scene, to make them visible
for obj in data_to.objects:
bpy.context.scene.collection.objects.link(obj)

# Set the current world to the first found in the loaded template
bpy.context.scene.world = data_to.worlds[0]


def initialize_target_armature():
"""Replace the bone names in the rtemplate armature and create a new action.
The original names in the template scene are "Bone Pelvis". This name doesn't work for the
skeletal animations which are of format "Bone_Pelvis". Thus, we modify
the name of bones in the original mesh itself as it is one time operation.

Then, creates the final target action and assign it as current action of the armature
"""

@param sentence_id: The id of the sentence to render.
@param generated_root: The root path for the corpus database.
@param glue: Glue object that is used to connect compile the final animation.
@param arguments: Command-line arguments.
target_armature_obj = bpy.data.objects[TARGET_ARMATURE_NAME]
assert isinstance(target_armature_obj, bpy.types.Object)
assert target_armature_obj.type == "ARMATURE"

for bone in target_armature_obj.pose.bones:
bone.name = bone.name.replace(" ", "_")
bone.rotation_mode = "ZXY"

target_armature_obj.animation_data_create()
action_ref = bpy.data.actions.new(TARGET_ACTION_NAME)
target_armature_obj.animation_data.action = action_ref


def execute_single_sentence_realization_pipeline(arguments: argparse.Namespace) -> None:
"""
For some use cases, it is necessary for us to only render the single original entence data into the avatar.
Inflections are not needed
Thus, the following block assures that we load the correct sentence animation render it, but bypassing the instancing and inflection overhead.
This function performs no inflection. Therefore, rendering is as straightforward as it can be.
"""
sentence_id = "Satz" + sentence_id.lstrip("0")
animation_data = Path(generated_root).joinpath(
"sentences", "trimmed", f"{sentence_id}.blend"

# Must be true otherwise this block is not called.
assert arguments.render_sentence is not None

sentence_id: int = arguments.render_sentence

# Initialize the target scene and armature
initialize_scene()
initialize_target_armature()

# Load the source animation data from the sentence file
sentence_file = "Satz" + str(sentence_id) + ".blend"
sentence_path = Path(arguments.corpus_generated_directory).joinpath(
"sentences", "trimmed", sentence_file
)
with bpy.data.libraries.load(str(animation_data)) as (data_from, data_to):

# Import the sentence animation
with bpy.data.libraries.load(str(sentence_path)) as (data_from, data_to):
data_to.actions = data_from.actions
glue.create_new_fcurves(f"updated_{sentence_id}")
glue.combine_animation("final_action", f"updated_{sentence_id}", 1)

glue = Glue(
mms=None, # It won't be needed when rendering a single sentence
target_armature_obj_name=TARGET_ARMATURE_NAME,
target_action_name=TARGET_ACTION_NAME
)

# Prepare the target animation curves, specifiyng the name of the source action
# By manually specifying the source action, the mms is not needed.
source_action_name = "updated_Satz" + str(sentence_id)
glue.prepare_target_fcurves(source_action_name)
glue.append_action(target_action_name="final_action", source_action_name=source_action_name, start=1)

post_bake(
armature_obj_name=glue.armature_obj_name,
action_name=glue.action_name,
armature_obj_name=glue.src_armature_obj_name,
action_name=glue.target_action_name,
mp4_path=arguments.export_mp4,
bvh_path=arguments.export_bvh,
fbx_path=arguments.export_fbx,
Expand All @@ -305,13 +388,14 @@ def render_sentence(sentence_id: str, generated_root: Path, glue: Glue, argument
)


def execute_pipeline(arguments: argparse.Namespace) -> None:

def execute_mms_realization_pipeline(arguments: argparse.Namespace) -> None:
"""Execute the mms pipeline.

What does this method do?
1. Read a mms file
2. Import the necessary gloss in the MMS file
3. Attach the IK controller
1. Read a mms file.
2. Import the necessary gloss in the MMS file.
3. Attach the necessary IK controllers.
4. Run the animation production pipeline.
"""
mms_file = arguments.source_mms_file
Expand All @@ -321,20 +405,7 @@ def execute_pipeline(arguments: argparse.Namespace) -> None:
# Read the MMS from the given MMS file.
mms = MMSParser(mms_file, generated_root).parse()
# Compose the MoCap file names and check for their availability
mms.find_mocap_data_files()

# During the comparison it is necessary for us to only compute the sentence.
# Thus, the following block assures that we load the correct sentence animation
# and render the animation.
if arguments.render_sentence:
glue = Glue(
mms=mms,
ignore_bone_list="./assets/ignorelist.json",
src_blendfile="./assets/defaults.blend",
action_name="final_action"
)
render_sentence(sentence_id, generated_root, glue, arguments)
return # Exit the function. Because we do not need to continue at all.
mms.ensure_mocap_data_files()

#
# READ INFLECTION CONFIGURATION
Expand Down Expand Up @@ -396,11 +467,12 @@ def execute_pipeline(arguments: argparse.Namespace) -> None:
logger.info("IK Target Config: %s", ik_target.dict)
logger.info("==================================")

# Remove all existing objects from the scene
# Remove all existing temporary objects from the scene
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)

# Iterate on MMS rows
# For each row, create a new action with the inflected gloss animation
for gloss in mms.glosses:
logger.info(f"Processing gloss {gloss}: {mms[gloss].path}")
# TODO -- Path might not exist if the "gloss" is <HOLD>
Expand All @@ -415,27 +487,45 @@ def execute_pipeline(arguments: argparse.Namespace) -> None:
# sentence, we are resampling the animation frames.
# The gloss animation data is loaded inside the ArmatureOperator constructor
armature_operator = ArmatureOperator(mms[gloss])

# Here the "imported_" action has been created
mmsline = mms[gloss]
assert "imported_" + mmsline.output_name in bpy.data.actions

inflected_armature = armature_operator.copy_armature()
if not arguments.ignore_gloss_duration:
if arguments.use_relative_time:
armature_operator.resample(mms[gloss].duration(), use_rel_time=True)
armature_operator.resample(timing=mms[gloss].duration(), target_action_name="resampled_" + mmsline.output_name, use_rel_time=True)
else:
armature_operator.resample(mms[gloss].timing(), use_rel_time=False)
armature_operator.resample(timing=mms[gloss].timing(), target_action_name="resampled_" + mmsline.output_name, use_rel_time=False)

# Here the "updated_" animation has been created
assert "resampled_" + mmsline.output_name in bpy.data.actions

# We add the extra controllers to ensure that we will be able to modify the
# animation down the pipeline.
controller = Controller(inflected_armature, armature_operator.src_armature.name, ik_target_list, gloss[0])
# We add the extra controllers to ensure that we will be able to modify the animation down the pipeline.
inflector = Controller(inflected_armature, armature_operator.src_armature.name, ik_target_list, gloss[0])
name = armature_operator.mms_line.output_name
controller.setup_chain(
inflector.setup_chain(
source_armature=armature_operator.src_armature,
target_armature=inflected_armature,
output_name=name,
inflected_action_name=name,
mms_line=mms[gloss],
without_inflection=arguments.without_inflection,
)

# Here the target "inflected_..." action has been already created
assert "inflected_" + mmsline.output_name in bpy.data.actions

# Perform the inflection !!!
if not arguments.without_inflection:
controller.execute(inflected_armature, mms[gloss])
inflector.execute(inflected_armature, mms[gloss])

#
# For each MMS line, the inflected action has been created
for gloss in mms.glosses:
mmsline = mms[gloss]
# print("Expected inflected action presence ", "inflected_" + mmsline.output_name)
assert "inflected_" + mmsline.output_name in bpy.data.actions

#
# Call the data extraction if requested
Expand All @@ -451,22 +541,45 @@ def execute_pipeline(arguments: argparse.Namespace) -> None:
)
return

# Load the template scene
initialize_scene()

# Checks
assert TARGET_ARMATURE_NAME in bpy.context.scene.objects

# Initialize the target armature
initialize_target_armature()

assert TARGET_ACTION_NAME in bpy.data.actions
assert bpy.context.scene.objects[TARGET_ARMATURE_NAME].animation_data is not None
assert bpy.context.scene.objects[TARGET_ARMATURE_NAME].animation_data.action is not None
assert bpy.context.scene.objects[TARGET_ARMATURE_NAME].animation_data.action.name == TARGET_ACTION_NAME

# Finally we merge individual signs to produce the final utterance of the full sentence.
print("Merging inflected glosses into the final timeline...")
glue = Glue(mms=mms,
ignore_bone_list="./assets/ignorelist.json",
src_blendfile="./assets/defaults.blend",
action_name="final_action")
logger.info("Merging inflected glosses into the final timeline...")
glue = Glue(
mms=mms,
target_armature_obj_name=TARGET_ARMATURE_NAME,
target_action_name=TARGET_ACTION_NAME
)

# Since the animation data is essentially empty after initializing a new one,
# it is necessary to create f-curves that match the source data.
glue.create_new_fcurves()
glue.prepare_target_fcurves()

assert bpy.context.active_object.name == TARGET_ARMATURE_NAME
# Also the referenced Armature instance has by default the same name,
# but might have been renamed while loading the animation data from the other scenes.
assert bpy.context.active_object.data.name.startswith(TARGET_ARMATURE_NAME), f"The name of the active object does not start with '{TARGET_ARMATURE_NAME}', but is '{bpy.context.active_object.data.name}'"

#
# Put all the inflected glosses/actions into a final timeline
glue.merge_animation(use_rel_time=arguments.use_relative_time)
glue.realize_mms(use_rel_time=arguments.use_relative_time)

# Finalize the scene and export as MP4, BVH, FBX, or binary blender scene
post_bake(
armature_obj_name=glue.armature_obj_name,
action_name=glue.action_name,
armature_obj_name=glue.src_armature_obj_name,
action_name=glue.target_action_name,
mp4_path=arguments.export_mp4,
bvh_path=arguments.export_bvh,
fbx_path=arguments.export_fbx,
Expand Down Expand Up @@ -499,7 +612,11 @@ def execute_pipeline(arguments: argparse.Namespace) -> None:
if args.log_to_console:
enable_log_to_stdout()

print("Realizing MMS... ")
execute_pipeline(args)
if args.render_sentence:
print(f"Realizing single sentence with numer {args.render_sentence} ...")
execute_single_sentence_realization_pipeline(args)
else:
print(f"Realizing MMS from file '{args.source_mms_file}' ...")
execute_mms_realization_pipeline(args)

print("All done.")
Loading