Skip to content

Armatures and skinned objects #436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Armatures and skinned objects #436

wants to merge 6 commits into from

Conversation

Jrius
Copy link
Collaborator

@Jrius Jrius commented Feb 8, 2025

Alright, this is a big one. I probably won't have a lot of time to implement suggestions until the RAD is over, so feel free to take your time reviewing. I also didn't check how optimized the code is (especially regarding Python generators), suggestions are welcome.

The first thing that needs to be mentioned: I decided to let Korman generate a LOT of temporary Blender objects during export. This may feel messy, but I've rewritten the code several times and AFAICT is still the best solution. Temporary objects should all get cleaned up nicely whatever happens.

Second, there are still two small problems, that I'll get around to later:

  • Warning messages about "Animation generated no applicators" - harmless.
  • Exported skinned objects don't automatically get runtime lighting. This can be forced using a Lighting Info modifier, but it might be worth making it automatic just like other animated objects.

Armatures

  • Armature objects are exported as regular SceneObjects.
  • Each bone of the armature is exported as two (!) new SceneObjects. Bone hierarchy is respected.
  • The two SceneObjects are: the rest position of the bone, and the actual deform bone. For instance, the following bone hierarchy:
    • Armature:
      • Bone1
        • Bone2
      • Bone3
  • ...Will be exported as:
    • Armature:
      • Bone1_REST
        • Bone1
          • Bone2_REST
            • Bone2
      • Bone3_REST
        • Bone3
  • Exporting two SceneObjects per bone avoids a whole slew of problems when exporting animations. Otherwise we'd be running in the same problem as an object having a matrix_parent_inverse.
  • For various reasons, those two bone objects are actually temporary Blender Empty objects that are created at export-time. (I rewrote this several times, but in the end this felt like the best solution.) (On cleanup: those empties are deleted.)

Animations

  • Armatures with an Animation modifier automatically get their Animation Group modifier toggled on during export. (On cleanup: modifier gets re-disabled if necessary.)
  • Animated bones generate two Empty objects as previously mentioned - one of those empties gets to be animated:
    • The bone's animations are copied to a temporary Action assigned to the empty object. (Cleanup: the Action is deleted.)
    • An Animation modifier that matches the armature's own is added to the empty object. This Animation modifier is referenced in the armature's own Animation Group modifier. (On cleanup: the reference is removed.)
  • This means one can easily play specific armature animations from nodes/responders, as Korman will reroute those messages to the armature's Animation Group / plMsgForwarder.
  • Note that just like Cyan, we generate one plATCAnim per bone, per animation. Ideally we would group all bones in a single plATCAnim, but in my tests this doesn't work - seems it's only available to special objects like avatars.
  • Animation modifiers now have two extra properties to bake animations.
    • Baking is required to get bone inverse kinematic (IK) to export.
    • It also helps fix some occasional animations issues: wrong tangents, missing keyframes on armature bones, etc. For this reason, it's also available to regular animations.
    • Baking is neither a silver bullet (can worsen animations), nor cheap (takes a long time to compute), so it is disabled by default. It's up to the user to know when to enable it.

