[Solved]Question about blender to Ogre_V2 meshes animation? Topic is solved

Discussion area about developing with Ogre-Next (2.1, 2.2 and beyond)


knn217
Halfling
Posts: 78
Joined: Wed Jan 25, 2023 9:04 am
x 5

[Solved]Question about blender to Ogre_V2 meshes animation?

Post by knn217 »

Hello, this is the first mesh that I tried to make seriously for ogre, now I'm trying to animate it.

Since this is a human with facial bones, I learned that to animate facial expressions, I likely need to keep the eyes, teeth and tongue meshes separated from the body mesh.

Since the body and eyes, tongue and teeth are separated, how do I "link" (if this is the right expression) them so that blender_to_ogre export them correctly? I need the exported meshes still separated but will bend to the animated bones.

After some searching on blender sites, I think what I'm looking for is either "join" or "parent" the meshes, but I want to ask here beforehand to know which one to use for blender_to_ogre.

Last edited by knn217 on Thu Oct 26, 2023 12:51 pm, edited 1 time in total.
User avatar
sercero
Bronze Sponsor
Bronze Sponsor
Posts: 495
Joined: Sun Jan 18, 2015 4:20 pm
Location: Buenos Aires, Argentina
x 179

Re: Question about blender to Ogre_V2 meshes animation?

Post by sercero »

Hello,

Unless you are using the DotScene plugin, then Blender does not have much to do here.

In Blender you have to parent the eyes, teeth and tongue objects to the body so they move with it and that structure remains in Blender.

When you export the meshes, they are exported as separate objects and the hierarchy is "lost" (you actually get it in an xml, but you have to import it into your application).

That is, in OGRE you have to do the parenting yourself using code or the DotScene plugin which might be unavailable for OGRE v2.

So in OGRE you have to use the Ogre::Node::addChild(Node *child) funtion to add the eyes, teeth and tongue nodes as children of the body.

knn217
Halfling
Posts: 78
Joined: Wed Jan 25, 2023 9:04 am
x 5

Re: Question about blender to Ogre_V2 meshes animation?

Post by knn217 »

Thanks for the answer, I think it might be supported in Ogre V2 since one of the samples feature shared skeleton.

I'll try the parenting method to make a test animation 1st for exporting and see if I can parent the meshes in the code.

User avatar
sercero
Bronze Sponsor
Bronze Sponsor
Posts: 495
Joined: Sun Jan 18, 2015 4:20 pm
Location: Buenos Aires, Argentina
x 179

Re: Question about blender to Ogre_V2 meshes animation?

Post by sercero »

Check out also the .xml exported with the scene.

That might come handy later when you get tired of doing that kind of stuff in code.

Also despite OGREv2 not having the DotScene plugin it is very easy to integrate it in your own app.

knn217
Halfling
Posts: 78
Joined: Wed Jan 25, 2023 9:04 am
x 5

Re: Question about blender to Ogre_V2 meshes animation?

Post by knn217 »

Hello again, now there is a new problem: after exporting, the eyes, tongue, teeth can be loaded to the code. However, the body even though could be loaded, is somehow creating vector index error at:

Code: Select all

/// Gets full transform of a bone by its index.
FORCEINLINE const SimpleMatrixAf4x3 &_getBoneFullTransform( size_t index ) const
{
    return mBones[index]._getFullTransform();
}

my mBones only has 221 elements so it crashes at index 221.

Here's the blender2ogre.log:

Code: Select all

2023-10-22 14:13:01 [ INFO] Target_path: D:\OGRE_NEXT\Blender_exports
2023-10-22 14:13:01 [ INFO] Target_file_name: test_animation.scene
2023-10-22 14:13:01 [DEBUG] Target_file_name_no_ext: test_animation
2023-10-22 14:13:01 [ INFO] * Processing Scene: test_animation, path: D:\OGRE_NEXT\Blender_exports
2023-10-22 14:13:01 [DEBUG] ABABA full(high_poly_reduced)
2023-10-22 14:13:01 [DEBUG] ABABA tongue
2023-10-22 14:13:01 [DEBUG] ABABA eyes
2023-10-22 14:13:01 [DEBUG] ABABA joined_teeth_lower
2023-10-22 14:13:01 [DEBUG] ABABA joined_teeth_upper
2023-10-22 14:13:01 [DEBUG] ABABA rig_custom
2023-10-22 14:13:01 [ INFO] * Processing Materials
2023-10-22 14:13:01 [ INFO] * Exporting root node: rig_custom 
2023-10-22 14:13:01 [ INFO]   - Vertices: 964
2023-10-22 14:13:01 [ INFO]   - Loop triangles: 1920
2023-10-22 14:13:01 [ INFO] * Generating: eyes.mesh.xml
2023-10-22 14:13:01 [ INFO] * Writing shared geometry
2023-10-22 14:13:01 [DEBUG] * Mesh has NO custom normals
2023-10-22 14:13:02 [ INFO] - Done at 1.57 seconds
2023-10-22 14:13:02 [ INFO] * Writing submeshes
2023-10-22 14:13:02 [ INFO] - Done at 1.59 seconds
2023-10-22 14:13:02 [DEBUG] Removing temporary object: eyes.001
2023-10-22 14:13:02 [ INFO] - Created eyes.mesh.xml at 1.64 seconds
2023-10-22 14:13:02 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\eyes.mesh.xml
2023-10-22 14:13:03 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\eyes.mesh.xml
2023-10-22 14:13:03 [ INFO] - Created eyes.mesh in total time 1.90 seconds
2023-10-22 14:13:03 [ INFO] * Generating: eyes.skeleton.xml
2023-10-22 14:13:03 [ INFO] + NLA track: idle
2023-10-22 14:13:03 [ INFO]   - Action name: idle
2023-10-22 14:13:03 [ INFO]   -  Strip name: idle
2023-10-22 14:13:04 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\eyes.skeleton.xml
2023-10-22 14:13:04 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\eyes.skeleton.xml
2023-10-22 14:13:04 [ INFO] - Done at 1.52 seconds
2023-10-22 14:13:04 [ INFO]   - Vertices: 5363
2023-10-22 14:13:04 [ INFO]   - Loop triangles: 10666
2023-10-22 14:13:04 [ INFO] * Generating: body.mesh.xml
2023-10-22 14:13:04 [ INFO] * Writing shared geometry
2023-10-22 14:13:04 [DEBUG] * Mesh has NO custom normals
2023-10-22 14:13:08 [ INFO] - Done at 3.58 seconds
2023-10-22 14:13:08 [ INFO] * Writing submeshes
2023-10-22 14:13:08 [ INFO] - Done at 3.67 seconds
2023-10-22 14:13:08 [WARNING] <body> vertex 1 is in more than 4 vertex groups (bone weights). This maybe Ogre incompatible
... (lots of vertices had this warning)
2023-10-22 14:13:11 [WARNING] <body> vertex 31997 is in more than 4 vertex groups (bone weights). This maybe Ogre incompatible
2023-10-22 14:13:11 [DEBUG] Removing temporary object: full(high_poly_reduced).001
2023-10-22 14:13:11 [ INFO] - Created body.mesh.xml at 6.84 seconds
2023-10-22 14:13:11 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\body.mesh.xml
2023-10-22 14:13:14 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\body.mesh.xml
2023-10-22 14:13:14 [ INFO] - Created body.mesh in total time 9.64 seconds
2023-10-22 14:13:14 [ INFO] Extern material: _missing_material_
2023-10-22 14:13:14 [ INFO] * Generating: body.skeleton.xml
2023-10-22 14:13:14 [ INFO] + NLA track: idle
2023-10-22 14:13:14 [ INFO]   - Action name: idle
2023-10-22 14:13:14 [ INFO]   -  Strip name: idle
2023-10-22 14:13:15 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\body.skeleton.xml
2023-10-22 14:13:15 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\body.skeleton.xml
2023-10-22 14:13:15 [ INFO] - Done at 1.67 seconds
2023-10-22 14:13:15 [ INFO] * Generating node animation for: full(high_poly_reduced)
2023-10-22 14:13:15 [ INFO] - Done at 0.00 seconds
2023-10-22 14:13:15 [ INFO]   - Vertices: 1604
2023-10-22 14:13:15 [ INFO]   - Loop triangles: 3200
2023-10-22 14:13:15 [ INFO] * Generating: teeth_lower.mesh.xml
2023-10-22 14:13:15 [ INFO] * Writing shared geometry
2023-10-22 14:13:15 [DEBUG] * Mesh has NO custom normals
2023-10-22 14:13:16 [ INFO] - Done at 0.69 seconds
2023-10-22 14:13:16 [ INFO] * Writing submeshes
2023-10-22 14:13:16 [ INFO] - Done at 0.72 seconds
2023-10-22 14:13:16 [ INFO] - Created teeth_lower.mesh.xml at 0.72 seconds
2023-10-22 14:13:16 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\teeth_lower.mesh.xml
2023-10-22 14:13:17 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\teeth_lower.mesh.xml
2023-10-22 14:13:17 [ INFO] - Created teeth_lower.mesh in total time 1.09 seconds
2023-10-22 14:13:17 [ INFO] Extern material: _missing_material_
2023-10-22 14:13:17 [ INFO]   - Vertices: 1604
2023-10-22 14:13:17 [ INFO]   - Loop triangles: 3200
2023-10-22 14:13:17 [ INFO] * Generating: teeth_upper.mesh.xml
2023-10-22 14:13:17 [ INFO] * Writing shared geometry
2023-10-22 14:13:17 [DEBUG] * Mesh has NO custom normals
2023-10-22 14:13:17 [ INFO] - Done at 0.73 seconds
2023-10-22 14:13:17 [ INFO] * Writing submeshes
2023-10-22 14:13:17 [ INFO] - Done at 0.75 seconds
2023-10-22 14:13:17 [ INFO] - Created teeth_upper.mesh.xml at 0.76 seconds
2023-10-22 14:13:17 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\teeth_upper.mesh.xml
2023-10-22 14:13:18 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\teeth_upper.mesh.xml
2023-10-22 14:13:18 [ INFO] - Created teeth_upper.mesh in total time 1.13 seconds
2023-10-22 14:13:18 [ INFO] Extern material: _missing_material_
2023-10-22 14:13:18 [ INFO]   - Vertices: 98
2023-10-22 14:13:18 [ INFO]   - Loop triangles: 192
2023-10-22 14:13:18 [ INFO] * Generating: tongue.mesh.xml
2023-10-22 14:13:18 [ INFO] * Writing shared geometry
2023-10-22 14:13:18 [DEBUG] * Mesh has NO custom normals
2023-10-22 14:13:18 [ INFO] - Done at 0.05 seconds
2023-10-22 14:13:18 [ INFO] * Writing submeshes
2023-10-22 14:13:18 [ INFO] - Done at 0.06 seconds
2023-10-22 14:13:18 [DEBUG] Removing temporary object: tongue.001
2023-10-22 14:13:18 [ INFO] - Created tongue.mesh.xml at 0.08 seconds
2023-10-22 14:13:18 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\tongue.mesh.xml
2023-10-22 14:13:18 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\tongue.mesh.xml
2023-10-22 14:13:18 [ INFO] - Created tongue.mesh in total time 0.14 seconds
2023-10-22 14:13:18 [ INFO] Extern material: _missing_material_
2023-10-22 14:13:18 [ INFO] * Generating: tongue.skeleton.xml
2023-10-22 14:13:18 [ INFO] + NLA track: idle
2023-10-22 14:13:18 [ INFO]   - Action name: idle
2023-10-22 14:13:18 [ INFO]   -  Strip name: idle
2023-10-22 14:13:19 [ INFO] Converting mesh from XML to binary: D:\OGRE_NEXT\ogre-next\build\bin\release\OgreMeshTool.exe -e -O puqs -v2 D:\OGRE_NEXT\Blender_exports\tongue.skeleton.xml
2023-10-22 14:13:20 [ INFO] Removing generated xml file after conversion: D:\OGRE_NEXT\Blender_exports\tongue.skeleton.xml
2023-10-22 14:13:20 [ INFO] - Done at 1.65 seconds
2023-10-22 14:13:20 [ INFO] - Exported Ogre Scene: D:\OGRE_NEXT\Blender_exports\test_animation.scene 

