Home Website Youtube GitHub

RBF Manager issues and feedback

There is no feedback category and this is no feature request either. I hope it’s OK to post here. If not, pls let me know and I’ll post somewhere else.

So to get straight to it, I found RBF Manager to be a sensitive tool, easy to mess up, in some cases less-than-ideally named, and overall quite confusing. These are my issues with it. Hope it’s helpful feedback, but if it’s not, maybe I can get some advice on how to use it better, to get more reliable results.

  1. The first issue for me was understanding the oddly named “Edit” button, which doesn’t put you in Edit mode but is rather an “Update Pose” or “Update Values” button. So it functions Post not Pre editing. I’ll get to this again in a bit, keep reading, thanks! ^ ^

  2. Seeing rotation values for the Driven, so we can pose accurately, by numbers, would be a huge help. Instead, RBF zeroes rotations for the driven ctrls immediately after setting any pose, and so posing accurately, by numbers, is only possible by constantly using the slow-to-reach, hidden-in-menus Reevaluate Nodes… which takes me to the next issue.

  3. Reevaluate Nodes is an essential tool that is used for updating manually edited values, right? We need to do this all the time, as I mentioned above. It should be one of the 4 main buttons [atm there are only 3]. Add Pose, Update Pose [instead of the poorly named Edit Pose], Update Pose Values [instead of the totally confusing Reevaluate Nodes…], and Delete Pose. But the Reevaluate Nodes/Update Pose, ideally, I’d expect it to work automatically every time the user inputs something manually in pose values! If that was the case, it wouldn’t need to be part of the UI.

  4. Display Keyable and NonKeyable are essential and shouldn’t be hidden in an obscure RMB menu. In fact, I would make Display Keyable default, not Display ALL, if there are any keyable attributes, if not, then look for non-keyable, and use that as default, and only if both of these fail, display the millions of attributes that you guys show by default now.

  5. A smallish… cosmetic issue. The UI default overall size is a bit too small and doesn’t remember its manual resizing. Every time I restart the tool, first thing I have to do is resize it to fit a lot more space, so I can see poses and values.

  6. Select Attributes. What attributes are we talking about?.. This is confusing and I think it would work better named Select Driver Attributes.

  7. The entire concept of Driver and Control is confusing. Maybe Driver and Controller… but it’s similarly confusing. It’s not immediately clear why there are 2 drivers. I would have first Driver Controller and then Driver Deformer or Driver Joint maybe. This I feel like it needs some rethinking, the whole logic of it is… meh.

  8. Instead of forcing to Specify Setup Name, give it a default name based on Driver, and then let the users Rename, if they want to. If possible.

  9. Anything past 180 degrees rotation will fail to work, unless manually, specifically edited numbers. Is this because of quaternions or because of my Maya settings? I think the weightDriver plugin has support for Euler rotations as well, wouldn’t it help having that as a choice in the setup?

  10. The behavior of the RBF poses, when driving past the set/posed limits, is wobbly and hard to understand. I’d expect no movement at all, why is there some wobbly jittery something? I’ve also had cases where the pose snaps back to default 000. Any advice?

  11. The Refresh UI button can [and in my case did, a few times] destroy the setup in the UI, and there is no going back to it, unless you’ve saved the scene or the setup. What happens is: the driven objects disappear in the UI, even if the setup still exists in the scene. Dangerous to use, why is it one of the main UI buttons?

  12. Delete Current Setup can fail big time, especially when mixed with Refresh. There are issues with refreshing the UI, importing, and deleting the setup. If I do it cleanly, it should work. If I save a setup called Test, and import it over an existing Test, it will make deleting impossible. Deleting a setup when not in 000 pose is also an issue, it will leave that pose at whichever values, and importing a new setup will not be correct because of that, it will start with a default that’s not set at 000.

I hope you will consider improving this tool in the future, and get it to be more intuitive and more robust, so it’s easier to understand and easier to use.

Cheers!

2 Likes