Rigged/deformed meshes

  • If the armature itself is not exported, the modifier/pose will be baked in the mesh and no expensive runtime deformation happens. (This is useful to make variations of the same mesh. For instance: trees with different branch shapes and sizes.)
  • Otherwise, the armature modifier is disabled (so it doesn't get baked into the mesh), and weight/indices are exported in the mesh's vertices. (On cleanup: modifier is reenabled.)
    • Vertices that are not deformed by any bone get assigned to a special "null" bone that never moves. This is very commonly used in Kemo for trees, since their roots don't move.
    • If possible, less than 3 skin indices will be used per vertex for better performances.
  • When generating the Drawable Spans from the collection of Geometry Spans, a few things need to happen.
    • Bone transforms get added to the DSpan, and bones get a Draw Interface referencing said bone.
    • Icicles get adjusted matrix indices so they know which bones they are using.
  • Worth noting that Blender objects can have multiple Armature modifiers. This is accepted by Korman, but results are completely untested.
  • Rigged meshes do NOT get a CoordinateInterface, ever. Since people usually parent the rigged mesh to the armature itself, Korman will simply ignore that relationship on export.
    • The reason for this is armature sharing. Armatures can be shared between rigs in Blender, but the way DrawableSpans work, this would require duplicating the bones for each object, so screw that.
    • There is rarely a good reason for the object to have its own coordinate system since it's supposed to move with the armature anyway... (except maybe floating-point precision ?)

Other

  • Regular (non deformable) objects can now be parented to armature bones. This provides an easy way to use IK for machines and the like, without the overhead of deforming the vertices.

Backwards compatibility: completely new feature so no problem expected unless I messed up. It does fix a bug with TemporaryCollectionItem though.

And here is a test file so you can see it in action.

Copy link
Member

@Hoikas Hoikas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot for me to wrap my head around. I think my major concern here is that I'm not sure that I like how the temporary bone empties are being handled. I worked pretty hard to try to establish a paradigm where temporary objects are yielded to the exporter for lifetime management, and I'd like to see that continued.

You may want to submit the bugfixes in a separate PR so we can get them merged quickly.

import functools
import itertools
import math
import mathutils
from typing import *
import weakref
import re
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be between mathutils and typing.


class AnimationConverter:
def __init__(self, exporter):
self._exporter = weakref.ref(exporter)
self._bl_fps = bpy.context.scene.render.fps
self._bone_data_path_regex = re.compile('^pose\\.bones\\["(.*)"]\\.(.*)$')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._bone_data_path_regex = re.compile('^pose\\.bones\\["(.*)"]\\.(.*)$')
self._bone_data_path_regex = re.compile("^pose\\.bones\\["(.*)"]\\.(.*)$")

Comment on lines +91 to +92
toggle.track(anim, "bake", False)
toggle.track(anim_group, "enabled", True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these changes need to be tracked and reset after the entire export process, or could we get away with only tracking them in this function?

Comment on lines +93 to +94
handle_temporary(toggle)
handle_temporary(exit_stack)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I like this idea, to be honest.


# Copy animation data.
anim_data = bone.animation_data_create()
action = bpy.data.actions.new("{}_action".format(bone.name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
action = bpy.data.actions.new("{}_action".format(bone.name))
action = bpy.data.actions.new(f"{bone.name}_action")

geospan.format |= plGeometrySpan.kSkin3Weights | plGeometrySpan.kSkinIndices
elif max_deform_bones == 2:
geospan.format |= plGeometrySpan.kSkin2Weights | plGeometrySpan.kSkinIndices
else: # max_bones_per_vert == 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this explicitly and error if there's an unexpected value.

# No skin indices required... BUT! We have assigned some weight to the null bone on top of the only bone, so we need to fix that.
for vtx in data.vertices:
weight = vtx.weights[0]
weight = 1 - weight
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
weight = 1 - weight
weight = 1.0 - weight

@@ -522,6 +559,11 @@ def _(temporary, parent):
return temporary

def do_pre_export(bo):
if bo.type == "ARMATURE":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little out of place, but I haven't wrapped my head around what's going on well enough to say what should change.

@@ -522,6 +559,11 @@ def _(temporary, parent):
return temporary

def do_pre_export(bo):
if bo.type == "ARMATURE":
so = self.mgr.find_create_object(plSceneObject, bl=bo)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize we can't do everything in one go, but I am thinking toward the future where we can (hopefully) export avatar animations from Korman, and IIRC avatar animation PRPs don't have a scene object in them at all.

so = self.mgr.find_create_object(plSceneObject, bl=bo)
self._export_actor(so, bo)
# Bake all armature bones to empties - this will make it easier to export animations and such.
self.armature.convert_armature_to_empties(bo, lambda obj: handle_temporary(obj, bo))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we need to be generator aware, instead of passing down this lambda.

@Jrius
Copy link
Collaborator Author

Jrius commented Feb 24, 2025

Thanks for the extensive feedback ! Yeah, the priority was to make the PR before the start of the contest, I expected a lot would require improvements. I'll clean this all up once the contest is out of the way...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants