Joint Group Indices don't correspond to joint indices #41

Open
opened 2024-01-20 10:21:25 +08:00 by mad-guru · 11 comments
mad-guru commented 2024-01-20 10:21:25 +08:00 (Migrated from github.com)

Hi,

I'm working with the dna calibration tools (thank you for these amazing additions to Metahumans).

Question:

I can get joint names using dnaReader.getJointName(num)
face control names using dnaReader.getGUIControlName(num)

I can get Group Indices connected to a control using dnaReader.getJointGroupJointIndices(index)

The joint indices returned by that command don't correspond to the index numbers from dnaReader.getJointName(num).

How can I get the joints names that go with the GroupJointIndices?

Thank you

Hi, I'm working with the dna calibration tools (thank you for these amazing additions to Metahumans). Question: I can get joint names using dnaReader.getJointName(num) face control names using dnaReader.getGUIControlName(num) I can get Group Indices connected to a control using dnaReader.getJointGroupJointIndices(index) The joint indices returned by that command don't correspond to the index numbers from dnaReader.getJointName(num). How can I get the joints names that go with the GroupJointIndices? Thank you
n0phx commented 2024-01-29 18:55:35 +08:00 (Migrated from github.com)

Hello!

I can get Group Indices connected to a control using dnaReader.getJointGroupJointIndices(index)

what you describe here as control is not actually the control index. The above used API, dnaReader.getJointGroupJointIndices(index) is accepting the joint group index, not the control index as its only argument.

Other than that, the joint indices that are returned by the dnaReader.getJointGroupJointIndices(jointGroupIndex) API can be used to get the joint names. This will indeed have the effect that you expect, which is to return you the joint names belonging to a particular joint group. (Bear in mind that joint groups are NOT controls, joints are separated into joint groups - see the whitepaper about RigLogic for the details about how this separation happens).

for jointGroupIndex in range(dnaReader.getJointGroupCount()):
    jointIndices = dnaReader.getJointGroupJointIndices(jointGroupIndex):
    for jointIndex in jointIndices:
        jointName = dnaReader.getJointName(jointIndex)

This above shown snippet will gather joint names within each joint group.

Now, finding which exact joints are affected by which exact controls is a more complicated process :)

Hello! > I can get Group Indices connected to a control using dnaReader.getJointGroupJointIndices(index) what you describe here as control is not actually the control index. The above used API, `dnaReader.getJointGroupJointIndices(index)` is accepting the joint group index, not the control index as its only argument. Other than that, the joint indices that are returned by the `dnaReader.getJointGroupJointIndices(jointGroupIndex)` API can be used to get the joint names. This will indeed have the effect that you expect, which is to return you the joint names belonging to a particular joint group. (Bear in mind that joint groups are NOT controls, joints are separated into joint groups - see the whitepaper about RigLogic for the details about how this separation happens). ```py for jointGroupIndex in range(dnaReader.getJointGroupCount()): jointIndices = dnaReader.getJointGroupJointIndices(jointGroupIndex): for jointIndex in jointIndices: jointName = dnaReader.getJointName(jointIndex) ``` This above shown snippet will gather joint names within each joint group. Now, finding which exact joints are affected by which exact controls is a more complicated process :)
mad-guru commented 2024-02-01 05:50:26 +08:00 (Migrated from github.com)

Thanks so much for the reply. I did know how to do that part, but could use some help with the following.

face_riglogic
I am developing Maya tools around DNA Calibration tools to create Custom Metahumans on meshes that would not work in Mesh to Metahuman. I’m trying to wrap my head around Behavior layer data, looking at the DNA file as JSON, reading the white paper on rig logic and documentation.

I have learned a lot but am still unclear on the exact nature of
input indices (contains expression index and other data?),
output indices (same question as input indices)
How to find specific data in it? Do I look at JointRowCount and JointColumnCount to help determine where the value for a particular expression or control resides etc?

I have some workarounds to get most of the data I need but do need to figure out jointgroup values at the very least so that I can save the resulting updates to control triggered joint positions.