One more thing. OK, two more things!
Instead of ninja editing the above, I’ll add and ninja edit this one. Writing is rewriting. I’m just adding info or fixing or clarifying things, I’m not deleting content btw. OK, on to the actual content:

  1. The RBF setup disappears - the driven nodes in the UI disappear. This is a bug reported by others as well. And in my case, extremely easy to trigger. Basically I find it impossible to keep the RBF setup editable because of this. So I set a few RBF poses and I save the Maya file. I even save the setup to disk. Nothing else, the setup works as intended, all looks clean. Now, if I close and reopen RBF Manager, or press Refresh, or Mirror the setup, or reopen the Maya scene, or restart Maya, the RBF Driven UI disappears and doesn’t load anymore. The setup in the scene continues to be functional, but not editable in the UI. The poses show up only in the upper UI, all the Driven nodes do not show up in the UI anymore, even though they are functional in the scene. I can delete it and reimport it, or simply save the scene and reopen it, it doesn’t matter, it’s gone forever. Any RBF Setup I’m making, no matter how simple, ends up having the same problem, at least during Mirroring, if not for simply restarting. And I’ve tried various setups. This pic is an example:

    All driven nods are hidden and if I try to add them back, I get the “Node is already driven” message, of course:
    image
    So the only way to edit would be to open the .rbf file, which is a text file. But it’s impossible to work like that. I’m using Maya 2023 btw, under Win 10 x64. And to conclude, it seems to me like that part of the UI has some problematic code and ends up not reloading content correctly.

And 14. Last but not least. Depending on how you setup things, it’s very likely that the Driven objects will pop from pose to pose, instead of Interpolating. The safest setup seems to not include joints, but rather have the same object both “Driver” and “Control” [which makes no sense and it’s not what the tutorials advise]. But honestly, RBF is such a complete mess, it’s impossible to tell what might work and what won’t, and why driven objects pop from pose to pose.

Cheers!

1 Like

Yes, I can see I’m the only one contributing here by myself, but since I’ve had such an adventurous time working with RBF Manager, I must continue to share my findings.

Problems 15-20:

  1. Deleting poses is bugged and unclear. Deleting a pose that is not the last pose is especially strange. Sometimes it doesn’t delete anything. Sometimes it will delete and because it renames the poses automatically, it’s impossible to tell if it delete something, and visibility over my poses is… ugh. Which leads to…

  2. Poses cannot be renamed to something useful. And remembering what Pose 12 means, by looking at crazy translation values, is very difficult. Which leads to…

  3. Joints drivers, or any driving RBF with rotations is impossible for anything that requires more than 90 vs -90 in any axis. That’s where joints and constraints alike will automatically flip. I had to use translation values as driver… which is odd and the values are complex, unlike with simple +60 +120 +180 rotations. I had to do some complicated setup, like a volume-grid-thing, to be able to translate arms and legs to exact locations, and have some logic in my posing.

  4. Editing poses not possible, it’s actually wrecking poses instead of editing them. I make some changes to a pose, I press Edit Pose, it seems to have edited something… but it’s hard to tell what it did. Because going back to that poses it seems like it wasn’t edited, yet maybe others were! It wrecks other poses in the process. I even tried to manually Reset after any edit, which shouldn’t be necessary in a nice, ideal world.

  5. And because after setting a pose there is no easy way to change it, switching between 2 poses and further editing is not possible [it would work only if I could edit manually the values, and then run the hidden Re-Evaluate, which is impossible when editing complicated Translation values, or in any case, it’s impractical in any complex scenario where you have a lot of Driven objects, so on]. So once I set a pose, if I want to compare to others… I have to nudge the Driver a bit and set a new pose, and then delete the old one. If that works well, but deleting poses is risky and can destroy my entire setup.

  6. Mirroring only works on Left Right controllers. If I have any Center ctrl, like the middle row of a skirt, no mirroring possible thank you very much.

And lastly, I guess I should mention that iterating with RBF has been really time consuming. I couldn’t save and reload a setup to continue to work on it, so I had to always start from scratch, make a new, similar setup, and hopefully remember what I did wrong and correct it this next time.

Oh, and take a look at this. After some time spent with the tool, the UI starts to disintegrate. Values disappear, sometimes they reappear… this should be 21 I guess, like a Bonus! It also shows how pose 10 doesn’t exist anymore in the Driver, only in the Driven, as a result of bugged Delete Pose.

And very lastly, as a call for help, does anyone know of a similar pose correcting tool for Maya?
Cheers!

1 Like

you could try SHAPES and its RBF setup possibilities

i also heard that that this version is great to use:

"
you need to get Epic Bridge, then export a MetaHuman out to Maya to get the RBF solvers, and then you can use the poseWrangler tool to handle RBFs
"

2 Likes

Thanks actoratwork, I will give poseWrangler a go. And probably I should try Brave Rabbit’s tool as well… hopefully their UI is more reliable, even if simpler.
Shapes on the other hand, I believe that’s only for Blendshapes? Does it support posing joints and controllers?
Edit: oh nvm, you’re right, I completely forgot about it. : ]
Cheers!

1 Like

you might be right,regarding SHAPES…
Posewrangle should indeed be a good alternative untill Mgears RBF manager is more reliable