I did notice the warnings for the body mesh (possibly mean that these vertices has weight for more than 4 bones) but I didn't think this is a problem since at first, the tongue mesh also had this warning but can be loaded and updated normally. This warning seems to appear for any meshes that I used automatic weight painting on.

I managed to remove warning for the tongue mesh by deleting the vertex groups that are not for tongue bones. But I can't do this for the body since the whole body mesh requires every bone.

For more info, I used blender's metahuman rig generated with rigify, and then parent the body mesh (almost 10000 vertices) to this rig with automatic weight.

One solution that I tried was to export with high trim weight (0.25 and 0.3) to reduce the warnings above (there are no warnings after trimmed at 0.3). However, the error still remains. Any suggestions?

Edit: I forgot to mention that if I unparent the body mesh to the rig, it can be loaded so the rig seems to be the problem. Is blender's metarig (the one generated with rigify) a bad idea? I saw it used in some tutorials and it is quite convenient and flexible. How do you guys usually animate for Ogre?

knn217
Halfling
Posts: 78
Joined: Wed Jan 25, 2023 9:04 am
x 5

Re: Question about blender to Ogre_V2 meshes animation?

Post by knn217 »

The problem really was the rig generated from rigify, probably since its bones are generated from a script. The meta human rig is suitable for Ogre V2 though, after recreating the animation I got Ogre V2 to load the mesh and skeleton successfully!