jointgroup values (I think based on y=kx or is it y=kx+b?
my understanding is that:
y is the resulting stored value
k is the delta (triggered transform - neutral transform) * x which is the control value, in the case of stored values, always 1.
b would be adding the neutral value again for the actual scene transform value I think

Any help in clarifying this data would be appreciated.

Thanks so much for the reply. I did know how to do that part, but could use some help with the following. ![face_riglogic](https://github.com/EpicGames/MetaHuman-DNA-Calibration/assets/25218814/ddf6e272-f91a-4fc5-a269-e2eae0875c79) I am developing Maya tools around DNA Calibration tools to create Custom Metahumans on meshes that would not work in Mesh to Metahuman. I’m trying to wrap my head around Behavior layer data, looking at the DNA file as JSON, reading the white paper on rig logic and documentation. I have learned a lot but am still unclear on the exact nature of input indices (contains expression index and other data?), output indices (same question as input indices) How to find specific data in it? Do I look at JointRowCount and JointColumnCount to help determine where the value for a particular expression or control resides etc? I have some workarounds to get most of the data I need but do need to figure out jointgroup values at the very least so that I can save the resulting updates to control triggered joint positions. jointgroup values (I think based on y=kx or is it y=kx+b? my understanding is that: y is the resulting stored value k is the delta (triggered transform - neutral transform) * x which is the control value, in the case of stored values, always 1. b would be adding the neutral value again for the actual scene transform value I think Any help in clarifying this data would be appreciated.
n0phx commented 2024-02-02 02:14:13 +08:00 (Migrated from github.com)

input indices (contains expression index and other data?),

this is correct, it contains only expression indices, where expressions are:

  1. the raw controls that you can set through RigLogic
  2. and corrective (PSD) expressions (which are automatically turned on by RigLogic (the user cannot set these)

output indices (same question as input indices)

output indices are joint attribute indices. Each joint has 9 attributes (translation.x, translation.y, translation.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z), so given any output index, when divided by 9, will result in the joint index (and this is how you can find / match which joint is affected by which row from the joint group values matrix). The joint group values matrix is in row major order, with the rows being joint deltas per each joint attribute and the columns being the expressions that turn them on. So:

  • dna.getJointGroupOutputIndices(jointGroupIndex)[3] will give you the joint attribute index affected by the fourth row from the joint group matrix.
  • dna.getRawControlName(dna.getJointGroupInputIndices(jointGroupIndex)[5]) would give the raw control name for the sixth column from the matrix.
  • There are len(dna.getJointGroupOutputIndices(jointGroupIndex)) rows and len(dna.getJointGroupInputIndices(jointGroupIndex)) columns in the matrix.

Using this information you should be able to pinpoint which exact raw controls are affecting which exact joint attributes.

jointgroup values (I think based on y=kx or is it y=kx+b?

the equation is indeex y=k*x+n, but n = 0 always for the joint behavior data, so we remain with k*x only. Neutral values are not considered in this equation, since rotations cannot be naively added onto the delta values.

> input indices (contains expression index and other data?), this is correct, it contains only expression indices, where expressions are: 1. the raw controls that you can set through RigLogic 2. and corrective (PSD) expressions (which are automatically turned on by RigLogic (the user cannot set these) > output indices (same question as input indices) output indices are joint attribute indices. Each joint has 9 attributes (translation.x, translation.y, translation.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z), so given any output index, when divided by 9, will result in the joint index (and this is how you can find / match which joint is affected by which row from the joint group values matrix). The joint group values matrix is in row major order, with the rows being joint deltas per each joint attribute and the columns being the expressions that turn them on. So: - `dna.getJointGroupOutputIndices(jointGroupIndex)[3]` will give you the joint attribute index affected by the fourth row from the joint group matrix. - `dna.getRawControlName(dna.getJointGroupInputIndices(jointGroupIndex)[5])` would give the raw control name for the sixth column from the matrix. - There are `len(dna.getJointGroupOutputIndices(jointGroupIndex))` rows and `len(dna.getJointGroupInputIndices(jointGroupIndex))` columns in the matrix. Using this information you should be able to pinpoint which exact raw controls are affecting which exact joint attributes. > jointgroup values (I think based on y=kx or is it y=kx+b? the equation is indeex y=k\*x+n, but n = 0 always for the joint behavior data, so we remain with k*x only. Neutral values are not considered in this equation, since rotations cannot be naively added onto the delta values.
xiancg commented 2024-02-02 21:03:14 +08:00 (Migrated from github.com)

Hi @n0phx !

Thanks for the replies you've been providing in the GitHub issues.

May I ask how is it possible that this affirmation is correct?

"so given any output index, when divided by 9, will result in the joint index"

Since output indices are consecutive integer numbers in sets of 6 (since scale values are ignored), how is it possible that any given set of consecutive integers divided by 9 results in the same number (the joint index)? By aproximation of the possible resulting float results to an integer value?

Thanks!

Hi @n0phx ! Thanks for the replies you've been providing in the GitHub issues. May I ask how is it possible that this affirmation is correct? "so given any output index, when divided by 9, will result in the joint index" Since output indices are consecutive integer numbers in sets of 6 (since scale values are ignored), how is it possible that any given set of consecutive integers divided by 9 results in the same number (the joint index)? By aproximation of the possible resulting float results to an integer value? Thanks!
n0phx commented 2024-02-02 21:18:19 +08:00 (Migrated from github.com)

Since output indices are consecutive integer numbers in sets of 6 (since scale values are ignored),

this is not a correct assumption, you may only assume that output indices will appear in ascending order, but it is a sparse storage, so rows will be frequently skipped entirely where all deltas would be zeros in the matrix (this is part of the pruning strategy described in the paper).
When I mentioned division, I implied integer division, not floating point, so the automatic rounding will give you correct results:
e.g.:

output index 17
17 / 9 = 1 (so this is joint index 1)
17 % 9 = 8 (so the attribute index within the joint is 8 which means it's the scale.z value) (attributes will always follow the order [translation.x, translation.y, translation.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z]

output index 112
112 / 9 = 12 (so this is joint 12)
112 % 9 = 4 (so this is the rotation.x attribute of the joint)

and scale values (while might not be frequent) but they certainly appear somewhere in the behavior data, they are not entirely absent, just not that frequently used.

> Since output indices are consecutive integer numbers in sets of 6 (since scale values are ignored), this is not a correct assumption, you may only assume that output indices will appear in ascending order, but it is a sparse storage, so rows will be frequently skipped entirely where all deltas would be zeros in the matrix (this is part of the pruning strategy described in the paper). When I mentioned division, I implied integer division, not floating point, so the automatic rounding will give you correct results: e.g.: output index 17 17 / 9 = 1 (so this is joint index 1) 17 % 9 = 8 (so the attribute index within the joint is 8 which means it's the scale.z value) (attributes will always follow the order `[translation.x, translation.y, translation.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z]` output index 112 112 / 9 = 12 (so this is joint 12) 112 % 9 = 4 (so this is the rotation.x attribute of the joint) and scale values (while might not be frequent) but they certainly appear somewhere in the behavior data, they are not entirely absent, just not that frequently used.
mad-guru commented 2024-02-08 11:52:15 +08:00 (Migrated from github.com)

Thanks so much for all of this clarification n0phx. I read it as soon as you posted, but needed a few days to really explore and try things out. I ended up rewriting code with this more detailed understanding, which makes for a much faster and more efficient tool.

I think I have some values updated correctly, by taking the entire joint group values and updating a few indices with new values.

setJointGroupValues(jointGroupIndex, values, count)

For saving out I am using something like this to keep previous dna data with setfrom and add an updated values list:

        outputPath = dna_path.replace('.dna', '_updated.dna')
    
        # Create a writer DNA from input DNA
        output_stream = dna.FileStream(outputPath, dna.FileStream.AccessMode_Write, dna.FileStream.OpenMode_Binary)
        writer = dna.BinaryStreamWriter(output_stream)
        writer.setFrom(dnaReader)
        writer.setJointGroupValues(joint_group, values)        
        writer.write()
        print(f'Finished writing out {dna_path}')

With this way, I wasn't sure how I might use count in setJointGroupValues. Would that be if I created a partial list of new values such as:

[0.003, 0.2012, 0.02342]

and somehow insert just that at the right spot, telling it how many values in the count?

Also, since it is for a specific group, if I am updating several groups, I need to save to dna file each time, which can slow down the process. Any advice for efficient use here?

Thank you.

Thanks so much for all of this clarification n0phx. I read it as soon as you posted, but needed a few days to really explore and try things out. I ended up rewriting code with this more detailed understanding, which makes for a much faster and more efficient tool. I think I have some values updated correctly, by taking the entire joint group values and updating a few indices with new values. setJointGroupValues(jointGroupIndex, values, count) For saving out I am using something like this to keep previous dna data with setfrom and add an updated values list: outputPath = dna_path.replace('.dna', '_updated.dna') # Create a writer DNA from input DNA output_stream = dna.FileStream(outputPath, dna.FileStream.AccessMode_Write, dna.FileStream.OpenMode_Binary) writer = dna.BinaryStreamWriter(output_stream) writer.setFrom(dnaReader) writer.setJointGroupValues(joint_group, values) writer.write() print(f'Finished writing out {dna_path}') With this way, I wasn't sure how I might use count in setJointGroupValues. Would that be if I created a partial list of new values such as: [0.003, 0.2012, 0.02342] and somehow insert just that at the right spot, telling it how many values in the count? Also, since it is for a specific group, if I am updating several groups, I need to save to dna file each time, which can slow down the process. Any advice for efficient use here? Thank you.
n0phx commented 2024-02-09 06:15:37 +08:00 (Migrated from github.com)

Glad this helps, sadly the API documentation / general usage guidelines for these lower level portions of the library are lacking in depth.

setJointGroupValues(jointGroupIndex, values, count)

The count parameter is only present / visible in the C++ API, it is not available in the Python API. With the C++ API, you give a pointer and a size (which is the count parameter), telling the DNA library how many floating point values are in the matrix.
When you use the Python API, this information is implicitly encoded in the list that you pass to the API, so the length of the given list will be used to infer the count. You may only update the whole matrix, sadly it is not possible to do a partial update of the values (at least with the current API, this might change in the future though)

Also, since it is for a specific group, if I am updating several groups, I need to save to dna file each time, which can slow down the process. Any advice for efficient use here?

you don't need to save the DNA for each single joint group update, you can do a bulk update, call writer.setJointGroupValues in a loop for each joint group, and save the DNA only one time at the end when all the updates are done.

Glad this helps, sadly the API documentation / general usage guidelines for these lower level portions of the library are lacking in depth. > setJointGroupValues(jointGroupIndex, values, count) The count parameter is only present / visible in the C++ API, it is not available in the Python API. With the C++ API, you give a pointer and a size (which is the count parameter), telling the DNA library how many floating point values are in the matrix. When you use the Python API, this information is implicitly encoded in the list that you pass to the API, so the length of the given list will be used to infer the count. You may only update the whole matrix, sadly it is not possible to do a partial update of the values (at least with the current API, this might change in the future though) > Also, since it is for a specific group, if I am updating several groups, I need to save to dna file each time, which can slow down the process. Any advice for efficient use here? you don't need to save the DNA for each single joint group update, you can do a bulk update, call `writer.setJointGroupValues` in a loop for each joint group, and save the DNA only one time at the end when all the updates are done.
mad-guru commented 2024-02-13 14:47:10 +08:00 (Migrated from github.com)

Thanks again for the clarifications n0phx. I've been working on organizing the dna file data and generating the correct transform deltas and the corresponding indices to place them into the joint values list. I've been able to write out data, but it is not correct. The basic idea was to subtract neutral transform values from control expression triggered transforms to get the delta values that would go into the joint group values. The joint group indices to place those values is figured out by cycling through output indices to find the correct control expression and input indices to find the correct joints moving row by row to figure out the correct index value. See code below.

    # first step get transform data
    # get neutral value
    joint_index = self.joint_name_list.index(joint_name)
    joint_neutral_translation = self.dnaReader.getNeutralJointTranslation(joint_index)
    joint_neutral_rotation = self.dnaReader.getNeutralJointRotation(joint_index)
    joint_neutral_transform = [joint_neutral_translation[0], joint_neutral_translation[1], joint_neutral_translation[2], joint_neutral_rotation[0], joint_neutral_rotation[1], joint_neutral_rotation[2]]#, 1, 1, 1]
    
    # triggered value
    joint_triggered_translation = cmds.getAttr(joint_name + '.translate')[0]
    joint_triggered_rotation = cmds.getAttr(joint_name + '.rotate')[0]
    joint_triggered_scale = cmds.getAttr(joint_name + '.scale')[0]
    joint_triggered_transform = [joint_triggered_translation[0], joint_triggered_translation[1], joint_triggered_translation[2], joint_triggered_rotation[0], joint_triggered_rotation[1], joint_triggered_rotation[2]]#, joint_triggered_scale[0], joint_triggered_scale[1], joint_triggered_scale[2]]

step 2 get the delta. I have scale commented out as I am not sure if this is used.
delta_transform = [triggered_transform[0] - neutral_transform[0], triggered_transform[1] - neutral_transform[1], triggered_transform[2] - neutral_transform[2], triggered_transform[3] - neutral_transform[3], triggered_transform[4] - neutral_transform[4], triggered_transform[5] - neutral_transform[5]]#, triggered_transform[6] - neutral_transform[6], triggered_transform[7] - neutral_transform[7], triggered_transform[8] - neutral_transform[8]]

I'm not sure if the above is the correct way to get the delta.

step 3 get the value list index for each of these transforms. This way with a list of transform deltas and value indices, I should be able to write out the data correclty.

def return_joint_value_indices(self, joint_group, control_name, joint_name):
    joint_output_indices = self.dnaReader.getJointGroupOutputIndices(joint_group) # joint
    joint_input_indices = self.dnaReader.getJointGroupInputIndices(joint_group) # raw  ctrl

    # within group get values related to the ctrl expression
    # from output indices /9 get joint
    update_value_list = []
    joint_index = 0
    for num in range(len(joint_output_indices)):
        output_index = joint_output_indices[num]

        # from input indices get ctrl expression
        for num in range(len(joint_input_indices)):
            input_index = joint_input_indices[num]
            
            if self.dnaReader.getRawControlName(input_index) == control_name and int(output_index/9) == self.joint_name_list.index(joint_name):
                update_value_list.append(joint_index)
                
            joint_index += 1

unfortunately this does not yield the correct results. I'm not quite sure how to fix this. Any thoughts?

Thanks again for the clarifications n0phx. I've been working on organizing the dna file data and generating the correct transform deltas and the corresponding indices to place them into the joint values list. I've been able to write out data, but it is not correct. The basic idea was to subtract neutral transform values from control expression triggered transforms to get the delta values that would go into the joint group values. The joint group indices to place those values is figured out by cycling through output indices to find the correct control expression and input indices to find the correct joints moving row by row to figure out the correct index value. See code below. # first step get transform data # get neutral value joint_index = self.joint_name_list.index(joint_name) joint_neutral_translation = self.dnaReader.getNeutralJointTranslation(joint_index) joint_neutral_rotation = self.dnaReader.getNeutralJointRotation(joint_index) joint_neutral_transform = [joint_neutral_translation[0], joint_neutral_translation[1], joint_neutral_translation[2], joint_neutral_rotation[0], joint_neutral_rotation[1], joint_neutral_rotation[2]]#, 1, 1, 1] # triggered value joint_triggered_translation = cmds.getAttr(joint_name + '.translate')[0] joint_triggered_rotation = cmds.getAttr(joint_name + '.rotate')[0] joint_triggered_scale = cmds.getAttr(joint_name + '.scale')[0] joint_triggered_transform = [joint_triggered_translation[0], joint_triggered_translation[1], joint_triggered_translation[2], joint_triggered_rotation[0], joint_triggered_rotation[1], joint_triggered_rotation[2]]#, joint_triggered_scale[0], joint_triggered_scale[1], joint_triggered_scale[2]] step 2 get the delta. I have scale commented out as I am not sure if this is used. delta_transform = [triggered_transform[0] - neutral_transform[0], triggered_transform[1] - neutral_transform[1], triggered_transform[2] - neutral_transform[2], triggered_transform[3] - neutral_transform[3], triggered_transform[4] - neutral_transform[4], triggered_transform[5] - neutral_transform[5]]#, triggered_transform[6] - neutral_transform[6], triggered_transform[7] - neutral_transform[7], triggered_transform[8] - neutral_transform[8]] I'm not sure if the above is the correct way to get the delta. step 3 get the value list index for each of these transforms. This way with a list of transform deltas and value indices, I should be able to write out the data correclty. def return_joint_value_indices(self, joint_group, control_name, joint_name): joint_output_indices = self.dnaReader.getJointGroupOutputIndices(joint_group) # joint joint_input_indices = self.dnaReader.getJointGroupInputIndices(joint_group) # raw ctrl # within group get values related to the ctrl expression # from output indices /9 get joint update_value_list = [] joint_index = 0 for num in range(len(joint_output_indices)): output_index = joint_output_indices[num] # from input indices get ctrl expression for num in range(len(joint_input_indices)): input_index = joint_input_indices[num] if self.dnaReader.getRawControlName(input_index) == control_name and int(output_index/9) == self.joint_name_list.index(joint_name): update_value_list.append(joint_index) joint_index += 1 unfortunately this does not yield the correct results. I'm not quite sure how to fix this. Any thoughts?
n0phx commented 2024-02-21 20:48:34 +08:00 (Migrated from github.com)
def return_joint_value_indices(self, joint_group, control_name, joint_name):
    joint_output_indices = self.dnaReader.getJointGroupOutputIndices(joint_group) # joint
    joint_input_indices = self.dnaReader.getJointGroupInputIndices(joint_group) # raw  ctrl

    # within group get values related to the ctrl expression
    # from output indices /9 get joint
    update_value_list = []

    row_count = len(joint_output_indices)
    col_count = len(joint_input_indices)

    for row in range(row_count):
        output_index = joint_output_indices[row]
        joint_index = output_index // 9

        # from input indices get ctrl expression
        for col in range(col_count):
            input_index = joint_input_indices[col]

            if self.dnaReader.getRawControlName(input_index) == control_name and joint_index == self.joint_name_list.index(joint_name):
                update_value_list.append(row * col_count + col)

this would collect the indices of the exact delta values from the joint group values matrix that are related to the given control and joint names (I think this is what you intended to implement with the above code).. the rest seems ok from a quick glance

```py def return_joint_value_indices(self, joint_group, control_name, joint_name): joint_output_indices = self.dnaReader.getJointGroupOutputIndices(joint_group) # joint joint_input_indices = self.dnaReader.getJointGroupInputIndices(joint_group) # raw ctrl # within group get values related to the ctrl expression # from output indices /9 get joint update_value_list = [] row_count = len(joint_output_indices) col_count = len(joint_input_indices) for row in range(row_count): output_index = joint_output_indices[row] joint_index = output_index // 9 # from input indices get ctrl expression for col in range(col_count): input_index = joint_input_indices[col] if self.dnaReader.getRawControlName(input_index) == control_name and joint_index == self.joint_name_list.index(joint_name): update_value_list.append(row * col_count + col) ``` this would collect the indices of the exact delta values from the joint group values matrix that are related to the given control and joint names (I think this is what you intended to implement with the above code).. the rest seems ok from a quick glance
mad-guru commented 2024-03-23 16:58:46 +08:00 (Migrated from github.com)

thank you! This was a huge help. The information you provided in this entire thread has really helped increase the understanding of how to work with the dna data in addition to the deep dive video. Much appreciated.

thank you! This was a huge help. The information you provided in this entire thread has really helped increase the understanding of how to work with the dna data in addition to the deep dive video. Much appreciated.
n0phx commented 2024-03-23 19:01:31 +08:00 (Migrated from github.com)

I'm glad it helped!

I'm glad it helped!
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: CGNICO/Metahuman_DNA_Calibration#41
No description provided.