This is a bit older but I also found an issue: The RBF manager does not manage custom attributes as it doesn’t copy them to the driver.

Since the rbfNode in mGear is the braverabbit node, you can use the braverabbit ( who make SHAPES ) interface with it. You can launch it with mel: weightDriverEditRBF

This doesn’t have import/export capabilities so you have to code that yourself if you want to use it data-centric.

To add the above capabilities - here’s my ‘universal’ node export script. It writes the node and it’s shape node with connections and values to a json file.
Theoretically this should also work for the weightDriver nodes. I’ll add the import when I’m done with it here as well :slight_smile:

( Edit: Handle nodes that don’t get a transform attached )

import pymel.core as pm
import json

def get_node_data(node, shape=False):
    node_type = 'transform' if pm.nodeType(node) == 'transform' else 'shape'
    if shape and node_type == 'transform':
        node = node.getShape()
        node_type = 'shape'
    elif not shape and node_type == 'shape':
        try:
            node = node.getTransform()
            node_type = 'transform'
        except:
            # some nodes don't have a transform
            pass
    if not node:
        return None
    
    data = {
        "name": node.name(),
        "type": node_type,
        "node_type": pm.nodeType(node),
        "connections": {
            "in": [],
            "out": []
            },
        "attributes": {}
        }
    for input_connection in (pm.listConnections(node, plugs=True, destination=False, connections=True, skipConversionNodes=True)):
        data['connections']['in'].append({
                                    'channel': str('.'.join(input_connection[0].split('.')[1:])),
                                    'plug': str(input_connection[1])
                                    })
    for output_connection in (pm.listConnections(node, plugs=True, source=False, connections=True, skipConversionNodes=True)):
        data['connections']['out'].append({
                                    'channel': str('.'.join(output_connection[0].split('.')[1:])),
                                    'plug': str(output_connection[1])
                                    })
    for attribute in pm.listAttr(node, output=True, settable=True, multi=True, write=True):
        value = pm.getAttr(f"{node}.{attribute}")
        try:
            data['attributes'][str(attribute)] = list(value)
        except: 
            data['attributes'][str(attribute)] = value
    return data

def write_json_file(data):
    exportPath = pm.fileDialog2(fileMode = 0, fileFilter='*.json')[0]
    with open(exportPath, "w") as f:
        f.write(json.dumps(data, indent=4))
        f.close()
    print ('Exported data to {}'.format(exportPath))
    