While animating, I've also written a script in Blender that can trim down unneeded keyframe points in an armature action, and also create a flipped version of it.
Here's the script, feel free to use it. The flip function requires the armature to have the same "unsymmetrical axis" for all bones, so it might not work correctly for all cases.

Code: Select all

import bpy
import os
# clear terminal
os.system('cls')

             #=============#
             # INSTRUCTION #
#===============================================#
# MARK THE ANIMATIONS YOU WANT TO TRIM AND FLIP # 
#       WITH A '_' AT THE END OF THE NAME       #
#===============================================#


# create new empty action
def create_new_action(name):
    new_act = bpy.data.actions.new(name)
    return new_act


# create a keyframe trimmed action from an original action
def create_trimmed_action(orig_action):
    print('trimming')
    # if an action with the same name already existed, delete it
    for action in bpy.data.actions:
        if action.name == orig_action.name + 'trimmed':
            bpy.data.actions.remove(action, do_unlink=True)
            
# create trimmed action trimmed_act = create_new_action(orig_action.name + 'trimmed') for fc in orig_action.fcurves: # This if filters curves with property of 'loc, rot, scale' if fc.data_path.endswith(('location','rotation_euler','rotation_quaternion','scale')): # Create a new fcurve for the trimmed action new_fc = trimmed_act.fcurves.new( fc.data_path, index=fc.array_index, action_group=fc.group.name) old_left_flag = True left_flag = True right_flag = True old_value = None previous_keyframe = None for key in fc.keyframe_points : # 1st keyframe if key == fc.keyframe_points[0]: # set flags left_flag = True right_flag = (key.co[1] == key.handle_right[1]) previous_keyframe = None # make sure not to copy last keyframe # always copy 1st keyframe to the new action new_key = new_fc.keyframe_points.insert( key.co[0], key.co[1], options={'NEEDED'}, keyframe_type=key.type) new_key.handle_left = key.handle_left new_key.handle_left_type = key.handle_left_type new_key.handle_right = key.handle_right new_key.handle_right_type = key.handle_right_type # final keyframe elif key == fc.keyframe_points[-1]: # set flags left_flag = right_flag & (key.co[1] == old_value) & (key.handle_left[1] == old_value) right_flag = True # Copy previous frame if any flag is false if not (left_flag & old_left_flag): if (previous_keyframe != None): # copy to new action new_key = new_fc.keyframe_points.insert( previous_keyframe.co[0], previous_keyframe.co[1], options={'NEEDED'}, keyframe_type=previous_keyframe.type) new_key.handle_left = previous_keyframe.handle_left new_key.handle_left_type = previous_keyframe.handle_left_type new_key.handle_right = previous_keyframe.handle_right new_key.handle_right_type = previous_keyframe.handle_right_type # still check to copy last keyframe, but also check to copy the ending keyframe # COPY AT THE END! # CHANGING THE ORDER WHILE COPYING WILL CHANGE THE F_CURVE if not left_flag: # copy to new action new_key = new_fc.keyframe_points.insert( key.co[0], key.co[1], options={'NEEDED'}, keyframe_type=key.type) new_key.handle_left = key.handle_left new_key.handle_left_type = key.handle_left_type new_key.handle_right = key.handle_right new_key.handle_right_type = key.handle_right_type # other keyframes else: # set flags left_flag = right_flag & (key.co[1] == old_value) & (key.handle_left[1] == old_value) right_flag = (key.co[1] == key.handle_right[1]) # save current keyframe if not start or end previous_keyframe = key # Copy last frame if any flag is false if not (left_flag & old_left_flag): if (previous_keyframe != None): # copy to new action new_key = new_fc.keyframe_points.insert( previous_keyframe.co[0], previous_keyframe.co[1], options={'NEEDED'}, keyframe_type=previous_keyframe.type) new_key.handle_left = previous_keyframe.handle_left new_key.handle_left_type = previous_keyframe.handle_left_type new_key.handle_right = previous_keyframe.handle_right new_key.handle_right_type = previous_keyframe.handle_right_type # save old value and old left flag old_value = key.co[1] old_left_flag = left_flag return trimmed_act # create a flipped action from an original action if the uneven axis is specified # The uneven axis: the axis that points at unsymetrical directions on left and right bones in edit mode # If your armature does not have the same uneven axis on all bones, the results might be wrong def create_flipped_action(orig_action, uneven_axis): print('flipping') if (uneven_axis == 'X') | (uneven_axis == 'x'): flip_axis_index = 0 elif (uneven_axis == 'Y') | (uneven_axis == 'y'): flip_axis_index = 1 elif (uneven_axis == 'Z') | (uneven_axis == 'z'): flip_axis_index = 2 # if an action with the same name already existed, delete it for action in bpy.data.actions: if action.name == orig_action.name + 'flipped': bpy.data.actions.remove(action, do_unlink=True)
# create flipped action flipped_act = create_new_action(orig_action.name + 'flipped') for fc in orig_action.fcurves: # This if filters curves with property of 'loc, rot, scale' if fc.data_path.endswith(('location','rotation_euler','rotation_quaternion','scale')): # create new fcurve for the new flipped action # thre are 3 cases: .L, .R and neutral if fc.data_path.endswith(('.R"].location','.R"].rotation_euler','.R"].rotation_quaternion','.R"].scale')): new_data_path = fc.data_path.replace('.R"].', '.L"].') new_group_name = fc.group.name.replace('.R','.L') elif fc.data_path.endswith(('.L"].location','.L"].rotation_euler','.L"].rotation_quaternion','.L"].scale')): new_data_path = fc.data_path.replace('.L"].', '.R"].') new_group_name = fc.group.name.replace('.L','.R') else: new_data_path = fc.data_path new_group_name = fc.group.name new_fc = flipped_act.fcurves.new( new_data_path, index=fc.array_index, # even if flipped, they should have the same index action_group=new_group_name) for key in fc.keyframe_points : # Handle left and right of key frames # location: flip axis not symmetrical, the rest are symmetrical if fc.data_path.endswith('location'): if fc.array_index == flip_axis_index: flip_value = -1 else: flip_value = 1 # rotation euler: flip axis is symmetrical, the rest are not # ALL EULER ROCATIONS (XYZ, YZX,...) SEEMS TO HAVE THE SAME ORDER: # X = 0; Y = 1; Z = 2 # I couldn't find the exact source that confirms this, # the conclusion is based on the order in the action editor and the script's result elif fc.data_path.endswith('rotation_euler'): if fc.array_index == flip_axis_index: flip_value = 1 else: flip_value = -1 # rotation quaternion: w and flip axis are symmetrical, the rest are not elif fc.data_path.endswith('rotation_quaternion'): if (fc.array_index == (flip_axis_index + 1)) | (fc.array_index == 0): flip_value = 1 else: flip_value = -1 # scale: all are symmetrical elif fc.data_path.endswith('scale'): flip_value = 1 new_key = new_fc.keyframe_points.insert( key.co[0], key.co[1] * flip_value, options={'NEEDED'}, keyframe_type=key.type) new_key.handle_left = key.handle_left new_key.handle_left[1] *= flip_value new_key.handle_left_type = key.handle_left_type new_key.handle_right = key.handle_right new_key.handle_right[1] *= flip_value new_key.handle_right_type = key.handle_right_type return flipped_act def iterate_through_actions(object): for action in bpy.data.actions: #===============================================# # MARK THE ANIMATIONS YOU WANT TO TRIM AND FLIP # # WITH A '_' AT THE END OF THE NAME # #===============================================# if action.name.endswith(('_')): # create trimmed action from the original action trimmed_action = create_trimmed_action(action) # create flipped action from the trimmed action flipped_action = create_flipped_action(trimmed_action, 'X')
# If there are many armatures in the scene for obj in bpy.data.objects: if obj.type != 'ARMATURE': continue iterate_through_actions(obj)

Here's the github link: https://github.com/knn217/Blender_Scripts

The script will go through all your actions, pick the ones you've marked with "_" at the end and make a trimmed and flipped version of it.

For example: to run the script on "action01"
_ Rename "action01" to "action01_" and run script
_ Recieve 2 new actions: "action01_trimmed" and "action01_trimmedflipped"

*DISCLAIMER: I actually don't have a reliable way to test if my script does create the correct action. My testing method was just to go over every frame and compare the new actions to the original action in side view.
In my test, the trim function was able to copy the action's F-curve correctly, unlike Blender's "clean key" option (which is a known problem). And the flip function also works as intended, as long as the "uneven axis" condition is met.