if __name__ == "__main__":
    sel = [pm.PyNode(selected) for selected in pm.ls(sl=True)]
    exp_data = []
    for node in sel:
        shape_data = get_node_data(node, shape=True)
        transform_data = get_node_data(node, shape=False)
        node_data = {
            'node_name': node.name(),
            'shape': None,
            'transform': None
            }
        if shape_data:
            node_data['shape'] = shape_data
        if transform_data:
            node_data['transform'] = transform_data
        exp_data.append(node_data)
        
    write_json_file(exp_data)```
1 Like

Just in case - here’s my ‘universal’ node loader - this has some very obvious issues but it does the job for my part - and for loading rbfs from the json written with the code above.

Let me know if it is useful :slight_smile:

( Edit: Updated for some smaller things like nodes not evaluating when they were created and handling nodes without a transform )

import pymel.core as pm
import json, re

class NodeData():
    def __init__(self, name):
        self.name = name
        self.transform = None
        self.shape = None
        self.node_type = None

        self.created_transform = None
        self.created_shape = None
    
    def detect_type(self):
        self.node_type = self.shape.get('node_type') if self.shape else self.transform.get('node_type')
        return self.node_type

    def create_node(self, create_if_exists=True):
        print(f"Creating Node: {self.name}, {self.node_type}")
        new_node = pm.createNode(self.node_type)
        if self.node_type == 'transform':
            self.created_transform = new_node
            self.created_transform.rename(self.transform['name'])
        else:
            self.created_shape = new_node
            try:
                self.created_transform = new_node.getTransform()
                self.created_transform.rename(self.transform['name'])
                self.created_shape.rename(f"{self.created_transform.name()}Shape")
            except:
                self.created_shape.rename(self.shape['name'])
                # some nodes don't have a transform node on top ( node editor nodes )
                pass

    def set_data(self):
        if self.transform and self.created_transform:
            self.set_attrs(self.transform, self.created_transform)
        else:
            pm.warning(f"No transform data or target node not created. Run 'create_node' first to set it up.")
        if self.shape and self.created_shape:
            self.set_attrs(self.shape, self.created_shape)
            try:
                pm.setAttr(self.created_shape.evaluate, 1)
            except:
                pass
        else:
            pm.warning(f"No shape data or target node not created. Run 'create_node' first to set it up.")

    def connect_data(self):
        if self.transform and self.created_transform:
            self.connect_attrs(self.transform, self.created_transform)
        else:
            pm.warning(f"No transform data or target node not created. Run 'create_node' first to set it up.")

        if self.shape and self.created_shape:
            self.connect_attrs(self.shape, self.created_shape)
        else:
            pm.warning(f"No shape data or target node not created. Run 'create_node' first to set it up.")

    def set_attrs(self, node_data, node):
        retry_attrs = []
        for attribute, value in node_data['attributes'].items():
            try:
                # print(f"Setting {node.name()}.{attribute} = {value}")
                pm.setAttr(f"{node.name()}.{attribute}", value, force=True)
                # print(f"Success!! {pm.getAttr(f'{node.name()}.{attribute}')}")
            except:
                retry_attrs.append((attribute, value))

        failed_attrs = retry_attrs

        # this breaks some data that was sucessfully set before but is useful for limits etc
        #  maybe use with detection of node type

        # failed_attrs = []
        # for attribute, value in retry_attrs:
        #     try:
        #         print(f"Retrying to set {node.name()}.{attribute} = {value} with mel")
        #         if isinstance(value, list):
        #             value_string = ""
        #             for val in value:
        #                 value_string += f"{float(val)} "
        #         else:
        #             value_string = str(float(value))
        #     except Exception as e:
        #         failed_attrs.append((attribute, value, e))

        if failed_attrs:
            print(pm.warning("Failed to set some attributes, check script editor for details"))
        for failed_attr in failed_attrs:
            print(failed_attr)

    def connect_attrs(self, node_data, node):
        failed_connections = []
        for connection in node_data['connections']['in']:
            channel = connection['channel']
            plug = connection['plug']
            try:
                # print(f"Connecting {plug} >> {node.name()}.{channel}")
                pm.connectAttr(plug, f"{node.name()}.{channel}", force=True)
            except:
                try:
                    reg_ends = re.search(r'W(\d+)$', channel)
                    if reg_ends and node_data['node_type'].endswith('Constraint'):
                        reg_index = int(reg_ends.group(1))
                        print(f"Connecting {plug} >> {node.name()}.target[{reg_index}].targetWeight")
                        print(channel.split('.'))
                        pm.connectAttr(plug, f"{node.name()}.target[{reg_index}].targetWeight", force=True)
                    else:
                        failed_connections.append([plug, channel])
                except Exception as e:
                    failed_connections.append([plug, channel, e])

        for connection in node_data['connections']['out']:
            channel = connection['channel']
            plug = connection['plug']
            try:
                # print(f"Connecting {plug} >> {node.name()}.{channel.split('.')[0]}")
                pm.connectAttr(f"{node.name()}.{channel}", plug, force=True)
            except:
                try:
                    # we want to try connecting constraint weights with their indicies
                    reg_ends = re.search(r'W(\d+)$', plug)
                    if reg_ends and node_data['node_type'].endswith('Constraint'):
                        reg_index = int(reg_ends.group(1))
                        print(f"Connecting {node.name()}.{channel} >> {plug.split('.')[0]}.target[{reg_index}].targetWeight")
                        pm.connectAttr(f"{node.name()}.{channel}", f"{plug.split('.')[0]}.target[{reg_index}].targetWeight", force=True)
                    else:
                        failed_connections.append([plug, channel])
                except Exception as e:
                    failed_connections.append([channel, plug, e])
        for failed_connection in failed_connections:
            print(failed_connection)
    
    def get_created_nodes(self):
        return [self.created_transform, self.created_shape]
    
    def __str__(self):
        return f"<<NodeDataObject>>\nself.name: {self.name}\nself.transform = {self.transform}\nself.shape = {self.shape}\nself.node_type = {self.node_type}"

def read_parse_json(filepath):
    nodes = []
    with open(filepath, "r") as f:
        data = json.load(f)
        f.close()
        for node_data in data:
            node_obj = NodeData(node_data['node_name'])
            node_obj.transform = node_data.get('transform')
            node_obj.shape = node_data.get('shape')
            node_obj.detect_type()
            nodes.append(node_obj)
    return nodes

if __name__ == "__main__":
    importPath = pm.fileDialog2(fileMode = 1, fileFilter='*.json', okCaption='Load')[0]
    nodes = read_parse_json(importPath)
    for node in nodes:
        node.create_node()
        node.set_data()
        node.connect_data()

1 Like